読者です 読者をやめる 読者になる 読者になる

くわふわ.NET

.NETについて、淡々と書いていきます。

INotifyPropertyChangedはViewModelで実装?Modelで実装?

MVVMを勉強してから実際にアプリを作ろうとして悩んだ点について。

Modelのプロパティの値をView(XAML)上のコントロールにバインドしたい時、INotifyPropertyChangedはViewModelで実装するべき?Modelで実装するべき?(あるいは両方で実装?)

結論:Modelで実装。

なぜこんな簡単な事で悩んでしまったかというと・・・
ViewModelのベースクラスがINotifyPropertyChangedインタフェースを実装しているサンプルを複数確認。
それを見て、ViewModelで実装するものだと勘違いしてしまった自分の理解不足が原因。

ModelはViewModelが公開するもの。
→ INotifyPropertyChanged を実装した ViewModel が Model の プロパティ をラップすればよいのでは?という飛躍した発想をしてしまいました。

この方法だと、一つの Model を複数の ViewModel から利用する時に困ります。
(Modelを利用する複数の ViewModel のうちいずれかが Model のプロパティに変更を加えた時、変更の通知を受け取れない View がある)

まずはMVVMの原則。
・プロパティ値の変更をViewに伝える方法は INotifyPropertyChanged を利用するのが一般的。(ただし、唯一の方法ではない)
・INotifyPropertyChanged は、変更の通知を行いたいクラスで実装する。

INotifyPropertyChanged は、View がバインドしているクラスで実装するべき。
Modelのプロパティをバインドしたいなら、Modelで実装するのが正解。
という結論に達しました。

ただし、Model の変更通知の話とは別に ViewModel が INotifyPropertyChanged を実装するケースが多いため、結果として ViewModel とModel の両方が INotifyPropertyChanged を実装する事になると思います。

ビヘイビアとトリガー&アクションが理解できずに困った

MVVMではコードビハインド無しでViewを実現するために次のような実装になると理解した。
 ・値の入力・表示はバインドを利用
 ・エラーの表示はINotifyDataErrorInfo(WPFではIDataErrorInfo)
 ・イベントはCommandをバインド

ただ、ViewとViewModel間で操作を行いたい場合にはCommandのバインド以外に次のような方法があるらしい。
(2012/10/23追記 ViewModelからViewを操作したい場合)

ビヘイビア
 処理のトリガーと実行が分離されていない。
 2種類ある。
 ・ビヘイビア(Expression Blend SDKを利用する事で実装可能。SDKがあればExpressionBlendは不要)
 ・添付ビヘイビア(.NET Frameworkだけで実装可能)

トリガー&アクション
 処理のトリガーと実行(アクション)が分離されている。

メッセージ
 ・ViewModelからViewにMessageを送信。
 ・ViewではTriggerによってMessageを受信、TriggerActionを実行する。
 ・Viewでの処理が終わるとViewModelのコールバックの処理を呼び出す。→TriggerActionの実行結果がViewModelに戻る。

私の頭ではいきなり理解するのは難しそうなので、時間をかけて整理していきたい。

INotifyDataErrorInfo

入力チェックによるエラー表示について調べたのでメモ。

バインディング時に入力チェックを行うにはいくつかの方法があります。

①BindingタグでValidatesOnExceptionsを"True"に設定する事で値入力時に発生した例外のメッセージを画面に表示する。

数値型のプロパティに数値以外が入力された場合など例外が発生するケースで便利です。

条件によって自分で例外を発生させるようにコードを書けば、文字数チェックや値の範囲チェックも可能。

②エンティティクラスでINotifyDataErrorInfoを実装。

SilverLight4以降で利用可能。WPFではIDataErrorInfoを利用するそうです。

リファレンスを読んだだけだと使い方がイマイチよくわからなかったので、サンプルを作ってみました。

XAMLはこんな感じ。

        <TextBox Height="20" HorizontalAlignment="Left" Margin="20,20,0,0" Name="textBox1" VerticalAlignment="Top" Width="100">
            <TextBox.Text>
                <Binding Path="Name" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged" ValidatesOnExceptions="True" />
            </TextBox.Text>
        </TextBox>

まずはINotifyDataErrorInfoを実装せずにBinding可能なエンティティクラスを作成。

    public class Employee : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private string name;
        public string Name
        {
            get
            {
                return name;
            }
            set
            {
                this.name = value;
                NotifyPropertyChanged("Name");
            }
        }

        public void NotifyPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

    }

上記コードにINotifyDataErrorInfoを実装してみます。

    public class Employee : INotifyPropertyChanged, INotifyDataErrorInfo
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private string name;
        public string Name
        {
            get
            {
                return name;
            }
            set
            {
                this.name = value;

                // ここから追加

                nameError = false;
                if (5 < this.name.Length)
                {
                    nameError = true;
                }
                OnErrorsChanged(new DataErrorsChangedEventArgs("Name"));

                // ここまで追加

                NotifyPropertyChanged("Name");
            }
        }

        public void NotifyPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        // 以下、INotifyDataErrorInfoを利用するために追加
        bool nameError = false;

        public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

        public bool HasErrors
        {
            get { return nameError; }
        }

        public System.Collections.IEnumerable GetErrors(string propertyName)
        {
            if (propertyName == "Name" && nameError)
            {
                return new[]{"名前は5文字以内で入力してください"};
            }
            return null;
        }

        private void OnErrorsChanged(DataErrorsChangedEventArgs e)
        {
            var handler = this.ErrorsChanged;
            if (handler != null)
            {
                handler(this, e);
            }
        }
    }

MVVM QuickStartその2

MVVM QuickStartの画面に質問テキストと入力項目が表示されるまでの流れを追ってみた。

MVVM QuickStartのViewは次のような関係を持っている。

MainPage.xamlの直下がQuestionnaireView.xaml

f:id:t_jin:20120916193422j:plain

QuestionnaireView.xamlは、ViewModelとしてQuestionnaireViewModelを利用している。

    <UserControl.DataContext>
        <ViewModels:QuestionnaireViewModel />
    </UserControl.DataContext>

質問テキストを表示しているのは同ファイルの下記部分。

                <ItemsControl Grid.Row="1" IsTabStop="False" ItemsSource="{Binding Questions}">
                    <ItemsControl.Resources>
                        <DataTemplate DataType="ViewModels:OpenQuestionViewModel">
                            <Views:OpenQuestionView DataContext="{Binding}"/>
                        </DataTemplate>
                        <DataTemplate DataType="ViewModels:MultipleSelectionQuestionViewModel">
                            <Views:MultipleSelectionView DataContext="{Binding}"/>
                        </DataTemplate>
                        <DataTemplate DataType="ViewModels:NumericQuestionViewModel">
                            <Views:NumericQuestionView DataContext="{Binding}"/>
                        </DataTemplate>
                    </ItemsControl.Resources>                   
                </ItemsControl>

ItemsSourceが持つ個々の要素の型によって表示するテンプレートを使い分けている事が分かる。

ItemsSourceとして指定されているQuestionsは、QuestionnaireViewModel.csの中で定義されている。

        public ObservableCollection<QuestionViewModel> Questions { get; private set; }

どこで質問テキストをセットしているのか辿ってみたところ、QuestionnaireService.csのDoGetQuestionnaire()メソッドの中で入力チェックの条件と共に指定されていた。

               var questionnaire = new Questionnaire(
                    new NumericQuestion("How many times do you eat out per month?") { MaxValue = 15 },

・・・

Questionnaireのインスタンス化時に指定されるQuestion型のクラスは、DomainObjects.csの中に定義されている。

(※ModelのベースクラスであるDomainObject.csではない(複数形である)事に注意)

DomainObjects.csの中には複数のクラスが定義されている。

MultipleSelectionQuestion (Questionクラスを継承)
NumericQuestion (Questionクラスを継承)
OpenQuestion (Questionクラスを継承)

この3つがQuestionnaireをインスタンス化する時に指定するパラメータ。

他にも次のクラスが定義されている。
Question (DomainObjectクラスを継承)
Questionnaire (DomainObjectクラスを継承)


MVVM QuickStart

BasicMVVM QuickStartに続いて、MVVM QuickStartも動かしてみました。

 

ファイル数が少なくクラス間の関係が単純だったBasicMVVMと比べると、ファイル数が増えて実現している事も複雑になっています。

 

BasicMVVMと比べて追加されている要素は次の通り。

①INotifyDataErrorを利用した入力項目に対するバリデート

②ViewModelによるテンプレートの使い分け

ユニットテスト

 

クラス間の関係についても整理。

1.ViewModelはViewModelフォルダに入っています。

  ベースクラスはViewModel.cs(INotifyPropertyChangedを実装)

2.ModelはModelフォルダに入っています。

  ベースクラスはIngrastructureフォルダのDomainObjects.cs(INotifyPropertyChangedとINotifyDataErrorInfoを実装)

 

Modelで入力項目に対するバリデートを実装している事が分かります。

 

 

BasicMVVMの構成

Prismで動かしたサンプルについて解析。

http://jin.hatenablog.jp/entry/2012/09/11/235422

 

PrismのMVVMサンプル(BasicMVVMApp)は下記ファイルで構成されている。

  • MainPage.xaml
    実行時に表示される画面。
    QuestionnaireView.xamlが埋め込まれている。
  • MainPage.xaml.cs
    MainPage.xamlのコードビハインドファイル。
    初期化(InitializeComponent();)のみが記述されている。
  • QuestionnaireView.xaml
    View。
    入力フォームの内容が記述されたユーザーコントロール。
  • QuestionnaireView.xaml.cs
    QuestionnaireView.xamlのコードビハインドファイル。
    初期化(InitializeComponent();)のみが記述されている。
  • QuestionnaireViewModel.cs
    ViewModel。
    イベント(コマンド)が書かれている。
  • Questionnaire.cs
    Model。
    INotifyPropertyChangedを実装。

 

次のように理解。 

MVVMは目的である「ビジネスロジックとプレゼンテーションロジックの分離」を下記の方法で実現。

  1. Viewのコードビハインドファイルにロジックを書かない。
  2. 変更を通知可能(INotifyPropertyChangedを実装)であるModelを用意。
  3. View上で入力・表示される値は、Modelのプロパティにバインド。
  4. イベント(コマンド)はViewModelに記述し、Viewからバインド。

 

BasicMVVM

MVVMについて勉強したいと思う。

Prismというライブラリ?について参考になるサイトが多そうなので試してみる。

http://compositewpf.codeplex.com/

 

サンプルを動かすまでの手順

  1. VisualStudio2010に SP1 を適用。
    (※VisualStudio2010Expressの場合はVisualWebDeveloper2010Expressもインストール)
  2. Microsoft Silverlight 5 Tools for Visual Studio 2010 をインストール。
  3. Prismのインストール
     Prism 4.1 - February 2012
     http://www.microsoft.com/en-us/download/details.aspx?displaylang=en&id=28950
     Prism4.1_Source.EXE を実行→解凍先を指定。
  4. 3で作成された次のファイルを実行
     Silverlight Only - Basic MVVM QuickStart.bat
     ※VisualStudioでプロジェクトが開かれる。以下、VisualStudio上での操作。
  5. BasicMVVMApp.Web プロジェクトを右クリック、スタートアッププロジェクトに設定。
  6. BasicMVVMAppTestPage.html を右クリック、開始ページに指定。
  7. F5でデバッグ実行。
  8. 入力フォームに値を入力してSubmit ボタンを押すと、Visual Studio の 出力ウィンドウに結果が表示される

 

Submitボタンを押しても、何も反応しない!と思っていたら、結果はVisualStudioの出力ウィンドウに表示されていた。

ボタンが反応してないのかと思って色々調べてしまった・・・

 

VisualStudioの規定のブラウザにGoogleChromeを設定していたが、F5実行で表示がエラーになっていたため規定のブラウザをIEに変更。

ChromeSilverLightプラグインがインストールされていないため?)

 

VisualStudio 規定のブラウザの変更方法】
「IE」 →「ツール」 → 「インターネットオプション」→「プログラム」→「既定の Web ブラウザー」→「Internet Explorer が既定の Web ブラウザーでない場合に通知する」にチェック