none
MVVMでModelからViewへの結果の伝播 RRS feed

  • 質問

  • MVVMに沿ったWPFアプリケーションについて学習をはじめました。

    「動作しない」のではなくて「果たしてこれでいいのだろうか?」という質問なのですが、

    例として掛け算を行うプログラムを考えます。Viewに2個のTextBoxと回答用にReadOnlyにした1個のTextBoxがあります。

    <TextBox Text="{Binding Path=左辺}"/>
    <TextBox Text="{Binding Path=右辺}"/>
    <TextBox Text="{Binding Path=結果,Mode=OneWay}" IsReadOnly="True"/>
    

    ViewModelはPrismのBindableBaseを既定クラスに使い、モデルへの参照_modelを持ち、コンストラクタ内でモデルのPropertyChangedに結果取得用のコードを設定しています。

       class MainWindowsViewModel:BindableBase
        {
            AppModel _model = new AppModel();
    
            private int _左辺;
            private int _右辺;
            private long _結果;
    
            public MainWindowsViewModel()
            {
                _model.PropertyChanged += _model_PropertyChanged;
            }
    
            public int 左辺
            {
                get { return _左辺; }
                set
                {
                    if (this.SetProperty(ref this._左辺, value)==true)
                    {
                        _model.左辺 = this._左辺;
                    }
                }
            }
    
            public int 右辺
            {
                get { return _右辺; }
                set
                {
                    if (this.SetProperty(ref this._右辺, value)==true)
                    {
                        _model.右辺 = this._右辺;
                    }
                }
            }
    
            void _model_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
            {
                結果 = _model.結果;
            }
    
            public long 結果
            {
                get { return _結果; }
                set
                {
                    this.SetProperty(ref this._結果, value);
                }
            }
    
        }

    Modelでは同じくPrismのBindableBaseを基底にして右辺左辺のプロパティが変更されると結果を計算するようコンストラクタで登録しています。

        class AppModel:BindableBase
        {
            private int _左辺;
            private int _右辺;
            private long _結果;
    
            public AppModel()
            {
                PropertyChanged += AppModel_PropertyChanged;
            }
    
            public int 左辺
            {
                get { return _左辺; }
                set
                {
                    this.SetProperty(ref this._左辺, value);
                }
            }
    
            public int 右辺
            {
                get { return _右辺; }
                set
                {
                    this.SetProperty(ref this._右辺, value);
                }
            }
    
            public long 結果
            {
                get { return _結果; }
                set
                {
                    this.SetProperty(ref this._結果, value);
                }
    
            }
    
            void AppModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
            {
                結果 = Calc(左辺, 右辺);
            }
    
            private long Calc(int a, int b)
            {
                return a * b;
            }
        }

    ViewからViewModel,Modelへの入力値の流れと、Modelから逆方向に結果を戻す流れを作ったつもりなのですが

    質問1.標準的(?)な方法としてはこのようなコードでいいのでしょうか?

    1-1. 今回はViewの入力箇所が2箇所しかありませんが、たとえば20箇所とかあって複雑なデータの流れがあった場合も、modelのxxx_PropertyChangedの中で全部やってしまっていいのでしょうか。

    1-2.xxx_PropertyChangedをコンストラクタでPropertyChangedに登録していますが、この場所でやるのは適当でしょうか。

    2014年11月8日 4:52

回答

  • コード全体としては誤った考え方ではないと思いますが、イベント購読によってModelがViewModelへの強い参照を得てしまいますので、ViewModelがModelよりも短命だった場合でもViewModelがGCに回収されずに残り続けてしまう可能性があります。MVVMでありがちなメモリーリークのパターンですので、今回は大丈夫かもしれませんが、安易にModelのイベントを購読するのは危険です。

    また、コード全体としては、Modelのプロパティを一度ViewModelで中継する必要はなく、Model自体がバインドされることを意識したプロパティを持っていますので、直接Viewにバインドしてあげればすっきりしますし、上記のようなイベントの購読も発生しません。

    追記)メモリーリークに関しては、「WPF MVVM メモリーリーク 弱い参照」辺りで検索してみて下さい。いろいろ見つかると思います。また、Modelを直接バインドするというのは、ViewModelがModelをプロパティとして公開し、それをViewがバインドするということです。私はこのようなオブジェクトを、ModelというよりUIオブジェクトと呼んでいます(私が考えたわけではなく、ネット上で知った単語です)。時にUIオブジェクトはModelをラップします。


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

    2014年11月8日 5:24
    モデレータ
  • 「WPF MVVM メモリーリーク 弱い参照」については検索してみます。

    参考になるか判りませんが、国産MVVMフレームワークである Livet なら、View 終了と連動して ViewModel を Dispose したり、イベントを一括管理し ViewModel のDispose時にイベント解除したり、弱参照でイベントを登録できる機能が多数提供されてます。
    詳しくは以下パワーポイント資料をご覧ください。

    WPF-MVVM-Infrastructure-Livet 

    MSDNフォーラムのヘルプは以下ご覧ください http://social.technet.microsoft.com/wiki/contents/articles/7359.forums-help-faq.aspx

    • 回答としてマーク 星 睦美 2014年12月2日 8:14
    2014年11月11日 7:42
    モデレータ

すべての返信

  • コード全体としては誤った考え方ではないと思いますが、イベント購読によってModelがViewModelへの強い参照を得てしまいますので、ViewModelがModelよりも短命だった場合でもViewModelがGCに回収されずに残り続けてしまう可能性があります。MVVMでありがちなメモリーリークのパターンですので、今回は大丈夫かもしれませんが、安易にModelのイベントを購読するのは危険です。

    また、コード全体としては、Modelのプロパティを一度ViewModelで中継する必要はなく、Model自体がバインドされることを意識したプロパティを持っていますので、直接Viewにバインドしてあげればすっきりしますし、上記のようなイベントの購読も発生しません。

    追記)メモリーリークに関しては、「WPF MVVM メモリーリーク 弱い参照」辺りで検索してみて下さい。いろいろ見つかると思います。また、Modelを直接バインドするというのは、ViewModelがModelをプロパティとして公開し、それをViewがバインドするということです。私はこのようなオブジェクトを、ModelというよりUIオブジェクトと呼んでいます(私が考えたわけではなく、ネット上で知った単語です)。時にUIオブジェクトはModelをラップします。


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

    2014年11月8日 5:24
    モデレータ
  • ありがとうございます。

    ViewModelがModelの参照を持って

            void _model_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
            {
                結果 = _model.結果;
            }

    コンストラクタ内でイベントに対して登録しているのは、Model内で完結している分にはOK. ViewModel内で_modelに参照してるのが問題ということでしょうか。

    「WPF MVVM メモリーリーク 弱い参照」については検索してみます。

    ModelがViewModel同然なのは試みとしてViewからModelまでの流れを確認してみたかったためで、実際作るとしたら私もModelをそのままViewにつなげると思います。

    もう少し調べてみようと思いますので、疑問が出てきたらまたポストするかと思います。

    2014年11月8日 7:07
  • コンストラクタ内でイベントに対して登録しているのは、Model内で完結している分にはOK. ViewModel内で_modelに参照してるのが問題ということでしょうか。

    簡単に書くと、ViewModelでModelのイベントを購読すると、ModelがViewModelの参照を握ってしまいます。よって、ViewModelが先に終わってもModelが終了しない限り、ViewModelはGCに回収されないことになります。言いかえれば、ViewModelとModelが同じタイミングで終了する場合は問題ないということです。


    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

    2014年11月8日 8:47
    モデレータ
  • 単純に考えると、 += したものを -= すれば良いような気がするのですが、

    デストラクタ内で

            ~MainWindowsViewModel()
            {
                _model.PropertyChanged -= _model_PropertyChanged;
            }
    

    したり、可能であればDisposeメソッドの中で -=したりするのは、結局 AppModelが強い参照を握ったままなのでGCに回収されることなく、結局、デストラクタもDisposeもいつまでたっても呼ばれない。と、いうことになるのでしょうか

    弱いイベントパターンで、WeakEventManager とか EventAggregator などのキーワードが引っかかったのですが、記事を読んでみてもさっぱりで、しばらく時間を置いてから再チャレンジしてみようと思います。

    2014年11月10日 4:20
  • 質問1.標準的(?)な方法としてはこのようなコードでいいのでしょうか?

    正直標準的ではないと思います。MVVMのよくある誤解のひとつに、

    「ViewModel は View と Model の仲介者であり、View と Model の接続は ViewModel を仲介しなければならない」

    というのがあるようですが、私としては、プレゼンテーション層とドメイン層のレイヤーがきちんと分離されているなら、直接Model を View にバインドしても構わないと思ってます。この辺りの考えは trapemiyaさんの仰られる

    コード全体としては、Modelのプロパティを一度ViewModelで中継する必要はなく、Model自体がバインドされることを意識したプロパティを持っていますので、直接Viewにバインドしてあげればすっきりしますし、上記のようなイベントの購読も発生しません。

    に全く同意です。


    MSDNフォーラムのヘルプは以下ご覧ください http://social.technet.microsoft.com/wiki/contents/articles/7359.forums-help-faq.aspx

    2014年11月11日 1:36
    モデレータ
  • 単純に考えると、 += したものを -= すれば良いような気がするのですが、

    もちろん、そのように購読を解除してあげれば問題ありません。一般的な画面のシナリオでは、ViewModelとModelの寿命は同じですので、メモリーリークすることはあまりないとは思いますが、MVVMの性質上、ViewModelとModelは異なる寿命を取り得ますから、その場合に注意が必要であり、このことを頭の隅に入れられておかれれば良いと思います。

    ★良い回答には回答済みマークを付けよう! わんくま同盟 MVP - Visual C# http://d.hatena.ne.jp/trapemiya/

    2014年11月11日 3:00
    モデレータ
  • 「WPF MVVM メモリーリーク 弱い参照」については検索してみます。

    参考になるか判りませんが、国産MVVMフレームワークである Livet なら、View 終了と連動して ViewModel を Dispose したり、イベントを一括管理し ViewModel のDispose時にイベント解除したり、弱参照でイベントを登録できる機能が多数提供されてます。
    詳しくは以下パワーポイント資料をご覧ください。

    WPF-MVVM-Infrastructure-Livet 

    MSDNフォーラムのヘルプは以下ご覧ください http://social.technet.microsoft.com/wiki/contents/articles/7359.forums-help-faq.aspx

    • 回答としてマーク 星 睦美 2014年12月2日 8:14
    2014年11月11日 7:42
    モデレータ
  • 返信ありがとうございます。

    今回は、ViewからViewModel経由でModelに入力値を渡し、Modelでの結果をViewまで戻してくるという流れを確認したかったのでわざと冗長な感じにしています。

    2014年11月12日 4:52
  • ありがとうございます。さっそく拝見させていただきます。

    2014年11月12日 4:54