May 2009

Volume 24 Number 05

.NET RIA Services - Silverlight 3 を使用してデータ ドリブンの経費アプリケーションを作成する

Jonathan Carter | May 2009

コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照

この記事は、.NET RIA Services のプレリリース版に基づいて書かれています。ここに記載されているすべての情報は、変更される場合があります。

この記事では、次の内容について説明します。

  • .NET RIA Services の使用を開始する
  • データ サービスとドメイン操作
  • コードのプロジェクション
  • 柔軟なデータ コントロール
この記事では、次のテクノロジを使用しています。
Silverlight 3、.NET RIA Services、WPF

目次

使用を開始する
Data Services ライブラリ
ドメイン操作
コードのプロジェクション
データ コントロール
ObjectDataSource
DataPager
DataForm
メタデータ
妥当性確認
共有コード
まとめ

ソフトウェア開発では、スタイルの異なるさまざまなアプリケーションが存在し、それぞれに固有の機能と要件、強みと弱みがあります。開発のプラットフォームやフレームワークを選択する場合、作成対象アプリケーションを簡単に実装できるフレームワークであるかどうかという点が重要なポイントになります。開発者としては、プラミング コードやインフラストラクチャ コードの作成に多くの時間を割かれるのは避けたいことですし、顧客もこのような作業に対する対価を払うことは望みません。業務要件に労力を集中できるようにすることが重要であり、そのためには業務に専念できる機能を備えた生産性の高い開発プラットフォームが不可欠です。

Silverlight は魅力的な Web アプリケーションを作成するための優れたテクノロジです。Silverlight 2.0 には Windows Presentation Foundation (WPF) のサブセットが含まれていますが、WPF のデータ ドリブン アプリケーションの作成を簡素化する機能の多くが継承されていません。そのため、より高度な Web ソリューションのデータ中心のインフラストラクチャを実装する場合に多大な労力が必要になり、結局、多くの開発者は Silverlight を使用することを断念しています。Silverlight 3 のリリースには、データ ドリブン アプリケーションの開発を簡素化するための専用の新しいコントロールや機能が含まれています。この中には、新しいデータ コントロール、ナビゲーション、妥当性確認、埋め込みダイアログ ウィンドウなどがあります。

これらのクライアント側の拡張機能には、それだけでも相当の価値がありますが、多層環境の Silverlight ではクライアント/サーバー通信の処理方法に関する情報が必要です。この問題を解決するため、.NET RIA Services (コード ネーム "Alexandria") には n 層の開発プロセスを簡素化する一連のサーバー コンポーネントおよび ASP.NET の拡張機能が用意されています。これらのコンポーネントや拡張機能を利用して、アプリケーションを単層で実行する場合と同様に簡単に開発することができます。認証、ロール、プロファイル管理などのサービスも用意されています。Silverlight 3 と ASP.NET のクライアントおよびサーバーの拡張機能の組み合わせと .NET RIA Services の追加によって、リッチ インターネット アプリケーション (RIA) とも呼ばれるデータ ドリブン Web アプリケーションのエンド ツー エンドのエクスペリエンスが簡素化されます。

fig01.gif

図 1 既定の .NET RIA Services プロジェクト

この記事では、.NET RIA Services で導入された機能と連携して動作し、データ中心の Web アプリケーションを開発するためのリッチな環境を提供する Silverlight 3 の拡張機能のしくみについて説明します。Silverlight 3 および .NET RIA Services の機能をわかりやすく説明するために、例として一連の新機能を利用した経費報告書追跡アプリケーションを作成します。このアプリケーションには、従業員がログインして、各自の経費報告書を作成および管理する機能があります。経費報告書が完成したら、送信して上司の承認を待機します。

はじめに

.NET RIA Services と Silverlight 3 をインストールすると、Visual Studio に新しいプロジェクト テンプレート、Silverlight Data Application が追加されます。このテンプレートを使用すると、すぐに起動して実行できます。ソリューションを作成すると、2 つのプロジェクト (Silverlight プロジェクトと ASP.NET プロジェクト) が表示されます (図 1 を参照)。Silverlight プロジェクトはクライアント アプリケーションを表し、ASP.NET プロジェクトにはバックエンド ロジックを格納します。

この既定のプロジェクト構造の利点はどこにあるのでしょうか。Silverlight プロジェクトは通常の Silverlight プロジェクトを作成した場合とまったく同じで、標準の App.xaml ファイルと既定のページで構成されますが、この Silverlight プロジェクトには開発目標を支援するための微妙な違いがあります。

ASP.NET プロジェクトも通常どおりに簡潔ですが、default.aspx ページを見ると、SilverlightApplication という新しいサーバー コントロールが使用されていることがわかります。

<ria:SilverlightApplication 
  ID="Xaml1" runat="server" 
  Source="~/ClientBin/ExpenseReports.xap" 
  MinimumVersion="1.0" 
  Width="100%" Height="100%" />

このコントロールは、ASP.NET WebForms アプリケーション内で Silverlight アプリケーションを簡単にホストできるようにするための機能です。SilverlightApplication コントロールには、Silverlight プロジェクトから作成する XAP がターゲットとして既に構成されています。

この段階で、Silverlight アプリケーションは完全に機能し、ホストされています。後は、カスタム ビジネス ロジックと UI を追加するだけです。

このアプリケーションには既にデータベースが設定されているものとしましょう。次に必要な作業は、サーバー プロジェクトに ADO.NET エンティティ データ モデルを追加することです。この記事では Entity Framework をオブジェクト リレーショナル マッピング (O/RM) として使用していますが、LINQ to SQL、NHibernate、従来の ADO.NET などのデータ アクセス方法を利用することもできます。

データ モデルはきわめて単純で、3 つのエンティティ (ExpenseReport、ExpenseReportDetail、Employee) のみで構成されます (図 2 を参照)。ADO.NET Entity Framework の詳細については、MSDN の「ADO.NET Entity Framework の概要」を参照してください。

fig02.gif

図 2 データ モデル

データ モデルを配置し、O/RM を選択したら、バックエンド ロジックの実装を開始します。このプロジェクトでは Silverlight をクライアントとして使用し、ASP.NET をホストとして使用するため、2 層間の通信方法を決める必要があります。要件に応じて、Windows Communication Foundation (WCF) や ADO.NET Data Services など、現在使用可能な任意の通信フレームワークを使用できます。

Data Services ライブラリ

.NET RIA Services には、データ ドリブン RIA の作成を簡素化するためのサーバー コンポーネント ライブラリが用意されています。このライブラリは特定の UI フレームワークに依存しません。この記事では Silverlight で使用する場合に重点を置いて説明しますが、これはクライアントの選択肢の 1 つにすぎません。今後のリリースでは、.NET RIA Services は ADO.NET Data Services とも連携する予定です。

.NET RIA Services の中心は、ビジネス ロジックおよびデータ モデルのデータを操作するためのサーバー エンドポイントとして機能する DomainService という新しいクラスです。DomainService クラスの作成方法は、Visual Studio の .NET RIA Services によってインストールされる Business Logic Class テンプレートを利用する場合と同様に単純です。

DomainService の実装には、データ モデル固有のフォームまたは汎用フォームを使用できます。データ モデル固有の実装では、データ モデルが効果的にラップされ、開発者がビジネス ロジックとデータ アクセス コードの組み合わせを作成することができます。使用するデータ モデルが既に存在する場合にすぐに起動して実行できるという利便性があります。

.NET RIA Services には 2 つのデータ モデル (ADO.NET Entity Framework と LINQ to SQL) 固有の DomainService の実装が用意されています。アプリケーションでこの 2 つの実装のうちのどちらかを使用する場合、DomainService を作成する際にウィザードで適切なオプションを選択できるようになっています。その他のタイプのデータ モデルまたは O/RM を使用する場合は、DomainService の固有の実装を作成することもできます。

この記事では、エンティティ データ モデルは既に用意してあるので、先に進んで Entity Framework 固有の DomainService を作成します。ここでは、LinqToEntitiesDomainService<T> を継承する次のような新しいクラスを取得します。

[EnableClientAccess]
public class ExpenseService : 
  LinqToEntitiesDomainService<ObjectContext> {

  //public IEnumerable<Employee> GetEmployees()
  //{
  //    return this.Context.Employee;
  //}
}

この場合のジェネリック パラメータは、エンティティ データ モデルへの接続を示す ObjectContext インスタンスの型を表します。まず、TODO コメントの指示に留意して、型パラメータのプレースホルダを実際の ObjectContext (この場合は ExpenseReportContext) に置き換えます。

DomainService (またはその派生クラス) を継承する方法以外に、ドメイン サービスに EnableClientAccessAttribute をアタッチする方法もあります。この属性によってクラスはドメイン サービスとして認識され、パブリック クラスとして公開する必要があるかどうかを開発者が指定できるようになります。クラスをパブリックに公開することにより、クライアント アプリケーションにアクセスできます。この方法で、特定のロジックをサーバーだけで使用するか、クライアントでも使用できるようにするかを指定することができます。

ドメイン操作

ドメイン サービスを有効に機能させるためには、ドメイン操作の形式で機能を追加する必要があります。ドメイン操作は、ドメイン サービスのエンドポイントを表し、データ モデルまたは任意のビジネス ロジック、あるいはその両方に対して作成、読み取り、更新、削除 (CRUD) の操作を実行する機能です。各ドメイン操作を特定の操作の種類 (クエリ、挿入、更新、削除、サービス操作、カスタムなど) にマップする必要があります。操作の種類は名前付け規則または構成によってマッピングすることができます。

ドメイン操作で実行する操作の種類に応じて、固有のシグネチャに従う必要があります。操作によっては、特定の名前付け規則に従うか、操作の種類を指定する属性を設定する必要があります。たとえば、クエリ操作の場合、戻り値の型は IEnumerable<T> または IQueryable<T> でなければなりません。ここで、T は操作対象のエンティティの種類を表します。パラメータはいくつでも指定可能です。これらのパラメータはフィルタとして使用されますが、いずれも必須パラメータではありません。

すべての経費報告書を取得するドメイン操作を作成するコードは次のようになります。

public IEnumerable<ExpenseReport> 
  GetExpenseReports() {

  return Context.ExpenseReports;
}

ExpenseReport は基になるデータ モデルのエンティティなので、有効な戻り値の型です。DomainService クラスには、データ モデルのインスタンス (ジェネリック パラメータとして指定した型) にアクセスする機能を提供する Context プロパティがあります。この例の場合、このプロパティですべての経費報告書の一覧を照会できます。この操作は名前付け規則によって型にマップされます。

特定の経費報告書を取得するデータ メソッドを作成するコードは次のようになります。

[Query(IsComposable = false)]
public IEnumerable<ExpenseReport> 
  GetExpenseReport(int id) {

  var expenseReport = 
    from rep in Context.ExpenseReports
    where rep.Id == id
    select rep;

  return expenseReport;
}

このデータ メソッドは、特定の経費報告書を取得するためのパラメータとして ID を受け取ります。基本クラス ObjectContext にアクセス可能であるため、LINQ を使用してクエリを作成することもできます。このメソッドは、QueryAttribute を使用して構成によってマップします。このメソッドでは、名前付け規則では使用できない追加プロパティを指定することもできます。QueryAttribute の IsComposable プロパティの動作については後で説明します。

DomainService には必要に応じていくつでもクエリ メソッドを追加できます。データの取得以外に、データをデータ モデルに永続化する操作も作成できます。経費報告書の基本的な永続化操作 (挿入、更新、削除) を実装するコードは次のようになります。

public void InsertExpenseReport(
  ExpenseReport expenseReport) {
  Context.AddToExpenseReports(expenseReport);
}

public void UpdateExpenseReport(
  ExpenseReport current, ExpenseReport original) {
  Context.AttachAsModified(current, original);
}

public void DeleteExpenseReport(
  ExpenseReport expenseReport) {
  Context.DeleteObject(expenseReport);
}

経費報告書明細の永続化操作もほぼ同様の方法で定義できます。要求が DomainService に到達して変更をデータ モデルに保存するとき、どのメソッドを呼び出すかを DomainService が認識している必要があるため、エンティティの種類 (この場合は ExpenseReport) ごとに 1 つの挿入、更新、または削除メソッドのみを指定できます。

これらの操作では、シグネチャに注意する必要があります。挿入または削除メソッドを作成する際、挿入または削除するエンティティの種類に一致した 1 つのパラメータを渡す必要があります。更新メソッドを作成する場合、2 つのパラメータを渡す必要があります。その 1 つは変更された (または現在の) エンティティ インスタンス、もう 1 つは元のエンティティ インスタンスです。メソッドのシグネチャとメソッド名によって、どのメソッドがどのエンティティの種類の操作を実行するかを DomainService に通知します。

永続化操作を名前付け規則によって定義する場合、メソッド名の前にプレフィックスとして操作の種類を付加します。たとえば、メソッド名が DeleteExpenseReport の場合、名前付け規則に従って、プレフィックス Delete は削除操作であることを示しています。次に、シグネチャは関連付けられたエンティティの種類 (ExpenseReport) を定義します。ご想像どおり、名前付け規則に従った更新操作のメソッド名のプレフィックスは Update、挿入操作のプレフィックスは Insert です。このように規則化されているため、これらの操作を処理するための追加構成は不要です。

操作が所定の名前付け規則に従っていない場合や、操作に追加メタデータを指定する必要がある場合 (GetExpenseReport メソッドがこれに該当します)、GetExpenseReport メソッドの場合と同様に、QueryAttribute、InsertAttribute、DeleteAttribute、UpdateAttribute などの構成属性を適用できます。

常に CRUD 操作に関連付けられるとは限らず、エンティティの種類に関連付けられるビジネス ロジックを定義する場合には、サービス操作を作成する方法があります。この例では、経費報告書の承認および拒否の操作が必要です。そのために必要な処理は、経費報告書のステータスの値を変更することだけです (図 3)。サービス操作のシグネチャは、関連付けられたエンティティの種類のインスタンスを受け取り、void を返す必要があります。サービス操作を定義する名前付け規則は存在しないので、適切なドメイン操作を構成するには ServiceOperationAttribute を適用してください。

図 3 サービス操作

[ServiceOperation]
public void ApproveExpenseReport(
  ExpenseReport expenseReport) {

  if (expenseReport.Status == 1) {
    if (expenseReport.EntityState == 
      System.Data.EntityState.Detached) {
      Context.Attach(expenseReport);
    }
    expenseReport.Status = 2;
  }
}

[ServiceOperation]
public void RejectExpenseReport(ExpenseReport er) {
  if (er.Status == 1) {
    if (er.EntityState == 
      System.Data.EntityState.Detached) {
       Context.Attach(er);
    }
    er.Status = 0;
  }
}

ドメイン サービスと操作を作成した後、ASP.NET サーバー ホストから Silverlight クライアント アプリケーションへのサービスを操作するには、WCF または ADO.NET Data Services を使用している場合は該当サービスを示す Silverlight プロジェクトの参照を作成してプロキシを生成します。.NET RIA Services の方がやや操作性が優れています。

コードのプロジェクション

.NET RIA Services ソリューションを作成する場合、Silverlight プロジェクト ファイルに特殊な MSBuild タスクを追加し、特定のサーバー コンポーネントをクライアントにプロジェクションする処理を行います。EnableClientAccessAttribute を適用した ASP.NET プロジェクトでドメイン サービスを作成すると、MSBuild タスクはドメイン サービスを Silverlight アプリケーションにプロジェクションします (ソリューションを作成する際に新しい Silverlight Data Application プロジェクト テンプレートを使用していない場合は、Visual Studio 内で Silverlight プロジェクトおよび ASP.NET プロジェクトを手動でリンクして同じ機能を実装することができます)。

Silverlight プロジェクトの Main.xaml ファイルの分離コードを見ると、ExpenseContext および ExpenseReport という型が存在します。データ プロバイダを Silverlight クライアント アプリケーションにプロジェクションすると、DomainService (またはそのサブタイプ) ではなくなり、DomainContext になります。ドメイン サービスに Service というサフィックスが付いている場合、このサフィックスはプロジェクション時に Context に置き換えられます (そのため、ExpenseService クラスは Silverlight プロジェクトには ExpenseContext として反映されます)。DomainContext クラスは DomainService のクライアント側プロキシとして機能し、2 層間の要求の通信に必要なロジックを保持します。このクラスは処理単位を表し、現在追跡中のあらゆるエンティティ インスタンスに対する一連の変更セットを作成できます。

また、特定のメソッド (名前付け規則または構成によるドメイン操作として扱われるパブリック メソッド) が親ドメイン サービスと共にプロジェクションされます。6 種類のデータ メソッドのうち、プロジェクションされるのはカスタム、サービス、クエリの 3 種類のみです。クエリ メソッドの名前にはプレフィックス Get が付けられ、プロジェクション時に Get は Load に置き換えられます。たとえば、クエリ メソッドを GetExpenseReports という名前で定義した場合、クライアントには LoadExpenseReports として表されます。2 つのサービス操作もプロジェクションされ、DomainContext のインスタンス メソッドとして反映されます。

ドメイン操作から返されたエンティティの種類もプロジェクションされます。例では、GetExpenseReports メソッドの戻り値の型は IEnumerable<ExpenseReport> であるため、ExpenseReport エンティティ クラスは Silverlight クライアントにプロジェクションされます。プロジェクションされたエンティティ クラスは、変更履歴、妥当性確認、WPF/SL と互換性のある編集機能などの動作を処理する特殊クラス Entity を継承します。API の観点からは、クライアント側のエンティティ クラスはサーバーのエンティティと同様に認識されますが、このクラスには Silverlight データ コントロールの操作に必要な追加機能が含まれています。

コード プロジェクションの動作には他にもさまざまな要素があり、ここでは紹介しきれません。この記事では、考えられるその他のさまざまなシナリオ (特定の種類のコードをクライアント アプリケーションにプロジェクションする前にコードの構築方法を指定するロジックをカスタマイズする機能など) については説明しません。

ここでは、処理内容を的確に表していると思われるコードのプロジェクションという用語を使用しています。ExpenseService クラスは単にプロジェクト間でコピーされるだけではなく (この処理の例外については後で説明します)、検査および構築された後、使いやすいプロキシとしてクライアント アプリケーションにプロジェクションされます。そのため、このデータ型は、クライアント層からローカル オブジェクトと同様に利用できます。

では、プロジェクションされたドメイン サービスはどこに隠されているのでしょうか。一見、Silverlight プロジェクトは作成時の状態から何も変更されていないように見えます。Silverlight プロジェクトのすべてのファイルを表示するオプションをオンにすると、その原因がわかります (図 4 を参照)。Generated_Code という名前のフォルダが作成され、このフォルダにパートナー サーバー プロジェクトからプロジェクションされたすべてのコードが保存されます。現在、存在するファイルは ExpenseReportsServer.g.cs (g は自動生成を意味する generated を表します) のみですが、このフォルダに ExpenseReportsServer プロジェクト全体のコードが保存され、このフォルダが ExpenseContext クラスのホームになります。

fig04.gif

図 4 Silverlight プロジェクト用に生成されたコード

新しいデータ プロバイダを作成した場合、または既存のデータ プロバイダを変更した場合、変更は Silverlight プロジェクト内で暗黙的に更新され、クライアントとサーバーの同期が保たれます。アプリケーションのクライアントをサポートすることだけを目的にサービスを作成する場合、開発時にサービスのプロキシを継続的に更新する必要に迫られることは大きな負担です。この暗黙的な動作は、プロジェクション時の再構築以外にもきわめて有益な機能です。

データ コントロール

これで、データベース、データ モデル、サービス、クライアント側プロキシの実装が完了しました。次に、Silverlight クライアント内で経費報告書のデータを表示および編集するための UI を作成する必要があります。データ ドリブン アプリケーションを処理する場合のデータの表示方法には、一般に表形式とフォーム ベースの 2 種類があります。データ ドリブン RIA を処理するとき、テーブル形式とフォーム ベースのデータをユーザーにとってわかりやすく、生産性の高い方法で表示する必要があります。

Silverlight 2 は、標準ではテーブル形式のデータを十分にサポートしていませんでした。Silverlight の ListView コントロール (WPF の基本的なグリッド表示機能) を使用しても、業務ソフトウェアの構築にはやや困難が伴います。その後、Silverlight に導入された DataGrid コントロールは有効な機能でしたが、まだ必要な多くの機能が不足していました。TextBox、ComboBox、Button、ListBox、RadioButton などの組み込みコントロールを使用してフォームを開発することは可能であったものの、データの妥当性確認やエラー通知など、必要なその他の動作のサポートが不十分でした。

Silverlight 3 には、データ中心の RIA の作成を簡素化する目的に特化した一連の新しいコントロールが導入されています。これらのコントロールには、DataGrid、DataForm、DataPager、FieldLabel、DescriptionViewer、ErrorSummary、ChildWindow などがあります。サンプル アプリケーションでは、DataGrid、DataForm、および DataPager を利用してテーブル形式およびフォーム ベースのデータを表示しています。FieldLabel、DescriptionViewer、ErrorSummary には、応答性の高いデータ入力の開発に必要な動的 UI、データの妥当性確認、エラー通知の機能があります。また、ChildWindow ではリッチなモーダル ダイアログ ボックスを実装できます。

Silverlight 3 の DataGrid コントロールは堅牢です。特に、並べ替えおよびサイズ変更可能な列、行のグループ化、インライン編集、妥当性確認などの機能が用意されていることが特徴です。また、グリッドに表示する列を定義する場合、列を自動生成する方法と明示的に定義する方法の 2 とおりがあります。

DataGrid の AutoGenerateColumns プロパティを True に設定すると、バインドするデータ型のパブリック プロパティごとに 1 つの列が自動的に作成されます。ただし、プロパティにアタッチされた BindableAttribute がバインド不可に設定されている場合、列は作成されません。これは、新しいデータ コントロールでエンティティ メタデータを利用する方法の 1 つの例です。

DataGrid で列を明示的に定義すると、表示されるデータのスコープは表示したい列だけに限定されます。これにより、使用する列の種類やヘッダー テキストなど、列の各種プロパティを制御できます。経費報告書のデータを表示する列を明示的に定義する DataGrid は図 5 のようになります。

図 5 経費報告書の DataGrid

<dataGrid:DataGrid
  x:Name="ExpenseReportDataGrid"
  AutoGenerateColumns="False">
  <dataGrid:DataGrid.Columns>
    <dataGrid:DataGridTextColumn 
      Binding="{Binding Company}" 
      Header="Company" />
    <dataGrid:DataGridTextColumn 
      Binding="{Binding Department}" 
      Header="Department" />
    <dataGrid:DataGridTextColumn 
      Binding="{Binding Description}" 
      Header="Description" />
    <dataGrid:DataGridCheckBoxColumn 
      Binding="{Binding Status}" 
      Header="Approved" />
  </dataGrid:DataGrid.Columns>
</dataGrid:DataGrid>

列のバインドで指定したプロパティは、ExpenseReport エンティティのプロパティを参照します。クラスが Silverlight アプリケーションにプロジェクションされてクライアントで使用できるようになっていることを思い出してください。DataGridTextColumn クラスは、読み取り専用モードの場合はデータをテキスト形式で表示し、編集モードの場合はテキスト ボックス形式で表示します。DataGridCheckBoxColumn は、すべてのモードでフィールドをチェック ボックス形式で表示し、3 つの状態に対応します。

DataGrid にデータを読み込むには、ItemSource プロパティを任意の IEnumerable オブジェクトに設定します。ExpenseService および GetExpenseReports データ メソッドを定義したとき、戻り値の型が IEnumerable<ExpenseReport> であったことを思い出してください。次に、生成されたデータ プロバイダのクライアント側プロキシの使用方法を確認する必要があります。

ExpenseService クラスをクライアント アプリケーションにプロジェクションしたときに、GetExpenseReports メソッドもプロジェクションされました (名前を LoadExpenseReports に変更した後)。そのため、ExpenseService のインスタンスを作成して、そのインスタンスの LoadExpenseReports メソッドを呼び出すことができるようにする必要があります。これは適切な方法ですが、このメソッド自体は結果を生成しません。LoadExpenseReports メソッドは何も返しません。それでは、経費報告書のデータをどのようにして取得するのでしょうか。

Silverlight は、UI スレッドがロックアップしないようにするために、すべてのブロッキング呼び出しを非同期に実行する必要があります。そのため、.NET RIA Services がドメイン サービスを Silverlight クライアントにプロジェクションすると、すべてのクエリ操作がリファクタリングされ、何も返さなくなります。.NET RIA Services がクエリ メソッド名のプレフィックスを Get (または Fetch、Find、Query、Retrieve、Select) ではなく Load に変更する理由はここにあります。つまり、その方が動作をより的確に反映しているからです。

.NET RIA Services によって生成されたクラスは、コールバックではなく、イベント モデルを使用して非同期呼び出しの完了を通知します。そこで、クエリ操作の読み込みに応答するには、DomainContext の Loaded イベントを登録するだけで済みます。このイベントは、クエリ操作が完了した直後にトリガされます。このイベント ハンドラは UI スレッドで呼び出されるため、UI スレッド内でデータ バインドを実行できます。

図 6 経費報告書データの読み込み

public partial class Main : Page {
  ExpenseContext _dataContext;

  public Main() {
    InitializeComponent();

    this.Loaded += Main_Loaded;

    _dataContext = new ExpenseContext();
    _dataContext.Loaded += dataContext_Loaded;
  }

  void dataContext_Loaded(object sender, LoadedDataEventArgs e) {
    ExpenseReportDataGrid.ItemsSource = e.LoadedEntities;
  }

  void Main_Loaded(object sender, RoutedEventArgs e)  {
    _dataContext.LoadExpenseReports();
  }
}

fig07.gif

図 7 経費報告書のグリッドの UI

Loaded イベント ハンドラがトリガされた後、要求されたデータを取得する方法は 2 つあります。1 つは、LoadedDataEventArgs.LoadedEntities プロパティを使用する方法で、もう 1 つは、ドメイン コンテキストで、厳密に型指定されたエンティティ プロパティ (ExpenseContext など) を使用する方法です。必要なデータだけが取得されるようにするため、イベント引数を使用してデータにアクセスする方法をお勧めします。クエリ操作を呼び出すたびに、返されたエンティティ インスタンスがドメイン コンテキストに追加されます。そのため、そのコンテンツにアクセスするたびに、実行した最新のクエリだけでなく、すべてのクエリの累積が取得されます。

すべての経費報告書を取得し、取得したデータを DataGrid に読み込むロジックを実装すると、図 6 のようになります。この時点で、データ ドリブン RIA は図 7 のように表示されます。

DomainContext クラス (および生成されたサブタイプ) には、データを読み込む機能以外に、変更履歴やデータの永続化を管理するメソッドがあります。DomainContext を使用して取得 (あるいは追加または削除) したエンティティに対するすべての変更が追跡されます。変更を保存するときは、SubmitChanges メソッドを呼び出すだけで済みます。

private void SaveChangesButton_Click(
  object sender, RoutedEventArgs e) {
  if (_dataContext.HasChanges) {
    _dataContext.SubmitChanges();
  }
}

DataGrid は既定でインライン編集が有効になっているため、経費報告書のレコードを変更できます。変更をサーバーに永続化するには、[Save Changes] ボタンをクリックします。SubmitChanges メソッドを呼び出すと、DomainContext は追跡されているすべてのエンティティの変更セットを収集し、対応する DomainService に送信します。次に、DomainService が変更セットを分解し、挿入、更新、または削除された各エンティティの個々のドメイン操作を呼び出します。

DataGrid コントロール内でデータを編集すると、自動的に妥当性確認とエラー通知が行われます。無効なデータを入力すると、エラー内容の説明が視覚的に表示されます (図 8 を参照)。この機能は構成しなくても動作しますが、開発者が構成することもできます。後述するように、DataGrid が選択し、確認するデータ モデルにカスタム検証規則を定義することもできます。

fig08.gif

図 8 DataGrid の妥当性確認

経費報告書の表示はこれでも十分ですが、ステータス単位にグループ化して表示するとさらに便利です。そのようにすると、上司は承認待ちの報告書をひとめで確認できます。Silverlight 3 では、このコードを既存の DataGrid 定義に追加することで簡単に実現できます。

<dataGrid:DataGrid.GroupDescriptions>
  <dataGrid:PropertyGroupDescription  
    PropertyName="Status" />
</dataGrid:DataGrid.GroupDescriptions>

結果は図 9 のようになります。

fig09.gif

図 9 DataGrid のグループ化

ObjectDataSource

命令型のデータ バインドを選択する開発者がいる一方で、WPF の ObjectDataProvider を使用する場合のように宣言型のデータ バインドを好む開発者もいます。そこで、Silverlight 3 では ObjectDataSource コントロールが用意されています。

ObjectDataSource は、DomainContext 型の動作を認識している専用の非ビジュアル コントロールです。このコントロールで、以前の命令型の方法で果たされるすべての機能を宣言と多少の処理によって完全に満たすことができると考えてよいでしょう。必要な処理は、操作対象の DomainContext 型と呼び出す必要があるクエリ操作を ObjectDataSource に通知することだけです。その他の処理は ObjectDataSource が行います。

以前のセクションからすべてのコードを削除して ObjectDataSource に置き換えれば、指定した読み込みメソッドが自動的に呼び出されます。

<ria:ObjectDataSource
  x:Name="ExpenseReportsObjectDataSource"
  DataContextType="ExpenseReports.ExpenseContext"
  LoadMethodName="LoadExpenseReports"
  PageSize="20">
  <ria:ObjectDataSource.Filter>
    <data:FilterDescriptor Member="Department" 
      Operator="IsEqualTo" Value="IT" />
  </ria:ObjectDataSource.Filter>
  <ria:ObjectDataSource.Sort>
    <data:SortDescriptor Member="Status" 
      Direction="Descending" />
  </ria:ObjectDataSource.Sort>
</ria:ObjectDataSource>

ObjectDataSource コントロールを使用すると、宣言型のデータ バインドが可能になるばかりでなく、クエリも編集できるようになります。並べ替え、グループ化、フィルタの式やページ サイズを追加すると、クエリ操作の呼び出しに適用されます。最大のメリットは、ObjectDataSource がドメイン サービスに対する要求を変更して、指定された並べ替えやフィルタをサーバー側に適用するため、不要なデータを受け渡す必要がないことです。

DomainOperationAttribute の IsComposable プロパティを思い出してください。このプロパティは、ドメイン操作でクエリの追加パラメータを渡して返されたデータを編集できるようにするかどうかを指定します。サンプル アプリケーションの GetExpenseReports メソッドでは並べ替えやフィルタのコードは追加していませんが、DomainService が編集可能であり、ObjectDataSource が編集されたクエリの作成方法を認識しているため、この機能を自動的に利用することができます。

ObjectDataSource は DomainContext インスタンスのラッパーであるため、DomainContext と同様の変更履歴の動作を利用できます。ObjectDataSource の Load メソッドと SubmitChanges メソッドは DataContext と同じなので、DomainContext と同様にプログラムで制御できます。

DataPager

現在、グリッドに表示されているデータ量はやや非効率なので、表示の目的にはページングを実施すると効果的です。DataGrid コントロールにページング動作は存在しませんが、Silverlight 3 には他のデータ コントロールとシームレスに連携してページング機能を簡単に利用できる新しい DataPager コントロールがあります。

DataPager コントロールは、指定されたデータ ソースを使用して、ページングに必要な UI を表示するだけの単純な機能です。同じデータ ソースに別のデータ コントロール (DataGrid など) として DataPager をバインドすると、DataPager を使用したデータによるページングで、バインドされた他のコントロールもページングされます。DataPager を経費報告書の一覧に追加するコードは次のようになります。

<data:DataPager
  Source="{Binding Mode=TwoWay, Source={StaticResource ExpenseReportDataSource}, Path=Data}"/>

fig10.gif

図 10 DataGrid の下部に配置した DataPager

コントロールを定義して適切なソースにバインドすれば、残りの処理はコントロールによって自動的に行われます (図 10)。

DataPager にはユーザーに対するページの表示方法を選択できるさまざまなモードがあります。また、DataPager を完全に再スキンし、既存の機能を維持したままで参照することができます。

DataGrid および DataPager には操作性に優れたインライン編集機能がありますが、フォーム ベースのレイアウトでデータを表示する場合はどうすればよいでしょう。経費報告書を作成または変更する場合、グリッド任せではなく、ユーザーに対して直感的なフォームを表示することが望まれます。このような場合に新しい DataForm コントロールが有効です。

DataForm

DataForm コントロールを使用して、フォーム ベースのレイアウトで表示する一連のフィールドを定義し、1 つのエンティティ インスタンスまたはコレクションにバインドすることができます。データを読み取り専用、挿入、および編集の各モードで操作し、各モードの外観をカスタマイズすることもできます。モード切り替え用のコントロールを表示することも可能です。コレクションにバインドした場合、DataForm でナビゲーション用のページャも表示できます。DataGrid と同様、DataForm にもさまざまな形式 (自動生成、明示的な定義、およびテンプレート) があります。

自動生成モードは DataGrid と同様の機能を果たします。バインドされたデータ型の各パブリック プロパティに対してフィールドとラベルのペアを作成します。DataForm は、エンティティ レベルでバインド可能なフィールドの一覧を定義する BindableAttribute も認識します。経費報告書の編集フォームの定義は次のように単純です。

<dataControls:DataForm
  x:Name="ExpenseReportDataForm" Header="Expense Report"
  ItemsSource="{Binding Source={StaticResource ExpenseReportsObjectDataSource}, Path=Data}" />

自動生成モードでは、DataForm で、エンティティのメタデータに基づいてすべての UI の前提データを作成できます。結果として表示されるフォームを図 11 に示します。

fig11.gif

図 11 DataForm から派生した経費入力フォーム

必須フィールド (RequiredAttribute が指定されたプロパティ) のラベルは太字で表示され、必須項目であることをユーザーに示します。また、入力コントロールの右側のアイコンは、マウス カーソルを置くと要求される入力内容の説明を示すツールヒントを表示します。説明の入力は任意であり、説明を取得する場合は、フィールドの対応するプロパティに DescriptionAttribute がアタッチされているかどうかを確認します。データ モデルのメタデータに応答して Silverlight 3 の新しいデータ コントロールを UI に追加する方法でデータ ドリブンのシナリオを実現する例をもう 2 つ示します。

DataGrid と同様、DataForm にもデータの妥当性確認とエラー通知の機能があります。DataGrid と DataForm の外観と機能は統一されているため、どのようにデータを表示しても全体的な操作性は優れています。

明示的なフォームを使用すると、表示するフィールド、使用するフィールドの種類、表示するラベル テキストなどを宣言できます。このフォームは、UI の作成を DataForm とエンティティ メタデータに任せきりにしたくない場合に便利です。DataForm を明示的に宣言すると、図 12 のようになります。

図 12 DataForm の明示的な作成

<dataControls:DataForm
  x:Name="ExpenseReportDataForm"
  ItemsSource="{Binding Source={StaticResource ExpenseReportsObjectDataSource}, Path=Data}"
  AutoGenerateFields="False">
  <dataControls:DataForm.Fields>
    <dataControls:DataFormTextField 
      Binding="{Binding Company}" Label="Company" />
    <dataControls:DataFormTextField 
      Binding="{Binding Department}" Label="Department" />                
    <dataControls:DataFormTextField 
      Binding="{Binding Description}" Label="Description" />
    <dataControls:DataFormCheckBoxField 
      Binding="{Binding Status}" Label="Approved" />
  </dataControls:DataForm.Fields>
</dataControls:DataForm>

テキストおよびチェック ボックスのフィールド以外に、日付、コンボ ボックス、テンプレート、区切り記号、ヘッダー、フィールド グループに使用可能なフィールドもあります。これらのフィールドを使用して、表示するフィールドを明示的に定義し、DataForm に基本的な表示方法を指定することができます。このような柔軟性を備えているとはいうものの、まだトップダウン フォームの制約があり、各フィールドは従来のラベルと入力コントロールのペアです。DataFormTemplateField ではすべてのモード (表示モードおよび編集モード) のテンプレートを定義できますが、定義はフィールド レベルに限定されます。では、フォーム全体のテンプレートが必要な場合はどうすればよいでしょうか。

UI を完全に制御する必要がある場合 (または完全に制御したい場合)、DataForm を使用して各モード (表示、挿入、編集) のカスタム データ テンプレートを定義できます。この機能を使用すると、既定のトップダウン フォームのスタイルに限定されることなく、状況に応じて最適な外観を作成できます。

ナビゲーション、妥当性確認、エラー通知など、一定の動作は DataForm の 3 つのフォームのすべてに対してグローバルです。ただし、データ テンプレートを再定義すると、モデルのメタデータと連動する自動フィールド ラベルおよび説明ビューアは失われます。データ ドリブン アプリケーションを開発する際に、レイアウトをカスタマイズするためだけにこれらの便利な動作を失うのは不合理です。さいわいなことに、Dataform によって内部的に使用されてこれらの動作を提供するコントロールは、手動でも使用できます。

メタデータ

DataForm でフィールドの一覧を自動生成すると、ラベルと説明の動作を提供する 2 つのコントロール、FieldLabel と DescriptionViewer が自動的に使用されます。これらのコントロールは使いやすく、カスタム DataForm テンプレートなど、データ バインドを使用するあらゆる場面で利用できます。

FieldLabel は、関連するバインドされたプロパティのメタデータから指定されたコントロールのラベルを表示する場合に便利です。ラベルのテキストはバインド先のプロパティにアタッチされた DisplayAttribute の Name プロパティから取得されます。また、プロパティが必須の場合 (true に設定された RequiredAttribute がアタッチされている場合)、フィールドのラベル テキストは太字で表示されます。

DisplayAttribute では、プロパティのカスタム名だけではなく、説明も指定できます。フィールドの説明を表示する場合は、DescriptionViewer コントロールを使用すれば自動的に処理されます。DescriptionViewer コントロールは、関連付けられたプロパティの説明を表すツールヒントを示すアイコンを表示します。

FieldLabel コントロールおよび DescriptionViewer コントロールを使用すると、フィールド名や説明などの情報を複製する必要なく、データ モデルのメタデータを利用するカスタム データ フォームを開発できます。これらのコントロールをアプリケーション全体で使用すると、UI はデータ モデルに依存するため、プロパティの名前、説明、または必須ステータス (モデル レベル) が変更されるたびに、UI に変更が自動的に反映されます。このような動作はデータ ドリブン アプリケーションの開発に適しています。

検証

この記事ではデータ ドリブン アプリケーションの開発に重点を置いているため、ビジネス ロジックと妥当性確認はデータ モデルに密接していることが望まれます。.NET RIA Services を使用する場合、妥当性確認のロジックにはデータ注釈とカスタム/共有ロジックの 2 種類があります。

Microsoft .NET Framework 3.5 SP1 のリリースでは、データ モデルにメタデータおよび妥当性確認の規則をアタッチするためのデータ注釈と呼ばれる一連の属性が導入されています。これらの注釈は ASP.NET Dynamic Data で初めて使用され、.NET RIA Services および新しい Silverlight 3 のデータ コントロールでも認知され、重視されています。データ注釈を使用して、文字列の長さ、範囲、データ型、正規表現の制約などの妥当性確認項目を表すことができます。

[Bindable(true, BindingDirection.TwoWay)]
[Display(Name = "Expense Amount", 
  Description = "The amount of the incurred expense.")]
[Range(0.0, 1000000.00)]
public object Amount;

[Bindable(true, BindingDirection.TwoWay)]
[Display(Name = "Category", 
  Description = "The category of expense, i.e., mileage.")]
[StringLength(10)]
public object Category;

.NET RIA Services のプロジェクション プロセスを実行すると、サーバー側からクライアントへのデータ注釈の伝達が指示されます。汎用データ注釈を利用してサーバー側データ モデルの妥当性確認規則を表現すれば、その妥当性確認が Silverlight アプリケーションに引き継がれ、対応コントロール (DataForm、DataGrid、FieldLabel など) で完全に使用できます。これにより、クライアントとサーバーの妥当性確認を行うことができます。

データ注釈の使い方は簡単ですが、妥当性確認の要件のすべての可能性を表すことはできません。表すことができるのは、特に一般的なケースのみです。それ以外は、多くの場合、妥当性確認を命令型で定義する必要があります。処理は単純ですが、プロジェクション プロセスは DataContext およびエンティティのプロキシ クラスの作成に限定されるため、.NET RIA Services のプロジェクション プロセスでカスタム ロジックをクライアントに伝達することはできません。

サーバーのカスタム ロジックを保持し、Silverlight クライアントからサービスを呼び出すことができますが、その場合はアプリケーションの応答性が損なわれ、データ コントロールがデータの妥当性を自動的に判断する機能が失われます。ロジックをクライアントにコピーして貼り付け、2 つの層の妥当性確認を手動で行うこともできますが、コードの複製は好ましくないので、やはり自動妥当性確認の問題が残ります。このような場合は、.NET RIA Services の共有コード機能を使用する方法が適しています。

共有コード

経費報告書アプリケーションでは、1) 1,000 ドルを超える経費報告書には経費の目的を示す説明を記載する必要がある、2) 将来の購買のための経費報告書は提出できないという 2 つのカスタム妥当性確認規則を確認する必要があります。これらの条件は、データ注釈を使用して満たすことはできませんが、命令型で簡単に表現することができます。

.NET RIA Services には、同期してクライアント アプリケーションでも使用可能なロジックをサーバー プロジェクトに定義できる共有コードと呼ばれる機能があります。コードのプロジェクション プロセスでは、共有が設定されたコードは変換およびプロキシ化されるのではなく、プロジェクト間でコピーされます。この機能を利用して、サーバー プロジェクトに新しいコード ファイルを作成し、名前に .shared.[言語拡張子] (たとえば、ExpenseData.shared.cs) というサフィックスを付けます。コードのプロジェクション プロセスを実行すると、このサフィックスが付いたプロジェクト内のファイルが検索され、共有コードとして扱われます。

.NET Framework 4.0 には、カスタム妥当性確認規則をエンティティ レベルまたはプロパティ レベルでデータ モデルに関連付けることができる CustomValidationAttribute という名前の新しいデータ注釈があります。2 つのカスタム妥当性確認規則を指定するコードは次のようになります。

[MetadataType(typeof(ExpenseReportDetailsMetadata))]
[CustomValidation(typeof(ExpenseReportValidation),
  "ValidateDescription")]
public partial class ExpenseReportDetails { }

public partial class ExpenseReportDetailsMetadata {
  [Bindable(true, BindingDirection.TwoWay)]
  [CustomValidation(typeof(ExpenseReportValidation), 
    "ValidateDateIncurred")]
  [Display(Name = "Date", 
    Description = "The date of when this expense was incurred.")]
  public object DateIncurred;

.NET RIA Services のプロジェクション プロセスは CustomValidationAttribute を認識し、クライアントのプロキシに伝達します。カスタム妥当性確認は (サーバーに定義することが望まれる) 検証の種類に含まれているため、共有コードを利用して同期を処理することができます。

カスタム妥当性確認メソッドのシグネチャは一定のパターンに従う必要があります。

[Shared]
public static class ExpenseReportValidation {
  public static bool ValidateDateIncurred(object property, 
    ValidationContext context, out ValidationResult validationResult) {

    validationResult = null;
    bool result =       DateTime.Compare((DateTime)property, DateTime.Now) < 0;

    if (!result)
      validationResult = new ValidationResult(context.DisplayName + 
        " must be today or in the past.");
      return result;
  }
}

ExpenseReportValidation クラスの SharedAttribute の使い方に注目してください。この処理により、クライアントへの変換が不要であることをプロジェクション プロセスに通知します。これは、クライアントへの変換は共有コードの処理に含まれているためです。

まとめ

かつては、経費報告書アプリケーションを開発する場合には、経費報告書データの CRUD 操作をラップしていました。新しい Silverlight 3 の DataGrid、DataForm、DataPager、ObjectDataSource では、UI を迅速に作成することが可能になりました。これらの機能を利用すれば、インフラストラクチャ開発に投資する必要がなく、組み込みコントロールも使用できます。また、.NET RIA Services では、サーバー側のビジネス ロジックを定義して妥当性確認規則およびデータ アクセスの機能を付加し、プロジェクション プロセスを利用してそのロジックを簡単に使用することができます。

サンプル経費報告書には、まだ報告書明細のセクションとナビゲーションおよび認証の機能が必要です。これらの機能を実装するには、Silverlight 3 で導入された追加コントロールや .NET RIA Services の一連のアプリケーション サービスを使用する必要があります。そのしくみについては、今後の記事で説明します。

Jonathan Carter は、Microsoft のテクニカル エバンジェリストです。