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

时间:2012-05-03 18:24:25

标签: c# silverlight mvvm domain-driven-design prism

我花了很多时间尝试为以下挑战找到一个优雅的解决方案。我一直无法找到解决问题的解决方案。

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

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

到目前为止一切顺利。现在,我需要进行两项调整,以使模型与Server保持同步。服务器的类型并不重要。只需知道我需要调用Save()才能将模型推送到Server.

调整1:

  • Model.Title媒体资源需要致电RaisePropertyChanged(),以便将ModelServer所做的更改转换为View。这很好用,因为ModelView
  • 的DataContext

还不错。

调整2:

  • 下一步是致电Save(),将View所做的更改保存到Model上的Server。这是我被卡住的地方。我可以在模型发生变化时处理调用Save()的Model.PropertyChanged上的ViewModel事件,但这会使服务器对其进行回应更改。

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

5 个答案:

答案 0 :(得分:66)

在过去,我编写了一个支持从多个位置“实时”编辑数据对象的应用程序:应用程序的许多实例可以同时编辑同一个对象,当有人将更改推送到服务器时得到通知并(在最简单的情况下)立即看到这些更改。以下是对其设计方式的总结。

设置

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

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

    为此,ViewModels可能希望克隆它们包装的模型,以便它们可以向应用程序的其余部分提供事务语义(即,您可以选择何时推送)对应用程序其余部分的更改,如果每个人都直接绑定到同一个Model实例,则无法执行此操作。隔离这样的更改需要仍然更多的工作,但它也开辟了强大的可能性(例如,撤消更改是微不足道的:只是不要推动它们)。

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

    这允许ViewModels通知其他ViewModel推送到数据存储并做出适当反应的“他们的”模型的更改。通过适当的抽象,数据存储也可以是任何东西(例如,该特定应用程序中的WCF服务)。

  4. 工作流

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

    2. 当用户完成编辑时,他们会与在VM上调用命令的View进行交互。然后,VM调用DS,推送对其克隆模型所做的更改。

    3. DS会保留更改并另外引发一个事件,通知所有其他感兴趣的VM已经对模型X进行了更改;新版本的模型作为事件参数的一部分提供。

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

    5. 备注

      • 模型INotifyPropertyChanged仅供视图使用;如果ViewModel想要知道模型是否“脏”,它总是可以将克隆与原始版本进行比较(如果它已被保留,如果可能的话,我建议这样做。)
      • ViewModel以原子方式将更改推送到服务器,这很好,因为它可以确保数据存储始终处于一致状态。这是一个设计选择,如果你想以不同的方式做事,另一种设计会更合适。
      • 如果ViewModel将this作为参数传递给“推送更改”调用,则服务器可以选择不为引发此更改的ViewModel引发“模型已更改”事件。即使它没有,ViewModel也可以选择不做任何事情,如果它看到模型的“当前”版本与它自己的克隆相同。
      • 通过足够的抽象,可以将更改推送到在其他计算机上运行的其他进程,就像将它们推送到shell中的其他视图一样容易。

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

答案 1 :(得分:6)

我建议将控制器添加到MVVM混合(MVCVM?)以简化更新模式。

控制器侦听更高级别的更改,并在Model和ViewModel之间传播更改。

保持清洁的基本规则是:

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

正如另一个答案中所提到的,DataContext应该是VM(或其属性),而不是模型。指向DataModel使得很难分离关注点(例如,用于测试驱动开发)。

大多数其他解决方案都将逻辑放在ViewModels中,这是“不对”,但我看到控制器的好处一直被忽视。 认为MVVM的缩写!:)

答案 2 :(得分:1)

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

如果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;
}