WPFの初歩であり難関でもある Binding について仕組みを探ってみます。
はじめに
WPF が登場したのは 2006年。約20年の歴史がありますがいまだにメジャーになり切れておらず、いまだにレガシーなFormが使用されています。WPF では MVVM (Model - View - ViewModel) という3階層のプログラム構造で組むことができるのですが、その中核である XAML と Binding は初心者どころか中級者にもとても高い壁になっています。
XAML と Binding により、XAML上で定義した名前が ViewModel のプロパティに連携する仕様は、C#の言語仕様の範疇を超えているように見えます。最初この仕様を見た時に気持ち悪さを感じました。XAML は C# のソースコードの異なる表現に過ぎず、不思議な Binding も C# のクラスライブラリで実現されています。つまり魔法に見える仕様は背後にいるクラスライブラリで実現しているということです。
この記事では C# のみで Binding の基本的な仕組みを探ります。
Binding とは
Binding は、異なるオブジェクト間のプロパティを同期させる機構ですが、正確には、Dependency Object 内の DependencyPropertyと、他のオブジェクトのプロパティを同期させる機構です。 Dependency (依存)というのは、別の情報と関連づいていることを示します。
日本語では「依存関係プロパティ」と呼びますが、中途半端な用語よりクラス名 DependencyProperty の方がしっくりきます。
プログラム
ターゲットクラス(View側)
ターゲット側のソースです。DependencyObject から派生したクラス内に、DependencyProperty を持ちます。
DependencyProperty は DependencyProperty.Register()により生成される static な object です。これも理解に苦しむところですが、プロパティの実体を作るのではなく、プロパティ生成に必要な情報を登録しています。この情報が連想配列のキーの役割をしているようです。
データ本体へのアクセスには基底クラスの DependencyObject の GetValue(), SetValue() を使用しています。
DependencyProperty は DependencyObject なしでは成立しないことが分かります。
public class Target : DependencyObject { // DependencyProperty の登録 public static readonly DependencyProperty NameProperty = // 名前は "プロパティ名 + Property" DependencyProperty.Register( "Name", // プロパティの名前 typeof(string), // プロパティの型 typeof(Target), // 属するクラス名 new PropertyMetadata(null)); // 通常のプロパティのようにアクセスするためのラッパー public string Name { get { return (string)base.GetValue(NameProperty); } set { base.SetValue(NameProperty, value); } } }
ソースクラス (ViewModel側)
ソースクラスはC#ですべて書く必要があります。
Binding でソースからターゲットにデータ同期するには INotifyPropertyChanged インターフェースを実装する必要があります。
具体的にはBindingしたプロパティの値を変更したときに、PropertyChangedイベントを発行して、ターゲット側に伝達する仕組みです。
internal class ViewModelBase : INotifyPropertyChanged { // interface public event PropertyChangedEventHandler? PropertyChanged; public void PropertyUpdate<T>(ref T var, T val, [System.Runtime.CompilerServices.CallerMemberName] string propertyName = "") { if (! var.Equals(val)) { var = val; if (this.PropertyChanged != null) { this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } } } public class Source : ViewModelBase { string petName = ""; public string PetName { get { return this.petName; } set { base.PropertyUpdate(ref this.petName, value); } } }
Binding のテスト
準備が整ったので Source, Target のインスタンスを作成し、Binding により連結します。
// Source (ViewModel側) var src = new Source() { PetName = "猫" }; // Target (View側) var tgt = new Target(); // Binding var bind = new System.Windows.Data.Binding() { Path = new PropertyPath("PetName"), // Bindingするソースのプロパティ Mode = BindingMode.TwoWay, // 両方向 Source = src // ソースの object }; // bindingと、dependecy object,property を連結 BindingOperations.SetBinding( tgt, // DependencyObject Target.NameProperty, // DependencyProperty bind // binding object ); // ソースを変更して、ターゲットが変更されるか src.PetName = "犬"; string zzz = tgt.Name; // ターゲットを変更して、ソースが変更されるか tgt.Name = "兎"; string zzz2 = src.PetName;
全体を図で表すとこうなります。
これを知ったうえで、XAML を見ると理解しやすくなります。
<TextBlock Text="{Binding PetName, Mode=OneWay}
TextBlock はText という DependencyProperty を持ち、ソース(ViewModel)側の PetName というプロパティと関連付けていることが一目でわかるようになります。Source によるターゲットの指定を省略していますが、その場合は DataContext を参照するようになっています。View - ViewModel の関係だと、View内の各コントロールの Source は同じ ViewModel のインスタンスになります。そのため表記がシンプルになるように1つに集約化しています。
簡単に表記できるからこそ、中の仕組みを理解することが必要です。