我花了很多时间尝试为以下挑战找到一个优雅的解决方案。我一直无法找到解决问题的解决方案。
我有一个简单的View,ViewModel和Model设置。为了便于解释,我会保持简单。
Model
有一个名为Title
的属性为String的属性。 Model
是View
的DataContext。 View
在模型上有一个TextBlock
数据绑定到Title
。ViewModel
有一个名为Save()
的方法,可将Model
保存到Server
Server
可以推送对Model
到目前为止一切顺利。现在,我需要进行两项调整,以使模型与Server
保持同步。服务器的类型并不重要。只需知道我需要调用Save()
才能将模型推送到Server.
调整1:
Model.Title
媒体资源需要致电RaisePropertyChanged()
,以便将Model
对Server
所做的更改转换为View
。这很好用,因为Model
是View
还不错。
调整2:
Save()
,将View
所做的更改保存到Model
上的Server
。这是我被卡住的地方。我可以在模型发生变化时处理调用Save()的Model.PropertyChanged
上的ViewModel
事件,但这会使服务器对其进行回应更改。我正在寻找一个优雅而合乎逻辑的解决方案,如果有意义,我愿意改变我的架构。
答案 0 :(得分:66)
在过去,我编写了一个支持从多个位置“实时”编辑数据对象的应用程序:应用程序的许多实例可以同时编辑同一个对象,当有人将更改推送到服务器时得到通知并(在最简单的情况下)立即看到这些更改。以下是对其设计方式的总结。
视图始终绑定到ViewModels。我知道这是很多样板,但直接绑定到模型是不可接受的,除了最简单的场景;它也不符合MVVM的精神。
ViewModels有唯一负责推送更改。这显然包括将更改推送到服务器,但它也可能包括将更改推送到应用程序的其他组件。
为此,ViewModels可能希望克隆它们包装的模型,以便它们可以向应用程序的其余部分提供事务语义(即,您可以选择何时推送)对应用程序其余部分的更改,如果每个人都直接绑定到同一个Model实例,则无法执行此操作。隔离这样的更改需要仍然更多的工作,但它也开辟了强大的可能性(例如,撤消更改是微不足道的:只是不要推动它们)。
ViewModels依赖于某种数据服务。数据服务是位于数据存储和使用者之间的应用程序组件,用于处理它们之间的所有通信。每当ViewModel克隆其模型时,它还会订阅数据服务公开的相应“数据存储已更改”事件。
这允许ViewModels通知其他ViewModel推送到数据存储并做出适当反应的“他们的”模型的更改。通过适当的抽象,数据存储也可以是任何东西(例如,该特定应用程序中的WCF服务)。
创建ViewModel并为其分配Model的所有权。它立即克隆模型并将此克隆公开给View。它依赖于数据服务,它告诉DS它想要订阅更新此特定模型的通知。 ViewModel不知道识别其模型(“主键”)是什么,但它不需要,因为这是DS的责任。
当用户完成编辑时,他们会与在VM上调用命令的View进行交互。然后,VM调用DS,推送对其克隆模型所做的更改。
DS会保留更改并另外引发一个事件,通知所有其他感兴趣的VM已经对模型X进行了更改;新版本的模型作为事件参数的一部分提供。
已分配相同模型所有权的其他虚拟机现在知道外部变更已到达。他们现在可以决定如何更新包含手头所有拼图的视图(模型的“先前”版本,克隆;“脏”版本,克隆;以及“当前”版本,被作为事件参数的一部分推送。)
INotifyPropertyChanged
仅供视图使用;如果ViewModel想要知道模型是否“脏”,它总是可以将克隆与原始版本进行比较(如果它已被保留,如果可能的话,我建议这样做。)this
作为参数传递给“推送更改”调用,则服务器可以选择不为引发此更改的ViewModel引发“模型已更改”事件。即使它没有,ViewModel也可以选择不做任何事情,如果它看到模型的“当前”版本与它自己的克隆相同。希望这会有所帮助;如果需要,我可以提供更多说明。
答案 1 :(得分:6)
我建议将控制器添加到MVVM混合(MVCVM?)以简化更新模式。
控制器侦听更高级别的更改,并在Model和ViewModel之间传播更改。
保持清洁的基本规则是:
正如另一个答案中所提到的,DataContext
应该是VM(或其属性),而不是模型。指向DataModel使得很难分离关注点(例如,用于测试驱动开发)。
大多数其他解决方案都将逻辑放在ViewModels中,这是“不对”,但我看到控制器的好处一直被忽视。 认为MVVM的缩写!:)
答案 2 :(得分:1)
绑定模型直接查看只有在模型实现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
}
如果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属性都可以用于绑定,而不会破坏它真正依赖的MVVM模式(模式!=规则)。
还有一件事...... ViewModel依赖于Model。如果可以通过外部服务/环境更改模型。它是“全球状态”,使事情变得复杂。
答案 3 :(得分:0)
如果您唯一的问题是服务器的更改立即重新保存,为什么不执行以下操作:
//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
}
}
此代码手动警告来自服务器的属性更改视图,而无需通过属性设置器,因此无需调用"保存到服务器"代码。
答案 4 :(得分:0)
您遇到此问题的原因是您的模型不知道它是否脏了。
string Title {
set {
this._title = value;
this._isDirty = true; // ??!!
}
}}
解决方案是通过单独的方法复制服务器更改:
public void CopyFromServer(Model serverCopy)
{
this._title = serverCopy.Title;
}