【问题标题】:Why do we use ViewModels?为什么我们使用 ViewModel?
【发布时间】:2013-01-20 09:02:39
【问题描述】:

我最近开始担任网络开发人员。我使用 ASP .NET MVC 4 和 NHibernate。

在我的工作场所,我们严格要求使用视图模型在控制器和视图之间来回传输数据。并且视图模型不应该包含模型的任何对象。 我知道它是控制器和视图之间的一种层。

但我发现即使我们可以直接将模型的对象发送到视图(在大多数情况下),编写视图模型类也是重复和多余的。

例如,如果我想显示订单,我可以在控制器的操作中执行此操作 -

return View(Repository.Get<Order>(id));

但是,相反,我必须编写一个视图模型,用获取的顺序填充它,然后将其传递给视图。

所以,我的问题是,当我们可以按原样使用模型的对象时,编写视图模型的目的是什么?

【问题讨论】:

  • 为了保持一致性,最好以抽象的方式完成,而不是由 Controller 执行实际工作。我们经常没有意识到的是,需求如何随时间变化。从技术上讲,Controller 的工作是协调而不是做实际的艰苦工作(尽管您已经有实体框架为您做这件事)。下一个问题是,如果你想改变你的数据库实现,或者只是想测试怎么办。 ViewModel 基本上是一个抽象,它包装了多个方法,因此它可以很容易地被视图使用。即,您的工作模型、UI 绑定等。
  • 拥有 ViewModel 的另一个巨大好处是,一旦您编写了控制器,您就不必再碰它了。未来需要完成的所有工作(重新塑造数据、业务规则、附加要求、验证、列表继续)都可以在 ViewModel 中完成。
  • 您的视图可能不需要您的订单中找到的所有字段和属性,那么为什么要用不需要的数据/信息浪费带宽呢?

标签: asp.net-mvc


【解决方案1】:

对于较小的项目,您是对的。我听到了你的论点并表示同情 - 然而,这是有充分理由的,繁重和重复的工作,尤其是在更大和更复杂的应用程序中:

  • 必须在控制器的操作中执行所有处理。但是在您给出的示例中,Repository.Get 方法可能会返回一个延迟评估的IQueryable 对象,这意味着在评估视图之前不会命中数据库。由于各种原因,这很糟糕。 (解决方法是在控制器中调用.ToList)。
  • “视图不应包含任何非呈现逻辑”和“您不应信任视图”(因为视图可能是用户提供的)。通过提供模型对象(可能仍连接到活动的 DatabaseContext),视图可以对您的数据库进行恶意更改。
  • 视图的数据显示并不总是与其模型的数据 1:1 映射,例如考虑用户详细信息页面:

    一个用户的 EF 模型对象表示它在数据库中的实体,所以它可能看起来像这样:User { UserId, UserName, PasswordHash, PasswordSalt, EmailAddress, CreatedDate },而“用户详细信息”页面上的字段将是 User { UserId, UserName, Password, ConfirmYourPassword, EmailAddress },你看到区别了吗?因此,您不能使用 EF User 模型作为视图模型,您必须使用单独的类。

  • 模型操作的危险:如果您让 ASP.NET MVC(或任何其他框架)将模型绑定到传入的 HTTP POST 请求(以上面的用户详细信息示例为例),用户可以重置任何人的通过伪造UserId 属性值来输入密码。 ASP.NET 将在绑定期间重写该值,除非您专门对其进行清理(这与制作单个 ViewModel 一样繁琐),否则此漏洞将仍然存在。

  • 在多个开发人员在团队环境中工作的项目中,保持一致很重要。某些页面使用定制的 ViewModel 而其他页面使用 EF 模型并不一致,因为团队没有共同的意识,事情必须记录在案并且通常是有意义的。出于同样的原因,单个开发人员无需在其源代码中添加过多的 XML 文档就可以侥幸逃脱,但在团队情况下,如果您不这样做,您就会分崩离析。

我将与您分享您的情况有一个轻微的解决方法,但请注意先决条件:

  • 您的观点值得信赖
  • 您的视图仅包含表示逻辑
  • 您的应用程序主要是 CRUD
  • 您的视图与每个 EF 实体模型 1:1 对应(即没有 JOIN)
  • 您的视图仅处理 POST 表单的单个简单模型,而不是复杂模型(即对象图)

...那么你可以这样做:

  • 将所有单向、与表单无关的数据放入您的 ViewData 集合,或 MVC 4 中的 ViewBag(如果您是铁杆,甚至是通用的 ViewData&lt;T&gt;)。这对于存储 HTML 页面标题和与母版页共享数据非常有用。
  • 使用您的完全评估和加载 EF 模型作为您的View&lt;TModel&gt; 模型。

但请谨慎使用这种方法,因为它会引入不一致。

【讨论】:

  • 我仍在思考如何安全地在您的安全问题示例(伪造 UserId 值)中使用视图模型,因为在视图模型中也有 UserId 属性
  • @AgungSetiawan 您的 ViewModel 不应包含任何不可变(只读)字段。不可变值应该在您的ViewData 中。并且您的控制器操作代码不得信任用户提供的任何数据。 Entity-as-ViewModel 安全问题的核心来自框架和 webapp 提供的自动绑定,然后假设来自客户端的数据可以被信任,不会覆盖敏感的实体成员。通过从视图模型中手动填充实体,在必要时执行安全检查,可以避免此问题。
  • @AgungSetiawan 您可以实际使用具有敏感字段的实体作为 ViewModel,前提是您不要将其重新附加到 DbContext,而是改为在它和一个新的、附加的实体对象之间手动复制成员值;但是,和以前一样,您必须确保不要复制和覆盖敏感成员。
  • 如果未来的访问者对“视图模型防止恶意数据库更改”位有类似的困惑:stackoverflow.com/questions/45307963/…
【解决方案2】:

嗯,我开始认为需要对每个问题采取务实的方法,而不是仅仅订阅那里的纯粹架构标准。您的应用可能需要在野外运行并由为大量客户端等服务的许多开发人员维护,这可能会指导或推动您的架构。

  • 当您想要分离 DomainModel (DataModel) 和其余代码之间的关注点时,ViewModel 是必不可少的。

模型、视图和控制器之间的依赖关系越少,在不破坏视图和控制器等接口契约的情况下更改 DomainModel 就越容易。务实。我喜欢这种方法,因为代码重构是系统维护的重要组成部分——重构可能包括对模型属性的简单拼写错误——如果不分离依赖关系,更改可能会通过代码波及到合同级别;例如。

  • ViewModel 用于在 DomainModel 和 View 之间转换数据

必须将存储在 Informix 中的日期时间的简单示例转换为 .Net 日期时间。 ViewModel 是进行这种翻译的理想场所,它不会强迫您将翻译代码放在各种不需要的地方。

[任何事物的] 良好设计的一个属性是能够替换或修改实现的一部分,而对系统的其余部分几乎没有影响。但这需要付出努力和时间来实现 - 您可以在完美的设计足够的设计之间找到实际的平衡

但是,是的,使用某些模式还有许多其他充分的理由 - 但最重要的是:

没有什么会强迫你使用 ViewModels...ASP.NET MVC 不会强迫你。听取内心实用主义者的建议。

【讨论】:

  • 我同意,增加一层分离意味着增加一层额外的代码!例如添加单元测试,以便在删除列时您知道它并且可以修复 Model->ViewModel 转换。但这确实有助于将模型更改影响隔离到单层代码,并将重新测试限制为仅对那些代码更改所需的重新测试......无论如何 - 我们都有自己的意见,我每天都在学习,我的意见会改变以适应;)
  • 我要补充一点,我认为应该做更多的工作来区分单向 ViewData 数据和双向 ViewModel 数据。人们不应该在 Model 中传递 HTML 页面标题和元数据等内容。
  • 我没有想过像你给出的例子那样使用 ViewModels 作为翻译器!感谢您的洞察力。
【解决方案3】:

如果您使用与 ViewModel 相同的模型,您的应用程序应该非常小且简单,并且应该只包含 CRUD 操作。但是,如果您正在与大型团队(有两个或更多开发人员)构建大型或企业应用程序,您应该有依赖注入、服务、存储库、外观、工作单元、数据访问对象等概念。

为了简化模型和视图模型之间的映射需求,您可以使用 AutoMapper https://github.com/AutoMapper/AutoMapper

或使用 nuget 安装 安装包 AutoMapper

【讨论】:

    【解决方案4】:

    在我看来,对于执行大部分 CRUD 操作的复杂应用程序来说,必须在模型层之上再增加一层(ViewModel),因为它具有以下优点:

    1. 在模型和控制器之间建立松散耦合。这样任何与DataModel相关的修改都不会影响到Controller。
    2. 如果您已正确实现应用程序的 ViewModel 层 通过提供最高级别的 IOC(控制反转) DI(使用 Unity/其他框架的依赖注入)等,它将 还可以帮助您 MOQ 您的 ViewModels(依赖项)仅用于测试 控制器的逻辑。

    【讨论】:

      猜你喜欢
      • 2020-11-11
      • 2011-04-02
      • 1970-01-01
      • 2016-05-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多