次の方法で共有


データ ポイント

Silverlight 3 と DataForm によるデータ検証

John Papa

Silverlight 3 では、DataForm コントロールの導入により、データの編集と検証を行う強力な Silverlight アプリケーションを構築するのが大幅に容易になりました。DataForm コントロールには、データの追加、編集、削除、およびデータ間の移動を行うことができるフォームの作成に役立ついくつかの機能が含まれています。DataForm の最大のメリットは、柔軟性とカスタマイズ オプションです。

今月のコラムでは、DataForm コントロールがどのように機能するか、およびこのコントロールをカスタマイズする方法について説明します。また、このコントロールを実際に使用するようすをお見せします。まずは、DataForm のいくつかの機能を使用してデータのバインド、データ間の移動、データの編集、およびデータの検証を行うサンプル アプリケーションをお見せします。次に、おすすめのコントロールやカスタマイズを使用するために知っておく必要がある基本事項をお伝えしながら、サンプル アプリケーションがどのように機能するかを説明します。DataAnnotations は、DataForm を使用する場合に必須ではありませんが、DataForm の外観、検証の側面、および動作に影響を与える場合があります。DataAnnotations では、プロパティまたはエンティティ全体を対象として呼び出すカスタム検証メソッドを指定することもできます。エンティティに DataAnnotations を実装する方法、および独自のカスタム検証メソッドを記述する方法についても説明します。この記事のコードはすべて、MSDN コード ギャラリー (code.msdn.microsoft.com/mag200910DataPoints、英語) からダウンロードできます。

概要

コードについて詳しく見ていく前に、DataForm のナビゲーション機能、検証機能、および編集機能について説明するためのサンプル アプリケーションを見てみましょう。図 1 は、従業員の一覧を表示する DataGrid、および各従業員のレコードを編集できる DataForm を示しています。DataGrid 内の従業員間を移動して、どの従業員が現在選択されているかを変更することができます。DataForm は、DataGrid と同じ一連の従業員にバインドされており、DataGrid で現在選択されている従業員を受け取ります。

ツール バー

図 1 に示す DataForm は編集モードになっているので、ユーザーは変更を加えてその変更を保存またはキャンセルすることができます。DataForm の右上にあるツール バーには、編集 (鉛筆)、新しいレコードの追加 (プラス記号)、およびレコードの削除 (マイナス記号) のためのアイコンがあります。これらのボタンは、必要に応じてスタイル設定したり、無効にしたりすることができます。ボタンの状態は、エンティティが実装するインターフェイスによって決定されます。一方、ボタンの可視性は、DataForm のプロパティを設定してカスタマイズすることができます。

ラベル

DataForm 内の各 TextBox のキャプションは、コントロールの横か上に配置することができます。ラベルの内容は、Display という DataAnnotations 属性を使用して、メタデータに基づいて設定することができます。エンティティ (今回の場合は Employee エンティティ) のプロパティに対して DataAnnotations 属性を指定すると、DataForm に情報 (フィールドのラベルに表示するテキストなど) を提供することができます。

DescriptionViewer

(図 1 に示すように) DataForm が編集モードのときは、ユーザーは、各 TextBox コントロールの横のキャプションの隣にあるアイコンをポイントして、フィールドにどのような値を入力すればよいかについての詳しい情報を提供するツール ヒントを表示することができます。この機能は、DescriptionViewer コントロールを、バインドされた TextBox コントロールの横に配置した場合に、DataForm によって実装されます。DescriptionViewer のツール ヒントに表示されるテキストは、Display という DataAnnotations 属性からも情報を得ます。


図 1 DataForm による検証と編集

キャンセルとコミット

編集モードのときは、DataForm の下部にある [Cancel] ボタンは有効になっています。また、編集モードで、ユーザーが DataForm 内の値を変更したときは、[Save] ボタンは有効になっています。この 2 つのボタンのテキストとスタイルはカスタマイズ可能です。


図 2 既定の状態の DataForm

検証に関する通知

サンプルの DataForm は、編集モードで、ユーザーが無効な電子メールと電話番号を入力したところを示しています。無効になったフィールドのキャプションが赤くなっていること、およびテキスト ボックスが赤く縁取られて右上隅に赤い印が表示されていることに注目してください。ユーザーが無効になったコントロールの上にカーソルを置くと、問題を修正するためにはどうすればよいかを示すメッセージ入りのツール ヒントが表示されます。図 1 では、画面下部に検証エラーの一覧も表示されています。こうした機能はすべて DataForm と DataAnnotations でサポートされており、カスタムのスタイルを使用して外観をカスタマイズすることができます。検証規則では、あらかじめ用意された規則 (必須のフィールドなど) やカスタムの規則を示すいくつかの DataAnnotations から得た情報が使用されます。

DataForm をデザインする

System.Windows.Controls.Data.DataForm.Toolkit.dll アセンブリを参照したら、DataForm (Silverlight Toolkit に含まれています) をプロジェクトに追加することができます。DataForm を作成するために必要な設定はほんの少しだけです。Blend のデザイン サーフェイス上にフィールドをドラッグしてくるか、XAML 内でフィールドを作成すると (作成方法は、DataForm の ItemsSource にエンティティのコレクションを設定するだけです)、DataForm に表示するフィールドが自動的に生成され、適切な編集機能が有効になります。次の XAML を使用すると、図 2 に示す DataForm が生成されます。

<dataFormToolkit:DataForm x:Name="dataForm" ItemsSource="{Binding Mode=OneWay}" />

注: 上記の例でバインド モードに OneWay が設定されているのは、単に、わかりやすさのためです。これは必須ではありません。ですが、バインド モードを明示的に設定すると、わかりやすさとサポート性の点で役に立つと考えました。

DataForm に追加の設定を指定して、DataForm の機能を強化することができます。一般的な設定の一部を図 3 に示します。

図 3 DataForm の一般的なカスタマイズ

多くの場合、アプリケーションの外観と合うように DataForm の視覚的特性をスタイル設定することは有益です。DataForm のいくつかの側面は、ドキュメント内のリソース、app.xaml、またはリソース ディクショナリを使用してスタイル設定することができます。Expression Blend の部分的なスクリーンショットである図 4 に示すように、設定できるスタイルはいくつかあります。CancelButtonStyle と CommitButtonStyle の両方に値が設定されていることに注目してください。これにより、図 1 の画面下部に表示されているように、ボタンが青くなります。Style プロパティを設定すると、DataForm 自体のスタイルを設定することができます。DataField や ValidationSummary など、DataForm の個々の側面もスタイル設定することができます。

いくつかの基本的なスタイルに加えて、こうした DataForm の一般的なプロパティのいくつかを設定すると、図 1 に示すような外観や機能が実現されます。この記事に付属のサンプル アプリケーションで使用されている設定を図 5 に示します。AutoEdit と AutoCommit がどちらも false に設定されていることに注目してください。そのため、ユーザーはツール バーの編集ボタンをクリックして DataForm を明示的に編集モードに切り替える必要があり、また、[Save] ボタンをクリックして変更をコミットする必要があります。


図 4 DataForm のスタイル設定

CommandButtonsVisibility プロパティにナビゲーション ボタンは指定されていません。つまり、ツール バーにナビゲーション オプションは表示されないということです。CommandButtonsVisibility にキーワード Navigation を追加するか設定をキーワード All に変更して、このオプションを含めると、DataForm にはツール バーのナビゲーション ボタンも表示されます。コミット ボタンとキャンセル ボタンは CommandButtonsVisibility プロパティに含まれているので、この 2 つのボタンは表示されます。DataForm が未編集の間は、図 6 に示すように、キャンセル ボタンは無効になります。ナビゲーション ボタンを使用すると、ユーザーはレコード間を移動することができます。ですが、このサンプル アプリケーションでは、ユーザーはデータバインドされた DataGrid を使用して移動することもできる (DataGrid も DataForm と同じ一連のデータにバインドされているため) ので、純粋にデザイン上の判断として、ツール バーのナビゲーション ボタンは無効にしました。ツール バーのナビゲーション ボタンがなくても、ナビゲーション機能は DataGrid を通じて既に実現できているからです。

DataAnnotations

DataForm の機能の多くは、DataForm にバインドされたエンティティ データに対して指定された DataAnnotations 属性によって実現されます。DataAnnotations 属性は、System.ComponentModel.DataAnnotations.dll アセンブリを参照すると使用できるようになります。DataForm は、こうした属性を調べ、適宜適用します。図 7 は、エンティティのプロパティを修飾できる DataAnnotations 属性のいくつか、およびその効果を示しています。

図 5 DataForm のカスタマイズ

<dataFormToolkit:DataForm x:Name="dataForm"
                 ItemsSource="{Binding Mode=OneWay}"
                 BorderThickness="0"
                 FontFamily="Trebuchet MS" FontSize="13.333"
                 CommitButtonContent="Save"
                 CancelButtonContent="Cancel"
                 Header="Employee Details"
                 AutoEdit="False"
                 AutoCommit="False"
                 AutoGenerateFields="True"
                 CommandButtonsVisibility="Edit, Add, Delete, Commit, Cancel"
                 CommitButtonStyle="{StaticResource ButtonStyle}"
                 CancelButtonStyle="{StaticResource ButtonStyle}"
                 DescriptionViewerPosition="BesideLabel"
                 LabelPosition="Left" />

Display 属性は、DataForm の Label コントロールと DescriptionViewer コントロールに Name パラメーターと Description パラメーターの情報をそれぞれ提供します。この属性が省略された場合は、プロパティの名前を使用して、こうしたコントロール用にこれらの値が自動的に生成されます。図 8 で、Employee エンティティの FirstName プロパティが 3 つの DataAnnotations 属性で修飾されていることに注目してください。Display 属性は、Label には "First Name" というテキストを表示し、DescriptionViewer では "Employee’s first name" というツール ヒントを表示する必要があることを示しています。


図 6 ボタンのカスタマイズ

Required 属性は、FirstName プロパティ用に値を入力する必要があることを示します。Required 属性は、すべての検証属性と同様に、ErrorMessage パラメーターを受け取ります。ErrorMessage パラメーターは、無効になったコントロールのツール ヒントと DataForm 内の ValidationSummary コントロールの両方に表示されるメッセージを示します。FirstName プロパティの StringLength 属性は、フィールドの値が 40 文字を超えてはならないことを示すために設定されています。40 文字を超えた場合は、エラー メッセージが表示されます。

図 8 のコードは、プロパティに値が設定されると Validate メソッドが呼び出されることを示しています。Validate は、私が、このサンプルのすべてのエンティティで使用した基本クラス ModelBase に作成したメソッドです。このメソッドでは、プロパティがすべての DataAnnotations 属性で指定されている条件を満たすことをチェックすることで、プロパティを検証します。条件が満たされていない属性が 1 つでもある場合、このメソッドは例外をスローし、プロパティに無効な値を設定せず、ユーザーへの通知を行うように DataForm に指示します。図 1 は、FirstName フィールドに何も入力しなかった結果を示しています。このフィールドは必須なので、このような結果になります。

各プロパティの設定時にプロパティを検証できるように、すべてのプロパティ setter 内で ModelBase クラスの Validate メソッドが呼び出されます。このメソッドは、新しい値、および検証するプロパティの名前を受け取ります。次のように、この 2 つのパラメーターはその後、Validator クラスの ValidateProperty メソッドに渡されます。実際にプロパティ値の検証を行うのはこの ValidateProperty メソッドです。

protected void Validate(object value,
string propertyName) {
Validator.ValidateProperty(value,
new ValidationContext(this, null, null) {
MemberName = propertyName });
}

Validator は、次のようないくつかの異なる手法を使用して検証を実行することを可能にする静的クラスです。

  • ValidateProperty メソッドは、1 つのプロパティの値をチェックし、値が無効な場合は DataAnnotations.ValidationException をスローします。
  • ValidateObject メソッドは、エンティティ全体のすべてのプロパティをチェックし、無効な値がある場合は DataAnnotations.ValidationException をスローします。
  • TryValidateProperty メソッドは、例外をスローするのではなくブール値を返す点を除けば、ValidateProperty メソッドと同じ検証チェックを実行します。
  • TryValidateObject メソッドは、例外をスローするのではなくブール値を返す点を除けば、ValidateObject メソッドと同じ検証チェックを実行します。
  • ValidateValue メソッドは、値、および検証属性のコレクションを受け取ります。値が、いずれかの属性で指定された条件を満たしていない場合、ValidationException がスローされます。
  • TryValidateValue メソッドは、例外をスローするのではなくブール値を返す点を除けば、ValidateValue メソッドと同じ検証チェックを実行します。

図 7 一般的な DataAnnotations

図 8 DataAnnotations で修飾された FirstName プロパティ

private string firstName;
[Required(ErrorMessage = "Required field")]
[StringLength(40, ErrorMessage = "Cannot exceed 40")]
[Display(Name = "First Name", Description = "Employee's first name")]
public string FirstName
{
get { return firstName; }
set
{
if (firstName == value) return;
var propertyName = "FirstName";
Validate(value, propertyName);
firstName = value;
FirePropertyChanged(propertyName);
}
}

ModelBase クラスには、IsValid というメソッドも作成しました。以下に示すように、このメソッドでは、TryValidateObject メソッドを呼び出します。いつでもエンティティを対象としてこのメソッドを呼び出して、エンティティが有効な状態かどうかを確認することができます。

public bool IsValid() {
return Validator.TryValidateObject(this,
new ValidationContext(this, null, null),
this.validationResults, true);
}

図 9 カスタムのプロパティ検証

public static class EmployeeValidator
{
public static ValidationResult ValidateAge(int age)
{
if (age > 150)
return new ValidationResult("Employee's age must be under 150.");
else if (age <= 0)
return new ValidationResult(
"Employee's age must be greater than 0.");
else
return ValidationResult.Success;
}
}

カスタム検証

あらかじめ用意された一連の DataAnnotations 属性だけでは不十分な場合は、カスタム検証メソッドを作成し、それを 1 つのオブジェクト全体または 1 つのプロパティ値に適用することができます。検証メソッドは、静的であり、ValidationResult オブジェクトを返す必要があります。ValidationResult オブジェクトには、DataForm にバインドされるメッセージが含まれています。図 9 は、ValidateAge というカスタム検証メソッドを示しています。このメソッドでは、値が 0 より大きく 150 以下であることを確認しています。これは単純な例で、Range 属性を使用することも可能でした。しかし、専用のメソッドを作成すると、複数のエンティティでこのメソッドを検証用に繰り返し使用することができます。

カスタム検証メソッドを作成して、1 つのオブジェクト全体を検証することもできます。ValidateEmployee メソッドは、Employee エンティティ全体を受け取り、このエンティティが有効な状態にあるかどうかを確認するためにいくつかのプロパティを調べます。以下に示すように、これは、エンティティ レベルの検証を実施するのに非常に役立ちます。

public static ValidationResult ValidateEmployee(Employee emp) {
string[] memberNames = new string[] { "Age", "AnnualSalary" };
if (emp.Age >= 50 && emp.AnnualSalary < 50000)
return new ValidationResult(
"Employee is over 50 and must make more than $50,000.",
memberNames);
else
return ValidationResult.Success;
}

1 つ以上の ValidationException がスローされると、DataForm によって ValidationSummary コントロールが表示されます (図 10 参照)。フィールドの名前が太字で強調表示され、その横に検証メッセージが表示されます。必要に応じて、リソースを使用するように ValidationSummary のスタイルをカスタマイズすることも可能です。


図 10 ValidationSummary

影響を与えるもの

DataForm は、DataAnnotations からだけでなく、バインド先のデータ ソースが実装するインターフェイスからもヒントを得ます。たとえば、Employee エンティティは、IEditableObject インターフェイスを実装します。このインターフェイスを実装する側は、DataForm の編集中の適切なタイミングで DataForm によって呼び出される BeginEdit メソッド、CancelEdit メソッド、および EndEdit メソッドを実装する必要があります。サンプル アプリケーションでは、これらのメソッドを使用してキャンセル動作を実装し、ユーザーが [Cancel] ボタンをクリックしてエンティティの値を元に戻せるようにしました。

この動作は、BeginEdit メソッド内でエンティティのインスタンス (名前は cache) を作成し、エンティティから元の値をコピーすることによって実現されています。EndEdit メソッドではキャッシュ オブジェクトをクリアしますが、CancelEdit メソッドでは、キャッシュ エンティティから元のエンティティにすべての値をコピーすることにより、エンティティを元の状態に戻します。

DataForm のバインド先のコレクションが CollectionViewSource の場合、DataForm は、編集、並べ替え、フィルター処理、および現在のレコードの追跡 ("現在位置" とも呼ばれます) をサポートします。現在位置機能は重要です。この機能により、ユーザーは、DataForm 上で、状態を修正してコミットするか変更をキャンセルしない限り、無効な状態のエンティティから別のエンティティに移動できないようにもなるためです。DataForm を PagedCollectionView にバインドした場合も、CollectionViewSource の場合と同じ一連の機能が提供され、さらに、項目の削除、追加、およびグループ化の機能も提供されます。ObservableCollection にバインドすると、こうした機能の一部が無効になります。ObservableCollection 自体は、DataForm が求めているインターフェイスを実装しないためです。

目的のエンティティが既に ObservableCollection に含まれている場合は、以下に示すように、PagedCollectionView のコンストラクターを通じて ObservableCollection インスタンスをコレクションに渡すことによって PagedCollectionView を作成することができます。

var employees = new DataService().GetPeople();
PagedCollectionView view = new PagedCollectionView(employees);
this.DataContext = view;

また、以下に示すように、従業員の ObservableCollection を CollectionViewSource のコレクション初期化子に渡し、Source プロパティを設定することによって、CollectionViewSource を作成することができます。

var employees = new DataService().GetPeople();
CollectionViewSource view = new CollectionViewSource
{Source = employees};
this.DataContext = view;

まとめ

DataForm は、アプリケーション、および一般に大量のコードを必要とする、データ間の移動、データの編集、およびデータの表示という単調な処理に、多くの付加価値を与えます。DataForm の機能はニーズに合わせてカスタマイズすることができ、アプリケーションのデザインと調和するように DataForm の視覚的なスタイルを設定することができます。

John Papa (johnpapa.net) は、上級コンサルタントです。野球ファンで、夏の夜を家族と共にヤンキースの応援に費やします。Silverlight の MVP、Silverlight の事情通、そして INETA の講演者でもある Papa は、何冊かの書籍を発表しており、最新の著書は『Data-Driven Services with Silverlight 2』(O'Reilly、2009 年) です。また、カンファレンス (VSLive!、DevConnections、MIX など) での講演もよく行っています。