Silverlight + MVVM モデルで DataGrid をバインド

昨日の続きです。

本当は「View の切替/換装ができる」ってのを書きたいのですが、その前にデータバインドのところを押さえないと訳がわからなくなるので、書いておきます。まあ、覚書ということで。

Silverlight にもグリッドで表示するための DataGrid コントロールがあります。Windows で言う DataGridView であったり、ASP.NET だと名前が違ったりと色々ありますが、まぁ似たような動きをします。
この DataGrid には、データベースから検索した DataTable や List のコレクションなどを指定して、自動的にグリッドに表示できます。

忘れちゃいけないのは「System.Windows.Control.Data.dll」を参照設定することですね。DataGridのコントロールは System.Windows.Control namespace にあるのですが、DLL は System.Windows.Control.Data という。
準備として xaml の namespace を付けます。
下記では「dt」という名前空間を設定しています。

<UserControl x:Class="SampleTextBinding.PageGrid"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:dt="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
    Width="400" Height="300">
    <Grid x:Name="LayoutRoot" Background="White">
  
    </Grid>
</UserControl>

そして、データグリッドを表示させるために「dt:DataGrid」を配置します。

<UserControl x:Class="SampleTextBinding.PageGrid"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:dt="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"
    Width="400" Height="300">
    <Grid x:Name="LayoutRoot" Background="White">
        <StackPanel>
            <dt:DataGrid Name="DGrid" Width="300" Height="200"/>
        </StackPanel>
    </Grid>
</UserControl>

ここでは名前を「DGrid」にしてあるので、データグリッドのコントロールを使って直接指定する場合は、

public PageGrid()
{
    InitializeComponent();
    // コレクションを作成
    List<Product> list = new List<Product>();
    list.Add( new Product(){ ID="001", Name="Silverlight 3" });
    list.Add( new Product(){ ID="002", Name="Visual Studio 2010" });
    list.Add( new Product(){ ID="003", Name="Expression Bend 3" });
    // グリッドに設定
    this.DGrid.ItemsSource = list;

}
public class Product {
    public string ID { get; set; }
    public string Name { get; set; }
}

を記述します。

ページを表示したときに既に表示されるようにする場合は、Page クラスのコンストラクタに。
通常は、ボタンなどの何らかのアクションを起こした後の検索結果を表示すると思います。
この場合はボタンのイベントに記述します。

private void BtnSearch_Click(object sender, RoutedEventArgs e)
{
    // コレクションを作成
    List<Product> list = new List<Product>();
    list.Add(new Product() { ID = "001", Name = "Silverlight 3" });
    list.Add(new Product() { ID = "002", Name = "Visual Studio 2010" });
    list.Add(new Product() { ID = "003", Name = "Expression Bend 3" });
    list.Add(new Product() { ID = "004", Name = "New Application 1" });
    list.Add(new Product() { ID = "005", Name = "New Application 2" });
    list.Add(new Product() { ID = "006", Name = "New Application 3" });
    // グリッドに設定
    this.DGrid.ItemsSource = list;
}

この検索ボタン(BtnSearch)をクリックすると、グリッドが再描画されます。

データグリッドの中身にバインドする場合は、ItemsSource プロパティにバインドします。
先の「落とし穴」と同様にコントロール自身にバインドしてはいけません。

<Grid x:Name="LayoutRoot" Background="White">
    <StackPanel>
        <dt:DataGrid Name="DGrid" Width="300" Height="200"
         ItemsSource="{Binding Items, Mode=TwoWay}" />
        <Button Name="BtnSearch" Width="100" Content="検索" Click="BtnSearch_Click" />
    </StackPanel>
</Grid>

バインドする時の記法はテキストボックスのときと同じです。違いはバインド先が「ItemsSource」となることです。

モデルクラスもテキストの場合と似たようなものですが、プロパティの型が違います。

/// <summary>
/// モデルクラス
/// </summary>
public class ModelGrid : INotifyPropertyChanged
{
    /// <summary>
    /// グリッドのデータ
    /// </summary>
    private IList _Items;
    public IList Items
    {
        get { return _Items; }
        set
        {
            if (_Items != value)
            {
                _Items = value;
                OnPropertyChanged("Items");
            }
        }
    }
   
    #region INotifyPropertyChanged メンバ
    /// <summary>
    /// プロパティ変更時のイベント
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, e);
    }
    protected virtual void OnPropertyChanged(string name)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(name));
    }
    #endregion
}

プロパティ名を ItemsSource としてもよいのですが、ここでは「Items」という名前を使います。
先の xaml を見るとわかるのですが、バインドするときに名前が違います。

<dt:DataGrid Name="DGrid" Width="300" Height="200"
 ItemsSource="{Binding Items, Mode=TwoWay}" />

つまり、コントロールで使う名前とモデルクラスで使う名前と異なってもいいんです。これは当然で、View には同じコントロールを複数貼り付けることもあるので、バインド先のモデルの名前を別にすることが可能です。
これにより Model と View との分離ができます。

さて、Model と View を結びつけるための DataContextプロパティを指定すればおしまいです。

public partial class PageGrid : UserControl
{
    public ModelGrid MyModel;

    public PageGrid()
    {
        InitializeComponent();
        MyModel = new ModelGrid();
        this.DataContext = MyModel;

これも便宜上、Model のオブジェクトを Page クラス内に作っていますが、本当は別の場所に作ります。

さて、バインドの状態をSilverlightのUnitTestを使って動かします。

[TestMethod]
public void TestBindItems()
{
    ModelGrid model = _page.MyModel;

    // コレクションを作成
    List<Product> list = new List<Product>();
    list.Add(new Product() { ID = "001", Name = "Silverlight 3" });
    list.Add(new Product() { ID = "002", Name = "Visual Studio 2010" });
    list.Add(new Product() { ID = "003", Name = "Expression Bend 3" });
    list.Add(new Product() { ID = "004", Name = "New Application 1" });
    list.Add(new Product() { ID = "005", Name = "New Application 2" });
    list.Add(new Product() { ID = "006", Name = "New Application 3" });
    // グリッドに設定
    Assert.AreEqual(6, list.Count);
    model.Items = list;
    Assert.AreEqual(6, model.Items.Count); 
    Assert.AreEqual(6, ((IList)_page.DGrid.ItemsSource).Count); ★
}

このUnitTestは通ります。
特に★の部分に注目してください。モデルクラスの Items プロパティに設定した時に即時、データグリッドの ItemsSource プロパティに値が入っていることがわかります。イコール、画面にも即時反映されるということです。

実は、これを DataContextに直接コントロールをバインドさせると、即時にはなりません。画面を描画するときにバインドが発生するので、UnitTestは常にエラーになってしまいます。
画面で動かす分には問題がない、ように見えますが、いろいろ問題が出るので ItemsSource に直接バインドするのがよいでしょう。

カテゴリー: 開発 パーマリンク