Tech・Ed Japan 2010、多くの皆様のご来場、誠に有難うございました!フォローアップ-1-DomainDataSource編
皆様、こんにちは!
Tech・Ed 2010 Japan、多くの方にご来場いただき、本当にありがとうございました。私のセッションも本当に大盛況で、立ち見の方まで出てしまい大変失礼いたしました。この場を借り増して本当御礼を申し上げます。今後ともSilverlight、Windows Phone 7、Internet Explorer 9 等、色々なUX / Client系のセッションでお目にかかれる日を楽しみにしております。
さて、早速、最初のフォローアップとして、まずはスクラッチでDomainDataSourceを使って作るアプリケーションの作成手順についてご紹介します。
サンプルプロジェクトはこちらにありますので、ぜひダウンロードしてご覧ください。
※アクセス許可を”全員”に変更しました。大変失礼しました。再度お試しください。
※※ラップアップセッション用に追加したwWebカム部分は未完成です。撮影はできますが、DomainDataSourceを準備してどこかに格納した方が良いかと思います。印刷機能は(本来はXAMLで帳票を作ってそちらに渡す方がSmartですが)、今回はBitmapで生成しています。
1.データベースの作成
今回は、BookClubというDBを使いますので、こちらのフォルダに入っているBookClub.SQLスクリプトを使って、まずは作成をお願いします。SQL Server Management Studioで、最初にDB名”BookClub"を作っておいて、それからこのBookClub.SQLを実行するのが一番速いでしょう。
※なお、このデータベースは英語版です。私がセッションで使用したものは独自に日本語化したものです。
2.Entity Data Modelの追加(TechEdDemo7.Web)
BookClubのDBを作って接続確認をしたら、Entity Data Modelを作ります。名前はBookShelfModel.edmxで、テーブルで全てのテーブルをチェックして、進みます。下記のようなModelが生成されたら、一度Buildしておきましょう。
3.DomainServicesの追加(TechEdDemo7.Web)
BookShelfService.csという名前で、DomainServiceを追加します。
出来上がったら、下記のようなダイアログが出ますので、Bookのみを選択し、Enable EditingをチェックしてOKです。
BookShelfService.csのソースウィンドウが開きますので、CRUDのメソッド(Get、Insert、Delete、Update)が入っていることを確認してください。
この後BookShelfService.csを開き、GetBooks()を下記のように登録日でソートするようにLINQのクエリーを書き加えておきます。
public IQueryable<Books> GetBooks()
{
return this.ObjectContext.Books.
OrderByDescending
(b => b.AddedDate);
}
以上で取り急ぎサーバー側は終了です。
4.MainPage.xamlの編集(TechEdDemo7)
クライアント側に移ります。まずは、MainPage.xamlを下記のように大きさ変更します。
[MainPage.xaml]
d:DesignHeight="600" d:DesignWidth="840">
Visual StudioのUIデザイナーでXAMLの方を表示して、最初の<Grid/>の直下に、下記のようにGridを分割するための値を設定します。
最初のGrid直下
[MainPage.xaml]
<Grid.RowDefinitions>
<RowDefinition Height="52*" />
<RowDefinition Height="436*" />
<RowDefinition Height="36*" />
<RowDefinition Height="39*" />
<RowDefinition Height="37*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="320*" />
<ColumnDefinition Width="112*" />
<ColumnDefinition Width="118*" />
<ColumnDefinition Width="118*" />
<ColumnDefinition Width="132*" />
</Grid.ColumnDefinitions>
次に、この段階で、Expression Darkのテーマを適用してしまいましょう。Visual StudioのUIデザイナーでXAMLの方を表示して、最初の<Grid/>の直ぐ上に、ツールボックスから、下記をドラッグ&ドロップで挿入します。これはToolkitに含まれているテーマです。
一番下の</Grid>の直ぐ下に、<・・と入力すると、インテリセンスが働きますので、ExpressionDarkThemeのタグを選んで挿入します。
5.メタデータ(アトリビュート)による列名表示の変更とバリデーション設定
次に、すでに出来上がったDomainDataSourceを、このMainPageの各分割されたGridにドラッグ&ドロップして、マスター・ディテール的な画面を作っていきます。
まずは、セッションでもご紹介したように、列名の変更とバリデーションを同時に設定する方法です。これには、BookShelfService.metadata.cs を編集します。これで宣言的に設定することができ、ビルドすると同時に、クライアントに波及させることができるのです。
まずは、BookShelfService.metadata.cs を開き、下記のように編集します。
[Display(Name = "登録日", Order = 4)]
public DateTime AddedDate { get; set; }
[Required(ErrorMessage = "Amazon IDは必須項目です。")]
[RegularExpression("^[A-Za-z0-9]{10}$",
ErrorMessage = "ASIN は、数字のディジットで 10 桁です。")]
[Display(Name = "Amazon ID", Order = 5)]
public string ASIN { get; set; }
[Required(ErrorMessage = "著者名は必須項目です。")]
[Display(Name = "著者", Order = 2)]
public string Author { get; set; }
[Display(Name = "書籍ID", Order = 6)]
public int BookID { get; set; }
[Display(Name = "カテゴリID", Order = 7)]
public int CategoryID { get; set; }
[Required(ErrorMessage = "詳細は必須項目です。")]
[Display(Name = "詳細", Order = 3)]
public string Description { get; set; }
[Display(Name = "メンバーID", Order = 8)]
public int MemberID { get; set; }
[Required(ErrorMessage = "出版日付は必須項目です。")]
[Display(Name = "出版日付", Order = 9)]
public DateTime PublishDate { get; set; }
[Required(ErrorMessage = "タイトルは必須項目です。")]
[Display(Name = "タイトル", Order = 1)]
public string Title { get; set; }
これを行ってから、再度ソリューションをビルドします。
その後は、MainPage.xamlの左側のエリアにDataGrid形式(Title以外”なし”に設定)、右側のエリアにDataForm形式で、同じDomainDataSourceをドラッグ&ドロップします。
実行した結果は、このような感じです。これを実行してみてください。左側にタイトル、右側がそれに連動して詳細が表示されるはずです。
なお、セッションでは割愛しましたが、この時点で、ツールボックスから、”DataPager”を選び、DataGridの下のエリアに貼り付けておきあす。そして、同じDomainDataSourceをこの上にドラッグ&ドロップすると、デフォルトで10件ごとにDataGridのページが切り替わるように設定できます。
6. 更新(U)・削除(D)および保存(MainPage.xaml.cs)
セッションでもご紹介しましたが、このDomainDataSourceを使ったコントロールは、最初から編集ができるようになっています。しかし、その編集を、データソースに反映させる(確定する)には、保存を明確に指示しなければなりません。またデータソースに反映させる必要がなければ、すべての編集を破棄すれば良い訳です。
まず削除ボタンは、この画面にある内容をビューとしてとらえて削除し、保存ボタンを使って変更を確定させるようにします。また保存ボタンは、更新と削除を確定させる処理を書きます。キャンセルの場合には、全ての変更を破棄します。
保存ボタンを配置し、Clickイベントハンドラで下記の記述をします。SubmitChanges()が確定、RejectChanges()がすべての変更の破棄を行うメソッドで、同じ名前のサーバー側(DomainService)のメソッドを呼び出します。
private void Save_Click(object sender, RoutedEventArgs e)
{
if (MessageBox.Show("書籍情報の編集を確定してよろしいですか?", "編集確定の確認",
MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
booksDomainDataSource.SubmitChanges();
}
else
{
booksDomainDataSource.RejectChanges();
booksDomainDataSource.Load();
booksDataGrid.SelectedItem = booksDataGrid.Cursor;
}
}
また非同期処理であるゆえ、実際に成功したかどうかを取得したいと考えます。その時には、DomainDataSourceのプロパティを開いて、SubmittedChanges()というイベントを探し、イベントハンドラ内を下記のように記述します。
private void booksDomainDataSource_SubmittedChanges(object sender, SubmittedChangesEventArgs e)
{
if (e.HasError)
{
MessageBox.Show(e.Error.Message, "データ保存エラー", MessageBoxButton.OK);
e.MarkErrorAsHandled();
}
else
{
MessageBox.Show("データが保存できました!", "データ保存成功", MessageBoxButton.OK);
}
}
次に削除処理です。これはあくまでも画面のViewに対して行えばいいので、そのような処理を書いて、確定させるときには、保存ボタンを使うことにします。
private void Delete_Click(object sender, RoutedEventArgs e)
{
if (MessageBox.Show("書籍情報を削除してよろしいですか?", "削除の確認",
MessageBoxButton.OKCancel) == MessageBoxResult.OK)
{
booksDomainDataSource.DataView.Remove(booksDomainDataSource.DataView.CurrentItem);
}
}
画面はこのようになります。
7. 追加( C )(MainPage.xaml.cs、ChildWindow1.xaml.cs)
次に追加の処理です。画面が一枚しかないので、クライアント側のアプリケーションに、ChildWindowを新規項目の追加→追加します。
そして、このWindowにも、UIのLook & Feelが同じになるよう、ExpressionDarkのThemeを適用し、大きさは、640 x 800にしておきます。
MainPage.xamlの右側と同じようにデータフォーム形式で入力したいので、booksDomainDataSourceをドラッグ&ドロップして、おきます。このとき、booksDomainDataSourceのAutoLoadプロパティ → falseにしておくことを忘れないでください。これを行わないと、最新のデータが入力された状態でChildWindow1が立ち上がってしまいます。
<ChildWindow1.xaml.cs>
下記の内容を追加します。
まずは、booksという変数を宣言します。これはデータ保持のために使われます。
Books books = null;
次に、
using TechEdDemo7.Web;
これを指定しておかないと、DomainContextやDomainDataSourceを参照できませんので、参照します。
public ChildWindow1()
{
InitializeComponent();
books = new Books();
((BookShelfContext)booksDomainDataSource.DomainContext).Books.Add(books);
grid1.DataContext = books;
}
新しい書籍情報のインスタンスを作成し、DomainDataSourceに追加した上で、それを入力フォームに関連付けます。
private void OKButton_Click(object sender, RoutedEventArgs e)
{
booksDomainDataSource.SubmitChanges();
if (!books.HasValidationErrors)
this.DialogResult = true;
}
OKボタンの処理の中で、SubmitChanges()を発行して更新を確定します。エラー処理はもっとちゃんとした方が良いかもしれません。
次にMainPage.xamlの方に移ります。
<MainPage.xaml>
追加ボタンを貼り付け名前をAddにして、Clickイベントハンドラに下記の内容を入力します。
private void Add_Click(object sender, RoutedEventArgs e)
{
ChildWindow1 cw = new ChildWindow1();
cw.Closed += (s, ev) =>
{
if (cw.DialogResult == true)
{
booksDomainDataSource.LoadedData +=
new EventHandler<LoadedDataEventArgs>(Inserted);
booksDomainDataSource.Load();
}
};
cw.Show();
}
データを再度Loadした時の処理を行うイベントハンドラ(Inserted)を追加したので、そちらの処理も記述します。
private void Inserted(object sender, LoadedDataEventArgs e)
{
dataPager1.PageIndex = dataPager1.PageCount;
booksDomainDataSource.LoadedData -= Inserted
}
とりあえず、モデル作成・サービス作成・データの追加/削除/保存/新規作成の箇所まで説明しました。
この後、WCF RIA Services Libraryを使った別のテーブルの参照や、[Include]による関連テーブルの読込と、表示(カテゴリ別)、を説明します。
8. WCF RIA Services Class Libraryによるメンバー参照
WCF RIA Servicesの特徴は、セッションでもご紹介した通り、コードの自動生成による、サーバー側(DomainService)とクライアント側(DomainContext(DomainDataSource))間における処理の共有です。ルールや制約、バリデーション等がすべて即時共有できるのです。
ただ、別のサービスとして管理しておきたい場合等、サービス側の変更を波及したくない場合もあります。疎結合的に参照できれば良い場合がある訳です。またLibraryにすれば、スケーラビリティが向上するので、多数のクライアントから参照される場合には、Libraryにしておいた方が都合が良いです。
そこで、WCF RIA Services Class Library を追加してみます。
やり方は、簡単です。同じソリューション内の場合には、プロジェクトの追加→WCF RIA Services Class Libraryを選んで、適当な名前を付けてOKを押してください。
これで通常のWCF RIA Services を有効にしたSilverlightアプリケーションと同じように、クライアント側のプロジェクトと、サーバー側のプロジェクトができますので、同じようにして、ここではMembersのテーブルをEntity Data Modelとして追加してください。これは名前も変えておいた方が良いでしょう。
(例;MembersDataModel.edmx / MembersDomainService.cs 等)
ビルドすると、サービス側のDomainServiceは同じように完成します(GetMembers、他)。
その後、ソリューション全体のBuildを行い、TechEdDemo7からRIASerivicesLibrary1(例)を、TechEdDemo7.WebからRIA ServicesLibrary.Web(例)を、それぞれ”参照の追加”で追加しておきます。
再度ソリューションをビルドします。
<ChildWindow2.xaml>
そこで、今度は元々のプロジェクトのクライアント(ここではTechEdDemo7)側に移り、表示・編集用にChildWindow2を準備し、ChildWindow1の時と同じように設定しておきます(640x800、ExpressionDarkのTheme適用)。ここにデータソースウインドウからMembersデータグリッドを、ドラッグ&ドロップします。
その前に、日本語表記にしたいので、例によって、MembersDomainServices.metadata.csを開き、下記のように編集しておきます。
[MemberDomainService.metadata.cs]
[Display(Name = "登録日", Order = 1)]
public DateTime JoinDate { get; set; }
[Display(Name = "初回ログイン日", Order = 2)]
public DateTime LoginDate { get; set; }
[Display(Name = "メンバーアリアス", Order = 3)]
[Required(ErrorMessage = "メンバーアリアスは必須項目です。")]
public string MemberAlias { get; set; }
[Display(Name = "メンバーID", Order = 4)]
public int MemberID { get; set; }
[Display(Name = "メンバー名", Order = 5)]
public string MemberName { get; set; }
[Display(Name = "所属オフィス", Order = 6)]
public string MemberOffice { get; set; }
[Display(Name = "パスワード", Order = 7)]
[Required(ErrorMessage = "パスワードは必須項目です。")]
public string Password { get; set; }
その後、ソリューション全体のBuildを行い、ChildWindow2を表示し、データソースウインドウから、MembersのDataGridをドラッグ&ドロップします。
MainPage.xamlにボタンを配置して、下記のように記述します(呼び出すだけ)。
private void showMembers_Click(object sender, RoutedEventArgs e)
{
ChildWindow2 cw2 = new ChildWindow2();
cw2.Show();
}
<RIAServicesLibrary1(例).WebのApp.ConfigからTechEdDemo7.WebのWeb.Configへの文字列コピー>
上記に加えて、RIAServicesLibrary1(例).WebのApp.configにある下記の部分(ConnectionStringセクション)を、TechEdDemo7.WebのWeb.Configの同セクションにコピー&ペーストしてください。
<connectionStrings>
<add name="BookClubEntities" connectionString="metadata=res://*/BookShelfModel.csdl|res://*/BookShelfModel.ssdl|res://*/BookShelfModel.msl;provider=System.Data.SqlClient;provider connection string="Data Source=.\sqlexpress;Initial Catalog=BookClub;Integrated Security=True;MultipleActiveResultSets=True"" providerName="System.Data.EntityClient" />
<add name="MembersEntities" connectionString="metadata=res://*/MembersModel.csdl|res://*/MembersModel.ssdl|res://*/MembersModel.msl;provider=System.Data.SqlClient;provider connection string="Data Source=.\sqlexpress;Initial Catalog=BookClub;Integrated Security=True;MultipleActiveResultSets=True"" providerName="System.Data.EntityClient" />
</connectionStrings>
これでBuildした上で実行すれば、疎結合的に、Membersテーブルを呼び出せます。
9. サーバー側に新しいメソッドを付加してカテゴリーによる参照(サーバー側DomainServiceを追加)
前半の最後に、メタデータによる拡張として、ご紹介したのが、この話です。”Service Methodのメタデータ公開”の一部になりますね。(本番ではDemoは最後にご紹介しました)
BookShelfModel.edmxは、Entity Data Modelとして作ってある訳ですから、Booksテーブルから参照されているテーブルもJoinできるはずです。そこで、CategoriesとBooksの1対多関係から、Category別にBooksを参照できる(番号を入れたらそのCategory番号の書籍を一覧表示できる)ウインドウを作りましょう。
まずは、BookShelfService.metadata.cs を再び開いて、ここにBooks以外のテーブル(ここではCategories)を読み込めるよう、[Include]設定を行います。最初の頃に実施した列名表記修正・制約/ルール設定の直前に、こちらを入れてください。
[Include]
public EntityCollection<Categories> Categories { get; set; }
そして、BookShelfService.cs を開いて、下記のメソッドをDomainServiceに追加します。GetBooks()の後に入れるのが良いでしょう。
public IQueryable<Books> GetBooksByCategory(int CategoryID)
{
return this.ObjectContext.Books.
Include("Categories").
Where(c => c.CategoryID == CategoryID);
}
修正したらソリューションをBuildします。
<ChildWindow3.xaml>
新しいChildWindowを追加し、それにこの新しいGetBooksByCategoryクエリーをドラッグ&ドロップします。データソースウインドウのBooksのDomainContextのところを展開すると下記のようになっていること(GetBooksByCategoryが選べること)が確認できます。
MainPage.xamlにボタンを配置し、これを呼び出します。
private void Categorized_Click(object sender, RoutedEventArgs e)
{
ChildWindow3 cw3 = new ChildWindow3();
cw3.Show();
}
以上で、前半の1)CRUD処理の部分と2)サービス拡張部分のデモは全てです。いかがでしょうか?
ぜひサンプルソリューションをダウンロードして確かめてみてください。
それでは、次回は、最後の3)MVVM編か、この続きのいずれか、をご紹介します。
鈴木 章太郎