BackacheEngineerの技術的な備忘録

技術系でいろいろ書けたらなーと

XAML でめっちゃ使うデータバインディング

MVVM で欠かせないデータバインディングについて今の理解をまとめる。

目次


データバインディングとは

View とデータの連携のこと。 View をいじれば通知が飛んでデータが書き換わり、データの構造によって View が変化する(ツリー構造や階層構造とか)。

データバインディングの使い方

2021 / 05 / 01 修正と追記

基本的な使い方は下記のとおり。

<Window.Resources>
    <!-- ビューモデルのインスタンス作成 -->
    <local:sampleViewMode x:Key="sampleVM"/>
</Window.Resources>

<!-- Resouceに作ったビューモデルインスタンスをGridに割り当て -->
<Grid DataContext="{StaticResource sampleVM}">
    <!-- sampleVM の DisplayText を TextBlock の Text にバインディング -->
    <TextBlock Text="{Binding DisplayText}" />
</Grid>

DataContext にビューモデルのインスタンスを割り当てておくことでバインディング時に「どのビューモデルか」っていうところを省略できる。

他のバインディングをまとめておく。 表中に出てくるバインディングターゲットとバインディングソースは後述する。 また、バインディングターゲットを「ターゲット」、バインディングソースを「ソース」と略記する。

バインディング記法 意味
{Binding ElementName=sampleVM txtblk
Path=Display Text}
sampleVM の DisplayText プロパティをバインディングする。明示的にビューモデルを指定する書き方。 View 要素である TextBlock (txtblk はインスタンス名)のプロパティ Text をバインディングする。
{Binding Source={StaticResource sampleVM},
Path=Display}
sampleVM の DisplayText プロパティをバインディングする。明示的に ViewModel を指定する書き方。
{Binding DisplayText Mode=Default/OneTime/
OneWay/OneWayToSource/TwoWay}
DisplayText をバインディングし、そのバインディング方向を指定する。ViewModel は DataContext としてすでにバインドされたものを暗黙的に対象としている。
Default:デフォルト。OneWay と同じ。
OneTime:アプリが始まったとき、または DataContext が変更されたときにターゲットのプロパティを更新する。
OneWay:ソースのプロパティが変更された場合にのみ、ターゲットのプロパティを更新する。
OneWayToSource:ターゲットのプロパティが変更されたときに、ソースのプロパティを更新する。
TwoWay:ターゲット、またはソースのプロパティのいずれかが変更されたとき、ターゲット、またはソースのプロパティを更新する。
{Binding DisplayText,
Converter={StaticResource converter},
ConverterParameter=X2}
DisplayText をバインディングする。バインディング時、指定した Converter によって値を変換できる。また、変換時の引数を ConverterParameter で指定できる。本例では X2 という文字列を渡している。想定しているのは、long.ToString("X2") という変換である。

上記の例で暗黙的に ViewModel を対象にしたりしているが、別に明示的に ViewModel を指定しても、ViewModel じゃなくて View のプロパティが対象でもバインディング方向や Converter は使用できる。

2021 / 04 / 17 追記

データバインディングの Converter についてまとめた。

backacheengineer.hatenablog.com


2021 / 05 / 01 追記 バインディング時の RelativeSource について勉強したので更新する。こいつは書くこと多いんで章を設ける。

バインディングにおける RelativeSource

バインディングターゲットからの相対的な位置にいるプロパティをバインディングソースにしたいときに使う。(ターゲットとかソースについては次章)

下記がその記法である。RelativeSouce には Mode が存在するため、一つの記法に4種類の書き方がある。

バインディング記法 意味
{Binding RelativeSource={RelativeSource Mode=Self}, Path=Value} 自分自身のプロパティ「Value」を対象にバインディングする。(いつ使うんだこれ)
{Binding RelativeSource={RelativeSource Mode=PreviousData}} 以前のデータ項目をバインディングする(っぽい)。使った例すら見たことなく、詳細は不明。そんな使わなさそうなので放置してよさげ。
{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=Value} ControlTemplate の対象になっている親タイプのプロパティ「Value」を対象にバインディングする。
これは ControlTemplate じゃないと使えないので注意。DataTemplate など他の Template だとソースが見つからないとバインディングエラーとなる。なお、MSDNに制約の明記はない。
{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=HOGE}, Path=Value 自身の先祖にあたる HOGE というクラスを探して、そのプロパティ「Value」を対象にバインディングする。探し方は AncestorLevel という階層指定も可能。いずれか、または両方を指定すればよい。なお、AncestorType や AncestorLevel は Mode が FindAncestor じゃないと使用できない。

PreviousData だけ情報がないに等しいが、使用頻度の高さでいえば最後の「FindAncestor」が圧倒的になると考えられるので、とりあえず良しとする。

この RelativeSource だが、使用する機会は Template をいじくるときのため、「デザインを作りこむ」となると必ず必要といっても過言ではないくらい大事なものになる。こいつがわかってこれば、必ず力になる。デザインはユーザにとって最も重要なので、プログラマだからといって勉強しないという選択肢はない。

TemplatedParent と TemplateBinding について

これは勉強中につっかかりまくったのでここでまとめておく。前述した制約「ControlTemplate」じゃないと使えないなど・・・。

この TemplatedParentだが、これは {TemplateBinding Value} と似た意味である。下記がこの二つの差分になる。

項目 説明
TemplatedParent 下記のように、バインディング方向を指定できる。
{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Mode=TwoWay ... }
TemplateBinding バインディング方向が指定できない。OneWayに固定。

TemplateBinding におけるバインディング方向が OneWay のみという制約は MSDN に明記されている。詳細は下記を参照する。TemplateBinding はControlTemplate のまとめで触れたいため。

TemplateBinding Markup Extension - WPF .NET Framework | Microsoft Docs


バインディングターゲットとバインディングソース

バインディングする際に注意して覚えたいのがバインディングターゲットとバインディングソースである。 よくこんがらがるのでまとめる。

  • バインディングターゲット:こっちがビュー側。つまり、TextBlock や Label などのこと。
  • バインディングソース:こっちがビューモデル側。データの基になる。

ソース(データの基)の値をターゲット( View )に反映するのが基本である。 そして、バインディング方向はデフォルトで OneWay なので、「データの基が変わったら、表示も切り替わる」ということになる。

その逆、「表示を変えたら、データも変わる」にする場合、バインディング方向を OneWayToSource か、TwoWay にする必要がある。

バインディング方向によって更新されるものが変化するため、方向は プロパティごとにしっかり考えて決める必要がある。大枠は下記と考えられる。

編集可能ということは、ユーザの入力を受け取るということである。つまり、その入力はデータ側に渡されることが多い。 よって、ターゲット( View )のプロパティが変更されたなら、ソース(データの基)の値が書き換わってほしいのだから、「表示を変えたら、データも変わる」OneWayToSource が適している。

編集不可なコントロールに値がバインディングされているということは、データの基を常に表示してほしいものである。 編集可能なコントロールによってソースの値が変更され、そのソースの値が更新されたことによって値が書き換わるソースが該当する。 例えば、フィルタリング機能のチェックボックスにチェックを入れたら表示一覧がフィルタリングされるときの「表示一覧」側である。 表示一覧は常にデータの基を表示してほしく、別の値によって中身が変化することが多い。よって、「データの基が変わったら、表示も切り替わる」OneWay が適している。

2021 / 04 / 17 追記

TwoWay を使うときは View でいじくったデータを保存、および復元したいとき。 データを保存するだけなら OneWayToSource でいいが、その後に View で復元する際は TwoWay じゃないと View 側が更新されない。


依存関係プロパティ

データバインディングで知っておくべき言葉である「依存関係プロパティ」。英語だと「DependencyProperty」。 実は、 バインディングターゲットは依存関係プロパティでないといけない、という制約がある。 なお、バインディングソース側は別に依存関係プロパティでなくてよい。つまり、ただのプロパティでよい。

カスタムコントロールやユーザーコントロールを作って表示対象にバインドさせたいときは依存関係プロパティを用意する必要がある。 割と定型的に定義できるのでまとめておく(サンプルはユーザーコントロールを想定)。

/// <summary>
/// 依存関係プロパティ
/// </summary>
public static DependencyProperty SampleProperty { get; private set; }

/// <summary>
/// 実際にXAML側とかで見えるプロパティ
/// </summary>
public string Sample
{
    get => (string)GetValue(SampleProperty);
    set => SetValue(SampleProperty, value);
}

/// <summary>
/// コンストラクタ(静的である必要がある。クラス名は適当。こいつは依存関係プロパティ用と割り切ってよし。)
/// </summary>
static Constructor()
{
    // SampleProperty のインスタンス作成。DependencyProperty クラスの static メソッドを用いる。
    // 第1引数:依存関係プロパティの名前(これは実際に見えるプロパティにすれば安心かも)
    // 第2引数:プロパティの型(実際にXAML側で見えるプロパティの型を登録する)
    // 第3引数:依存関係プロパティを所有する親の型(これはクラスの Type インスタンスを登録する。)
    // 第4引数:依存関係プロパティのメタデータ。PropertyMetadataの第1引数は デフォルト値( null 許容型じゃないと null の時怒られる。)、第2引数は依存関係プロパティの Changed イベントメソッド。
    SampleProperty = DependencyProperty.Register(nameof(Sample), typeof(string), typeof(Constructor), new PropertyMetadata(null, onSampleChange));
}

/// <summary>
/// コンストラクタ(開発者なら見慣れた UI を初期化するやつ)
/// </summary>
public Constructor()
{
    InitializeComponent();
}

/// <summary>
/// 依存関係プロパティの変更イベント
/// </summary>
static void onSampleChange(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    // 何か別のコントロールに新しい値を渡したり、メソッド呼び出したりなどする。
    (obj as Constructor).border.Child = (string)args.NewValue;
}

上記はもはやテンプレートに近いと思っている。PropetyMetadata の初期値とそのコールバック関数だけは都度違うので、そこは開発するものによって柔軟に変えていく。 依存関係プロパティとして登録する名前が重複したり、デフォルト値が許容できない値(依存関係プロパティの型が bool なのに初期値が null )はエラーとなる。