ASP.NET MVC 架构:ViewModel 通过组合、继承还是复制?

Posted

技术标签:

【中文标题】ASP.NET MVC 架构:ViewModel 通过组合、继承还是复制?【英文标题】:ASP.NET MVC Architecture : ViewModel by composition, inheritance or duplication? 【发布时间】:2011-10-20 17:05:28 【问题描述】:

我使用的是 ASP.NET MVC 3 和 Entity Framework 4.1 Code First。

假设我有一个User 实体:

public class User

    public int Id  get; set; 
    public string Name  get; set; 
    public string Email  get; set; 
    public string Password  get; set;         

在我的UserController 中编辑它时,我想添加一个PasswordConfirmation 字段并验证PasswordConfirmation == Password

1。按组成

我的第一次尝试是:

public class EditUserModel

    [Required]
    public User User  get; set; 

    [Compare("User.Password", ErrorMessage = "Passwords don't match.")]
    public string PasswordConfirmation  get; set; 

在这种情况下,客户端验证有效但编辑:客户端验证工作是巧合。)不起作用服务器端验证失败并显示以下消息:找不到名为 User.Password 的属性

编辑:我认为在这种情况下,最好的解决方案是创建一个自定义CompareAttribute

实现IValidatableObject

public class EditUserModel : IValidatableObject

    [Required]
    public User User  get; set; 
    public string PasswordConfirmation  get; set; 

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    
        if(this.PasswordConfirmation != this.User.Password)
            return new[]  new ValidationResult("Passwords don't match", new[]  "PasswordConfirmation " ) ;

        return new ValidationResult[0];
    

在这种情况下,服务器端验证工作,但客户端验证不再工作。实现IClientValidatable 似乎有点太复杂了,在这种情况下我更喜欢不进行客户端验证。

2。通过继承

public class EditUserModel : User

    [Compare("Password", ErrorMessage = "Passwords don't match.")]
    public string PasswordConfirmation   get; set; 

当尝试使用 EF 直接保存 EditUserModel 时,它不起作用,我收到一些关于 EditUserModel 元数据的错误消息,所以我使用 AutoMapperUser 转换到EditUserModel 和向后。 这个解决方案有效,但它更复杂,因为我必须从模型转换为视图模型并向后转换。

3。通过复制

(由 Malte Clasen 建议)

视图模型将具有模型的所有属性以及其他属性。 AutoMapper 可用于从一种转换到另一种。

public class EditUserModel     
  public string Name  get; set;     
  public string Email  get; set;     
  public string Password  get; set;    
  [Compare("Password", ErrorMessage = "Passwords don't match.")]     
  public string ConfirmPassword  get; set;         

这是我最不喜欢的解决方案,因为代码重复(DRY)

问题

这种情况下继承、组合和重复的优缺点是什么?

有没有一种简单的方法可以同时进行客户端和服务器端验证,而无需将模型转换为视图模型并向后转换?

【问题讨论】:

【参考方案1】:

之前我曾为这个问题苦苦挣扎,但在各种情况下我都解决了这三个问题。一般来说,我看到的大多数意见都支持在 MVC 项目中重复,为每个视图专门构建一个 ViewModel。这样,您将使用的约定类似于UserDetailsViewModelUserCreateViewModel。正如您所说,届时将使用 AutoMapper 或其他一些自动映射工具将您的域对象转换为这些平面视图模型。

虽然我也不喜欢重复代码,但我也不喜欢用验证或其他特定于视图的属性来污染我的域对象。另一个优点,尽管公认几乎没有人需要与之抗衡(不管所有专业人士怎么说),是您可以以某些方式操纵域对象,而不必操纵您的 ViewModel。我提到它是因为它被普遍引用,而不是因为它对我来说很重要。

最后,使用真正扁平的 ViewModel 可以使标记更清晰。当我使用组合时,我在创建名称类似于User.Address.Streethtml 元素时经常出错。平面 ViewModel 至少降低了我这样做的可能性(我知道,我总是可以使用 HtmlHelper 例程来创建元素,但这并不总是可行的)。

无论如何,我最近的项目现在也几乎需要单独的 ViewModel。它们都是基于 NHibernate 的,在 NHibernate 对象上使用代理使其无法直接用于视图。

更新 - 这是我过去提到的一篇好文章:http://geekswithblogs.net/michelotti/archive/2009/10/25/asp.net-mvc-view-model-patterns.aspx

【讨论】:

是的,每个视图一个视图模型(在大多数情况下)和自动映射器以避免来回传递东西,其他方法的问题是您最终会混合关注点,并且有数据视图中并不总是需要(然后您必须排除字段等),或者视图中需要多个对象,视图模型可以将所有这些都展平并具有该责任以及数据注释的东西 也看这篇文章,我不是100%追,但是很接近lostechies.com/jimmybogard/2009/06/30/how-we-do-mvc-view-models 是的,那是我引用的另一篇文章来了解我现在所处的位置。不过,我并不是 100% 快乐。特别是我的编辑和创建视图模型之间的重叠很烦人。这是重复,我不能轻易反驳。 另外,不要将重复与违反 DRY 混淆。仅仅因为您在两个实体上具有相同名称的属性并不意味着您在重复自己。从语法上讲,是的,但从概念上讲,你的视图模型和你的实体是完全不同的——尤其是当你超越一个简单的 CRUD 应用程序时。【参考方案2】:

您还可以考虑域模型和视图模型的独立类,例如在这种情况下

public class EditUserModel     
  public string Name  get; set;     
  public string Email  get; set;     
  public string Password  get; set;         
  public string ConfirmPassword  get; set;         

如果 Id 存储在 url 中。如果您想避免在 User 和 EditorUserModel 实例之间手动复制,AutoMapper 可以帮助您。这样,您可以轻松地将视图模型中的密码字符串与域模型中的密码哈希解耦。

【讨论】:

我个人更喜欢这种技术;拥有一组专门用于视图和客户端验证的“ViewModel”类。这使我的模型与任何 UI 问题完全分开。我当然无法忍受模型上存在的“确认密码”属性。 这有一个额外的好处,它允许您向模型添加属性,而不必担心它们会被无意更新,因为除非您明确添加它们,否则 ViewModel 不包含这些属性。换句话说,只有 ViewModel 中的那些项可以更新为 Model。【参考方案3】:

我试图解决这个问题,我找到了一个不涉及复制代码的解决方案。这是一种解决方法,但在我看来,它比其他提议的解决方案更好。

您拥有经过所有验证的用户模型:

public class UserModel

    [Required]
    public int Id  get; set; 
    [Required]
    public string Name  get; set; 
    public string Email  get; set; 
    public string Password  get; set;         

您将旧模型与新模型组合在一起

public class EditUserModel

    public UserModel User  get; set; 

    [Required]
    public string PasswordConfirmation  get; set; 

诀窍在于行动,您可以收到多个模型:

[HtttPost]
public ActionResult UpdateInformation(UserModel user, EditUserModel editUserModel) 
    if (ModelState.IsValid) 
         // copy the inner model to the outer model, workaround here:
         editUserModel.User = user
         // do whatever you want with editUserModel, it has all the needed information
    

通过这种方式,验证按预期工作。

希望这会有所帮助。

【讨论】:

在这种情况下如何比较属性?例如在您的情况下,将 Password 和 PasswordConfirmation 与 OP 要求的数据注释进行比较?【参考方案4】:

我没有过多使用实体模型,我更喜欢 LINQ - SQL 模型,所以这可能是不正确的:

为什么不使用应用于实体的元数据类? 使用 LINQ - SQL 时,客户端和服务器端验证都会考虑分配的元数据。

据我了解,[MetaDataType] 属性的应用类似于继承,只是它无需实现新类(模型)即可对基本实体进行更改。

另外,您可能想尝试的另一个选项是创建自定义属性 - 我曾出于类似目的这样做过。本质上是一个标志,表明成员的持久性。

所以我会有一个实体定义如下:

public class User

    public int Id  get; set; 
    public string Name  get; set; 
    public string Email  get; set; 
    public string Password  get; set;      

    [DoNotPersist]   
    public string ConfirmPassword get; set;


另外,我不知道你在做什么来存储数据,但我已经为我的 DataContext 连接了一个覆盖到 OnInserting 、 OnEditing 、 OnDeleting 函数中,这基本上删除了任何具有我的自定义属性的成员。

我喜欢这种简单的方法,因为我们为每个模型(为商业智能构建良好的 UI)使用了大量临时的算法数据,这些数据没有保存在数据库中,而是在模型函数、控制器等内部的任何地方使用 - 所以我们在所有模型存储库和控制器中使用依赖注入,因此我们为每个表提供了所有这些额外的数据点。

希望有帮助!

PS:- 组合 vs 继承 - 这真的取决于应用程序的目标用户。如果是针对安全性不那么重要且用户/浏览器环境受到控制的 Intranet 应用程序,则只需使用客户端验证,即:组合。

【讨论】:

【参考方案5】:

我更喜欢组合而不是继承。

如果是您的用户密码,看起来您实际上是以明文形式将密码存储在用户表中,这非常非常糟糕。

您应该只存储一个加盐哈希,并且您的EditUserModel 应该有两个字符串属性用于密码和密码确认,这不是您表中的字段。

【讨论】:

我知道以明文形式存储密码,这不是重点 比我的第一句话还有效 ;-) 组合比继承更灵活(尤其是 C# 中的单继承) 这也是我的首选解决方案,我可以为它交易客户端验证。 :) 您不一定要交易客户端验证。 IE。我建议的视图模型上的两个属性将与客户端验证一起使用。您可能希望使用 AutoMapper 在业务对象和视图模型之间轻松复制数据。

以上是关于ASP.NET MVC 架构:ViewModel 通过组合、继承还是复制?的主要内容,如果未能解决你的问题,请参考以下文章

ASP .Net MVC 模型 - ViewModel - 视图

ASP.NET MVC 条件 ViewModel 抽象

Asp.net Core 如何将 ReflectionIT.Mvc.Paging 与 ViewModel 一起使用?

ASP.NET MVC:让 ViewModel 进入 ViewPage 的部分视图

将表数据映射到 asp .Net MVC 中的 ViewModel 列表

ASP.NET MVC 敲除绑定不起作用