Data Binding

ここでは実際に Reactive を利用して WPF の Data Binding を試してみましょう。

サンプルアプリの仕様

  • 3つのテキストボックス X, Y, Z があるだけのアプリケーションです。
  • X, Y は入力可能、Z は read-only とします。
  • X, Y に数値を入力すると、Z には常に X + Y の数値が反映されます。
  • X や Y の入力値を編集すると即座に(フォーカスを抜けたときに) Z に反映されます。
../../_images/binding_appcapture.png

プロジェクトの構成

ビューとビューモデル

WPF アプリケーションは MVVM(Model-View-ViewModel)の3層で設計するのが良いとされています。

ここでは View と ViewModel の2層にプロジェクトを分けて作ることにします。

  • View は C# で作成します。WPFのプロジェクトテンプレートなど、ビュー部分はF#よりもC#のほうが Visual Studio の手厚いサポートを享受できます。
  • ViewModel は F# で作成します。C# でも可能ですが、ここでは F# で Reactive を操作するサンプルとしたいためです。

3層ではなく2層にする(Model層を作らない)理由は次の2つです。

  • Model層を別に設けるのがバカバカしく感じられるほど非常に単純なアプリケーションであるため。
  • Model層をどのように設計するかはMVVMアーキテクチャの「関心の対象外」であるため。

後者については補足が必要かもしれません。

MVVMは View と ViewModel および両者の関係について言及するアーキテクチャパターンです。 結果としてMVVMアプリケーションは「ViewとViewModelとそれ以外(=Model)」の3層に別れることになります。 Modelはあくまで「それ以外」であって、MVVMは「Modelが何であるか」「Modelはどうあるべきか」については言及しません。

ソフトウェア設計の一般論として「関心の分離」が推奨されますので、当然ながらGUIと無関係なもの(つまりViewやViewModelとは無関係なもの)は「それ以外」として分離されるべきです。 しかし、そうして分離されたModel層をどのように設計するかは、アプリケーション設計者に委ねられています。

プロジェクトの作成

Visual Studio で次のような2つのプロジェクトを作成して下さい。

BindingSample.sln
  |
  +--- BindingSample.View.csproj ... C# の "WPF アプリケーション"
  |
  +--- BindingSample.ViewModel.fsproj ... F# の "ライブラリ"

参照設定は次の通りです。

  • 両方のプロジェクトに MoNo.FSharp.dll への参照を加えます。
  • BindingSample.View に BindingSample.ViewModel への参照を加えます。

ビューの作成

まずWindowにTextBoxを3つ、Labelを2つ以下のように配置します。

../../_images/layout.png

最後のTextBoxは結果表示用のTextBoxなのでIsReadOnlyプロパティをTrueにしておきます。 XAMLは以下のようになります。ここではStackPanelを使用してレイアウトを整えています。

<Window x:Class="BindingSample.View.MainWindow"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                xmlns:local="clr-namespace:BindingSample.View"
                mc:Ignorable="d"
                Title="MainWindow" SizeToContent="Height" Width="150">
<StackPanel Margin="8">
        <TextBox Text="TextBox"/>
        <Label HorizontalContentAlignment="Center">+</Label>
        <TextBox Text="TextBox"/>
        <Label HorizontalAlignment="Center">=</Label>
        <TextBox Text="TextBox" IsReadOnly="True"/>
</StackPanel>
</Window>

ビューモデルの作成

ViewModel.fsprojにViewModel.fsを追加しビューモデルを記述します。 コードは以下の通りです。

namespace BindingSample
open MoNo

type ViewModel() =
        let x = Reactive 0.0
        let y = Reactive 0.0
        let z =
                reactive {
                let! x = x
                let! y = y
                return x + y
                }
        member __.X = x
        member __.Y = y
        member __.Z = z

このコードは特に説明の必要はないでしょう。(前節のReactiveの項を参照して下さい。) 以下の点を抑えておきましょう。

  • このViewModelクラス自体は何のクラスも継承していません。何のインターフェースも実装していません。
  • プロパティX, Y, ZはIReactive<float>、特にX, YはIReactiveW<float>なのでValは読み書き可能です。
  • X, Yの値(Val)を変更するとZの値も連動して変化します。

DataBindingの設定

次にXAMLを修正してTextBoxにBindingを設定します。

  1. xmlns:vm としてビューモデルのアセンブリを設定します。
  2. DataContentにvm:ViewModelを設定します。
  3. TextBoxのTextプロパティにX.Val, Y.Val, Z.Valをバインドします。Zは読み取り専用なのでMode=OneWayとします。

XAMLは以下のようになります。

<Window x:Class="BindingSample.View.MainWindow"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
                xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
                xmlns:local="clr-namespace:BindingSample.View"
                xmlns:vm="clr-namespace:BindingSample;assembly=BindingSample.ViewModel"
                mc:Ignorable="d"
                Title="MainWindow" SizeToContent="Height" Width="150">
<Window.DataContext>
        <vm:ViewModel/>
</Window.DataContext>
<StackPanel Margin="8">
        <TextBox Text="{Binding Path=X.Val}"/>
        <Label HorizontalContentAlignment="Center">+</Label>
        <TextBox Text="{Binding Path=Y.Val}"/>
        <Label HorizontalAlignment="Center">=</Label>
        <TextBox Text="{Binding Path=Z.Val, Mode=OneWay}" IsReadOnly="True"/>
</StackPanel>
</Window>

これで完成です。 2つのTextBoxの値を変えると結果が変わるのが確認できます。

TextBoxのTextプロパティの変更通知が飛ぶタイミングはデフォルトではLostFocus(フォーカスを失った時)なので、TextBoxに値を入力してから一度フォーカスを外さないと結果も更新されません。 入力中にも結果を表示したいのであれば、入力となるTextBoxのバインディングでUpdateSourceTriggerにPropertyChangedを指定すればよいでしょう。

    ...
<StackPanel Margin="8">
    <TextBox Height="23" TextWrapping="Wrap" Text="{Binding Path=X.Val, UpdateSourceTrigger=PropertyChanged}"/>
    <Label Content="+" HorizontalContentAlignment="Center"/>
    <TextBox Height="23" TextWrapping="Wrap" Text="{Binding Path=Y.Val, UpdateSourceTrigger=PropertyChanged}"/>
    <Label Content="=" HorizontalContentAlignment="Center"/>
    <TextBox Height="23" TextWrapping="Wrap" Text="{Binding Path=Z.Val, Mode=OneWay}" IsReadOnly="True"/>
</StackPanel>
    ...