web-dev-qa-db-ja.com

MVVM:モデルをサーバーバージョンと同期させたままモデルにバインドする

私はかなりの時間を費やして、次の課題に対するエレガントな解決策を見つけようとしました。問題をハックする以上の解決策を見つけることができませんでした。

View、ViewModel、Modelの簡単なセットアップがあります。説明のために、非常に単純にしておきます。

  • Modelには、String型のTitleと呼ばれる単一のプロパティがあります。
  • Modelは、ViewのDataContextです。
  • Viewには、モデルのTextBlockにデータバインドされたTitleがあります。
  • ViewModelにはSave()というメソッドがあります。このメソッドはModelServerに保存します。
  • Serverは、Modelに加えられた変更をプッシュできます

ここまでは順調ですね。モデルをServerと同期させるために、2つの調整を行う必要があります。サーバーの種類は重要ではありません。モデルを_Server._にプッシュするには、Save()を呼び出す必要があることを知っておいてください。

調整1:

  • ModelによってServerに加えられた変更をViewに変換するには、_Model.Title_プロパティはRaisePropertyChanged()を呼び出す必要があります。 ModelViewのDataContextであるため、これはうまく機能します

悪くない。

調整2:

  • 次のステップは、Save()を呼び出して、ViewからModelServerに加えられた変更を保存することです。これは私が立ち往生しているところです。モデルが変更されたときにSave()を呼び出すViewModelの_Model.PropertyChanged_イベントを処理できますが、これにより、サーバーによって行われた変更がエコーされます。

私はエレガントで論理的な解決策を探しており、それが理にかなっている場合はアーキテクチャを変更したいと思っています。

29
ndsc

過去に、複数の場所からのデータオブジェクトの「ライブ」編集をサポートするアプリケーションを作成しました。アプリの多くのインスタンスが同じオブジェクトを同時に編集でき、誰かがサーバーに変更をプッシュすると、他のすべてのユーザーに通知が届き、 (最も単純なシナリオでは)これらの変更をすぐに確認します。これがどのように設計されたかの要約です。

セットアップ

  1. ビュー常に ViewModelsにバインドします。ボイラープレートがたくさんあることは知っていますが、モデルに直接バインドすることは、最も単純なシナリオ以外では受け入れられません。また、MVVMの精神ではありません。

  2. ViewModelsにはsole変更をプッシュする責任があります。これには明らかにサーバーへの変更のプッシュが含まれますが、アプリケーションの他のコンポーネントへの変更のプッシュも含まれる場合があります。

    これを行うために、ViewModelsはcloneラップするモデルを使用して、サーバーに提供するときにアプリの残りの部分にトランザクションセマンティクスを提供できるようにします(つまり、変更をプッシュするタイミングを選択できますアプリの残りの部分。全員が同じモデルインスタンスに直接バインドしている場合は実行できません)。このように変更を分離するには、まだより多くの作業が必要ですが、強力な可能性も開かれます(たとえば、変更を元に戻すのは簡単です。プッシュしないでください)。

  3. ViewModelは、ある種のデータサービスに依存しています。データサービスは、データストアとコンシューマーの間に位置し、それらの間のすべての通信を処理するアプリケーションコンポーネントです。 ViewModelがそのモデルのクローンを作成するときはいつでも、DataServiceが公開する適切な「データストア変更」イベントにもサブスクライブします。

    これにより、他のViewModelがデータストアにプッシュした「自分の」モデルへの変更をViewModelに通知し、適切に対応することができます。適切な抽象化があれば、データストアは何でもかまいません(たとえば、その特定のアプリケーションのWCFサービス)。

ワークフロー

  1. ViewModelが作成され、モデルの所有権が割り当てられます。モデルのクローンをすぐに作成し、このクローンをビューに公開します。データサービスに依存しているため、この特定のモデルを更新するための通知をサブスクライブすることをDSに通知します。ViewModelは、そのモデルを識別するものが何であるかを認識していません(「プライマリ」キー」)、しかしそれはDSの責任であるため、そうする必要はありません。

  2. ユーザーが編集を終了すると、VMでコマンドを呼び出すビューを操作します。次に、VMはDSを呼び出し、クローンモデルに加えられた変更をプッシュします。

  3. DSは変更を永続化し、さらに、モデルXへの変更が行われたことを他のすべての対象VMに通知するイベントを発生させます。モデルの新しいバージョンはイベント引数の一部として提供されます。

  4. 同じモデルの所有権が割り当てられている他のVMは、外部の変更が到着したことを認識します。パズルのすべてのピースを手元に置いてビューを更新する方法を決定できるようになりました(クローンされたモデルの「前の」バージョン、クローンである「ダーティ」バージョン、および現在の」バージョン。イベント引数の一部としてプッシュされました)。

ノート

  • モデルのINotifyPropertyChangedは、ビューによってのみ使用されます。 ViewModelがモデルが「ダーティ」であるかどうかを知りたい場合は、いつでもクローンを元のバージョンと比較できます(保持されている場合は、可能であればお勧めします)。
  • ViewModelは、変更をアトミックにサーバーにプッシュします。これは、データストアが常に一貫した状態にあることを保証するためです。これは設計上の選択であり、別の方法で作業を行う場合は、別の設計がより適切です。
  • サーバーは、ViewModelがパラメーターとしてthisを「Pushchanges」呼び出しに渡す場合、この変更の原因となったViewModelの「Modelchanged」イベントを発生させないことを選択できます。そうでない場合でも、モデルの「現在の」バージョンがそれ自体のクローンと同一であることがわかった場合、ViewModelは何もしないことを選択できます。
  • 十分な抽象化があれば、変更をシェル内の他のビューにプッシュするのと同じくらい簡単に、他のマシンで実行されている他のプロセスにプッシュできます。

お役に立てれば;必要に応じて、さらに詳しい説明を提供できます。

67
Jon

更新パターンを単純化するために、コントローラーをMVVMミックス(MVCVM?)に追加することをお勧めします。

コントローラは、より高いレベルで変更をリッスンし、ModelとViewModelの間で変更を伝播します。

物事をきれいに保つための基本的なルールは次のとおりです。

  • ViewModelは、特定の形状のデータを保持する単なるダムコンテナです。 データがどこから来たのか、どこに表示されているのかわかりません。
  • ビューは、特定の形状のデータを表示します(ビューモデルへのバインディングを介して)。 彼らはデータがどこから来たのかを知らず、それを表示する方法だけを知っています。
  • モデルは実際のデータを提供します。 彼らはそれがどこで消費されるかを知りません。
  • コントローラはロジックを実装します。 VMでICommandのコードを提供したり、データへの変更をリッスンしたりします。モデルからVMにデータを入力します。 VMの変更をリッスンし、モデルを更新することは理にかなっています。

別の回答で述べたように、DataContextはモデルではなくVM(またはそのプロパティ)である必要があります。DataModelをポイントすると、関心の分離が困難になります(テストなど)駆動開発)。

他のほとんどのソリューションは、「正しくない」ロジックをViewModelsに配置しますが、コントローラーの利点は常に見過ごされているようです。 MVVMの頭字語だ! :)

6
Gone Coding

モデルを直接表示するためのバインディングは、モデルがINotifyPropertyChangedインターフェイスを実装している場合にのみ機能します。 (例:Entity Frameworkによって生成されたモデル)

モデル実装INotifyPropertyChanged

あなたはこれを行うことができます。

public interface IModel : INotifyPropertyChanged //just sample model
{
    public string Title { get; set; }
}

public class ViewModel : NotificationObject //prism's ViewModel
{
    private IModel model;

    //construct
    public ViewModel(IModel model)
    {
        this.model = model;
        this.model.PropertyChanged += new PropertyChangedEventHandler(model_PropertyChanged);
    }

    private void model_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "Title")
        {
            //Do something if model has changed by external service.
            RaisePropertyChanged(e.PropertyName);
        }
    }
    //....more properties
}

DTOとしてのViewModel

モデルがINotifyPropertyChangedを実装している場合(状況によって異なります)、ほとんどの場合、それをDataContextとして使用できます。ただし、DDDでは、ほとんどのMVVMモデルは実際のドメインのモデルではなくEntityObjectと見なされます。

より効率的な方法は、ViewModelをDTOとして使用することです

//Option 1.ViewModel act as DTO / expose some Model's property and responsible for UI logic.
public string Title
{
    get 
    {
        // some getter logic
        return string.Format("{0}", this.model.Title); 
    }
    set
    {
        // if(Validate(value)) add some setter logic
        this.model.Title = value;
        RaisePropertyChanged(() => Title);
    }
}

//Option 2.expose the Model (have self validation and implement INotifyPropertyChanged).
public IModel Model
{
    get { return this.model; }
    set
    {
        this.model = value;
        RaisePropertyChanged(() => Model);
    }
}

上記のViewModelの両方のプロパティは、実際に依存するMVVMパターン(パターン!=ルール)を壊さずにバインドに使用できます。

もう1つ..ViewModelはModelに依存しています。モデルが外部サービス/環境によって変更される可能性がある場合。物事を複雑にするのは「グローバルな状態」です。

1
aifarfa

この問題が発生する理由は、モデルが汚れているかどうかを認識していないためです。

string Title {
  set {
    this._title = value;
    this._isDirty = true; // ??!!
  }
}}

解決策は、別の方法でサーバーの変更をコピーすることです。

public void CopyFromServer(Model serverCopy)
{
  this._title = serverCopy.Title;
}
0
Chui Tey

サーバーからの変更がすぐに再保存されることが唯一の問題である場合は、次のようなことをしてみませんか。

//WARNING: typed in SO window
public class ViewModel
{
    private string _title;
    public string Title
    {
        get { return _title; }
        set
        {
            if (value != _title) 
            {
                _title = value;
                this.OnPropertyChanged("Title");
                this.BeginSaveToServer();
            }
        }
    }

    public void UpdateTitleFromServer(string newTitle)
    {
        _title = newTitle;
        this.OnPropertyChanged("Title"); //alert the view of the change
    }
}

このコードは、プロパティセッターを経由せずに、したがって「サーバーに保存」コードを呼び出さずに、サーバーからのプロパティ変更のビューを手動で警告します。

0
Steve Greatrex