Share via


入力の検証

WPF で複雑なビジネス データの規則を適用する

Brian Noyes

コード サンプルのダウンロード

Microsoft Windows Presentation Foundation (WPF) には、機能豊富なデータ バインド システムが備わっています。このデータ バインド システムは、モデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) パターンを利用して、サポートロジックの UI 定義とデータとを疎結合できるようにする重要な要素となるだけでなく、ビジネスでのデータ検証のシナリオを強力かつ柔軟にサポートします。WPF のデータ バインド メカニズムには、編集可能なビューの作成時に入力データが有効かどうか評価する複数のオプションがあります。また、コントロールのテンプレートとスタイルを設定する WPF の機能を使用して、検証エラーをユーザーに通知する方法を簡単にカスタマイズできます。

複雑なルールをサポートして検証エラーをユーザーに表示するためには、通常、使用可能な複数の検証メカニズムを組み合わせて使用する必要があります。ビジネス ルールが複雑になると、一見単純そうに見えるデータ入力フォームでさえ、検証が難しくなることがあります。一般的なシナリオでは、個別のプロパティ レベルでの簡単なルールだけでなく、あるプロパティの有効性が別のプロパティ値に依存するような相互に依存するプロパティも関係します。しかし、WPF のデータ バインドの検証サポートを利用すれば、このような課題に簡単に対処できます。

今回の記事では、IDataErrorInfo インターフェイスの実装、ValidationRule、BindingGroup、例外、および検証関連の添付プロパティや添付イベントを使用して、データ検証のニーズに対処する方法について説明します。また、独自の ErrorTemplate とツールヒントを使用して検証エラーの表示をカスタマイズする方法についても説明します。この記事は、既に WPF の基本的なデータ バインド機能の知識がある方を対象としています。この機能の背景知識については、John Papa による 2007 年 12 月号の MSDN Magazine の記事「WPF でのデータ バインド」を参照してください。

データ検証の概要

アプリケーションでデータを入力または変更する場合はほぼ必ず、変更元 (この場合はユーザー) の手元からデータが離れる前に、入力したデータが有効であることを確認する必要があります。また、ユーザーが入力したデータが無効であれば、わかりやすくエラーをユーザーに通知し、できればその修正方法を示すことも必要です。どの機能をいつ使用するかを把握していれば、WPF ではこのような処理を非常に簡単に実行できます。

WPF のデータ バインドを使用してビジネス データを表示するときは、通常、Binding オブジェクトを使用して、ターゲット コントロールの 1 つのプロパティとデータ ソース オブジェクトのプロパティの間にデータのパイプラインを確立します。検証を関連付ける場合は、通常、TwoWay データ バインドを実行します。つまり、データがソース プロパティからターゲット プロパティに送信されて表示されるだけでなく、編集済みのデータがターゲット プロパティからソース プロパティに送信されます (図 1 参照)。

Figure 1 Data Flow in TwoWay Data Binding
図 1 TwoWay データ バインドのデータ フロー

データ バインド コントロールを使用して入力したデータが有効かどうかを判断するメカニズムは 3 つあります。図 2 に、これらのメカニズムの概要を示します。

図 2 バインドの検証メカニズム

検証メカニズム 説明
例外 Binding オブジェクトの ValidatesOnExceptions プロパティを設定しておくと、変更済みの値をソース オブジェクトのプロパティに設定するときに例外が発生すると、その Binding オブジェクトに検証エラーが設定されます。
ValidationRule Binding クラスには、ValidationRule から派生したクラス インスタンスのコレクションを提供するプロパティがあります。このコレクションに含まれる ValidationRule では、バインドされたコントロール内のデータが変更されるたびに Binding から呼び出される Validate メソッドをオーバーライドする必要があります。Validate メソッドから無効な ValidationResult オブジェクトが返されると、その Binding オブジェクトに検証エラーが設定されます。
IDataErrorInfo バインドされるデータ ソース オブジェクトに IDataErrorInfo インターフェイスを実装し、Binding オブジェクトの ValidatesOnDataErrors プロパティを設定すると、バインドされたデータ ソース オブジェクトから公開される IDataErrorInfo API が Binding によって呼び出されます。このようなプロパティ呼び出しから null でも空でもない文字列が返されると、その Binding オブジェクトに検証エラーが設定されます。

TwoWay データ バインドでユーザーがデータを入力または変更すると、次のワークフローが開始されます。

  • ユーザーがキーストローク、マウス、タッチ、またはペンで要素を操作してデータを入力または編集した結果として、その要素のプロパティが変更されます。
  • 必要に応じて、データがデータ ソース プロパティの型に変換されます。
  • ソース プロパティの値が設定されます。
  • Binding.SourceUpdated 添付イベントが発生します。
  • データ ソース プロパティのセッターから例外がスローされると、Binding でその例外がキャッチされます。この例外を使用して、検証エラーを通知できます。
  • IDataErrorInfo プロパティが実装されていれば、データ ソース オブジェクトの IDataErrorInfo のプロパティが呼び出されます。
  • 検証エラーの通知がユーザーに表示され、Validation.Error 添付イベントが発生します。

ご覧のとおり、採用するメカニズムに応じて、複数の処理段階で検証エラーが発生する可能性があります。このワークフローに含まれていないのは、ValidationRule が起動される処理段階です。これは、ValidationRule の ValidationStep プロパティに設定した値に応じて、さまざまな処理段階で ValidationRule が起動されるためです。たとえば、型の変換前、変換後、プロパティの更新後、変更済みの値がコミットされたとき (データ オブジェクトで IEditableObject を実装している場合) などに起動されます。既定値は、型の変換前に起動される RawProposedValue です。データがターゲット コントロールのプロパティの型からデータ ソース オブジェクトのプロパティの型に変換される処理段階は、通常、コード (TextBox の数値入力コードなど) が実行されることなく暗黙のうちに実行されます。この型変換処理から例外がスローされることがあり、この例外を使用して検証エラーをユーザーに通知します。

ソース オブジェクトのプロパティに値を書き込むことさえできない場合は、明らかに無効な入力です。ValidationRule をフックすると、ValidationStep プロパティで指定した処理段階で ValidationRule が呼び出され、その ValidationRule に埋め込まれたりそこから呼び出されたりしている任意のロジックに基づいて検証エラーを返すことができます。ソース オブジェクトのプロパティ セッターから例外がスローされると、型変換の場合と同様に、ほぼ確実に検証エラーとして処理されます。

最後に、IDataErrorInfo を実装すると、IDataErrorInfo から返される文字列に基づいて検証エラーの有無を確認するよう設定したプロパティに対して、IDataErrorInfo のデータ ソース オブジェクトに追加したインデクサー プロパティが呼び出されます。これらの各メカニズムについては、この後詳しく説明します。

検証するタイミングについても決定しておきます。検証は、基になるソース オブジェクトのプロパティに Binding からデータが書き込まれるときに行われます。検証するタイミングは、Binding の UpdateSourceTrigger プロパティで指定します。ほとんどのソース オブジェクトのプロパティの場合、UpdateSourceTrigger プロパティの値は PropertyChanged に設定されています。TextBox.Text など一部のプロパティの場合は、UpdateSourceTrigger プロパティの値を LostFocus に変更します。このように変更することで、データ編集に使用されているコントロールからフォーカスが離れたときに検証が行われるようにします。UpdateSourceTrigger プロパティの値を Explicit に設定して、バインドで明示的に検証を呼び出す必要があることを指定できます。後半で説明する BindingGroup では、Explicit モードを使用しています。

検証シナリオ (特に、TextBox を使用する検証シナリオ) では、通常、ユーザーに即時フィードバックを返すことをお勧めします。即時フィードバックをサポートするには、次のように Binding の UpdateSourceTrigger プロパティの値を PropertyChanged に設定します。

Text="{Binding Path=Activity.Description, UpdateSourceTrigger=PropertyChanged}

実際の多くの検証シナリオでは、これらのメカニズムのうち複数を利用する必要に迫られるでしょう。注目している検証エラーの種類や検証ロジックの配置場所に応じて、どのメカニズムにも長所と短所があります。

ビジネスの検証シナリオ

より具体的にメカニズムを把握するために、ある程度実在のビジネス コンテキストに基づいた編集シナリオについて順を追って説明し、各メカニズムが役に立つしくみを説明しましょう。このシナリオと検証規則は、ある顧客向けに作成した実際のアプリケーションに基づいています。このアプリケーションでは、検証の対象となるビジネス ルールによって、非常に簡単なフォームであっても、ほぼすべての検証メカニズムを使用する必要がありました。ここで使用するさらに簡単なアプリケーションでは、すべてのメカニズムが明らかに必要というわけではありませんが、すべてのメカニズムの使用方法を例を挙げて説明します。

在宅顧客のサポート問い合わせに対応する現場技術者を支援するアプリケーションを作成する必要があるとします (ケーブル会社の従業員を想像してください。ただし、追加機能やサービスも売り込もうとしている従業員です)。技術者は、現場で行う作業ごとに、作業内容を記録して、いくつかのデータに関連付けた作業レポートを入力する必要があります。図 3 に、このオブジェクト モデルを示します。

Figure 3 Object Model for the Sample Application
図 3 サンプル アプリケーションのオブジェクト モデル

ユーザーが入力する主なデータは、Activity (作業) オブジェクトです。このオブジェクトには、Title (タイトル)、ActivityDate (作業日)、ActivityType (ドロップダウン リストで選択する定義済みの作業の種類)、および Description (説明) が含まれています。また、行った作業を 3 つの選択肢の 1 つに関連付ける必要もあります。そのためには、作業を行った Customer (顧客) をユーザーに割り当てられた顧客の一覧から選択するか、または作業に関連する企業の Objective (目的) を企業の目的の一覧から選択する必要があります。あるいは、その作業に該当する Customer も Objective も存在しなければ、Reason (理由) を手作業で入力することもできます。

アプリケーションで適用する必要がある検証規則は、次のとおりです。

  • Title と Description は必須フィールドです。
  • ActivityDate は、現在日付の 7 日前から 7 日後までの間の日付である必要があります。
  • ActivityType で Install (設置) を選択する場合、Inventory (在庫状況) フィールドが必須になり、技術者のトラックから降ろして使用した機材の数をこのフィールドに示す必要があります。在庫状況の項目は、入力項目の予期されたモデル番号形式の、コンマで区切られたリストとして入力する必要があります。
  • Customer、Objective、または Reason のうち、少なくとも 1 つを指定する必要があります。

これらの規則はかなり単純な要件のように思えますが、特に最後の 2 つの規則は、プロパティが相互に依存するため対処はそれほど簡単ではありません。図 4 に、無効データ (赤いボックスの箇所) が含む実行中のアプリケーションを示します。

Figure 4 A Dialog Showing ToolTips and Invalid Data
図 4 ツールヒントと無効データが表示されたダイアログ ボックス

例外による検証

最も単純な形式の検証は、ターゲット プロパティの設定処理中に例外が発生したら、それを検証エラーとして処理することです。例外は、Binding でターゲット プロパティが設定される前の型変換処理で発生することも、プロパティ セッターから例外が明示的にスローされて発生することも、セッターからビジネス オブジェクトが呼び出された結果として、スタックのさらに深い階層で例外がスローされて発生することもあります。

この例外メカニズムを使用するには、次のように Binding オブジェクトの ValidatesOnExceptions プロパティを true に設定するだけです。

Text="{Binding Path=Activity.Title, ValidatesOnExceptions=True}"

ソース オブジェクトのプロパティ (この場合は Activity.Title) を設定しようとしているときに例外がスローされると、このコントロールに検証エラーが設定されます。既定の検証エラー通知は、コントロールを囲む赤い境界線です (図 5 参照)。

Figure 5 A Validation Error
図 5 検証エラー

例外は型変換処理で発生することがあるため、バッキング プロパティでメンバー変数値を設定するだけで、例外が発生する可能性がなくても、型変換が失敗する可能性があるときは、必ず入力用の Binding のこのプロパティを設定することをお勧めします。

たとえば、TextBox を DateTime プロパティの入力コントロールとして使用するとします。変換できない文字列をユーザーが入力すると、ソース オブジェクトのプロパティが呼び出されないため、ValidatesOnExceptions プロパティ以外に Binding からエラーを通知できる手段はありません。

無効な値が存在していたらコマンドを無効にするなど、特定の処理を実行する必要がある場合は、コントロールの Validation.Error 添付イベントをフックできます。また、次のように Binding の NotifyOnValidationError プロパティの値を true に設定することも必要です。

<TextBox Name="ageTextBox" 
  Text ="{Binding Path=Age, 
    ValidatesOnExceptions=True, 
    NotifyOnValidationError=True}" 
    Validation.Error="OnValidationError".../>

ValidationRule による検証

シナリオによっては、UI レベルで検証を関連付ける場合や、入力が有効かどうか判断するためのさらに複雑なロジックが必要になる場合もあります。サンプル アプリケーションの Inventory フィールドの検証規則について考えてみましょう。データを入力する場合、そのデータは、特定のパターンに従ったモデル番号をコンマで区切ったリストになっている必要があります。ValidationRule を使用すると、設定する値に応じて全面的に動作を変更できるので、簡単にこのような検証に対処できます。ValidationRule では、string.Split の呼び出しを使用して入力を文字列の配列に変換してから、正規表現を使用して、各部分が特定のパターンに従っているかどうか確認できます。これを行うには、図 6 のように ValidationRule を定義します。

図 6 文字列の配列を検証する ValidationRule

public class InventoryValidationRule : ValidationRule {

  public override ValidationResult Validate(
    object value, CultureInfo cultureInfo) {

    if (InventoryPattern == null)
      return ValidationResult.ValidResult;

    if (!(value is string))
      return new ValidationResult(false, 
     "Inventory should be a comma separated list of model numbers as a string");

    string[] pieces = value.ToString().Split(‘,’);
    Regex m_RegEx = new Regex(InventoryPattern);

    foreach (string item in pieces) {
      Match match = m_RegEx.Match(item);
      if (match == null || match == Match.Empty)
        return new ValidationResult(
          false, "Invalid input format");
    }

    return ValidationResult.ValidResult;
  }

  public string InventoryPattern { get; set; }
}

ValidationRule で公開されているプロパティは、使用時に XAML で設定できるので、多少柔軟に操作できます。この例の検証規則では、文字列の配列に変換できない値は無視します。しかし、規則で string.Split を実行できる場合は、RegEx を使用して、コンマで区切られたリストの各文字列が InventoryPattern プロパティで設定されたパターンに従っているかどうか検証します。

有効性に関するフラグに false を設定した ValidationResult を返すと、後で説明するように、指定したエラー メッセージ使用して UI 経由でユーザーにエラーを表示できます。ValidationRule の短所の 1 つは、次のコードに示すように、ValidationRule をフックするため XAMLで Binding 要素を拡張する必要があることです。

<TextBox Name="inventoryTextBox"...>
  <TextBox.Text>
    <Binding Path="Activity.Inventory" 
             ValidatesOnExceptions="True" 
             UpdateSourceTrigger="PropertyChanged" 
             ValidatesOnDataErrors="True">
      <Binding.ValidationRules>
        <local:InventoryValidationRule 
          InventoryPattern="^\D?(\d{3})\D?\D?(\d{3})\D?(\d{4})$"/>
      </Binding.ValidationRules>
    </Binding>
  </TextBox.Text>
</TextBox>

この例でも、ValidatesOnExceptions プロパティを true に設定しているため、例外発生時に Binding で検証エラーが発生します。また、ValidatesOnDataErrors プロパティを true に設定していることから IDataErrorInfo による検証もサポートしています (これについてはこの次に説明します)。

複数の ValidationRule を同じプロパティにアタッチしている場合は、これらの規則にそれぞれ異なる ValidationStep プロパティ値を設定することも、同じ値を設定することもできます。同じ ValidationStep に含まれる規則は、宣言順に評価されます。当然、早いタイミングの ValidationStep の規則は遅いタイミングの ValidationStep の規則よりも先に実行されます。当然と思えないこともある点は、ValidationRule からエラーが返されるとそれ以降の規則が評価されないことです。つまり、ValidationRule でエラーが発生すると、最初の検証エラーだけが通知されます。

IDataErrorInfo による検証

IDataErrorInfo インターフェイスには、次のような 1 つのプロパティと 1 つのインデクサーを公開する実装が必要です。

public interface IDataErrorInfo {
  string Error { get; }
  string this[string propertyName] { get; }
}

Error プロパティは、オブジェクト全体のエラーを通知するために使用し、インデクサーは、個々のプロパティ レベルでエラーを通知するために使用します。どちらも動作は同じで、null でも空でもない文字列が返されると検証エラーが通知されます。また、後で説明するように、返される文字列を使用してユーザーにエラーを表示できます。

データ ソース オブジェクトの個別のプロパティにバインドされた個別のコントロールを操作するときに、インターフェイスで最も重要なのはインデクサーです。Error プロパティを使用するのは、オブジェクトが DataGrid や BindingGroup に表示されるようなシナリオのみです。Error プロパティは行レベルのエラーを通知するために使用しますが、インデクサーはセル レベルのエラーを通知するために使用します。

IDataErrorInfo の実装には、大きな短所が 1 つあります。通常、インデクサーを実装すると、オブジェクトのプロパティ名ごとに case ステートメントを 1 つ使用する大規模な switch-case ステートメントを作成することになるうえ、文字列に基づいて場合分けして条件を照合し、エラーを通知する文字列を返す必要があります。また、IDataErrorInfo の実装は、プロパティが既にオブジェクトに設定されてから呼び出されます。オブジェクトの INotifyPropertyChanged.PropertyChanged が他のオブジェクトからサブスクライブされている場合、既にそのようなオブジェクトに変更が通知されているため、IDataErrorInfo の実装でデータが無効だと宣言しようとしているデータに基づいた処理が開始されていることがあります。このような動作がアプリケーションで望ましくない場合は、設定される値が不適切なときはプロパティ セッターで例外をスローする必要があります。

IDataErrorInfo の長所は、相互依存するプロパティに対処しやすいことです。たとえば、ValidationRule を使用して Inventory フィールドの入力形式を検証することに加えて、ActivityType の値が Install の場合は Inventory フィールドにも入力しなければならないという要件もあったことを思い出してください。ValidationRule 自体では、データ バインド オブジェクトの他のプロパティにアクセスできません。Binding のフック先プロパティに設定された値を受け取るだけです。この要件に対処するには、ActivityType プロパティが設定されたら Inventory プロパティで検証を実行し、ActivityType が Install に設定されている場合に Inventory の値が空のときは無効という結果を返す必要があります。

これを実現するには、次のように Inventory の評価時に Inventory プロパティと ActivityType プロパティの両方を調査できるよう、IDataErrorInfo が必要です。

public string this[string propertyName] {
  get { return IsValid(propertyName); }
}

private string IsValid(string propertyName) {
  switch (propertyName) {
    ...
    case "Inventory":
      if (ActivityType != null && 
        ActivityType.Name == "Install" &&  
        string.IsNullOrWhiteSpace(Inventory))
        return "Inventory expended must be entered for installs";
      break;
}

また、ActivityType プロパティが変更されたら Inventory の Binding で検証を呼び出す必要もあります。通常、UI でこのプロパティが変更された場合、Binding では IDataErrorInfo の実装がクエリされるか ValidationRules プロパティが呼び出されるだけです。ここでは、Inventory プロパティが変更されていなくても、関連している ActivityType プロパティが変更されていれば Binding の検証の再評価をトリガーします。

ActivityType プロパティが変更されたときに Inventory の Binding を更新する方法は 2 つあります。1 つ目の簡単な方法は、次のように、ActivityType を設定するときに Inventory の PropertyChanged イベントを発行することです。

ActivityType _ActivityType;
public ActivityType ActivityType {
  get { return _ActivityType; }
  set { 
    if (value != _ActivityType) {
      _ActivityType = value;
      PropertyChanged(this, 
        new PropertyChangedEventArgs("ActivityType"));
      PropertyChanged(this, 
        new PropertyChangedEventArgs("Inventory"));
    }
  }
}

このようにすると、Binding が更新され、その Binding の検証が再評価されます。

2 つ目の方法は、次のように、ActivityType に関する ComboBox 要素かそのいずれかの親要素の Binding.SourceUpdated 添付イベントをフックし、分離コードに存在するそのイベントのハンドラーで Binding の更新をトリガーすることです。

<ComboBox Name="activityTypeIdComboBox" 
  Binding.SourceUpdated="OnPropertySet"...

private void OnPropetySet(object sender, 
  DataTransferEventArgs e) {

  if (activityTypeIdComboBox == e.TargetObject) {
    inventoryTextBox.GetBindingExpression(
      TextBox.TextProperty).UpdateSource();
  }
}

Binding の UpdateSource をプログラムから呼び出すと、バインドされているターゲット要素の現在値がソース プロパティに書き込まれ、ユーザーがコントロールを編集し終えた場合と同様に一連の検証がトリガーされます。

相互依存するプロパティでの BindingGroup の使用

BindingGroup の機能は、Microsoft .NET Framework 3.5 SP1 で追加されました。BindingGroup は、1 つのグループのバインドの検証すべてを一度に評価できるよう特別に設計されています。たとえば、ユーザーがフォーム全体に入力できるようにし、フォームの検証規則を評価するためにユーザーが送信ボタンや保存ボタンをクリックするまで待機してから、検証エラーを一度にすべて表示できます。サンプル アプリケーションには、Customer、Objective、または Reason のうち、少なくとも 1 つは指定しなければならないという要件がありました。BindingGroup を使用すると、このようなフォームのサブセットも評価できます。

BindingGroup を使用するには、親要素が共通し、通常の Binding が含まれている一連のコントロールが必要です。サンプル アプリケーションでは、Customer の ComboBox、Objective の ComboBox、および Reason の TextBox がすべて同じ Grid に配置されてレイアウトされています。BindingGroup は、FrameworkElement のプロパティの 1 つです。BindingGroup には、1 つ以上の ValidationRule オブジェクトを設定できる ValidationRules コレクション プロパティがあります。次の XAML は、サンプル アプリケーションの BindingGroup をフックしているところを示しています。

<Grid>...
<Grid.BindingGroup>
  <BindingGroup>
    <BindingGroup.ValidationRules>
      <local:CustomerObjectiveOrReasonValidationRule 
        ValidationStep="UpdatedValue" 
        ValidatesOnTargetUpdated="True"/>
    </BindingGroup.ValidationRules>
  </BindingGroup>
</Grid.BindingGroup>
</Grid>

この例では、CustomerObjectiveOrReasonValidationRule のインスタンスをコレクションに追加しました。ValidationStep プロパティを使用すると、規則に渡される値をある程度制御できます。UpdatedValue は、データ ソースに書き込まれた値を書き込み後に使用するよう指定する値です。また、ValidationStep の値には、ユーザーによる入力を変換前に使用できる値、型と値の変換が適用された後の値、または "コミット" された値 (つまり、オブジェクトのプロパティをトランザクションで変更する IEditableObject の実装を示す値) も指定できます。

ValidatesOnTargetUpdated フラグを有効にすると、Binding を通じてターゲット プロパティを設定するたびに規則が評価されます。ターゲット プロパティを最初に設定するときも規則が評価されるので、BindingGroup に含まれるコントロールの値をユーザーが変更するたびに検証エラーが通知されるだけでなく、初期データが無効な場合もすぐに通知されます。

BindingGroup にフックされた ValidationRule の動作は、1 つの Binding にフックされた ValidationRule の動作と少し異なります。図 7 に、前のコード サンプルに示した BindingGroup にフックされた、ValidationRule を示します。

図 7 BindingGroup の ValidationRule

public class CustomerObjectiveOrReasonValidationRule : 
  ValidationRule {

  public override ValidationResult Validate(
    object value, CultureInfo cultureInfo) {

    BindingGroup bindingGroup = value as BindingGroup;
    if (bindingGroup == null) 
      return new ValidationResult(false, 
        "CustomerObjectiveOrReasonValidationRule should only be used with a BindingGroup");

    if (bindingGroup.Items.Count == 1) {
      object item = bindingGroup.Items[0];
      ActivityEditorViewModel viewModel = 
        item as ActivityEditorViewModel;
      if (viewModel != null && viewModel.Activity != null && 
        !viewModel.Activity.CustomerObjectiveOrReasonEntered())
        return new ValidationResult(false, 
          "You must enter one of Customer, Objective, or Reason to a valid entry");
    }
    return ValidationResult.ValidResult;
  }
}

1 つの Binding にフックされた ValidationRule の場合、渡される値は、Binding の Path として設定されたデータ ソース プロパティの 1 つの値です。BindingGroup の場合、ValidationRule に渡される値は、BindingGroup 自体です。BindingGroup には、その BindingGroup を含む要素 (この場合は Grid) の DataContext が設定された Items コレクションが含まれます。

サンプル アプリケーションでは、MVVM パターンを使用しているため、ビューの DataContext はビューモデル自体です。Items コレクションには、ビューモデルへの参照が 1 つだけ含まれています。ビューモデルを使用すると、ビューモデルの Activity プロパティを参照できます。この場合の Activity クラスには、Customer、Objective、または Reason が少なくとも 1 つ入力されているかどうかを判断する検証メソッドが含まれているため、ValidationRule で同じロジックを繰り返す必要はありません。

既に説明した他の ValidationRule と同様に、渡されたデータの値に問題がない場合は、ValidationResult.ValidResult を返します。問題がある場合は、新しい ValidationResult を作成して、有効性に関するフラグの値に false を設定し、問題を通知する文字列メッセージ (表示目的で使用可能) を指定します。

ただし、ValidatesOnTargetUpdated フラグを設定するだけでは、ValidationRule は自動的に実行されません。BindingGroup は、グループ全体のコントロールに対する検証を明示的に (通常はフォームの送信ボタンや保存ボタンをクリックするなどで) トリガーするという考え方に基づいて設計されました。シナリオによっては、入力作業が完了したと見なされるまで検証エラーが通知されないことをユーザーが希望するため、このような手法を念頭に BindingGroup は設計されました。

サンプル アプリケーションでは、ユーザーがフォームのデータを変更するたびに、ただちにユーザーに検証エラーのフィードバックを返すことにします。このような処理を BindingGroup で実行するには、グループに含まれる個々の入力コントロールの適切な変更イベントをフックし、フックしたイベントのイベント ハンドラーで BindingGroup の評価をトリガーする必要があります。具体的には、サンプル アプリケーションの場合、2 つの ComboBox の ComboBox.SelectionChanged イベントをフックし、TextBox の TextBox.TextChanged イベントをフックします。これらすべてのイベントでは、次のような分離コードに配置されている 1 つの処理メソッドを指定できます。

private void OnCommitBindingGroup(
  object sender, EventArgs e) {

  CrossCoupledPropsGrid.BindingGroup.CommitEdit();
}

検証結果を表示する際は、BindingGroup が配置されている FrameworkElement (サンプル アプリケーションの Grid など) に、既定の赤い境界線が表示されることに注意してください (図 4 参照)。Validation.ValidationAdornerSite 添付プロパティと Validation.ValidationAdornerSiteFor 添付プロパティを使用すると、検証の通知を表示する場所を変更することもできます。既定では、各コントロールにも、そのコントロールごとの検証エラーを通知する赤い境界線が表示されます。サンプル アプリケーションでは、Style を使用して ErrorTemplate の値を null に設定し、このような境界線を無効にしています。

.NET Framework 3.5 SP1 の BindingGroup の場合、ValidationRule の ValidatesOnTargetUpdated プロパティを設定していても、フォームを最初に読み込んだときに検証エラーが正しく表示されない問題が発生することがあります。私が見つけたこの問題の回避策は、BindingGroup のバインドされたプロパティの 1 つを微調整することでした。サンプル アプリケーションの場合、次のように、ビューの Loaded イベントで TextBox に最初に表示される任意のテキストの末尾に、スペースを追加または削除できます。

string originalText = m_ProductTextBox.Text;
m_ProductTextBox.Text += " ";
m_ProductTextBox.Text = originalText;

このようにすると、含まれている Binding のプロパティの 1 つが変更されると BindingGroup の ValidationRules が実行され、その結果、各 Binding の検証が呼び出されます。この動作は .NET Framework 4.0 で修正されたため、最初に検証エラーを表示するための回避策は必要ありません。検証規則の ValidatesOnTargetUpdated プロパティを true に設定するだけです。

検証エラーの表示

既に説明したように、WPF で検証エラーを表示する既定の方法は、コントロールを囲む赤い境界線を描画することです。多くの場合は、この表示方法をカスタマイズして別の方法でエラーを表示することを考えるでしょう。しかも、既定では、検証エラーに関連付けられたエラー メッセージは表示されません。一般的な要件は、検証エラーが存在する場合にだけツールヒントにエラー メッセージを表示することです。Style と、検証に関連付けられた一連の添付プロパティを組み合わせて使用すれば、検証エラーの表示を非常に簡単にカスタマイズできます。

エラー テキストを表示するツールヒントは、簡単に追加できます。入力コントロールに適用する Style を定義し、検証エラーが存在するときは必ずこのコントロールの ToolTip プロパティの値を検証エラー テキストに設定するだけです。このような機能をサポートするには、Validation.HasError と Validation.Errors という 2 つの添付プロパティを使用する必要があります。以下に、ToolTip プロパティを設定する TextBox 型をターゲットとする Style を示します。

<Style TargetType="TextBox">
  <Style.Triggers>
    <Trigger Property="Validation.HasError" 
             Value="True">
      <Setter Property="ToolTip">
        <Setter.Value>
          <Binding 
            Path="(Validation.Errors).CurrentItem.ErrorContent"
            RelativeSource="{x:Static RelativeSource.Self}" />
        </Setter.Value>
      </Setter>
    </Trigger>
  </Style.Triggers>
</Style>

ご覧のとおり、Style には、Validation.HasError 添付プロパティのプロパティ トリガーが 1 つ含まれているだけです。Binding のソース オブジェクトのプロパティを更新する際に検証メカニズムでエラーが発生すると、HasError プロパティが true に設定されます。このエラーは、例外、ValidationRule、または IDataErrorInfo の呼び出しによって発生することがあります。続いて、Style で Validation.Errors 添付プロパティを使用します。検証エラーが存在する場合、このプロパティにはエラー文字列のコレクションが含まれています。このコレクション型の CurrentItem プロパティを使用すると、コレクションの最初の文字列だけを取得できます。また、コレクションにデータ バインドしてリスト形式のコントロールの項目ごとに ErrorContent プロパティを表示する処理を設計することもできます。

コントロールでの既定の検証エラーの表示を赤い境界線以外の図形に変更するには、カスタマイズするコントロールの新しいテンプレートに Validation.ErrorTemplate 添付プロパティを設定する必要があります。サンプル アプリケーションでは、赤い境界線の代わりに、赤でグラデーションされた小さな円が、エラーが発生している各コントロールの右側に表示されます。このように変更するには、ErrorTemplate として使用する次のようなコントロール テンプレートを定義します。

<ControlTemplate x:Key="InputErrorTemplate">
  <DockPanel>
    <Ellipse DockPanel.Dock="Right" Margin="2,0" 
             ToolTip="Contains invalid data"
             Width="10" Height="10">
      <Ellipse.Fill>
        <LinearGradientBrush>
          <GradientStop Color="#11FF1111" Offset="0" />
          <GradientStop Color="#FFFF0000" Offset="1" />
        </LinearGradientBrush>
      </Ellipse.Fill>
    </Ellipse>
    <AdornedElementPlaceholder />
  </DockPanel>
</ControlTemplate>

このコントロール テンプレートをコントロールにフックするには、そのコントロールの Validation.ErrorTemplate プロパティを設定するだけです。このプロパティも、Style を使用して次のように設定できます。

<Style TargetType="TextBox">
  <Setter Property="Validation.ErrorTemplate" 
    Value="{StaticResource InputErrorTemplate}" />
  ...
</Style>

まとめ

ここでは、WPF のデータ バインドの 3 つの検証メカニズムを使用して、多数のビジネス データの検証シナリオに対処する方法を紹介しました。具体的には、例外、ValidationRule、または IDataErrorInfo インターフェイスを使用して、1 つのプロパティの検証に対処する方法や、コントロールに含まれる他のプロパティの現在値に検証規則が依存しているプロパティの検証に対処する方法について説明しました。また、BindingGroup を使用して複数の Binding を同時に評価する方法や、エラーの表示をカスタマイズして WPF の既定の表示を変更する方法についても説明しました。

サンプル アプリケーションには、ここで説明したビジネス ルールを満たす完全な検証が備わっています。このサンプル アプリケーションでは、MVVM を使用して、サポート データにビューをフックしています。

Brian Noyes は、IDesign (idesign.net、英語) のチーフ アーキテクトと Microsoft Regional Director を兼任し、Microsoft MVP でもあります。Noyes は書籍を執筆し、Microsoft Tech·Ed、DevConnections、DevTeach など世界中のカンファレンスで頻繁に講演しています。また、briannoyes.net (英語) にブログを公開しています。

この記事のレビューに協力してくれた技術スタッフの Sam Bent に心より感謝いたします。