かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

WPF4.5入門 その61「データバインディングを前提としたプログラミングモデル その2」

前回のアプリケーションはシンプルすぎてModelがありませんでしたが、今度はシンプルな四則演算アプリケーションでModelまで含んだコード例を示したいと思います。MVVMの基本クラスは、Prism.Mvvmのクラスを使用します。

Modelの作成

ModelはなるべくプレーンなC#のクラスになるように心がけます。そして状態の変更をINotifyPropertyChangedを通じて外部に通知します。今回は、左辺値、右辺値、計算方法、計算結果をステートとして持たせます。INotifyPropertyChangedを実装したクラスなので、PrismのBindableBaseクラスを基本クラスとして使用します。 左辺値、右辺値、計算結果はdouble型で保持して、計算結果はOperationTypeというenum型を定義してそれを使用しています。コードを以下に示します。

public class Calc : BindableBase
{
    private double lhs;

    public double Lhs
    {
        get { return this.lhs; }
        set { this.SetProperty(ref this.lhs, value); }
    }

    private double rhs;

    public double Rhs
    {
        get { return this.rhs; }
        set { this.SetProperty(ref this.rhs, value); }
    }

    private OperatorType operatorType;

    public OperatorType OperatorType
    {
        get { return this.operatorType; }
        set { this.SetProperty(ref this.operatorType, value); }
    }

    private double answer;

    public double Answer
    {
        get { return this.answer; }
        set { this.SetProperty(ref this.answer, value); }
    }
}
public enum OperatorType
{
    Add,
    Sub,
    Mul,
    Div,
}

次に、アプリケーション全体を示すクラスを作成します。趣味の問題ですが、私は、このクラスから各Modelのクラスへたどれるように作っています。クラス名はAppContextという名前で作成しました。このクラスでは、アプリケーション全体でグローバルに持たせる状態を定義しています。今回は、アプリケーションのメッセージを表示させるようにしています。そして、先ほど定義したCalcクラスもプロパティとして定義します。Calcクラスから必要に応じてメッセージが設定できるようにCalcクラスに自分自身を渡しています。

public class AppContext : BindableBase
{
    private string message;

    public string Message
    {
        get { return this.message; }
        set { this.SetProperty(ref this.message, value); }
    }

    public Calc Calc { get; private set; }

    public AppContext()
    {
        this.Calc = new Calc(this);
    }
}

Calcクラス側には、AppContextクラスを受け取るコンストラクタとフィールドを定義します。

private AppContext appContext;

public Calc(AppContext appContext)
{
    this.appContext = appContext;
}

そして、Calcクラスに計算ロジックを記述します。計算ロジックは0除算のときにエラーメッセージを出す以外は直に計算するだけにしました。

public void Execute()
{
    switch (this.OperatorType)
    {
        case OperatorType.Add:
            this.Answer = this.Lhs + this.Rhs;
            break;
        case OperatorType.Sub:
            this.Answer = this.Lhs - this.Rhs;
            break;
        case OperatorType.Mul:
            this.Answer = this.Lhs * this.Rhs;
            break;
        case OperatorType.Div:
            if (this.Rhs == 0) 
            {
                this.appContext.Message = "0除算エラー";
                return;
            }
            this.Answer = this.Lhs / this.Rhs;
            break;
        default:
            throw new InvalidOperationException();
    }
}

ViewModelの作成

Modelが完成したのでViewModelを作成します。ViewModelでは、まず計算方法のOperationTypeを文字列と非もづけるためのOperationTypeViewModelクラスを作成します。

public class OperatorTypeViewModel
{
    public OperatorType OperatorType { get; private set; }
    public string Label { get; private set; }

    public OperatorTypeViewModel(string label, OperatorType operatorType)
    {
        this.Label = label;
        this.OperatorType = operatorType;
    }

    public static OperatorTypeViewModel[] OperatorTypes = new[]
    {
        new OperatorTypeViewModel("足し算", OperatorType.Add),
        new OperatorTypeViewModel("引き算", OperatorType.Sub),
        new OperatorTypeViewModel("掛け算", OperatorType.Mul),
        new OperatorTypeViewModel("割り算", OperatorType.Div),
    };
}

計算方法のViewModelができたので、次は、MainWindow用のViewModelを作成します。クラス名はMainWindowViewModelにしました。MainWindowViewModelクラスには、左辺値、右辺値を受け取るstring型のプロパティを定義します。ここに入力値を受け取って、計算処理のときにdouble型に変換してModelの左辺値と右辺値に設定します。そして、計算結果を格納するためのAnswerプロパティを定義します。これは、Modelから正しい値が来ることが期待できるので、素直にdouble型として定義します。

左辺値と右辺値は、あとで定義する計算をするためのDelegateCommand型のExecuteCommandプロパティに対して呼び出し可能かどうかが変更されたというイベントを発生させるためにRaiseCanExecuteChangedメソッドを呼び出しています。

最後に、画面に表示するメッセージを表示するプロパティも定義しています。

public class MainWindowViewModel : BindableBase
{
    private string lhs;

    public string Lhs
    {
        get { return this.lhs; }
        set 
        { 
            this.SetProperty(ref this.lhs, value);
            this.ExecuteCommand.RaiseCanExecuteChanged();
        }
    }

    private string rhs;

    public string Rhs
    {
        get { return this.rhs; }
        set 
        { 
            this.SetProperty(ref this.rhs, value);
            this.ExecuteCommand.RaiseCanExecuteChanged();
        }
    }

    private double answer;

    public double Answer
    {
        get { return this.answer; }
        set { this.SetProperty(ref this.answer, value); }
    }

    private string message;

    public string Message
    {
        get { return this.message; }
        set { this.SetProperty(ref this.message, value); }
    }
}

次に、計算方法のプロパティを定義します。これは先ほど作成したOperationTypeViewModel型の配列と、実際に選択されたOperationTypeViewModel型のインスタンスを格納するプロパティを定義します。計算方式のプロパティは、コンストラクタで初期化を行います。

また、現在選択されたOperationTypeViewModel型を現すプロパティでは、変更されたときに、ExecuteCommandプロパティのRaiseCanExecuteChangedメソッドを呼び出して、コマンドが実行可能かどうかに変化があったことを伝えています。

public OperatorTypeViewModel[] OperatorTypes { get; private set; }

private OperatorTypeViewModel selectedOperatorType;

public OperatorTypeViewModel SelectedOperatorType
{
    get { return this.selectedOperatorType; }
    set 
    { 
        this.SetProperty(ref this.selectedOperatorType, value);
        this.ExecuteCommand.RaiseCanExecuteChanged();
    }
}

public MainWindowViewModel()
{
    this.OperatorTypes = OperatorTypeViewModel.OperatorTypes;
}

次に、ModelをViewModelと接続します。今回は1画面のアプリなので、MainWindowViewModel内にModelのルートであるAppContextクラスのインスタンスを持たせる形にしました。複数画面のアプリなどで複数のViewModelからAppContextを参照するようなケースではAppContextクラスのインスタンスをもう少しグローバルにアクセス可能な形で定義するとよいと思います。(例としてAppクラスとか)AppContextクラスをフィールドとして定義したら、コンストラクタでPropertyChangedを監視して、必要に応じてModelの変更をViewModelに反映するコードを書きます。ここでは、Modelのメッセージと計算結果を監視するコードを追加します。

private AppContext appContext = new AppContext();

public MainWindowViewModel()
{
    this.OperatorTypes = OperatorTypeViewModel.OperatorTypes;

    // Modelの監視
    this.appContext.PropertyChanged += this.AppContextPropertyChanged;
    this.appContext.Calc.PropertyChanged += this.CalcPropertyChanged;
}

private void CalcPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == "Answer")
    {
        this.Answer = this.appContext.Calc.Answer;
    }
}

private void AppContextPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    if (e.PropertyName == "Message")
    {
        this.Message = this.appContext.Message;
    }
}

今回のようなModelとViewModelが1対1の関係にあるアプリでは問題になりませんが、ModelとViewModelが1対Nの関係にあるようなケースの場合には、ModelのPropertyChangedイベントの購読をWindowがとじたタイミングなどで解除する必要がある点に注意してください。そうしないと、ViewModelのインスタンスがいつまでだってもGCの回収対象にならないという問題があります。

最後に計算を行うCommandを定義します。ExecuteCommandという名前でDelegateCommand型のプロパティを定義してコンストラクタで初期化します。DelegateCommandのExecuteの処理では、Modelの状態をViewModelの状態をもとに最新化して、計算処理を呼び出しています。CanExecuteの処理では、入力に応じてCommandが実行可能かどうかを返しています。

public DelegateCommand ExecuteCommand { get; private set; }

public MainWindowViewModel()
{
    this.OperatorTypes = OperatorTypeViewModel.OperatorTypes;

    this.ExecuteCommand = new DelegateCommand(this.Execute, this.CanExecute);

    // Modelの監視
    this.appContext.PropertyChanged += this.AppContextPropertyChanged;
    this.appContext.Calc.PropertyChanged += this.CalcPropertyChanged;
}

private void Execute()
{
    this.appContext.Calc.Lhs = double.Parse(this.Lhs);
    this.appContext.Calc.Rhs = double.Parse(this.Rhs);
    this.appContext.Calc.OperatorType = this.SelectedOperatorType.OperatorType;
    this.appContext.Calc.Execute();
}

private bool CanExecute()
{
    double dummy;
    if (!double.TryParse(this.Lhs, out dummy))
    {
        return false;
    }

    if (!double.TryParse(this.Rhs, out dummy))
    {
        return false;
    }

    if (this.SelectedOperatorType == null)
    {
        return false;
    }

    return true;
}

Viewの作成

最後にViewModelとViewを接続します。ViewはシンプルにViewModelに対応した入力項目と出力項目とボタンを持つだけの画面です。見た目は以下のようになります。

f:id:okazuki:20141227173003p:plain

XAMLを以下に示します。

<Window 
    x:Class="MVVMSample02.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:l="clr-namespace:MVVMSample02"
    Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <l:MainWindowViewModel />
    </Window.DataContext>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="5"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="25*"/>
            <RowDefinition Height="37*"/>
        </Grid.RowDefinitions>
        <Label Content="左辺値"/>
        <Label Content="計算方法" Grid.Row="1"/>
        <Label Content="右辺値" Grid.Row="2"/>
        <TextBox Grid.Column="2" TextWrapping="Wrap" Text="{Binding Lhs, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
        <ComboBox Grid.Column="2" Grid.Row="1" ItemsSource="{Binding OperatorTypes}" SelectedItem="{Binding SelectedOperatorType}" DisplayMemberPath="Label"/>
        <TextBox Grid.Column="2" Grid.Row="2" TextWrapping="Wrap" Text="{Binding Rhs, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
        <Label Content="答え" Grid.Row="4"/>
        <TextBlock Grid.Column="2" Grid.Row="4" TextWrapping="Wrap" Text="{Binding Answer}"/>
        <TextBlock Grid.ColumnSpan="3" Grid.Row="5" TextWrapping="Wrap" Text="{Binding Message}"/>
        <Button Grid.ColumnSpan="3" Content="計算" Grid.Row="3" Command="{Binding ExecuteCommand, Mode=OneWay}"/>
    </Grid>
</Window>

実行して動作確認

実行すると以下のような画面が立ち上がります。

f:id:okazuki:20141227173052p:plain

左辺値と計算方法と右辺値を適当に入力して計算ボタンを押すと以下のように答えに計算結果が表示されます。

f:id:okazuki:20141227173114p:plain

0除算をしようとすると以下のようにメッセージが表示されます。

f:id:okazuki:20141227173139p:plain

過去記事