MVVM:绑定到模型,同时保持模型与服务器版本同步

我花了相当一段时间来尝试find一个优雅的解决scheme,以应付下面的挑战。 我一直无法find解决问题的办法。

我有一个View,ViewModel和Model的简单设置。 为了解释,我会保持简单。

  • Model有一个名为stringTitle的单一属性。
  • ModelView的DataContext。
  • View有一个TextBlock这就是模型上的Title绑定。
  • ViewModel有一个名为Save()的方法,将Model保存到Server
  • Server可以推送对Model所做的更改

到现在为止还挺好。 现在为了保持模型与Server同步,我需要做两个调整。 服务器的types并不重要。 只要知道我需要调用Save()以将模型推送到Server.

调整1:

  • Model.Title属性将需要调用RaisePropertyChanged()以将ServerModel所做的更改转换为View 。 这很好,因为ModelView的DataContext

不错。

调整2:

  • 下一步是调用Save()将从View所做的更改保存到Server上的Model 。 这是我卡住的地方。 我可以在ViewModel上处理Model.PropertyChanged事件,该事件在模型被更改时调用Save(),但是这使得它响应服务器所做的更改。

我正在寻找一个优雅和合乎逻辑的解决scheme,如果有意义,我愿意改变我的架构。

在过去,我编写了一个应用程序,支持从多个位置“实时”编辑数据对象:应用程序的许多实例可以同时编辑同一个对象,当有人将更改推送到服务器时,其他人都会收到通知(在最简单的情况下)立即看到这些变化。 下面是它是如何devise的摘要。

build立

  1. 视图总是绑定到ViewModels。 我知道这是很多的样板,但直接绑定到模型是不可接受的,除了最简单的情况, 这也不符合MVVM的精神。

  2. ViewModel有责任推动更改。 这显然包括将更改推送到服务器,但也可能包括将更改推送到应用程序的其他组件。

    要做到这一点,ViewModels可能想要克隆他们包装的模型,以便他们可以提供事务语义到应用程序的其余部分,因为他们提供给服务器(即您可以select何时推送更改到应用程序的其余部分,如果每个人都直接绑定到相同的Model实例,则不能这样做)。 隔离这样的变化需要更多的工作,但是它也开辟了强大的可能性(例如,撤销更改是微不足道的,只是不要推它们)。

  3. ViewModels依赖于某种数据服务。 数据服务是位于数据存储和消费者之间的应用程序组件,处理它们之间的所有通信。 每当ViewModel克隆它的模型时,它也会订阅数据服务公开的适当的“数据存储改变”事件。

    这允许ViewModel被通知其他ViewModel推送到数据存储并适当反应的“他们的”模型的变化。 有了适当的抽象,数据存储也可以是任何东西(例如在特定应用程序中的WCF服务)。

工作stream程

  1. 一个ViewModel被创build并分配一个模型的所有权。 它立即克隆模型并将此克隆公开给View。 依赖于数据服务,它告诉DS它想要订阅通知来更新这个特定的模型。 ViewModel不知道识别它的模型是什么(“主键”),但它不需要,因为这是DS的责任。

  2. 当用户完成编辑时,他们与在VM上调用命令的View进行交互。 虚拟机然后调用DS,推动对其克隆模型的改变。

  3. DS持续进行更改,并通过所有其他感兴趣的VM通知对X型进行更改的事件; 该模型的新版本作为事件参数的一部分提供。

  4. 其他已分配相同模型所有权的虚拟机现在知道外部更改已经到达。 他们现在可以决定如何更新视图,包含手头所有难题(被克隆的“旧版”模型,作为克隆的“脏”版本以及“当前”版本作为事件参数的一部分被推送)。

笔记

  • 该模型的INotifyPropertyChanged只用于View; 如果ViewModel想要知道模型是否“脏”,它总是可以将克隆与原始版本进行比较(如果它已经保留在周围,我build议如果可能的话)。
  • ViewModel以primefaces方式将更改推送到服务器,这很好,因为它确保数据存储始终处于一致状态。 这是一个deviseselect,如果你想做不同的事情,另一种devise会更合适。
  • 如果ViewModel this参数作为parameter passing给“push changes”调用,则服务器可以select不引发负责此更改的ViewModel的“Model changed”事件。 即使不这样做,如果ViewModel发现模型的“当前”版本与自己的克隆相同,它也可以select不做任何事情。
  • 有了足够的抽象,可以将更改推送到在其他计算机上运行的其他进程,就像将它们推送到shell中的其他视图一样。

希望这可以帮助; 如果需要,我可以提供更多的说明。

我会build议添加控制器到MVVM混合(MVCVM?)来简化更新模式。

控制器监听更高级别的变化并在Model和ViewModel之间传播变化。

保持清洁的基本规则是:

  • ViewModels只是一个容纳一定形状的数据的愚蠢的容器。 他们不知道数据来自何处或显示的位置。
  • 视图显示特定形状的数据(通过绑定到视图模型)。 他们不知道数据来自何处,只是如何显示数据。
  • 模型提供真实的数据。 他们不知道它在哪里被消耗。
  • 控制器实现逻辑。 比如在虚拟机中提供ICommands的代码,监听数据的改变等。它们从模型中填充虚拟机。 让他们监听VM更改并更新模型是有道理的。

正如在另一个答案中提到的,你的DataContext应该是VM(或者它的属性),而不是模型。 指向DataModel使得很难区分顾虑(例如对于testing驱动开发)。

大多数其他的解决scheme把ViewModel中的逻辑放在“不正确”的位置,但是我看到控制器一直忽略了它的好处。 这是MVVM的首字母缩写! 🙂

绑定模型直接查看只有在模型实现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

如果Model实现INotifyPropertyChanged(取决于),则可以在大多数情况下将其用作DataContext。 但在DDD中,大多数MVVM模型将被视为EntityObject而不是真正的Domain模型。

更有效的方法是使用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的属性都可以用于Binding,而不是打破MVVM模式(pattern!= rule),这真的取决于。

还有一件事ViewModel依赖于Model。 如果模型可以被外部服务/环境改变。 让事情变得复杂的是“全球状态”。

如果你唯一的问题是服务器的改变立即被重新保存,为什么不做如下的事情:

 //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 } } 

此代码手动警告从服务器的属性更改的视图,而不通过属性设置器,因此不调用“保存到服务器”代码。

你有这个问题的原因是因为你的模型不知道它是不是很脏。

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

解决方法是通过单独的方法复制服务器更改:

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