かずきのBlog

C#やJavaやRubyとメモ書き

ホーム 連絡をする 同期する ( RSS 2.0 ) Login
投稿数  515  : 記事  1  : コメント  549  : トラックバック  140

ニュース

WCF, WPF, Silverlight2がアツイ
コメント
プログラマ的自己紹介
  • C#とRubyを趣味で。Javaを仕事で使ってやってます。 WPFをコツコツ勉強中。 IDE大好き。Visual Studio, Eclipse, NetBeansを使用中
お気に入りのツール/IDE
  • Visual Studio 2008 std
  • Eclipse
  • NetBeans6.0以降
  • 自作のツール
プロフィール
  • 大田 一希
  • 1981年1月30日産まれ
  • AB型
  • 左利き
経歴
  • 1993年 海田中学校 入学
  • 1996年 広島県立安芸南高等学校 入学
  • 1999年 某大学 環境情報学科 入学
  • 2003年 某大学 大学院 環境学研究科 入学
  • 2005年 就職して上京
  • 今に至る
アクセサリ
  • あわせて読みたい
  • ログ解析ネット証券
  • 最近読んだ本
    WPF Recipes in C# 2008

書庫

日記カテゴリ

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パターンのハローワールドでした。

投稿日時 : 2009年2月23日 0:09

コメント

# re: [WPF][C#]Model View ViewModelパターンでハローワールド 2009/02/23 1:24 えムナウ
ViewModel の元クラスを使わなかったのはなぜ?

# re: [WPF][C#]Model View ViewModelパターンでハローワールド 2009/02/23 7:16 かずき
ViewModelが1クラスしかないので、あえてくくり出す必要性も無いと感じたので。
あとは、勉強用というのと、少しでもクラス構造とかを単純にしたかったからです。


# re: [WPF][C#]Model View ViewModelパターンでハローワールド 2009/02/23 10:17 biac
> 検証の結果で、メッセージ作成ボタン(まだ作ってない)が押せるかどうかを切り替えたいので、CommandManagerにCanExecuteChangedイベントを発行してもらうコードも追加します。

このあたりも、 WPF の面白いとこですね。

ボタンとかの enable / disable を切り替えるのも、 ViewModel から直接 btnHoge.IsEnabled = false; とかやってはイカンわけです。 ( っていうか、 View から ViewModel へバインドしてると、 素直にはできない。 )
どうするか、 っていうと…
簡易的には、 ViewModel に IsButtonHogeEnabled みたいなプロパティを用意して、 btnHoge の IsEnabled プロパティにバインドしてやれば、 たぶん OK。
まじめにやるには、 かずきさんが示してくれたように、 ちゃんと Command を用意して、 ボタンのほうで
Command="{Binding CreateMessageCommand}"
みたいにバインドしてやる。 で、 ViewModel の方から CanExecuteChanged イベントを発行してやると、 ボタンが ViewModel の CanExecute() を見て enable / disable を切り替えてくれる、 ってな感じ。

…ただねぇ。 依存関係プロパティやら Notify 関連やらで、 ワンパターンなコードを延々と書かなくちゃいかんのよねぇ orz
.NET 4.0 では、 もちっとシンタックスシュガーを振りかけてくれんかなぁ。 f(^^;

# re: [WPF][C#]Model View ViewModelパターンでハローワールド 2009/02/23 17:42 中吉
いつもサンプル見て思うこと。

これをVBで書くのめんどくさそうorz



# re: [WPF][C#]Model View ViewModelパターンでハローワールド 2009/02/24 17:30 かずき
>biacさん
めんどくさいですよね~。私も常々思います。
本気でやらないといけないときは、自動生成させてしまいたいですね…
私もシンタックスシュガーか、何か細工がほしいです。
[Notify]
public string Name { get; set; }
とかみたいに。



# re: [WPF][C#]Model View ViewModelパターンでハローワールド 2009/02/24 17:33 かずき
> 中吉さん
VBですかぁ。私は、VB弱者なのでどうなるかあまり想像つきません(^^;
MSDNマガジンのModel View ViewModelの記事にVBのサンプルコードのダウンロードもあったので、ダウンロードしてどうなるか見てみるのもいいかもです。

# [.NET] WPF アプリケーションの Model-View-ViewModel 2009/02/24 18:43 biac の それさえもおそらくは幸せな日々@nifty
Model-View-ViewModel …よくもそんな舌を噛みそうな名前にしてくれたな! (w MSDN マガジン 2009年 2月号より Model-View-ViewModel デザイン パターンによる WPF アプリケーション ViewModel はビューへの参照を必要としません。 ビューは ViewModel

# re: [WPF][C#]Model View ViewModelパターンでハローワールド 2009/02/24 19:20 biac
> 私もシンタックスシュガーか、何か細工がほしいです。
> [Notify]
> public string Name { get; set; }
> とかみたいに。

そうなんですよ。
というわけで、 中身を公開できないんだけど、 今やってるプロジェクトでは、
[勝手にNotify( Notify先 )]
みたいな感じに書けてます f(^^;
# でも、 まだまだ足りないw

# 【Love VB】 レッツWPF M-V-VM モデル 2009/02/24 21:27 ちゅき
めんどくさい、だけのコメントじゃVBへの愛が疑われるのでそのまんまVBにしてみた。
やるんじゃなかったorz
http://blogs.wankuma.com/chuki/archive/2009/02/24/168717.aspx

#事後になってすいません。おもいっきりパクりましたm(_ _)m

# [WPF][C#]Model View ViewModelパターンでハローワールド その2 2009/02/25 1:34 かずきのBlog
[WPF][C#]Model View ViewModelパターンでハローワールド その2

# プロパティを検証するコード @MVVM 2009/02/25 20:01 katamari.wankuma.com
プロパティを検証するコード @MVVM

Post Feedback

タイトル
名前
Url:
コメント