【WPF】Binding入門1。DataContextの伝搬

C#

こんにちは、働くC#プログラマーのさんさめです。

今回はWPF入門編として、
Bindingの対象がどのように決まるのかについて解説します。

WPFにおいてBindingは
「データとビューの疎結合化」
「コード記述の省力化」
などなど非常に重要な役割を果たします。
(後者は慣れないと恩恵を感じにくいですが)

その一方で、
何が起きているのか分かりにくいため、
一度Bindingでハマってしまうと
「まずどこから調べればいいのか分からない」
ということになりがちです。

そこで、私自身の知識の整理も兼ねて
Bindingについてまとめることにしました。

  • WPFのBindingに苦手意識を持っている
  • なぜかわからないけどデータが表示されない

といった方に向けて書いています。

本記事では「Bindingの対象となるDataContext」にしぼって
解説してみようと思います。

さんさめ
さんさめ

「なぜかわからないけど
Bindingがうまくいかない」
の「なぜか」の部分を
解決できるようになることが目標です

スポンサーリンク

Bindingの対象は原則としてDataContext

まず始めに最も大事なことを確認しておきます。

それは、
「Bindingの対象は原則としてDataContextプロパティである」
ということです。

WPFでMVVMパターンを説明するサンプルでも、
必ずビューモデル(VM)をDataContextに設定している記述があります。

以下は、xaml上に直接DataContextを記述している例です。

<Window x:Class="BindingTargetSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:BindingTargetSample"
        Title="MainWindow" Height="450" Width="800">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <Grid>
        <TextBlock Text="{Binding Name}"/>
    </Grid>
</Window>

一方で、この例のように
DataContextを一意に定めるようなビューというのは、
裏を返すと非常にビューとビューモデルの
結合が強いと言えます。

MainWindowのように、アプリケーション実行中に
1つしか出てこないことが確実と言い切れるビューならまだしも、
個々のビューのxamlにこのように直接DataContextを記述するのは、
全くおススメしません

そのため、さんさめはたとえMainWindowであっても、
コード上でDataContextを指す形を推奨しています。

その場合の手順は以下のようになります。

  1. App.xamlのMainStartupUriプロパティを削除
  2. App.xaml.csでOnStartupメソッドをオーバーライド
  3. OnStartupメソッドの中でMainWindowと
    そのDataContextとなるクラスインスタンスを生成
  4. MainWindow.DataContextに
    3で生成したインスタンスを代入してShow

文章にすると長いですが、
結果を例にすると以下のようになります。

DataContextはビジュアルツリーの子要素に伝搬する

さて、このように設定されたDataContextは、
通常、VisualTreeの子要素にそのまま伝搬されます。

最初のコード例でいうと、
Windowに設定されたはずのDataContextは、
その子要素であるGridに伝搬し、
さらにその子要素であるTextBlockに伝搬しています。

これは、デバッグ実行中に使用可能な、
ライブビジュアルツリーでも
確認することができます。
(もっとも、私はSnoop推しのため、
ライブビジュアルツリーは滅多に使わないのですが)

DataContextの伝搬があるからこそ、
<TextBlock Text=”{Binding Name}”/>
とだけ書いてもちゃんとHogeが表示されるのですね。
(サンプルを出してませんがMainViewModelには
Nameプロパティがあり、”Hoge”を返すようにしています)

さんさめ
さんさめ

基本的にDataContextは子要素に伝搬する。
この前提を抑えておくことが大事です。

この前提があった上で、
シンプルに子要素には伝搬しない、
例外的な挙動をするケースを見ていきましょう。

ItesmSourceを使うとコレクションの各要素がDataContxetになる

シンプルに子要素にDataContextに伝搬しない、
かつWPFでアプリケーション開発をしていたら絶対に
避けては通れないコントロールがあります。

それが、ListBoxやDataGrid、ComboBoxなどの
ItemsControl継承クラスにおける
ItemsSourceプロパティ
コレクションを設定した場合です。

とてもシンプルな例を見てみましょう。

<Window x:Class="BindingTargetSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:BindingTargetSample"
        Title="MainWindow" Height="450" Width="800">
    <DockPanel Margin="4">
        <TextBlock Text="{Binding Name}"
                   DockPanel.Dock="Top"/>
        <ListBox ItemsSource="{Binding Items}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Id}"/>
                        <TextBox Text="{Binding ItemName}" Margin="8 0 0 0"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </DockPanel>
</Window>

最初のコード例と比べてListBoxと、
そのItemTemplateが増えています。

今度はViewModel側もコードを見てみましょう。

public class MainViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public string Name => "Hoge";

    public ObservableCollection<ItemViewModel> Items { get; }
        = new ObservableCollection<ItemViewModel>();

    public MainViewModel()
    {
        var index = 0;
        Items.Add(new ItemViewModel(index++) { ItemName = "foo" });
        Items.Add(new ItemViewModel(index++) { ItemName = "bar" });
        Items.Add(new ItemViewModel(index++) { ItemName = "buz" });
    }
}

public class ItemViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public string ItemName
    {
        get => itemName;
        set => SetProperty(ref itemName, value);
    }

    public int Id { get; }

    public ItemViewModel(int id)
    {
        Id = id;
    }

    private void SetProperty<T>(ref T field, T value, [CallerMemberName]string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) { return; }

        field = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private string itemName;
}

MainViewModelさんが、
ItemViewModelのコレクションをプロパティとして持っている、
という形です。

重要な点は、MainViewModelには、
「Id」とか「ItemName」といったプロパティは無い
ということです。

先ほど子要素にはDataContextは伝搬する、
と言いましたが、
これではIdやItemNameといったプロパティは
見つからずバインディングエラーになってしまうのではないでしょうか。

ところが、実行してみると結果は次のようになります。

ちゃんと各行に値が入っています。

ここから分かることは、
ItemsSourceプロパティにコレクションを設定した場合、
それぞれのDataContxtは
そのコレクションの各要素に変わるということですね。

変わっていることを確認するために、
ItemTemplateを次のように書き換えてみましょう。

<ListBox.ItemTemplate>
    <DataTemplate>
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="{Binding Id}"/>
            <!-- ItemNameではなくNameにBindingするようにしてみた -->
            <TextBox Text="{Binding Name}" Margin="8 0 0 0"/>
        </StackPanel>
    </DataTemplate>
</ListBox.ItemTemplate>

これを実行してみると、当然名前は出てきません。
なぜならItemViewModelには
Nameプロパティなんてものはないからです。

VisualStudioのデバッグ出力のところを見ると、
BindingErrorが起きたことがちゃんと出力されています。

実はこのデバッグ出力、非常に大事です

「なんか設定しているはずの値が、
ビューに出てこないな~」
と思ったらとにかくまずはデバッグ出力を確認しましょう。

BindingErrorが起きていたら、
探そうとしているBindingの対象のクラスは何で、
どのパスを探そうとして見つからなかったのか、
詳細に記述されています。

少し話が逸れてしまいました。

改めて、実行結果と、
各種コントロールのDataContextを図解すると次のようになります。

コントロールにおけるDataContextのイメージ図

個々のコントロール要素に、
ItemsSourceの要素がDataContextとして
設定され直してくれることで、
xamlの記述は非常に直感的になります。

ですが、個々のコントロール要素に
DataContextが置き換わっていることを知らないと、
以下のようなことをしようとして、
ハマることになってしまいます。

  • コレクションを持っている親側のデータを表示したい
  • コレクションを持っている親側のCommandを実行できるようにしたい

このようなことをしたい時には、
Bindingの対象をDataContext以外の別のもの
変えてあげる必要があります。

どのようにすればBinding対象を
変えることができるのかについては、
次回の更新で詳しく解説したいと思います。

まとめ

まとめです。

  • WPFにおいてBindingは非常に重要
  • Bindingに対する理解を深めることで
    WPFにハマりづらくなる
  • Bindingの対象はDataContextである
  • DataContextは通常ビューの子要素に伝搬する
  • ただし、ItemsSourceにコレクションを設定した時の、
    各子コントロールについては
    設定したコレクションの各要素がDataContextとなる

最後までお読みいただき、ありがとうございました。

コメント

タイトルとURLをコピーしました