かずきのBlog

C#やJavaやRubyとメモ書き

目次

Blog 利用状況

ニュース

わんくまBlogが不安定になったため、前に書いてたはてなダイアリーにメインを移動します。
かずきのBlog@Hatena
技術的なネタは、こちらにも、はてなへのリンクという形で掲載しますが、雑多ネタははてなダイアリーだけに掲載することが多いと思います。
コメント
プログラマ的自己紹介
お気に入りのツール/IDE
プロフィール
経歴
広告
アクセサリ

書庫

日記カテゴリ

2009年2月23日 #

[WPF][C#]Model View ViewModelパターンでハローワールド

MSDNマガジン 2009年2月号にある「Model-View-ViewModel デザイン パターンによる WPF アプリケーション」にあるModel-View-ViewModelパターンが素敵です。

ざっくり説明すると…

  • Model
    通常のクラス。
    レガシーなC#やVBで作ったクラスたちです。
  • View
    XAMLです。大体UserControlです。
  • ViewModel
    INotifyPropertyChangedインターフェースや、IDataErrorInfoインターフェースを実装したViewに特化したクラスです。
  • ViewModelのデータをViewへ表示する仕組み
    ViewのDataContextにViewModelを入れてBindingして表示します。
    IDataErrorInfoや、ValidationRuleを使って入力値の検証を行います。
  • Viewでのボタンクリック等の操作をViewModelに通知する仕組み
    Commandを使用します。WPF組み込みのRoutedCommandではなく、Delegateを使うCommandを作って使います。
    Commandは、ViewModelのプロパティとして公開して、Bindingでボタン等に関連付けます。

といった感じになります。
基本的に、ViewがViewModelを使い、ViewModelがModelを使うという関係になります。
ViewModelがViewを使ったり、ModelがViewModelを使うといったことは原則ありません。

ということで、ハローワールドアプリケーションを作ってみます。
「WpfMVVMHelloWorld」という名前でWPFアプリケーションを新規作成します。

今回作るのは、TextBoxに人の名前を入力してボタンを押すと、TextBlockに「こんにちは○○さん」と表示されるものにします。名前が未入力の場合は、TextBoxの下に名前の入力を促すメッセージを表示してボタンが押せないようにします。

DelegateCommandの作成

とりあえず、アプリケーション本体を作る前に、Delegateを使うICommandの実装を作成します。

using System;
using System.Windows.Input;

namespace WpfMVVMHelloWorld
{
    /// <summary>
    /// 実行する処理と、実行可能かどうかの判断を
    /// delegateで指定可能なコマンドクラス。
    /// </summary>
    public class DelegateCommand : ICommand
    {
        private Action<object> _executeAction;
        private Func<object, bool> _canExecuteAction;

        public DelegateCommand(Action<object> executeAction, Func<object, bool> canExecuteAction)
        {
            _executeAction = executeAction;
            _canExecuteAction = canExecuteAction;
        }

        #region ICommand メンバ

        public bool CanExecute(object parameter)
        {
            return _canExecuteAction(parameter);
        }

        // CommandManagerからイベント発行してもらうようにする
        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameter)
        {
            _executeAction(parameter);
        }

        #endregion
    }
}

こいつは、一回作ったら使いまわすのがいいでしょう。もしくはComposite Application Guidance for WPFにあるDelegateCommand<T>を使うのもいいです。

Modelの作成

次にModelを作成します。といってもこのサンプルでは、Nameプロパティをもつだけのシンプルなクラスです。

namespace WpfMVVMHelloWorld
{
    public class Person
    {
        public string Name { get; set; }
    }
}

ViewModelの作成

続いてViewModelを作成します。
こいつが今回のサンプルでは一番大変です。気合を入れていきましょう。

INotifyPropertyChangedの実装

ViewとBindingする際に、プロパティの変更を通知するためにINotifyPropertyChangedを実装します。いつもの定型句なのでさらっとコードだけを示します。

namespace WpfMVVMHelloWorld
{
    public class HelloWorldViewModel : INotifyPropertyChanged
    {
        #region INotifyPropertyChanged メンバ

        public event PropertyChangedEventHandler PropertyChanged = delegate { };
        protected void OnPropertyChanged(string name)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }

        #endregion
    }
}

次に、内部にModelクラスを保持させるフィールドと、コンストラクタで初期化するコードを書きます。

public class HelloWorldViewModel : INotifyPropertyChanged
{
    // ModelクラスであるPersonを保持する。
    // コンストラクタでModelを指定するようにしている。
    private Person _model;

    public HelloWorldViewModel(Person model)
    {
        _model = model;
    }

    #region INotifyPropertyChanged メンバ
    // 省略
    #endregion
}

続いてViewに公開するプロパティを定義します。
定義するプロパティはユーザに入力してもらうためのNameプロパティと、ボタンを押したあとに表示するMessageプロパティの2つになります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;

namespace WpfMVVMHelloWorld
{
    public class HelloWorldViewModel : INotifyPropertyChanged
    {
        #region コンストラクタと、コンストラクタで初期化するフィールド
        // 省略
        #endregion

        #region 入力・出力用プロパティ
        // ModelクラスのNameプロパティの値の取得と設定
        public string Name
        {
            get { return _model.Name; }
            set
            {
                if (_model.Name == value) return;
                _model.Name = value;
                OnPropertyChanged("Name");
            }
        }

        // こちらは通常のプロパティ
        private string _message;
        public string Message
        {
            get { return _message; }
            set
            {
                if (_message == value) return;
                _message = value;
                OnPropertyChanged("Message");
            }
        }
        #endregion

        #region INotifyPropertyChanged メンバ
        // 省略
        #endregion
    }
}

どちらのプロパティもPropertyChangedイベントを発行していることに注意してください。
こうすることで、ViewModel内での変更をViewに通知できます。

入力値の検証ロジックの作成

次に、ViewModelに入力値の検証ロジックを追加します。
入力値の検証は、IDataErrorInfoインターフェースを実装して追加します。IDataErrorInfoのthis[string columnName]に、columnNameで指定されたプロパティの検証を行うようなコードを追加します。
検証エラーがあった場合は、エラーメッセージを返します。

さらに、検証の結果で、メッセージ作成ボタン(まだ作ってない)が押せるかどうかを切り替えたいので、CommandManagerにCanExecuteChangedイベントを発行してもらうコードも追加します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Windows.Input;

namespace WpfMVVMHelloWorld
{
    public class HelloWorldViewModel : INotifyPropertyChanged, IDataErrorInfo
    {
        #region コンストラクタと、コンストラクタで初期化するフィールド
        // 省略
        #endregion

        #region 入力・出力用プロパティ
        // 省略
        #endregion

        #region INotifyPropertyChanged メンバ
        // 省略
        #endregion

        #region IDataErrorInfo メンバ

        string IDataErrorInfo.Error
        {
            get { return null; }
        }

        string IDataErrorInfo.this[string columnName]
        {
            get 
            {
                try
                {
                    if (columnName == "Name")
                    {
                        if (string.IsNullOrEmpty(this.Name))
                        {
                            return "名前を入力してください";
                        }
                    }
                    return null;
                }
                finally
                {
                    // CanExecuteChangedイベントの発行
                    // (DelegateCommandでのCanExecuteChangedイベントで
                    //  RequerySuggestedイベントに登録する
                    //  処理を書いてるからこうできます)
                    CommandManager.InvalidateRequerySuggested();
                }
            }
        }

        #endregion
    }
}

Commandの作成

次にCommandを作成します。
Commandも単純に、ViewModelのプロパティとして作成します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Windows.Input;

namespace WpfMVVMHelloWorld
{
    public class HelloWorldViewModel : INotifyPropertyChanged, IDataErrorInfo
    {
        #region コンストラクタと、コンストラクタで初期化するフィールド
        // 省略
        #endregion

        #region 入力・出力用プロパティ
        // 省略
        #endregion

        #region INotifyPropertyChanged メンバ
        // 省略
        #endregion

        #region IDataErrorInfo メンバ
        // 省略
        #endregion

        #region コマンド
        private ICommand _createMessageCommand;
        public ICommand CreateMessageCommand
        {
            get
            {
                // 作成済みなら、それを返す
                if (_createMessageCommand != null) return _createMessageCommand;

                // 遅延初期化
                // 今回は、処理が単純なのでラムダ式で全部書いたが、通常は
                // ViewModel内の別メソッドとして定義する。
                _createMessageCommand = new DelegateCommand(
                    param => this.Message = string.Format("こんにちは{0}さん", this.Name),
                    param => ((IDataErrorInfo)this)["Name"] == null);
                return _createMessageCommand;
            }
        }
        #endregion
    }
}

最初に作成したDelegateCommandを使っています。
こんにちは○○さんというメッセージの作成と、入力値にエラーが無ければ実行可能になるように、ラムダ式で処理をDelegateCommandにお願いしています。

Viewの作成

ついにViewの作成です。HelloWorldViewという名前で、ユーザコントロールを作成します。
このViewには、DataContextにHelloWorldViewModelが入る前提でXAMLを書いていきます。

<UserControl x:Class="WpfMVVMHelloWorld.HelloWorldView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <StackPanel Orientation="Vertical">
        <TextBlock Text="名前:" />
        <!--^Nameプロパティのバインド、即座に変更がViewModelに通知されるようにする -->
        <TextBox Name="textBoxName" 
                 Text="{Binding Name, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
        <!-- あればエラーメッセージを標示する -->
        <TextBlock 
            Text="{Binding ElementName=textBoxName, Path=(Validation.Errors).CurrentItem.ErrorContent}" 
            Foreground="Red"/>
        <Separator />
        <Button Content="Create Message" Command="{Binding CreateMessageCommand}" />
        <TextBlock Text="{Binding Message}" />
    </StackPanel>
</UserControl>

さくっとね。

結合!!

今までバラバラに作ってきたものを1つにまとめます。
まずは、ViewModelとViewの繋ぎを書きます。これには、DataTemplateを使います。
App.xamlに以下のように追加します。

<Application x:Class="WpfMVVMHelloWorld.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:l="clr-namespace:WpfMVVMHelloWorld"
    StartupUri="Window1.xaml">
    <Application.Resources>
        <!-- ViewModelとViewの関連付け -->
        <DataTemplate DataType="{x:Type l:HelloWorldViewModel}">
            <l:HelloWorldView />
        </DataTemplate>
    </Application.Resources>
</Application>

次にメインとなるウィンドウを作成します。
ここには、ContentPresenterを使って、何処にHelloWorldViewModelを表示するかかきます。

<Window x:Class="WpfMVVMHelloWorld.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
    <Grid>
        <ContentPresenter Content="{Binding}" />
    </Grid>
</Window>

最後にApp.xamlのStartupUriを消してStartupイベントを定義します。
そこで、Windowの作成と、ViewModelの作成・初期化を行います。

<Application x:Class="WpfMVVMHelloWorld.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:l="clr-namespace:WpfMVVMHelloWorld"
    Startup="Application_Startup">
    <Application.Resources>
    <!-- 中身は省略 -->
    </Application.Resources>
</Application>

using System.Windows;

namespace WpfMVVMHelloWorld
{
    /// <summary>
    /// App.xaml の相互作用ロジック
    /// </summary>
    public partial class App : Application
    {
        private void Application_Startup(object sender, StartupEventArgs e)
        {
            // ウィンドウとViewModelの初期化
            var window = new Window1 
            {
                DataContext = new HelloWorldViewModel(new Person())
            };
            window.Show();
        }
    }
}

これで完成です。

実行!

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

WindowのContentPresenterの表示がDataTemplateが適用されてHelloWorldViewになっているのがわかります。
テキストボックスに何かを入力すると、バリデーションエラーが消えてボタンが押せるようになります。
image

ボタンを押すと、メッセージも表示されます。
image

ということで、Model View ViewModelパターンのハローワールドでした。

posted @ 0:09 | Feedback (533)