Silverlight + MVVM モデルで DataGrid をバインドの落とし穴

動的バインドの話を書こうと思ったのですが、DataGrid のバインドの落とし穴の件を書くことにします。
多分、検証結果を具体的にみるほうが「何故だめなのか?」がわかりやすいので。

先に注意しておきますが、ここに書くのは「やってはいけない」方法です。

さて、ItemSource プロパティにバインドする場合は、前回書いたように即時実行が行われます。
これを「楽だから」という理由で、DataContextを使ってコントロール自身にバインドしてみましょう。

xaml ファイルを次のように設定します。

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

DataContext=”{Binding DGridData, Mode=TwoWay}” しているところが、バインドです。

モデルクラスはこんな感じになります。

/// <summary>
/// モデルクラス
/// </summary>
public class ModelGrid2 : INotifyPropertyChanged
{
/// <summary>
/// グリッドのデータ
/// </summary>
private DataGrid _DGridData = new DataGrid();
public DataGrid DGridData
{
    get { return _DGridData; }
    set
    {
        if (_DGridData != value)
        {
            _DGridData = value;
            OnPropertyChanged("DGridData");
        }
    }
}

#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
}

データグリッド本体に対してバインドするので、

public DataGrid DGridData

のようにプロパティを書きます。バインド名は「DGridData」です。
検索ボタンを押下したときの処理は、次のようにモデルクラスのほうを使います。

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" });
    // グリッドに設定(バインド版)
    MyModel.DGridData.ItemsSource = list;
}

先ほどバインドしたのがコントロール自体なので、

MyModel.DGridData.ItemsSource = list;

な形でアクセスをします。
一見、これで十分動くような気もしますが、動きません。
画面上で動かしても、データグリッドの内容が更新されません。
また次のようなUnitTestでもエラーになります。

[TestMethod]
public void TestGridData()
{
    ModelGrid2 model = _page.MyModel;
    Assert.AreEqual(3, ((IList)_page.DGrid.ItemsSource).Count);

    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" });
    model.DGridData.ItemsSource = list;

    Assert.AreEqual(6, ((IList)model.DGridData.ItemsSource).Count);
    // ↓はバインドされずにエラーになる。
    // Assert.AreEqual(6, ((IList)_page.DGrid.ItemsSource).Count);
}

最後の Assert.AreEqual のところで、((IList)_page.DGrid.ItemsSource).Count は 3 を返します。
これは既にデータグリッドに設定したデータ数で、バインドが正常に働いていないように見えます。

ですが、これはバインドが働いていないのではなくて、バインドの仕方が間違っているのです。
バインドはコントロール自身となっているので、コントロール自体の変更は通知しますが、コントロールが持つ各種プロパティの変更には応答しません。
通知しないものだから、データグリッドの画面の更新も行われません。ItemsSourceプロパティにバインドしている場合は、正常に画面が更新されます。
これを通知するためには、モデルクラスのItemsSourceプロパティのほうをoverrideして、ObservableCollectionを使って、その通知を受けたものを、バインド先のコントロールに更に通知する、というような処理をする必要があります。これは、無駄です。こんな変なことをしなくてはいけないのは、コントロール自体にバインドをしたためであって、やってはいけないことなのです。まぁ、やってもいいけど、痛い目に会うという余計にややこしいことになります。

というわけで、「DataContext」を使ってコントロールを直接バインドしていはいけません、という話です。

さて、余談ですがここの DataContext 属性は何に使うのでしょうか?

実は先の PageGrid クラスのコンストラクタで DataContext にモデルクラスを設定していました。つまり、DataContext はモデルの元ネタを設定して、それぞれのプロパティ(TextプロパティやItemsSourceプロパティなど)にモデルの各プロパティをバインドするのです。

例では元のページクラスで DataContext を設定しましたが、それぞれのコントロールにデータ/モデルを設定することが可能です。例えば、ページクラスは MyModel クラスを使い、データグリッドは MyGirdModel を使うという使い分けが可能です。
このような場合には、DataGrid の DataContext に直接記述します。

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

このように、DataContext プロパティと ItemsSource プロパティにバインドを行います。DataContext のほうは一度設定したモデルクラス自身を切り替えないのであれば、Mode を TwoWay にする必要はありません。

このあたり、ASP.NET のデータバインドと同じなので合わせて参考にしてください。

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