DataGrid列のヘッダ表示内容とか列幅とか、Columnのプロパティに対して
DataContext経由でViewModelのプロパティをバインドしようとすると
意外と一筋縄でいかないよという話。
まずは、DataGridにコレクションをバインドするシンプルな例から。
MainWindow.xaml
<Window x:Class="ProxyElementSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <DataGrid ItemsSource="{Binding Path=Rows}" AutoGenerateColumns="False"> <DataGrid.Columns> <DataGridTextColumn Width="*" Header="列A" Binding="{Binding Path=Value}"/> </DataGrid.Columns> </DataGrid> </Grid> </Window>
MainWindow.xaml.cs
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); this.DataContext = new MainWindowViewModel(); } }
MainWindowViewModel.cs
public class MainWindowViewModel { public object[] Rows { get; private set; } public MainWindowViewModel() { //Valueプロパティを持つ匿名クラスオブジェクトを100レコード作る Rows = Enumerable.Range(1, 100).Select(n => new { Value = n }).ToArray(); } }
画面のxaml側では、DataGridを1つおいて、ItemsSourceにバインドパスを指定。
その中に列を1つだけ作って、同じくこの列に表示するデータのプロパティをバインド指定。
コードビハインド側では、DataContextを入れてるだけ。
ViewModel側はValueプロパティを持つ適当なコレクションを作ってるだけ。
ここまでは良いと思うんだけど、この列のヘッダの「列A」という文字列を
ViewModel側からバインドしたいと思ったとき、
ViewModelに↓こんなふうにHeaderTextプロパティを持つとして、
MainWindowViewModel.cs
public class MainWindowViewModel { //列ヘッダ表示文字列 public string HeaderText { get { return "列A"; } } public object[] Rows { get; private set; } public MainWindowViewModel() { Rows = Enumerable.Range(1, 100).Select(n => new { Value = n }).ToArray(); } }
これを使うためにView側を↓のように書いても失敗しますよという話。
MainWindow.xaml
<Window x:Class="ProxyElementSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <DataGrid ItemsSource="{Binding Path=Rows}" AutoGenerateColumns="False"> <DataGrid.Columns> <DataGridTextColumn Width="*" Header="{Binding Path=HeaderText}" Binding="{Binding Path=Value}"/> </DataGrid.Columns> </DataGrid> </Grid> </Window>
これがなんで失敗するかっていうと、↓に同じような議論があったんだけど、
c# - Bind datagrid column visibility MVVM - Stack Overflow
要はDataGridColumnsはヴィジュアルツリーに含まれていないから
DataGrid自体のDataContextを辿れないよという話なんだな。
それで上記リンク先の解決策は、なるほどーと思ってしまったんだけど、
DataContextを参照するための「プロキシ」的なものとして適当な要素を作って
リソースディクショナリに入れておきましょうというやり方をしている。
↓こんな感じ。
MainWindow.xaml
<Window x:Class="ProxyElementSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Grid> <Grid.Resources> <FrameworkElement x:Key="proxyElement" /> </Grid.Resources> <ContentControl Visibility="Collapsed" Content="{StaticResource proxyElement}" /> <DataGrid ItemsSource="{Binding Path=Rows}" AutoGenerateColumns="False"> <DataGrid.Columns> <DataGridTextColumn Width="*" Header="{Binding Path=DataContext.HeaderText, Source={StaticResource proxyElement}}" Binding="{Binding Path=Value}"/> </DataGrid.Columns> </DataGrid> </Grid> </Window>
もっといえば、最初からViewModelのオブジェクト自体を
リソースに入れちゃえば、もう少しシンプルに書けるようになるかな。
その場合は、ViewのDataContext設定もxaml側で書いちゃうのが楽。
※追記:コメント欄にてリソースリークのご指摘あり
MainWindow.xaml
<Window x:Class="ProxyElementSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:my="clr-namespace:MyProject" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <my:MainWindowViewModel x:Key="vm"/> </Window.Resources> <Window.DataContext> <StaticResourceExtension ResourceKey="vm" /> </Window.DataContext> <Grid> <DataGrid ItemsSource="{Binding Path=Rows}" AutoGenerateColumns="False"> <DataGrid.Columns> <DataGridTextColumn Width="*" Header="{Binding Path=HeaderText, Source={StaticResource vm}}" Binding="{Binding Path=Value}"/> </DataGrid.Columns> </DataGrid> </Grid> </Window>
ViewModelのオブジェクトが不変なときはこれでもいい気がする。
けど、プロキシを作ってリソースに入れるってのは、
いろいろ応用が利きそうなテクニックですね。