XAMLの動的バインド

さて、Silverlight+MVVMの話の続きです。
単純に xaml のコントロール名を使えば楽なのですが、MVVM に則るといちいち{Binding なんとか}を書かないといけません。Binding の構文は、定型といえば定型なのですが、View の中に乱立するのがいまいち解せません。

解せない理由はと言うと、

・そもそも View と Model を分離して View はデザイナ、Model はプログラマ、と切り分けができるのではなかったか?
・View 自体は Microsoft Exprssion Blend を使って編集するわけだから、いちいち Binding の分が書いてあるのはどうかと思う。
・間違って消しちゃったら、Binding は直すの大変。ここのコードを書くのはプログラマなんだから、Viewに手を加えるの問題あり。

というわけで、xaml のほうに Binding を書くのは、かなり解せないのです。
勿論、xaml に書かずに済ますには?というか、方法を模索しないといけないわけで、そうなると対応する *.xaml.cs に書くのがいいですよね。

<Grid x:Name="LayoutRoot" Background="White">
    <StackPanel>
        <Button Name="BtnSave" Width="100" Content="保存" Click="BtnSave_Click" />
        <TextBox Name="TextName" Width="100" Text="{Binding LastName, Mode=TwoWay}" />
        <TextBox Name="TextOut" Width="100" Text="{Binding OutName, Mode=TwoWay}" />
    </StackPanel>
</Grid>

名前は仕方がないとして、Text=”{Binding LastName, Mode=TwoWay}” はデザインから見ると異様です。第一文字列が長いし、何を書いているのかさっぱりわかりません。xaml を直接編集しようにも Binding の部分が長くて編集しづらいです。

なので、できるならば↓な風にしたい。

<Grid x:Name="LayoutRoot" Background="White">
    <StackPanel>
        <Button Name="BtnSave" Width="100" Content="保存"/>
        <TextBox Name="TextName" Width="100" />
        <TextBox Name="TextOut" Width="100" />
    </StackPanel>
</Grid>

ほら、すっきりしたじゃないですか。これならば、WidthプロパティとかContentプロパティとかを編集しやすいですよね。Expression Blend を使っても名前(Name)を設定できますが、バインドのほうは無理です。となれば、デザイナが想像するのはこれくらいシンプルなxamlなはずです。
# まぁ、ストーリーボードとかフィルタが入るとごちゃごちゃしますが。

というわけで、バインドはどうするのかと言うと *.xaml.cs のほうで書きます。コードビハインドを利用するわけですね。

まずはコードを見てください。

public Page2()
{
    InitializeComponent();

    // 動的にバインディング
    Binding bi;
    bi = new Binding("LastName"); 
    bi.Mode = BindingMode.TwoWay;
    this.TextName.SetBinding(TextBox.TextProperty, bi);
    bi = new Binding("OutName"); 
    bi.Mode = BindingMode.TwoWay;
    this.TextOut.SetBinding(TextBox.TextProperty, bi);
    
    // コマンドも動的に設定
    this.BtnSave.Click += BtnSave_Click;

    // データコンテキストに設定
    this.DataContext = MyModel;
}
public Model2 MyModel = new Model2();

動的バインドにはBindingクラスを使います。これを、バインド名を指定して new をします。
バインドの方向は「TwoWay」で指定します。
で、コントロールに結びつけるために SetBinding( どのプロパティか、バインドオブジェクト ) で設定しておしまいです。
これは先の「Text=”{Binding LastName, Mode=TwoWay}”」と同じことです。

まあ3行書くのが面倒な場合は、適当なメソッドを作るとよいでしょう。

private void SetBinding( 
    Control ctrl, DependencyProperty prop, 
    string path, BindingMode mode )
{
    Binding bi = new Binding(path);
    bi.Mode = mode;
    ctrl.SetBinding(prop, bi);
}

public Page()
{
    InitializeComponent();

    // 動的にバインディング
    SetBinding(TextName, TextBox.TextProperty, "LastName", BindingMode.TwoWay);
    SetBinding(TextOut, TextBox.TextProperty, "OutName", BindingMode.TwoWay);

    // コマンドも動的に設定
    this.BtnSave.Click += BtnSave_Click;

    // データコンテキストに設定
    this.DataContext = MyModel;
}

配列にしたりして設定っぽくしておけば間違いが少ないと思います。
どっちにせよ、このバインドという代物は、プログラマ側が設定するので、xaml に入れるのは MVVM 的な視点で言えば望ましくありません。

ちなみに、ボタンクリックのイベントのほうは、

this.BtnSave.Click += BtnSave_Click;

な形で簡単に設定できます。
Visual Studio 2008 上で作ると

this.BtnSave.Click += new RoutedEventHandler(BtnSave_Click);

な形を勝手に作ってくれます。
わざわざ new RoutedEventHandler で包む必要はないと思うのですが、どうなんでしょうね?
RoutedEventHandler を継承した独自なハンドラを作ったら、Click に直接入れられないから new しないと駄目とか。

これをUnitTestを使って動かすと、問題なく動きます。

[TestMethod]
public void TestTextAutoBinding()
{
    Model2 model = _page.MyModel;

    Assert.AreEqual("", model.LastName);
    model.LastName = "masuda";
    Assert.AreEqual("masuda", model.LastName);

    /// 動的にバインディングした場合でも即時
    Assert.AreEqual("", model.OutName);
    _page.TestBtnSaveClick();
    Assert.AreEqual("masuda", model.OutName);
}

直接 xaml に書いても、コードから動的にバインドしても同じ動きですね。当たり前ですが。

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