OITA: Oika's Information Technological Activities

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

WPF DataGridへのBindingに関する基本設計

なんか「wpf datagrid binding」で検索して以下の記事に流れてくる人が
異様に多いようだ。
[WPF]DataGridColumnへのバインディング

しかし↑の記事ではあんまり一般的でないケースの話しか書いていなくて
申し訳ない気持ちになるので、たまにはちょっと入門編っぽいことも自分なりに書いてみる。
WPFでDataGridにデータを一覧表示する場合の値の持ち方とかについて。

ちなみに、一覧上のセルから直接値を編集するような使い方は個人的にあんまりやらないので、
今回もあくまでDataGridは表示だけの用途を想定して書きます。

とにかく表示する

まずはコレクションの値をとにかく一覧表示する例から。

<Window x:Class="DataGridBindingSample.MainWindow"  
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
        Title="MainWindow" Height="200" Width="360">  
    <Grid>  
        <DataGrid Name="dataGrid" IsReadOnly="True"/>  
    </Grid>  
</Window>  
public partial class MainWindow : Window  
{  
    public MainWindow()  
    {  
        InitializeComponent();  
  
        this.dataGrid.ItemsSource = new[]   
        {  
            new Person { No = 1, Name = "Tanaka", BirthDay = new DateTime(2000, 1, 1) },  
            new Person { No = 2, Name = "Yamada", BirthDay = new DateTime(1990, 5, 5) },  
            new Person { No = 3, Name = "Sato", BirthDay = new DateTime(2001, 12, 31) },  
        };  
    }  
}  
public class Person  
{  
    public int No { get; set; }  
    public string Name { get; set; }  
    public DateTime BirthDay { get; set; }  
}  

画面イメージ

DataGridのItemsSourceにとにかくコレクションをつっこめば
勝手にプロパティ名で列が作られて表示される。
ここではMainWindowのコードビハインドから直接Person配列を入れているけど
もちろんViewModelを作る場合はそっちでコレクションを持ってItemsSourceにバインドしてもよい。

その場合はこんな感じか。一応。

<Window x:Class="DataGridBindingSample.MainWindow"  
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
        Title="MainWindow" Height="200" Width="360">  
    <Grid>  
        <DataGrid ItemsSource="{Binding Persons, Mode=OneWay}"   
                  IsReadOnly="True" />  
    </Grid>  
</Window>  
public partial class MainWindow : Window  
{  
    public MainWindow()  
    {  
        InitializeComponent();  
  
        this.DataContext = new MainWindowViewModel();  
    }  
}  
public class MainWindowViewModel : INotifyPropertyChanged  
{  
    public event PropertyChangedEventHandler PropertyChanged;  
  
    private Person[] _persons;  
  
    public Person[] Persons  
    {  
        get  
        {  
            return _persons;  
        }  
        private set  
        {  
            _persons = value;  
  
            //更新通知  
            var h = PropertyChanged;  
            if (h != null) h(this, new PropertyChangedEventArgs("Persons"));  
        }
    }

  
    public MainWindowViewModel()  
    {  
        Persons = new[]   
        {  
            new Person { No = 1, Name = "Tanaka", BirthDay = new DateTime(2000, 1, 1) },  
            new Person { No = 2, Name = "Yamada", BirthDay = new DateTime(1990, 5, 5) },  
            new Person { No = 3, Name = "Sato", BirthDay = new DateTime(2001, 12, 31) },  
        };  
    }  
}  

Personsを後から更新しないならPropertyChangedの呼び出しはいらない。

以後の例はViewModelを作らない形に戻します。

表示列を明示的に指定する

たいてい上の例のまんまで使えることはあまりなくて、
列ヘッダをプロパティと違う名前にしたいよとか、
列幅を指定したいよとかいう話になる。

ので、DataGridのほうで最初から列を用意しておいて
そこにPersonクラスのプロパティをバインドしてやる。

<Window x:Class="DataGridBindingSample.MainWindow"  
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
        Title="MainWindow" Height="200" Width="360">  
    <Grid>  
        <DataGrid Name="dataGrid" IsReadOnly="True"  
                  AutoGenerateColumns="False" >  
            <DataGrid.Columns>  
                <DataGridTextColumn Header="番号" Width="80"  
                                    Binding="{Binding No, StringFormat=D2}" />  
                <DataGridTextColumn Header="名前" Width="100"  
                                    Binding="{Binding Name}" />  
                <DataGridTextColumn Header="誕生日" Width="*"  
                                    Binding="{Binding BirthDay, StringFormat=yyyy/MM/dd}" />  
            </DataGrid.Columns>  
        </DataGrid>  
    </Grid>  
</Window>  
public partial class MainWindow : Window  
{  
    public MainWindow()  
    {  
        InitializeComponent();  
  
        this.dataGrid.ItemsSource = new[]  
        {  
            new Person { No = 1, Name = "Tanaka", BirthDay = new DateTime(2000, 1, 1) },  
            new Person { No = 2, Name = "Yamada", BirthDay = new DateTime(1990, 5, 5) },  
            new Person { No = 3, Name = "Sato", BirthDay = new DateTime(2001, 12, 31) },  
        };  
    }  
}  
public class Person  
{  
    public int No { get; set; }  
    public string Name { get; set; }  
    public DateTime BirthDay { get; set; }  
}  

datagrid_sample02

xaml以外はさっきと全く同じだ。
ここでは列ヘッダと列幅と書式を指定した。
あとはDataGridのAutoGenerateColumns="False"を忘れずに。

レコードを追加する

以下のコードの追加ボタンは動きません。

<Window x:Class="DataGridBindingSample.MainWindow"  
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
        Title="MainWindow" Height="200" Width="360">  
    <Grid>  
        <DataGrid Name="dataGrid" IsReadOnly="True"  
                  AutoGenerateColumns="False"   
                  VerticalAlignment="Top" Height="140">  
            <DataGrid.Columns>  
                <DataGridTextColumn Header="番号" Width="80"  
                                    Binding="{Binding No, StringFormat=D2}" />  
                <DataGridTextColumn Header="名前" Width="100"  
                                    Binding="{Binding Name}" />  
                <DataGridTextColumn Header="誕生日" Width="*"  
                                    Binding="{Binding BirthDay, StringFormat=yyyy/MM/dd}" />  
            </DataGrid.Columns>  
        </DataGrid>  
          
        <Button Content="吉田を追加" HorizontalAlignment="Center" VerticalAlignment="Bottom"  
                Click="OnAddButtonClick" />  
    </Grid>  
</Window>  
public partial class MainWindow : Window  
{  
    List<Person> personList;  
  
    public MainWindow()  
    {  
        InitializeComponent();  
          
        personList = new List<Person> {  
            new Person { No = 1, Name = "Tanaka", BirthDay = new DateTime(2000, 1, 1) },  
            new Person { No = 2, Name = "Yamada", BirthDay = new DateTime(1990, 5, 5) },  
            new Person { No = 3, Name = "Sato", BirthDay = new DateTime(2001, 12, 31) },  
        };  
        this.dataGrid.ItemsSource = personList;  
    }  
  
    private void OnAddButtonClick(object sender, RoutedEventArgs e)  
    {  
        personList.Add(new Person { No = 4, Name = "Yoshida", BirthDay = new DateTime(2002, 2, 2) });  
    }  
}  

なんでかっていうと、ItemsSourceのコレクションが更新されましたっていう通知が
DataGridに伝わらないからである。

伝えたい場合は、INotifyCollectionChangedを実装したコレクションを使う。

public partial class MainWindow : Window  
{  
    ObservableCollection<Person> personList;  
  
    public MainWindow()  
    {  
        InitializeComponent();  
  
        personList = new ObservableCollection<Person> {  
            new Person { No = 1, Name = "Tanaka", BirthDay = new DateTime(2000, 1, 1) },  
            new Person { No = 2, Name = "Yamada", BirthDay = new DateTime(1990, 5, 5) },  
            new Person { No = 3, Name = "Sato", BirthDay = new DateTime(2001, 12, 31) },  
        };  
        this.dataGrid.ItemsSource = personList;  
    }  
  
    private void OnAddButtonClick(object sender, RoutedEventArgs e)  
    {  
        personList.Add(new Person { No = 4, Name = "Yoshida", BirthDay = new DateTime(2002, 2, 2) });  
    }  
}  

これは動く。

datagrid_sample03

ただ、このObservableCollectionの場合、
非UIスレッドからコレクションを更新するのがNGだったり、
いちいち更新通知が走って無駄に重くなったりするので、
個人的にはあまり使わないかも。

個人的には、コレクション自体はreadonlyにして
ItemsSourceをまるごと入れ替えるようなやり方のほうが好き。

プロパティの値を変更する

以下の林氏と結婚ボタン(苗字を林に変更)も動かない。

<Window x:Class="DataGridBindingSample.MainWindow"  
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"  
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"  
        Title="MainWindow" Height="200" Width="360">  
    <Grid>  
        <DataGrid Name="dataGrid" IsReadOnly="True"  
                  AutoGenerateColumns="False"   
                  VerticalAlignment="Top" Height="140">  
            <DataGrid.Columns>  
                <DataGridTextColumn Header="番号" Width="80"  
                                    Binding="{Binding No, StringFormat=D2}" />  
                <DataGridTextColumn Header="名前" Width="100"  
                                    Binding="{Binding Name}" />  
                <DataGridTextColumn Header="誕生日" Width="*"  
                                    Binding="{Binding BirthDay, StringFormat=yyyy/MM/dd}" />  
            </DataGrid.Columns>  
        </DataGrid>  
          
        <Button Content="林氏と結婚" HorizontalAlignment="Center" VerticalAlignment="Bottom"  
                Click="OnRenameButtonClick" />  
    </Grid>  
</Window>  
public partial class MainWindow : Window  
{  
    ObservableCollection<Person> personList;  
  
    public MainWindow()  
    {  
        InitializeComponent();  
  
        personList = new ObservableCollection<Person> {  
            new Person { No = 1, Name = "Tanaka", BirthDay = new DateTime(2000, 1, 1) },  
            new Person { No = 2, Name = "Yamada", BirthDay = new DateTime(1990, 5, 5) },  
            new Person { No = 3, Name = "Sato", BirthDay = new DateTime(2001, 12, 31) },  
        };  
        this.dataGrid.ItemsSource = personList;  
    }  
  
    private void OnRenameButtonClick(object sender, RoutedEventArgs e)  
    {  
        //選択項目の名前を"Hayashi"に  
        var person = this.dataGrid.SelectedItem as Person;  
        if (person == null) return;  
  
        person.Name = "Hayashi";  
    }  
}  

今度は、Nameプロパティの変更がDataGridに伝わらないからだ。
この場合は、PersonクラスでINotifyPropertyChangedを実装する。

public class Person : INotifyPropertyChanged  
{  
    public event PropertyChangedEventHandler PropertyChanged;  
  
    public int No { get; set; }  
    public DateTime BirthDay { get; set; }  
  
    private string _name;  
    public string Name  
    {  
        get  
        {  
            return _name;  
        }  
        set  
        {  
            if (_name == value) return;  
            _name = value;  
  
            //更新通知  
            var h = PropertyChanged;  
            if (h != null) h(this, new PropertyChangedEventArgs("Name"));  
        }  
    }  
}  

これも個人的にはあまりやらない。
そもそも(自分で作っといてあれだけど)Personのプロパティは
できればすべてsetterを消して不変なものにしておきたい。

以上を全部ひっくるめて、自分の場合は
以下のようなつくりにすることが多いです。
場合によるっちゃ場合によるんですけども。

public partial class MainWindow : Window  
{  
    readonly List<Person> baseList;  
  
    public MainWindow()  
    {  
        InitializeComponent();  
  
        baseList = new List<Person> {  
            new Person(1, "Tanaka", new DateTime(2000, 1, 1)),  
            new Person(2, "Yamada", new DateTime(1990, 5, 5)),  
            new Person(3, "Sato",   new DateTime(2001, 12, 31)),  
        };  
        UpdateDispList();  
    }  
  
    //表示用リストを再設定  
    private void UpdateDispList()  
    {  
        this.dataGrid.ItemsSource = new ReadOnlyCollection<Person>(baseList);  
    }  
  
    //追加ボタンクリック時  
    private void onAddButtonClick(object sender, RoutedEventArgs e)  
    {  
        baseList.Add(new Person(4, "Yoshida", new DateTime(2002, 2, 2)));  
        UpdateDispList();  
    }  
  
    //名前変更ボタンクリック時  
    private void onRenameButtonClick(object sender, RoutedEventArgs e)  
    {  
        var person = this.dataGrid.SelectedItem as Person;  
        if (person == null) return;  
  
        var idx = baseList.IndexOf(person);  
  
        //名前だけ変えた新しいインスタンスを作って入れ替える  
        var newPerson = new Person(person.No, "hayashi", person.BirthDay);  
        baseList[idx] = newPerson;  
  
        UpdateDispList();  
  
        //選択行を再現  
        this.dataGrid.SelectedIndex = idx;  
    }  
}  
public class Person  
{  
    //publicなsetterを作らず、コンストラクタで受け取る  
    public int No { get; private set; }  
    public string Name { get; private set; }  
    public DateTime BirthDay { get; private set; }  
  
    public Person(int no, string name, DateTime birthDay)  
    {  
        this.No = no;  
        this.Name = name;  
        this.BirthDay = birthDay;  
    }  
}  

コレクションに変更が発生するたびに、
元のコレクションをラップ(≠コピー)するコレクションを作って
ItemsSourceを入れなおしてやる感じ。

もっとベターな方法があるかもですけども、
よろしければひとつのアイデアとしてどうぞ。