ASP.NET MVC 局部视图与局部模型






4.98/5 (25投票s)
在多个页面上使用具有自己模型的局部视图,
引言
本文解决了在局部视图中包含表单元素时,将局部视图数据回发的问题。
代码重用是一个非常有用的省时功能,任何优秀的工程师在工作中都会积累大量有用的函数。Web 应用程序重复使用相同的代码是常识。然而在这种情况下,我们还有 HTML 标记代码。ASP.NET MVC 提供了局部视图 (Partial Views)、子操作 (Child Actions) 和编辑器/显示模板 (Editor/Display templates) 来解决这个问题。局部视图可以使用页面模型的数据,而子操作则使用来自控制器的独立数据。编辑器/显示模板将模型中的项传递给系统,但可以被用户局部视图覆盖。本文以公司地址为例,其中一部分内容是邮政地址,探讨了从局部视图回发数据时遇到的问题。当邮政地址作为局部视图实现时,可以在其他页面(例如销售地址)中重复使用。一旦这个问题解决,大型页面就可以通过分解成几个部分来更好地管理。一个很好的例子就是选项卡下的内容。
背景
在我们深入代码之前,我想提供一个简短的背景。我是一名在一家小公司工作的专业软件工程师。自 Visual Basic 6 以来,我一直在使用 Microsoft 的技术。当我解决了一个我认为对未来的项目有用并能造福同事的问题时,我就会感到兴奋。这种情况不常发生,但一旦发生,我就会将其添加到类库中。我已将其中一部分包含在示例项目中。我还使用 One-Note 快速记录这些想法,并链接到其他工程师的发现。
遇到问题时,首先要做的就是看看是否有人遇到过类似的问题。所有工程师都需要具备良好的搜索能力。有很多论坛,包括 Code Project,在自己动手之前可以获得很多想法。
在使用 Microsoft MVC 进行 Web 设计时,大多数功能都已包含在内,但偶尔您需要其他功能。表单领域就是其中之一。Microsoft 使用 HTML 助手生成表单元素。这些助手功能强大,但也有很多遗漏。幸运的是,它们很容易扩展,并且可以在互联网上找到许多示例。
本文将提出一个问题,并通过首先查看其他人所做的工作,然后生成一个新的 HTML 助手来解决问题。
问题
首先让我们看看用于表单的标准 MVC 控制器模式
[HttpGet]
public ActionResult Index()
{
// load the test data
TestViewModel model = new TestViewModel();
return View(model);
}
[HttpPost]
public ActionResult Index(TestViewModel model)
{
if (ModelState.IsValid)
{
// save the test data
}
return View(model);
}
HttpGet
动作将数据加载到模型中并调用视图来显示数据。视图在表单上显示数据,用户可以编辑。单击提交按钮将表单回发到控制器的 HttpPost
动作,将表单上的数据绑定到模型。控制器验证模型,如果一切正常,则保存数据。然后控制器调用相同的视图来显示编辑后的数据。请注意,在两个动作中都使用了相同的模型,并且坚持这种模式可以保证数据绑定有效,即使是包含其他类和/或列表的复杂模型。事实上,如果我们坚持这种模式,这是 MVC 为我们做的非常聪明的事情之一。
这是一个非常简单的模型
public class TestViewModel
{
public TestModel Test { get; set;}
}
public class TestModel
{
[Display(Name = "Name:")]
[Required(ErrorMessage = "Please provide a name")]
public string Name { get; set; }
public TestPartialModel Partial { get; set; }
}
public class TestPartialModel
{
[Display(Name="Partial Name:")]
[Required(ErrorMessage="Please provide a name")]
public string Name { get; set; }
}
这是视图
@model CSE.Partial.WebApp.Models.TestViewModel
@{
ViewBag.Title = "Test";
}
<h2>@ViewBag.Title</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div>
<hr />
<dl class="dl-horizontal">
<dt>@Html.LabelFor(m => m.Test.Name)</dt>
<dd>
@Html.EditorFor(m => m.Test.Name)
@Html.ValidationMessageFor(m => m.Test.Name)
</dd>
<dt>@Html.LabelFor(m => m.Test.Partial.Name)</dt>
<dd>
@Html.EditorFor(m => m.Test.Partial.Name)
@Html.ValidationMessageFor(m => m.Test.Partial.Name)
</dd>
<dt></dt>
<dd><input class="btn btn-primary" type="submit" value="Save" /></dd>
</dl>
</div>
}
这按预期工作,当按下保存按钮时,任何输入的值都会回发到控制器,并且基于这些值返回新的视图。因此,测试只是值的持久化。
下一步是将局部代码移到局部视图中。
_TestPartial
可以位于相同的视图文件夹中或位于 Shared 文件夹中。
@model CSE.Partial.WebApp.Models.TestViewModel
<dt>@Html.LabelFor(m => m.Test.Partial.Name)</dt>
<dd>
@Html.EditorFor(m => m.Test.Partial.Name)
@Html.ValidationMessageFor(m => m.Test.Partial.Name)
</dd>
现在视图是
@model CSE.Partial.WebApp.Models.TestViewModel
@{
ViewBag.Title = "Test";
}
<h2>@ViewBag.Title</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div>
<hr />
<dl class="dl-horizontal">
<dt>@Html.LabelFor(m => m.Test.Name)</dt>
<dd>
@Html.EditorFor(m => m.Test.Name)
@Html.ValidationMessageFor(m => m.Test.Name)
</dd>
@Html.Partial("_TestPartial")
<dt></dt>
<dd><input class="btn btn-primary" type="submit" value="Save" /></dd>
</dl>
</div>
}
这按预期工作正常,因为整个模型默认传递给局部视图。但我希望局部视图是可重用的,在上面的例子中,它与页面的模型绑定在一起。
所以让我们尝试将局部模型传递给局部视图。修改局部视图以使用其自己的模型
@model CSE.Partial.Service.Models.TestPartialModel
<dt>@Html.LabelFor(m => m.Name)</dt>
<dd>
@Html.EditorFor(m => m.Name)
@Html.ValidationMessageFor(m => m.Name)
</dd>
从页面视图中传递正确的模型
@Html.Partial("_TestPartial", Model.Test.Partial)
现在回发时会有一个异常。检查回发数据时,局部模型丢失了。这就是问题所在!
解决问题
那么哪里出了问题?使用 F12 工具或查看源代码时,查看生成的 HTML 会非常明显。在工作测试中存在的元素命名在最终测试中不同。
这有效
<input name="Test.Partial.Name" id="Test_Partial_Name" type="text" value="">
这失败了
<input name="Name" id="Name" type="text" value="">
局部 HTML 中缺少名称前缀。这很明显;它怎么可能知道它是更大模型的一部分!
搜索论坛发现其他人也在尝试做同样的事情,我们发现 MVC 中内置了一个名为 TemplateInfo
的类。它有一个名为 HtmlFieldPrefix
的属性。如果我们f在调用局部视图之前设置它,我们可以强制所有元素名称都带有一个前缀。
@{ Html.ViewData.TemplateInfo.HtmlFieldPrefix = "Test.Partial"; }
@Html.Partial("_TestPartial", Model.Test.Partial)
这在这种情况下有效,但局部视图之后的任何元素也会获得前缀。
然后是 @Html.Partial( , , ViewDataDictionary)
中的第三个参数。我们可以将一个新的 ViewData 传递给我们的局部视图,并按照上面的方式设置 HtmlFieldPrefix。
@Html.Partial("_TestPartial", Model.Test.Partial, new ViewDataDictionary
{
TemplateInfo = new System.Web.Mvc.TemplateInfo
{
HtmlFieldPrefix = "Test.Partial"
}
})
这将前缀隔离到局部视图。但是现在 ModelState 和 ViewData 丢失了!错误消息现在无法显示。在第三个参数中丢失了很多默认传递的东西。如果我们在复制视图数据后修改 TemplateInfo
,这应该可以解决问题。ViewDataDictionary
构造函数将为我们复制 Html.ViewData
。
@Html.Partial("_TestPartial",
Model.Test.Partial,
new ViewDataDictionary(Html.ViewData)
{
TemplateInfo = new System.Web.Mvc.TemplateInfo
{
HtmlFieldPrefix = "Test.Partial"
}
})
还有一个最后一个问题。如果我们将此技术用于嵌套局部视图,前缀将不得不附加而不是设置。我在 MVC 中找到了一个方法来做到这一点。所以这是一个可行的解决方案。
@{
string name = Html.NameFor(m => m.Test.Partial).ToString();
string prefix = Html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
ViewDataDictionary viewData = new ViewDataDictionary(Html.ViewData)
{
TemplateInfo = new TemplateInfo { HtmlFieldPrefix = prefix }
};
}
@Html.Partial("_TestPartial", Model.Test.Partial, viewData)
这确实有效,但这有点混乱,而且在每个 Html.Partial
前面都写这么多代码有点多。所以现在它正在工作,让我们把它变成一个 HTML 助手。这就是我们想要的
@Html.PartialFor(m => m.Test.Partial)
这是扩展方法。请注意 lambda 表达式。这是一种非常类型安全的方式,用于将局部模型的名称和值传递给助手。局部视图名称可以作为参数传递,或者如果存在,它将设置为 TProperty
的类名或 UIHint("模板名称")
。
/// <summary>
/// Return Partial View.
/// The element naming convention is maintained in the partial view by setting the prefix name from the expression.
/// The name of the view (by default) is the class name of the Property or a UIHint("partial name").
/// @Html.PartialFor(m => m.Address) - partial view name is the class name of the Address property.
/// </summary>
/// <param name="expression">Model expression for the prefix name (m => m.Address)</param>
/// <returns>Partial View as Mvc string</returns>
public static MvcHtmlString PartialFor<tmodel, tproperty>(this HtmlHelper<tmodel> html,
Expression<func<TModel, TProperty>> expression)
{
return html.PartialFor(expression, null);
}
/// <summary>
/// Return Partial View.
/// The element naming convention is maintained in the partial view by setting the prefix name from the expression.
/// </summary>
/// <param name="partialName">Partial View Name</param>
/// <param name="expression">Model expression for the prefix name (m => m.Group[2])</param>
/// <returns>Partial View as Mvc string</returns>
public static MvcHtmlString PartialFor<TModel, TProperty>(this HtmlHelper<TModel> html,
Expression<Func<TModel, TProperty>> expression,
string partialName
)
{
string name = ExpressionHelper.GetExpressionText(expression);
string modelName = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
ModelMetadata metaData = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
object model = metaData.Model;
if (partialName == null)
{
partialName = metaData.TemplateHint == null
? typeof(TProperty).Name // Class name
: metaData.TemplateHint; // UIHint("template name")
}
// Use a ViewData copy with a new TemplateInfo with the prefix set
ViewDataDictionary viewData = new ViewDataDictionary(html.ViewData)
{
TemplateInfo = new TemplateInfo { HtmlFieldPrefix = modelName }
};
// Call standard MVC Partial
return html.Partial(partialName, model, viewData);
}
这里额外的部分是来自 MVC 的两个方法
ExpressionHelper.GetExpressionText gets the name of the expression m => m.Test.Partial would return "Test.Partial".
ModelMetadata.FromLambdaExpression gets the value of the expression. e.g. the model Partial.
我并非声称自己是这项扩展的完全发明者,而更像是一个融合了多人想法,形成了一个可行方法的人。
编辑器/显示模板
解决了上述问题后,我想研究一下编辑器模板。根据 Microsoft MSDN 的说法
EditorFor 方法用于根据传入表达式的数据类型生成 MVCHtmlString 标记。
网上有很多这样的例子,你会觉得它是一个单一的方法,可以根据属性的类型调整其渲染输出,并使用属性的元数据(属性特性)来添加额外细节。例如,它会为字符串显示一个文本框,为布尔值显示一个复选框。它会将输入类型设置为相应的 Html5 属性。当你使用一个对象时,它会为每个属性吐出带有标签和输入元素的
PartialFor
扩展方法完全相同。它还将前缀通过层级向下传递。
在项目(可通过zip链接获取)中,我将所有局部视图都命名为其模型类名,并将其放在 ViewName/DisplayTemplate 或 Shared/DisplayTemplate 文件夹中。然后你可以这样使用它们
@Html.EditorFor(m => m.Partial.First)
Html.Partial
期望局部视图位于当前视图文件夹或共享文件夹中,而不是子文件夹中。为了测试,我通过使用局部名称参数来覆盖此设置,以使用与 EditorFor 相同的局部视图。
@Html.PartialFor(m => m.Partial.First, "EditorTemplates/FirstPartialModel")
PartialFor 与 EditorFor
那么有什么区别呢?据我所知,差别不大。主要区别在于局部视图的位置。
PartialFor
– 与 Partial 相同;在当前视图文件夹或 Shared 文件夹中EditorFor
– 在视图或共享文件夹的 EditorTemplates 子文件夹中
我运行调试器并跟踪到 MVC EditorFor 代码(参见附录如何操作)。它找到了位置,然后测试了类型以确定它是一个复杂的类,复制了 ViewData,在 TemplateInfo 中设置了附加的前缀,并调用了与 @Html.Partial
相同的局部视图代码。如果你在互联网上搜索“html.editorfor vs partial view”,你会发现很多讨论。我真希望很久以前就发现这个了。PartialFor
的一个优点是当你想控制局部视图的绝对位置时。PartialFor
也适用于继承、接口和集合。
@Html.PartialFor(expression, partialName); partialName
可以是名称、完整路径或相对路径。@HtmlEditorFor(expression, templateName); templateName
可以是名称或相对路径。“EditorTemplates”在源代码中是硬编码的。
接口与继承
掌握了这些新知识,我开始使用模板来定义用于页面表单的数据对象的视图。一切进展顺利,直到我尝试将类的继承部分拆分为自己的局部视图。我认为这是一个非常好的代码重用候选。EditorFor
渲染结果为空,而 PartrialFor
按预期工作。在调试器中跟踪 MVC 代码发现 EditorFor
代码正在检查先前访问过的对象并拒绝了对象的继承部分。它基本上试图保护我们免受递归,但在我们的例子中,它不会递归,它只是将对象的派生部分和继承部分渲染为单独的局部视图。所以我很高兴我没有删除我的 PartialFor
版本。
局部视图 vs 子操作 vs EditorFor
中心性 | 描述 | 用途 | |
---|---|---|---|
局部视图 | 以视图为中心 | 适用于没有模型或页面模型的场景。如果子模型是只读的,也可以使用。 | 适用于复杂页面的标记拆分,例如选项卡。在布局页面上使用时,实现动态视图选择。 |
子操作 | 以视图为中心 | 将局部视图创建为控制器方法 [ChildActionOnly]。使用 @Html.Action("action") 调用。在渲染视图之前可以使用控制器逻辑。不使用页面模型。 | 适用于在多个页面上重用独立视图。 |
EditorFor | 以模型为中心 | 适用于简单类型和复杂模型,并保持元素命名约定以实现正确的模型回发绑定。元素命名遵循模型层次结构。 | 非常适合在表单中使用的可重用数据模型视图。 |
DisplayFor | 以模型为中心 | 按照惯例,是 EditorFor 的只读版本。 | |
PartialFor | 以模型为中心 | 类似于 EditorFor,用于复杂对象。不进行递归对象测试。 | 适用于对象的继承或接口部分。适用于集合。 |
选哪个?由您决定,以鼓励最大限度的代码重用,避免重复。
使用代码
您将需要 Microsoft Visual Studio 2013 才能运行此解决方案。测试项目可以下载为 zip 文件。将文件解压到一个测试文件夹中,然后双击解决方案文件(CSE.Partial.WebApp.sln)。在尝试运行项目之前,您需要恢复 NuGet 包。这可能会根据您的 Visual Studio 设置自动工作。如果不行,请尝试:
- 右键单击解决方案,然后单击“启用 Nuget 包恢复”。
- 确保“工具/选项/NuGet 包管理器/通用包恢复”已选中“允许 NuGet 下载缺失的包”。
菜单项中包含一个名为“Partial”的下拉菜单。点击此菜单将显示 3 个测试视图。
- “邮政地址”显示一个包含公司详细信息和邮政地址的表单。邮政地址在局部视图中实现。这是一个简单的演示,但展示了局部视图的分离。未来,邮政地址可以扩展为使用邮政编码查找服务,从而提供一个非常好的可重用“组件”。
- “嵌套局部视图”是一个测试局部视图中嵌套局部视图的页面,并展示了多层数据绑定工作。它还证明 ModelState 错误处理仍然有效,并且 ViewData 正在传递到局部视图中。我从论坛上找到的几种替代解决方案在这方面都失败了。
- “测试”是上述“问题”和“解决问题”的来源。
关注点
我相信上面使用的控制器模式很棒,而且数据绑定神奇地起作用。我发现跟踪 MVC 代码的能力非常有帮助。事实上,如果没有它,我将无法完成上述工作。CSE 命名空间使用我公司 Cambridge Software Engineering 的首字母缩写。
附录
深入 .NET 框架代码
MSDN 中关于符号/源服务器的说明。
要配置 Visual Studio 以使用符号/服务器,请按照以下说明操作:
- 转到“工具”->“选项”->“调试器”->“常规”。
- 取消选中“仅启用我的代码(仅限托管代码)”。
- 取消选中“启用 .NET Framework 源步进”。是的,这具有误导性,但如果不取消选中,Visual Studio 将忽略您的自定义服务器顺序(参见下文)。
- 勾选“启用源服务器支持”。
- 取消勾选“要求源文件与原始版本完全匹配”。
- 转到“工具”->“选项”->“调试器”->“符号”。
- 为本地符号/源缓存选择一个文件夹。
- http://srv.symbolsource.org/pdb/Public 添加 MVC
- 按如下设置
历史
版本 1 – 2016 年 2 月 10 日