MVC 中的泛型 ViewModel






4.08/5 (7投票s)
MVC 支持将复杂对象(包括泛型)作为模型。我们可以利用此功能在视图和控制器之间传递标准模型类型。我们可以编写可重用代码来操作这些标准模型。这可以改变我们使用 MVC 的方式。
引言
在 MVC 应用程序中,我们通常在视图中将简单对象用作模型。MVC 支持将复杂对象(包括泛型)作为模型。我们可以利用此功能在视图和控制器之间传递标准模型类型。通过拥有标准模型类型,我们可以编写可重用代码来操作模型。这可以改变我们使用 MVC 的方式。
本文介绍了如何使用此功能。它阐述了一种消息驱动的 MVC 方法,其中包含简单的状态管理。
状态管理
状态管理是软件设计中最棘手的问题之一。HTTP 的无状态特性使得 Web 应用程序中的状态管理更加复杂。
在典型的 Web 应用程序中,需要处理三种对象状态——对象在读取并发送给用户时的状态、从用户返回的对象状态以及从用户获取修改后的对象时数据存储中对象的原始状态。大多数 MVC 应用程序忽略了对象的原始状态。这是因为它们中的大多数都修改了视图模型。典型的 MVC 生命周期如下:
- 读取数据库并获取原始值。
- 创建 ViewModel 并将其发送给用户。
- 用户修改 ViewModel 并将其发送回来。
- 应用程序再次读取数据库。
- 比较当前数据库值和用户 ViewModel,以查看状态是否已更改。
- 如果持久化对象已更改,则确定新的持久化对象并保存它。
问题
存在一个微妙的问题。用户所做的更改是基于他们收到的对象状态。这可能与您刚刚读取的对象不同。如果用户看到当前对象,他们的响应可能会有所不同。根据返回值更新数据库可能会导致错误数据。为了做出明智的决策,需要所有三种状态。
解决方案
保留原始状态的常用方法是在将其发送到服务器之前缓存模型。这可行,但需要存储和获取数据以及清除过期数据的策略。另一种方法是使用(我称之为)对称模型。对称模型本质上是成对的相同模型类型。这种方法为视图发送和返回两个模型。返回的模型是原始模型和更新的模型。这提供了简单的状态管理。使用模型对提供了其他功能,例如提供动态默认值。
复合模型
在 MVC 应用程序中,我们通常在视图和控制器之间交换简单的“数据”对象(我们的模型)。我们的表单和标签显示模型中的值,接受更改,并将更改后的模型发送回来。我们通过 @model 指令告诉系统我们正在使用的对象类型。我们发送和接收的对象通常非常扁平,但它们可以根据我们的喜好变得复杂。它们可以是泛型。
Razor 页面只允许在 @model 指令中使用单个对象。因为这个对象可以是复杂的,所以这不是问题。在消息驱动系统中,我们在端点之间发送消息。在此示例中,我们将使用消息的概念,并构建一个通用消息类:Message<TIn, TOut>
。我们的有效负载将是一个 Person:Person { string name, int age}
。我们现在可以创建、发送和接收人员消息——return View("ViewName", new Message<Person, Person>())
、@model Message<Person, Person>
、public ViewResult PersonHandler(Message<Person, Person> message){}
。如果我们愿意,我们可以创建消息类型——PersonMessage : Message<Person, Person>
。Razor 引擎和 ModelBinder 都可以很好地处理这两种情况。通过使用Message
,我们发送和接收两个模型——一个视图输入模型(TIn
)和一个视图输出模型(TOut
),它们包含在一个容器中——Message
。消息类可以包含其他属性,例如上下文或控制对象。
管理状态
使用复合模型的方法有很多种。对于简单的状态管理,我们用从数据库读取的信息填充 TIn
,并用一个空的 Person
填充 TOut
,然后将其发送到视图。我们将标签绑定到 TOut
,并从 TIn
设置初始值——<input type="text" name="@Model.TOut.Name" value="@Model.TIn.Name">
。MVC 将发布一条消息,其中包含 TOut
中的用户值,以及(在一点帮助下)TIn
中的原始值。一旦我们在控制器操作上收到消息,我们就可以再次读取数据库并获得所有三个可用状态。当我们将其发送到视图时,TOut
不需要是一个空的 Person
。我们还可以发送基于实时条件的默认值等内容。
代码
要尝试消息传递和对称模型,请创建一个新的 MVC 项目。我推荐一个 MVC Core 2 / C#7.1 项目,但 MVC Core 和 C# 6 应该也能正常工作。这可能适用于 .Net Framework,但我没有检查过。其他文章基于本文,并将需要 Core。
报文类别
Message 类是一个具有两种泛型类型的泛型。
public class Message<TIn, TOut> { public Message() { } public Message(TIn inpart, TOut outpart) { Input = inpart; Output = outpart; } public TIn Input { get; set; } public TOut Output { get; set; } }
Person 类
这个类只是一个有效负载。你可以使用任何你想要的类。
public class Person{ public string Name{ get; set; } public int Age{ get; set; } }
控制器 (Controller)
你可以使用默认的 Home 控制器。替换它的主体。
public class HomeController : Controller { public IActionResult Index() { var inPerson = new Person { Name = "Max", Age = 22 }; var outPerson = new Person(); var message = new Message<Person, Person>(inPerson, outPerson); return View(message); } [Route("home-message-handler", Name = "Home.MessageHandler")] public IActionResult MessageHandler(Message<Person, Person> message){ return View(message); } }
输入视图
使用 Index.cshtml
页面作为主表单。表单的上方部分有表单字段。MVC 将绑定到输入标签的 name
属性。它足够智能,知道你在做什么,所以你只需要使用 Output.[field]
。这暗示了这种方法的强大之处——我们可以编写可操作 Output
的可重用代码,并将其与任何模型一起使用。如果你以 @Model.
开头,你可以获得智能感知完成。之后删除 @Model
。使用 value
属性设置显示值。这需要使用 @Model.Input.[field]
。
在这个简单的例子中,状态管理采用了常见的隐藏输入方法。创建它们的一种方法是为每个字段手动创建一个标签——<input type="hidden" name="Input.Name" value="@Model.Input.Name" />
。另一种方法是使用一点 C# 遍历对象属性并生成标签。在另一篇文章中,我将展示一种使用 StateTagHelper 的更好方法。
@using System.Reflection @model Message<Person, Person> <form> Name: <input type="text" name="Output.Name" value="@Model.Input.Name" /> Age: <input type="text" name="Output.Age" value="@Model.Input.Age"/> <button asp-route="Home.MessageHandler"></button> @* State *@ @foreach (PropertyInfo p in Model.Input.GetType().GetProperties()) { dynamic value = p.GetValue(@Model.Input); dynamic name = p.Name; <text> <input type="hidden" name="Input.@name" value="@value" /> </text> } </form>
旁注——Razor 页面实际上是内部包含 HTML/脚本的 C# 类,而不是内部包含一些 C# 的 HTML 页面。过去,以另一种方式看待它很方便。Core、页面中的 DI 和 TagHelpers 使 C# 的本质更加明显和易于访问。将页面(.cshtml
)视为 C# 而不是 HTML,有助于在设计中释放这些技术的强大功能。
结果视图
创建一个简单的页面 MessageHandler.cshtml
来显示结果。
@model Message<Person,Person> Name: @Model.Input.Name -> @Model.Output.Name<br/> Age: @Model.Input.Age -> @Model.Output.Age<br />
运行它
运行应用程序,更改文本框中的值,然后提交表单。你可以在 MessageHandler 方法中设置一个断点,并检查消息以查看它是否包含原始状态和新状态。
总结
本文简要介绍了泛型模型的概念,以及如何将其用作通用模型类型。它展示了如何创建和使用泛型模型 Message<Input, Output>
来在简单的 CRUD 类型页面中维护状态。
一个核心底层概念是,通过创建像 Container<T1, T2>
这样的泛型类型,我们可以将它们用作许多或所有视图的标准模型。我们可以编写可操作容器或泛型参数的可重用代码。这在示例中通过变量 Output
进行了触及。手动执行此操作既简单又强大,但 TagHelper 将其提升到另一个层次,我们将在其他文章中看到。
当我们对 T1 和 T2 使用相同的对象类型(对称模型)时,我们可以实现状态管理或动态默认值等功能。我们还可以使用非对称模型方法,其中模型不同。一种用途是在输出模型中返回输入模型的子集。这可以替代在我们的操作上使用 [Bind(
。我们还可以使用不同的模型类型,并让我们的视图成为转换。在所有这些情况下,我们可以将 Pre 和 Post 对象打包在一起。这些概念将在其他文章中探讨。
通过传递 Message
对象而不是数据对象,我们可以编写消息处理和基于内容路由的代码。这使我们不仅可以在常见的 CRUD 模式下使用 MVC,还可以在消息驱动或工作流方法中使用它。这些场景也将在后续文章中探讨。
历史
初始 - 2017年9月20日。