かずきのBlog@hatena

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

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

WPFでは、強力なデータバインディングを活かした設計パターンとしてModel View ViewModelパターンというアプリケーションを設計するうえでの定石となる設計パターンがあります。Model View ViewModelパターンはMVVMパターンと略されます。MVVMパターンは、WPFだけでなくWebアプリ開発や、その他のアプリ開発にも波及していて、それぞれの状況に応じて形を変えて存在しています。 MVVMパターンは、MSDNマガジンの以下の記事をきっかけに世間に認知されるようになりました。

また、MicrosoftはオープンソースでPrismというMVVMパターンをサポートするライブラリを提供しています。

ここでは、簡単にMVVMパターンの考えについて説明したあと、Prismの一部の機能を使って実際にMVVMパターンのサンプルプログラムを作成していきたいと思います。

MVVMパターンとは

MVVMパターンは、View(XAML + コードビハインド)とViewModelと呼ばれるModelをViewに適したインターフェースに変換するレイヤと、アプリケーションを記述するModelのレイヤからなります。ViewとViewModel間は、基本的にデータバインディングによって連携を行います。ViewModelはModelの変更を監視したり、必要に応じてModelのメソッドの呼び出しを行います。この関係を図で表すと以下のようになります。

f:id:okazuki:20141223175811p:plain

変更通知の仕組み

MVVMパターンの、変更通知や双方向データバインディングのViewModelからView方向の変更通知にはINotifyPropertyChangedインターフェースを実装したクラスを使用します。INotifyPropertyChangedインターフェースはPropertyChangedイベントのみをもつシンプルなインターフェースです。このイベントを通じてModelからViewModel、ViewModelからViewへの変更通知が行われます。

INotifyPropertyChangedインターフェースの実装をすべてのプロパティに実装するのは負荷が高いため、一般的に以下のようなヘルパークラスが作成されます。

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace MVVMSample01
{
    public class BindableBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual bool SetProperty<T>(ref T field, T value, 
[CallerMemberName]string propertyName = null)
        {
            if (Equals(field, value)) { return false; }
            field = value;
            var h = this.PropertyChanged;
            if (h != null) { h(this, new PropertyChangedEventArgs(propertyName)); }
            return true;
        }
    }
}

このクラスを継承すると、プロパティの変更通知機能を持ったクラスが以下のように簡単に作成できます。

namespace MVVMSample01
{
    public class Person : BindableBase
    {
        private int age;

        public int Age
        {
            get { return this.age; }
            set { this.SetProperty(ref this.age, value); }
        }

        private string name;

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

単一のクラスの変更通知はINotifyPropertyChangedインターフェースで行いますが、コレクションの変更通知には、これまでも使ってきたObservableCollectionクラスを使用します。基本的に、この2通りの変更通知を通してModelからViewModelとViewModelからViewの間のやり取りを行います。

ユーザーからの入力の処理

ボタンをクリックするなどのユーザーの処理をViewからViewModelに伝えるには、Commandを使用します。この時使用するCommandはRoutedCommandではなく、デリゲートにExecuteとCanExecute処理を移譲する実装のDelegateCommand(RelayCommandという名前で作られることも多いです)クラスを使用します。DelegateCommandをViewModelクラスのプロパティとして定義して、ViewのButtonやMenuItemなどのCommandプロパティとバインドして使用します。

最初のアプリケーションの作成

各レイヤの連携方法がわかったので、簡単なアプリケーションを作成します。このアプリケーションは、入力した文字列を、ボタンを押したタイミングで大文字に変換して出力するものです。ボタンは、入力が空の場合は押すことができません。また、このサンプルプログラムは、処理が単純すぎるためModelに該当する部分は存在しません。あくまでViewとViewModelが連携した場合の動きを示すものです。

WPFアプリケーションを作成してNuGetでPrism.Mvvmのパッケージを追加します。Prism.MvvmはBindableBaseクラスやDelegateCommandクラスなどのMVVMパターンに必須のクラスだけを持ったシンプルなライブラリです。

ライブラリを追加したら、ViewModelを作成します。MainWindowViewModelという名前でクラスを作って以下のようなコードを作成します。入力用のプロパティと出力用のプロパティと変換用のコマンドを定義しています。コマンドの実行可否は、入力値が変化するたびに評価が必要なのでDelegateCommandのCanExecuteを再評価するためのメソッドを呼び出しています。

クラスを定義します。

using Microsoft.Practices.Prism.Commands;
using Microsoft.Practices.Prism.Mvvm;

namespace MVVMSample01
{
    public class MainWindowViewModel : BindableBase
    {
    }
}

入力、出力を受け取るプロパティを定義します。

private string input;
/// <summary>
/// 入力値
/// </summary>
public string Input
{
    get { return this.input; }
    set 
    {
        this.SetProperty(ref this.input, value);
        // 入力値に変かがある度にコマンドのCanExecuteの状態が変わったことを通知する
        this.ConvertCommand.RaiseCanExecuteChanged();
    }
}

private string output;

/// <summary>
/// 出力値
/// </summary>
public string Output
{
    get { return this.output; }
    set { this.SetProperty(ref this.output, value); }
}

そして、Commandを定義します。

/// <summary>
/// 変換コマンド
/// </summary>
public DelegateCommand ConvertCommand { get; private set; }

public MainWindowViewModel()
{
    // 変換コマンドに実際の処理をわたして初期化
    this.ConvertCommand = new DelegateCommand(
        this.ConvertExecute,
        this.CanConvertExecute);
}

/// <summary>
/// 大文字に変換
/// </summary>
private void ConvertExecute()
{
    this.Output = this.Input.ToUpper();
}

/// <summary>
/// 何か入力されてたら実行可能
/// </summary>
/// <returns></returns>
private bool CanConvertExecute()
{
    return !string.IsNullOrWhiteSpace(this.Input);
}

ビルドしてView(XAML)を作成します。ViewModelをXAMLで参照できるように名前空間の定義を行います。

xmlns:l="clr-namespace:MVVMSample01"

そして、DataContextプロパティに先ほど作成したViewModelクラスを設定します。

<Window.DataContext>
    <l:MainWindowViewModel />
</Window.DataContext>

画面を作成していきます。入力用のTextBoxと出力用のTextBlockとコマンドを実行するためのButtonを置いて、ViewModelの対応するプロパティとバインディングしています。

<Window 
    x:Class="MVVMSample01.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:l="clr-namespace:MVVMSample01"
    Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <l:MainWindowViewModel />
    </Window.DataContext>
    <StackPanel>
        <TextBox Text="{Binding Input, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
        <Button Content="Convert" Command="{Binding ConvertCommand}" />
        <TextBlock Text="{Binding Output}" />
    </StackPanel>
</Window>

実行すると、以下のような画面が表示されます。

f:id:okazuki:20141223180133p:plain

TextBoxに文字を入力するとConvertボタンが押せるようになり、押すとConvertボタンの下に大文字に変換された結果が表示されます。

f:id:okazuki:20141223180203p:plain

過去記事