WPF ViewModelでLOB開発 Vol 03 - バインディング

- - posted in wpf | Comments

これまでは必要最低限の予備学習でしたが、本質に切り込んで バインディング について紹介します。
WPFの肝要は バインディングとテンプレートだと言えるくらい重要です。

今回はDateTimeを表示するだけの簡単なサンプルですが、WPF プロパティ システム の仕組みを覗いてみたいと思います。

wpf-03-07

前回までと同じですが、今度はViewModelのプロパティがDateTime型となっており、バインディング時に一つだけ表示書式が適用されています。

ViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[VisualElements(typeof(BizViewModelName1View))]
public class BizViewModelName1ViewModel : BizViewModel
{
    public DateTime? SampleDate
    {
        get { return _sampleDate; }
        set
        {
            SetProperty(ref _sampleDate, value);
        }
    }
    private DateTime? _sampleDate = DateTime.Now;

    public void SampleAction()
    {
        MessageBox.Show(SampleDate.GetValueOrDefault().ToString("yyyy年MM月dd日"));
    }
}

Xaml

1
<TextBox Name="SampleTextBox" Text="{Binding Path=SampleDate, StringFormat=yyyy年MM月dd日}"/>

TextBox.Text プロパティにBindingクラスの指定と、Path プロパティおよびStringFormat プロパティを指定しています。 C#で記述するとしてもほぼ同じことを記述します。

1
BindingOperations.SetBinding(SampleTextBox, TextBox.TextProperty, new Binding("SampleDate") { StringFormat = "yyyy年MM月dd日" });

WPF プロパティ システムを理解するということは、このBindingクラスの利用方法に他なりません。 どういうことか動かしながら確認したいと思います。

今まで通り、ボタンを押下するとViewModelのプロパティから値を取得してメッセージボックスを表示するアプリケーションです。

wpf-03-08

テキストボックスにキー入力で日付を変えボタンを押下すると、それがViewModelに自動で反映されている状態です。

wpf-03-09

現在、Binding クラスにはStringFormat以外何も指定していないにもかかわらず、いくつかの規定の動作が注入されています。
Binding クラスの機能を全て紹介するのは大変ですが、上記を通じて基本的なものを紹介します。

Mode / UpdateSourceTrigger

まず、現在の動作を確認するとTextBox.Text プロパティにキーボード入力で値を設定し、 ボタンを押下した(テキストボックスからフォーカスが外れた)タイミングでViewModelに画面上のデータが送信されています。

これはMode プロパティとUpdateSourceTrigger プロパティが関係しています。

TextBox.Text プロパティにバインディングした場合ですが、Mode のデフォルト値は BindingMode.TwoWay となっています。
テキストボックスなのでキーボード入力が前提で、ViewModelのデータも表示する必要があるため、お互いにデータを双方向でやり取りするモードとなっています。

これがラベルのようにキーボード入力がない場合は、デフォルトのモードは OneWay となります。 多くの提供されているWPFコンテンツは Default で適切ですが、これを変更することができるのがMode プロパティです。

先ほどの画面でMode プロパティに OneWay と設定した場合、以下のようにViewModelにはデータが反映されなくなります。

wpf-03-10

次に、ロストフォーカスでView上のデータがViewModelに自動転送されている設定ですが、MSDNに記述があります。

TextBox.Text プロパティの UpdateSourceTrigger の既定値は LostFocus です。

WPF プロパティ システム はこれらの設定が既定で適用されている結果、開発者が深く意識しなくても動くアプリケーションが構築できるようになっています。

テキストボックスがロストフォーカス時にViewModelに値を転送するのには理由があります。

TextBox.Text プロパティはキー入力およびIMEでの候補を選択中でも値が変わります。 仮に UpdateSourceTrigger を PropertyChanged に、つまりText プロパティが変わる(≒キータイピング)のたびに ViewModelへとデータ送信するようになると、想像通りアプリケーションは機能しなくなります。

ValidationRules

ところで、ViewModel上はDateTime型で値を保持しているので、先ほどのテキストボックスで日付に変換できないような値を入力した場合、どうなるでしょう? 試してみます。

wpf-03-11

ボタンを押下するためにフォーカスを外れると…

wpf-03-12

テキストボックスが赤枠表示され、デバックログにはエラーがあった旨が出力されています。

1
2
3
4
5
System.Windows.Data Error: 7 : ConvertBack cannot convert value '2014年10月99日' (type 'String'). BindingExpression:Path=SampleDate; DataItem='BizViewModelName1ViewModel' (HashCode=21817343); target element is 'TextBox' (Name='SampleTextBox'); target property is 'Text' (type 'String') FormatException:'System.FormatException: 文字列で表される DateTime がカレンダー System.Globalization.GregorianCalendar でサポートされていません。
   場所 System.DateTime.Parse(String s, IFormatProvider provider)
   場所 System.Convert.ToDateTime(String value, IFormatProvider provider)
   場所 System.Convert.ChangeType(Object value, Type conversionType, IFormatProvider provider)
   場所 System.Windows.Data.BindingExpression.ConvertBackHelper(IValueConverter converter, Object value, Type sourceType, Object parameter, CultureInfo culture)'

これはValidationRules プロパティに深く関係する、入力値の妥当性検証が規定で実行された結果になります。

この機能は奥深く、大きく三つのことを検討しなければなりません。

  • 検証ルールの定義
  • 検証結果の視覚的フィードバック
  • 検証を実行するタイミング

これについてもMSDNに丁寧な記載があります。

カスタム ErrorTemplate を提供しない場合、検証エラーがあった際にユーザーに視覚的にフィードバックするために、既定のエラー テンプレートが使用されることに注意してください。 詳細については、「データ バインドの概要」の「データの検証」を参照してください。 さらに WPF は、バインド ソース プロパティの更新中にスローされる例外をキャッチするための、組み込みの検証規則を提供します。 詳細については、「ExceptionValidationRule」を参照してください。

先ほど学習したように、TextBox.Text プロパティの規定のMode プロパティは TwoWay つまり双方向通信します。
さらにそのタイミングを決定するUpdateSourceTrigger プロパティは LostFocus のため、フォーカスが外れるタイミングでバインド ソース プロパティ、つまりViewModelのプロパティを更新します。 ただし、2014年10月99日はDateTime型に変換できずエラーが発生した結果、組み込みの検証規則ExceptionValidationRuleが実行、赤枠の規定エラーテンプレートが適用されました。

  • 検証ルール
    • バインド ソース プロパティの更新中にスローされる例外をキャッチするための、組み込みの検証規則
  • 結果の外観
    • 検証エラーがあった際にユーザーに視覚的にフィードバックするために、既定のエラー テンプレート
  • 検証を実行するタイミング
    • バインド ソース プロパティの更新(ViewのデータをViewModelへ送信しようとするタイミング)

これらが全て規定動作で動いた結果、現在の状態になっています。

では、Lob開発をするにあたっては、様々な入力検証(たとえば必須入力や型桁検証)を実行したい状況もあれば、その検証を適用するタイミングもロストフォーカスではなく、任意のタイミングで実行したいなどの要件も発生します。

これらの要件についても、今まで得たバインディングの知識を組み合わせることで、柔軟に対応できます。

UpdateSourceTrigger.Explicit という設定があります。 現在はロストフォーカス時にデータをViewModelへ転送する設定ですが、Explicit では開発者が明示的にデータ転送用のメソッドを呼び出すタイミングでそれが行われます。

では、さっそくコーディングしましょう!

1
<TextBox Text="{Binding Path=SampleDate, UpdateSourceTrigger=Explicit, StringFormat=yyyy年MM月dd日}" />

これだけで、ロストフォーカスでValidationRuleが適用されることはなくなりました。

wpf-03-13

基本ですが、バインディングの肝要です。

ルールを新しく追加することなどもっと簡単で、ValidationRule クラスを派生して値を検証するロジックを記述し、それを利用する開発者に提供するだけです。

では、必須入力ルールを作成してみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class RequiredRule : ValidationRule
{
    public object ErrorContent
    {
        get { return errorContent; }
        set { errorContent = value; }
    }
    private object errorContent = "Required fields.";

    public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
    {
        if (value == null)
            return new ValidationResult(false, ErrorContent);
        if (value is string && string.IsNullOrEmpty(value as string))
            return new ValidationResult(false, ErrorContent);
        return new ValidationResult(true, null);
    }
}

これを Binding.ValidationRules プロパティに Add する作業が必要ですが、それはどうとでもなります。
気にするとすれば、開発者が簡単かつ柔軟にValidationRule プロパティを拡張して適用できるように気を使うことです。

余談ですが、こういうライブラリを設計するアプローチもいろいろあり、楽しいものです。

『桁や必須かどうかというものは、データの属性である』というアプローチであったり、これを利用する開発イメージを処理と捉えてそこからスタートする方法もあります。

設計するアプローチは様々あり、それを選択していることを受け入れるいうことは重要です。
与太話でしたが、まとめるとこんなイメージで!

wpf-03-14

開発者は、バインドしているデータの書式および必須ルールを属性として利用できるようなイメージです。 このライブラリを開発する際に気にすることがあるとすれば、ルールは開発者が容易に拡張できるようなソリューションだと思います。

ところで、上述の RequiredRule ではデータ単体に着目したのですが、相関的な検証ルールはどうでしょうか。

ValidationRule クラスをMSDNで確認すると、すでにいくつかの派生クラスが規定で用意されています。

これらはValidationRules プロパティに追加せずとも、自動で有効になるようなオプションが Binding クラスに用意されています。

1
2
3
<TextBox Text="{Binding Path=SampleDate, ValidatesOnDataErrors=True}"/>
<TextBox Text="{Binding Path=SampleDate, ValidatesOnExceptions=True}"/>
<TextBox Text="{Binding Path=SampleDate, ValidatesOnNotifyDataErrors=True}"/>

多くは規定で適用されており、これらを明示的に意識する必要はありません。

また、NotifyDataErrorValidationRule クラスはDataErrorValidationRule クラスの上位機能になります。
(NotifyDataErrorValidationRule は .Net Framework4.5 で提供されました。)

話を戻すと、この用意されているルールを利用して相関的な検証ロジックを実装する方法について紹介します。

この機能はView(ターゲット)からViewModel(データソース)へと値を転送した後にその値を保持するクラスが INotifyDataErrorInfo インターフェイスを実装していれば、自動で有効になる機能です。

今のサンプルを例にしてみると、 ViewModel の SampleDate プロパティを TextBox.Text プロパティにバインドしています。 TextBoxに値を入力しそれが例外なくSampleDateに転送できた場合に、ViewModelがINotifyDataErrorInfo インターフェイスを実装していれば、自動で検証メソッドが実行されます。

この仕組みによって ViewModel上で、複数のプロパティを比較しながら、データの妥当性検証が可能になります。
これはビジネス エンティティのカプセル化を支援します。

どういうことかといえば、ある業務機能を実装するクラスがあったとします。
それは Entity Framework を利用したPOCOクラスかもしれないし、型付きDataSetかもしれないし、もしくは業務専用クラスかもしれません。

それらの属性をバインドしているならば、そのクラスがINotifyDataErrorInfo インターフェイスを実装することで、そのクラスに閉じた形で検証処理を実装することができます。 このインターフェイスは標準で参照している .Net Framework の System.dll が提供するので、WPFなどのプレゼンテーション テクノロジーとは切り離されています。

MSが提供する Application Architecture Guide 2.0 では一般的な開発レイヤとして以下の図で説明しています。

wpf-03-15

ビジネスレイヤーでビジネスエンティティに検証ロジックをカプセル化し、それが自動でプレゼンテーション レイヤで有効化できるようなソリューションです。

ここまでで、単項目の検証ルールおよび複数項目の検証ルールが定義できるようになりました。

  • 検証ルールの定義
  • 検証結果の視覚的フィードバック
  • 検証を実行するタイミング

あとは『検証結果の視覚的フィードバック』ですが、これはWPFテクノロジーのもう一つの肝要であるテンプレートを紹介する際にしたいと思います。 今は既定の視覚的フィードバックをそのまま利用しましょう。

Converter

さて、最後にConverter プロパティについて紹介します。

これは StringFormat プロパティと比べ、よりリッチな変換を適用できます。

例えば、ViewModel上のプロパティ値が列挙体(None,Error,Complete)だった場合に、その列挙体にあったイメージ画像を返すみたいなグル(接着剤)コードをConverterクラスにカプセル化できます。

MSDNのサンプルコードでは

1
2
3
4
5
[ValueConversion(typeof(DateTime), typeof(String))]
public class DateConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {

という風にIValueConverterインターフェイスの実装と合わせて、ValueConversion属性も付与しています。
これは自分が作業する分だけなのであれば特に不要なのですが、共通ライブラリなどを作成した際に、それを利用する開発者に対してのメッセージになります。

WPFは自由度が高い反面、ルールがないと意図しない結果になる可能性があります。 たとえばですが、開発者はXaml上でコンボボックスを正しくコーディングしていたとしても、テンプレートというスタイルを書き換える機能が適用された場合は、それがラジオボタンに差し替わって表示されることなど容易に可能です。

MSDNのこの記事など読むと面白いのですが

このベスト プラクティスは、組み込みの WPF コントロール セット用のテーマ コントロール スタイルでの作業の際に、多くの試行錯誤を通じて得られたものです。

マイクロソフト内部でもいろいろと苦労したことが伺えます。

例としてComboBoxクラスを見ると様々な属性を持っていますが、この情報は後に柔軟なスタイルおよびデザイン(機能性)を実現する上でとても重要なものとなります。

1
2
3
4
5
6
7
8
9
10
11
12
13
[Localizability(LocalizationCategory.ComboBox)]
[TemplatePart(Name = "PART_EditableTextBox", Type = typeof(TextBox))]
[TemplatePart(Name = "PART_Popup", Type = typeof(Popup))]
[StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(ComboBoxItem))]
public class ComboBox : Selector

/*
今はWPFの基本を解決している最中でが、応用してテンプレートを利用する世界では、
技術資産を蓄積できるようになります。

その際にはコンボボックスをカスタマイズしたスタイルの適用や、
添付プロパティによるカスタマイズなどを紹介したいと思います。
*/

ライブラリとしてIValueConverterを提供する場合は、開発者へのメッセージとしてValueConversion属性をつけてね、ということなのですが・・・

確かに、Xaml上でIValueConverterを適用するためには日付用とか●×用とか何個も専用のクラスを作成する必要があります。

1
2
3
4
5
6
    <StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Margin="5" >
        <StackPanel.Resources>
            <local:DateConverter x:Key="dateConverter"/>
        </StackPanel.Resources>
        <TextBox Name="ConverterTextBox" Text="{Binding Path=SampleDate, Converter={StaticResource dateConverter}, ConverterParameter=arg}"/>
    </StackPanel>

ただ、Expression Builderパターンでコードビハインド上でのコーディングを採用するとしたら、以下で多くは解決できます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

    public class SimpleConverter : IValueConverter
    {
        public Func<object, Type, object, CultureInfo, object> ConvertMethod { get; set; }
        public Func<object, Type, object, CultureInfo, object> ConvertBackMethod { get; set; }
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (ConvertMethod != null)
                return ConvertMethod(value, targetType, parameter, culture);
            throw new NotImplementedException();
        }
        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (ConvertBackMethod != null)
                return ConvertBackMethod(value, targetType, parameter, culture);
            throw new NotImplementedException();
        }
    }

WPFテクノロジー依存であり、Viewのコードビハインド上に静的メソッドを用意すればかなりシンプルになります。

IValueConverter としてはシステムで共通的なロジックの部品化という視点で拡張すればと思います。 ただ、多くはアドホックで業務(≒画面)固有なので、上記のようなメソッド指定が活躍することは多いです。

以上で、バインディングの基本でありながら、WPFの肝要となる機能の紹介でした。

これらの動作を確認するためのサンプルはコチラ


Comments