[WPF]DataGridColumnへのバインディング

DataGrid列のヘッダ表示内容とか列幅とか、Columnのプロパティに対して
DataContext経由でViewModelのプロパティをバインドしようとすると
意外と一筋縄でいかないよという話。


まずは、DataGridにコレクションをバインドするシンプルな例から。

<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>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        this.DataContext = new MainWindowViewModel();
    }
}
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プロパティを持つとして、

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側を↓のように書いても失敗しますよという話。

<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を参照するための「プロキシ」的なものとして適当な要素を作って
リソースディクショナリに入れておきましょうというやり方をしている。

↓こんな感じ。

<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側で書いちゃうのが楽。

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