MVVM:绑定到模型,同时保持模型与服务器版本同步
Posted
技术标签:
【中文标题】MVVM:绑定到模型,同时保持模型与服务器版本同步【英文标题】:MVVM: Binding to Model while keeping Model in sync with a server version 【发布时间】:2012-05-13 07:32:17 【问题描述】:我花了很长时间尝试为以下挑战找到一个优雅的解决方案。我一直无法找到解决问题的方法。
我有一个 View、ViewModel 和 Model 的简单设置。为了解释起见,我将保持非常简单。
Model
有一个名为Title
的字符串类型的属性。
Model
是 View
的 DataContext。
View
有一个 TextBlock
数据绑定到模型上的 Title
。
ViewModel
有一个名为Save()
的方法,可以将Model
保存到Server
Server
可以推送对Model
所做的更改
到目前为止一切顺利。现在我需要进行两项调整以使模型与Server
保持同步。服务器的类型并不重要。只知道我需要调用Save()
才能将模型推送到Server.
调整一:
Model.Title
属性将需要调用RaisePropertyChanged()
,以便将Server
对Model
所做的更改转换为View
。这很好用,因为 Model
是 View
的 DataContext
还不错。
调整 2:
下一步是调用Save()
以保存从View
到Model
上的Model
所做的更改Server
。这就是我卡住的地方。我可以处理 ViewModel
上的 Model.PropertyChanged
事件,该事件在模型发生更改时调用 Save(),但这会使其响应服务器所做的更改。
我正在寻找一个优雅且合乎逻辑的解决方案,如果可行,我愿意更改我的架构。
【问题讨论】:
有些奇怪.. 使用模型作为数据上下文?这不是实际的 MVVM。首先只是测试 ViewModel,就好像“不存在 View”一样。 应用程序的 UI 很重,我选择尝试一种将模型直接转换为视图的方法。但是,某些属性由 ViewModel 处理。看到这个:***.com/a/10324065/1120175 @ndsc 请深入了解您引用的问题,尤其是我的回答。作为 DataContext 的模型确实严重违反了 MVVM 模式。 【参考方案1】:我建议将控制器添加到 MVVM 组合(MVCVM?)中以简化更新模式。
控制器在更高级别侦听更改并在模型和视图模型之间传播更改。
保持清洁的基本规则是:
ViewModel 只是保存特定形状数据的哑容器。 他们不知道数据来自哪里或显示在哪里。 视图显示特定形状的数据(通过绑定到视图模型)。 他们不知道数据来自哪里,只知道如何显示。 模型提供真实数据。 他们不知道在哪里消费。 控制器实现逻辑。诸如在 VM 中为 ICommand 提供代码、监听数据更改等事情。它们从模型中填充 VM。让他们监听 VM 更改并更新模型是有意义的。正如另一个答案中提到的,您的 DataContext
应该是 VM(或它的属性),而不是模型。指向 DataModel 会使关注点难以分离(例如,对于测试驱动开发)。
大多数其他解决方案将逻辑放在“不正确”的 ViewModel 中,但我看到控制器的好处一直被忽视。 该死的 MVVM 首字母缩写词! :)
【讨论】:
在这个架构中,控制器是否监听模型属性变化(和集合变化)并相应地更新虚拟机? @redcurry:是的,在这种模式下,所有逻辑都在控制器中。它决定如何处理数据更改。 谢谢。我一直在使用这种 MVCVM 或 MVMC 模式(至少是我的版本),它很棒。我的视图模型没有太多的依赖和责任,并且更容易实现撤消功能、日志记录、错误报告等。但是,这种模式的示例并不多(任何?)可以看到其他人是如何实现的它。您知道使用此模式的任何在线可用代码(或您可以共享的代码)吗?再次感谢。 @redcurry:我们在生产中看到它的唯一地方是第 3 方框架生成器 Sculture:sculpture.codeplex.com,但感觉不错。【参考方案2】:过去,我编写了一个应用程序,它支持从多个位置“实时”编辑数据对象:应用程序的许多实例可以同时编辑同一个对象,当有人将更改推送到服务器时,其他所有人得到通知并且(在最简单的情况下)立即看到这些更改。以下是其设计方式的摘要。
设置
视图总是绑定到 ViewModel。我知道它有很多样板,但是直接绑定到模型是不可接受的,除了最简单的场景;它也不符合 MVVM 的精神。
ViewModel 单独负责推动更改。这显然包括将更改推送到服务器,但也可能包括将更改推送到应用程序的其他组件。
为此,ViewModel 可能希望克隆它们包装的模型,以便它们可以向应用程序的其余部分提供事务语义,就像它们提供给服务器一样(即,您可以选择何时推送更改应用程序的其余部分,如果每个人都直接绑定到同一个模型实例,您将无法做到这一点)。像这样隔离更改仍然需要更多的工作,但它也开辟了强大的可能性(例如,撤消更改是微不足道的:不要推动它们)。
ViewModel 依赖于某种数据服务。数据服务是一个应用程序组件,位于数据存储和消费者之间,并处理它们之间的所有通信。每当 ViewModel 克隆其模型时,它还会订阅数据服务公开的适当“数据存储更改”事件。
这允许 ViewModel 被通知其他 ViewModel 已推送到数据存储的“他们的”模型的更改并做出适当的反应。通过适当的抽象,数据存储也可以是任何东西(例如,该特定应用程序中的 WCF 服务)。
工作流程
创建一个 ViewModel 并分配一个模型的所有权。它立即克隆模型并将这个克隆暴露给视图。它依赖于数据服务,它告诉 DS 它想要订阅通知以更新此特定模型。 ViewModel 不知道是什么标识了它的模型(“主键”),但它不需要,因为这是 DS 的责任。
当用户完成编辑后,他们会与调用 VM 上的命令的视图交互。然后 VM 调用 DS,将所做的更改推送到其克隆模型。
DS 保持更改并另外引发一个事件,通知所有其他感兴趣的 VM 已对 Model X 进行了更改;新版本的模型作为事件参数的一部分提供。
已分配同一模型所有权的其他虚拟机现在知道外部更改已经到来。他们现在可以决定如何更新手头所有拼图的视图(模型的“先前”版本,它被克隆;“脏”版本,它是克隆;和“当前”版本,它作为事件参数的一部分被推送)。
注意事项
模型的INotifyPropertyChanged
仅由视图使用;如果 ViewModel 想知道模型是否“脏”,它总是可以将克隆版本与原始版本进行比较(如果它一直保留,如果可能的话,我建议这样做)。
ViewModel 以原子方式将更改推送到服务器,这很好,因为它确保数据存储始终处于一致状态。这是一种设计选择,如果您想以不同的方式做事,另一种设计会更合适。
如果 ViewModel 将 this
作为参数传递给“推送更改”调用,服务器可以选择不为负责此更改的 ViewModel 引发“模型更改”事件。即使不这样做,如果 ViewModel 发现模型的“当前”版本与其自己的克隆相同,它也可以选择不执行任何操作。
有了足够的抽象,就可以将更改推送到其他机器上运行的其他进程,就像将它们推送到 shell 中的其他视图一样容易。
希望这会有所帮助;如果需要,我可以提供更多说明。
【讨论】:
+1 用于简明扼要地阐述 MVVM 基础知识。 我为此创建了一个概念证明,我觉得这正是我正在寻找的方式。您还说服我使用视图模型,更重要的是明确为什么我应该使用它们。 在这种情况下,我将如何处理视图所做的无法数据绑定的更改?在 ViewModel 上公开方法以供 View 使用? 顺便说一句:我尝试按照您的建议使用克隆。我只是遇到了一些我认为不值得使用克隆的复杂性。我的模型获得了父/子关系并获得了需要保留的导航属性。您是否建议使用反射来制作副本或让 ViewModel 实现类似 ModelChanged() 并让 DS 提供已更改的属性列表(因为 DS 在接收新模型时可以访问旧副本)。跨度> @SteffenWinkler deep vs shallow 并不能直接转化为大 vs 小。至于共享模型实例,当然,当您仅将它们用于阅读时。对于写作,在教授 MVVM 101 时,业务考虑显然比书籍所建议的更重要。当您在文件系统资源管理器应用程序中点击重命名文件时,您是否希望在操作系统中的其他任何地方看到文件名自动更新以反映您尚未确认的击键?【参考方案3】:仅当模型实现 INotifyPropertyChanged 接口时绑定模型才能直接查看。 (例如,您的模型由实体框架生成)
模型实现 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
ViewModel 作为 DTO
如果模型实现了 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 模式(模式!= 规则),这真的取决于。
还有一件事.. ViewModel 依赖于 Model。如果模型可以被外部服务/环境改变。让事情变得复杂的是“全局状态”。
【讨论】:
【参考方案4】:你有这个问题的原因是因为你的模型不知道它是否脏。
string Title
set
this._title = value;
this._isDirty = true; // ??!!
解决方案是通过单独的方法复制服务器更改:
public void CopyFromServer(Model serverCopy)
this._title = serverCopy.Title;
【讨论】:
模型不应该知道它是否脏。这就是 ViewModel 存在的原因。【参考方案5】:如果您唯一的问题是来自服务器的更改会立即重新保存,为什么不执行以下操作:
//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
此代码手动提醒来自服务器的属性更改视图,无需通过属性设置器,因此无需调用“保存到服务器”代码。
【讨论】:
视图的数据上下文是模型。您是否建议在 ViewModel 上保留单独的属性?我选择直接绑定到模型,所以我不需要复制属性。 鉴于您想要Title
属性(保存到服务器)的不同的、特定于视图的行为,那么我建议它属于视图模型。您可以实现它,而不是使用 string
来存储它的值,您只需使用模型属性(例如 get return _model.Title;
等)
我确实需要模型来通知 ViewModel 有关更改。直接将属性值传递给 Model 会导致它回显 View。
什么会导致需要通知视图的模型更改?只是上面描述的服务器?
服务器作为用户与视图交互。以上是关于MVVM:绑定到模型,同时保持模型与服务器版本同步的主要内容,如果未能解决你的问题,请参考以下文章
将 [VisualStateManager] 视图状态绑定到 MVVM 视图模型?
WPF MVVM 文本框文本绑定与 changedText 事件