こんにちは、働く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を指す形を推奨しています。
その場合の手順は以下のようになります。
- App.xamlのMainStartupUriプロパティを削除
- App.xaml.csでOnStartupメソッドをオーバーライド
- OnStartupメソッドの中でMainWindowと
そのDataContextとなるクラスインスタンスを生成 - 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を図解すると次のようになります。
個々のコントロール要素に、
ItemsSourceの要素がDataContextとして
設定され直してくれることで、
xamlの記述は非常に直感的になります。
ですが、個々のコントロール要素に
DataContextが置き換わっていることを知らないと、
以下のようなことをしようとして、
ハマることになってしまいます。
- コレクションを持っている親側のデータを表示したい
- コレクションを持っている親側のCommandを実行できるようにしたい
このようなことをしたい時には、
Bindingの対象をDataContext以外の別のものに
変えてあげる必要があります。
どのようにすればBinding対象を
変えることができるのかについては、
次回の更新で詳しく解説したいと思います。
まとめ
まとめです。
- WPFにおいてBindingは非常に重要
- Bindingに対する理解を深めることで
WPFにハマりづらくなる - Bindingの対象はDataContextである
- DataContextは通常ビューの子要素に伝搬する
- ただし、ItemsSourceにコレクションを設定した時の、
各子コントロールについては
設定したコレクションの各要素がDataContextとなる
最後までお読みいただき、ありがとうございました。
シリーズ記事
Binding入門はシリーズ記事となっております。
全ての記事に以下からアクセスできます
コメント