私はかなりの時間を費やして、次の課題に対するエレガントな解決策を見つけようとしました。問題をハックする以上の解決策を見つけることができませんでした。
View、ViewModel、Modelの簡単なセットアップがあります。説明のために、非常に単純にしておきます。
Model
には、String型のTitle
と呼ばれる単一のプロパティがあります。Model
は、View
のDataContextです。View
には、モデルのTextBlock
にデータバインドされたTitle
があります。ViewModel
にはSave()
というメソッドがあります。このメソッドはModel
をServer
に保存します。Server
は、Model
に加えられた変更をプッシュできますここまでは順調ですね。モデルをServer
と同期させるために、2つの調整を行う必要があります。サーバーの種類は重要ではありません。モデルを_Server.
_にプッシュするには、Save()
を呼び出す必要があることを知っておいてください。
調整1:
Model
によってServer
に加えられた変更をView
に変換するには、_Model.Title
_プロパティはRaisePropertyChanged()
を呼び出す必要があります。 Model
はView
のDataContextであるため、これはうまく機能します悪くない。
調整2:
Save()
を呼び出して、View
からModel
のServer
に加えられた変更を保存することです。これは私が立ち往生しているところです。モデルが変更されたときにSave()を呼び出すViewModel
の_Model.PropertyChanged
_イベントを処理できますが、これにより、サーバーによって行われた変更がエコーされます。私はエレガントで論理的な解決策を探しており、それが理にかなっている場合はアーキテクチャを変更したいと思っています。
過去に、複数の場所からのデータオブジェクトの「ライブ」編集をサポートするアプリケーションを作成しました。アプリの多くのインスタンスが同じオブジェクトを同時に編集でき、誰かがサーバーに変更をプッシュすると、他のすべてのユーザーに通知が届き、 (最も単純なシナリオでは)これらの変更をすぐに確認します。これがどのように設計されたかの要約です。
ビュー常に ViewModelsにバインドします。ボイラープレートがたくさんあることは知っていますが、モデルに直接バインドすることは、最も単純なシナリオ以外では受け入れられません。また、MVVMの精神ではありません。
ViewModelsにはsole変更をプッシュする責任があります。これには明らかにサーバーへの変更のプッシュが含まれますが、アプリケーションの他のコンポーネントへの変更のプッシュも含まれる場合があります。
これを行うために、ViewModelsはcloneラップするモデルを使用して、サーバーに提供するときにアプリの残りの部分にトランザクションセマンティクスを提供できるようにします(つまり、変更をプッシュするタイミングを選択できますアプリの残りの部分。全員が同じモデルインスタンスに直接バインドしている場合は実行できません)。このように変更を分離するには、まだより多くの作業が必要ですが、強力な可能性も開かれます(たとえば、変更を元に戻すのは簡単です。プッシュしないでください)。
ViewModelは、ある種のデータサービスに依存しています。データサービスは、データストアとコンシューマーの間に位置し、それらの間のすべての通信を処理するアプリケーションコンポーネントです。 ViewModelがそのモデルのクローンを作成するときはいつでも、DataServiceが公開する適切な「データストア変更」イベントにもサブスクライブします。
これにより、他のViewModelがデータストアにプッシュした「自分の」モデルへの変更をViewModelに通知し、適切に対応することができます。適切な抽象化があれば、データストアは何でもかまいません(たとえば、その特定のアプリケーションのWCFサービス)。
ViewModelが作成され、モデルの所有権が割り当てられます。モデルのクローンをすぐに作成し、このクローンをビューに公開します。データサービスに依存しているため、この特定のモデルを更新するための通知をサブスクライブすることをDSに通知します。ViewModelは、そのモデルを識別するものが何であるかを認識していません(「プライマリ」キー」)、しかしそれはDSの責任であるため、そうする必要はありません。
ユーザーが編集を終了すると、VMでコマンドを呼び出すビューを操作します。次に、VMはDSを呼び出し、クローンモデルに加えられた変更をプッシュします。
DSは変更を永続化し、さらに、モデルXへの変更が行われたことを他のすべての対象VMに通知するイベントを発生させます。モデルの新しいバージョンはイベント引数の一部として提供されます。
同じモデルの所有権が割り当てられている他のVMは、外部の変更が到着したことを認識します。パズルのすべてのピースを手元に置いてビューを更新する方法を決定できるようになりました(クローンされたモデルの「前の」バージョン、クローンである「ダーティ」バージョン、および現在の」バージョン。イベント引数の一部としてプッシュされました)。
INotifyPropertyChanged
は、ビューによってのみ使用されます。 ViewModelがモデルが「ダーティ」であるかどうかを知りたい場合は、いつでもクローンを元のバージョンと比較できます(保持されている場合は、可能であればお勧めします)。this
を「Pushchanges」呼び出しに渡す場合、この変更の原因となったViewModelの「Modelchanged」イベントを発生させないことを選択できます。そうでない場合でも、モデルの「現在の」バージョンがそれ自体のクローンと同一であることがわかった場合、ViewModelは何もしないことを選択できます。お役に立てれば;必要に応じて、さらに詳しい説明を提供できます。
更新パターンを単純化するために、コントローラーをMVVMミックス(MVCVM?)に追加することをお勧めします。
コントローラは、より高いレベルで変更をリッスンし、ModelとViewModelの間で変更を伝播します。
物事をきれいに保つための基本的なルールは次のとおりです。
別の回答で述べたように、DataContext
はモデルではなくVM(またはそのプロパティ)である必要があります。DataModelをポイントすると、関心の分離が困難になります(テストなど)駆動開発)。
他のほとんどのソリューションは、「正しくない」ロジックをViewModelsに配置しますが、コントローラーの利点は常に見過ごされているようです。 MVVMの頭字語だ! :)
モデルを直接表示するためのバインディングは、モデルがINotifyPropertyChangedインターフェイスを実装している場合にのみ機能します。 (例:Entity Frameworkによって生成されたモデル)
あなたはこれを行うことができます。
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
}
モデルが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に依存しています。モデルが外部サービス/環境によって変更される可能性がある場合。物事を複雑にするのは「グローバルな状態」です。
この問題が発生する理由は、モデルが汚れているかどうかを認識していないためです。
string Title {
set {
this._title = value;
this._isDirty = true; // ??!!
}
}}
解決策は、別の方法でサーバーの変更をコピーすることです。
public void CopyFromServer(Model serverCopy)
{
this._title = serverCopy.Title;
}
サーバーからの変更がすぐに再保存されることが唯一の問題である場合は、次のようなことをしてみませんか。
//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
}
}
このコードは、プロパティセッターを経由せずに、したがって「サーバーに保存」コードを呼び出さずに、サーバーからのプロパティ変更のビューを手動で警告します。