OITA: Oika's Information Technological Activities

@oika 情報技術的活動日誌。

WPF DataGridColumnへのバインディング

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のオブジェクトが不変なときはこれでもいい気がする。
けど、プロキシを作ってリソースに入れるってのは、
いろいろ応用が利きそうなテクニックですね。