为什么在Silverlight MVVM中不应该通过ViewModel公开Model?

8

在使用MVVM开发WPF应用程序时,我从未通过viewmodel的公共属性来暴露模型。无论如何,在接触Silverlight和WCF RIA的世界之后,我发现了一种新的实现数据验证的方法,也就是所谓的“Required”属性(还有其他属性)。

这一次,我可以在模型本身内部完成几乎所有的验证逻辑,而不是在viewmodel中创建验证逻辑。

public class TestUserPM {
    [Key]
    public int ID { get; set; }

    [Required]
    public string FirstName { get; set; }

    [Required]
    public string Email { get; set; }
}

接下来,在ViewModel中,我只需要公开TestUserPM类型的公共属性,并让View直接绑定到该模型即可。

我认为这不是一种优雅的解决方案,但它可以工作,并且没有必要在ViewModel属性内创建繁琐的验证。

这种方法有什么不足之处吗?

更新1

我刚刚发现了一个缺点,也许有解决方法。我想将按钮的命令绑定到ViewModel中的命令,例如,保存按钮绑定到IDataErrorInfo的public string this[string columnName]中的OnCanExecuteChanged(),以便仅当所有信息都有效时才能执行此按钮。根据我对WPF MVVM的经验,我会使用帮助类来处理这个问题。

如何处理这种需求?

5个回答

11

为了让事情简单并且避免重复(DRY),我经常通过ViewModel公开Model。

唯一需要避免在模型中添加属性以适应UI的方法(正如Benjamin所指出的),是将模型保留为viewModel的一个属性,这样您就可以向viewModel添加属性,而不会影响模型。

即:ViewModel是DataContext,它具有返回模型的Model属性。

<TextBlock Text={Binding Path=Model.Name} />
<TextBlock Text={Binding Path=Model.Address} />

2
我也是这样做的。ViewModel 的作用是将 Model 暴露和适应到 View 上。 - Benjamin Baumann
1
直接使用模型的想法违反了封装的规则,因为没有人应该这样做MyObject.Child.ChildChild.NChild.SomeProperty。此外,这也违背了模型和视图之间的分离原则,因为如果您在模型中更改属性名称,则必须在视图中进行相应的更改。 - WiiMaxx

5

我认为主要问题在于您的模型(可能是业务对象)必须适应UI。它可能会影响其他UI或业务层。

您可以想象使用相同对象时,不同验证级别的几个UI。这在您的示例中是不可能的。


4
问题就像其他人所说的那样,你无法适应视图。 然而,我经常不想重复自己 - 正如Eduardo所说的那样,将模型暴露给绑定。 我发现这个解决方案在你想要改变视图的值时有点不一致 - 然后有些人会绑定“Model.Name”,而其他人只是“Name”用于更改的属性 - 并且有些情况下无法按照这种方式工作。
我的解决方案是创建一个ViewModelProxy类,在其中可以从另一个类转发属性并免费获取属性通知。通过派生DynamicObject(我省略了代码通知,IDataerror等),很容易做到这一点。酷的事情是,如果你实现/覆盖一个将被绑定的属性,所有来自Data的属性都将被转发 - 所以在这种方式下,你不必重复代码,并且你有一个合理的使用DynamicObject的东西。
public class ViewModelProxy<T> : DynamicObject, INotifyPropertyChanged

public T Data { get; set; }

private PropertyInfo[] objectProperties;
private PropertyInfo[] ObjectProperties
{
  get
  {
    if (objectProperties == null)
      objectProperties = typeof(T).GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
    return objectProperties;
  }
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
  var pinfo = ObjectProperties.FirstOrDefault((pi) => pi.Name == binder.Name);

  if (pinfo != null)
  {
    result = Data != null ? pinfo.GetValue(Data, null) : null;
    return true;
  }
  else
    return base.TryGetMember(binder, out result);
}


public override bool TrySetMember(SetMemberBinder binder, object value)
{
  var pinfo = ObjectProperties.FirstOrDefault((pi) => pi.Name == binder.Name);

  if (pinfo != null)
  {
    if (Data != null)
      pinfo.SetValue(Data, value, null);
    RaisePropertyChanged(binder.Name);
    return true;
  }
  else
    return base.TrySetMember(binder, value);
}

}


性能如何?看起来会有点慢,不是吗? - Jonathan ANTOINE
性能很好。当然会有一些开销,但反射调用并不是那么昂贵的。如果您有许多属性,可以使用字典查找来替换数组查找。 - Rune Andersen

1

在Silverlight中,使用注释进行验证而不是用代码填充ViewModel是正确的做法。

对于任何特殊的验证规则,您可以创建自定义验证器并使用[CustomValidation...]修饰成员,这样可以将验证与ViewModel分离。

无论如何,您所描述的业务规则通常在视图之间共享。特定视图的特定验证可以添加到控制器中。

总的来说:ViewModel是一个相对简单的对象,用于保存视图的值。如果您发现自己正在添加逻辑、事件处理程序和其他内容,那么您可能应该考虑引入控制器对象...即使MVVM中没有C :)


我刚刚从文档中读到这些内容,但我不理解为什么我们需要直接从服务中注入类似于ViewModel的东西?这是与UI相关的任务,必须在UI层面上完成,而不是在服务中。(在我看来)基本上这意味着我需要把所有东西都放在服务中,这对我来说很奇怪。 :) - Anonymous
@In The Pink:很抱歉,我不理解你的问题(你指的是哪个服务,RIA服务吗?)。ViewModel是UI,但它公开的数据模型是从Web服务填充的业务对象。使用RIA服务,验证可以在客户端和服务端都进行。 - iCollect.it Ltd

1

为了在ViewModel中公开Model,您需要准备好Model以适应View,因此您应该使用特定于视图的代码来污染您的Model:

  • 通知属性更改支持
  • 数据错误信息支持(内部验证)
  • 可编辑支持。

不将其他内容污染到您的Model中是必要的,完美的Model应该具有最少的依赖关系与其他库,因此它可以在同一应用程序中与不同平台(asp.net、移动、mono、winform、wpf等)共享。或者进行升级/降级。

无论如何...

我制作了一个小型WPF应用程序(尚未完成),我使用uNhAddins、NHibernate、Castle构建它。我不说这是最好的解决方案,但我真的很高兴能够使用它...首先检查代码,然后查看实体、验证逻辑、业务逻辑的分离。程序集分离旨在最小化核心应用程序、UI和应用程序逻辑之间的依赖关系。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接