【发布时间】:2019-11-20 21:51:57
【问题描述】:
我想将模型表达式(例如属性)绑定到视图组件——就像我使用 HTML 助手(例如,@Html.EditorFor())或标签助手(例如,<partial for />)一样—和在视图中使用嵌套的 HTML 和/或标签助手重用这个模型。我能够将ModelExpression 定义为视图组件上的参数,并从中检索许多有用的元数据。除此之外,我开始遇到障碍:
- 如何中继和绑定到底层源模型,例如
asp-for标签助手? - 如何确保来自
ViewData.ModelMetadata的属性元数据(例如验证属性)得到尊重? - 如何为字段
name属性组装一个完全合格的HtmlFieldPrefix?
我在下面提供了一个(简化的)场景,其中包含代码和结果,但代码暴露的未知数多于答案。众所周知,大部分代码是不正确的,但我将其包括在内,以便我们可以有一个具体的基线来评估和讨论替代方案。
场景
<select> 列表的值需要通过数据存储库填充。假设将可能的值填充为例如的一部分是不切实际或不可取的。原始视图模型(请参阅下面的“替代选项”)。
示例代码
/Components/SelectListViewComponent.cs
using system;
using Microsoft.AspNetCore.Mvc.Rendering;
public class SelectViewComponent
{
private readonly IRepository _repository;
public SelectViewComponent(IRepository repository)
{
_repository = repository?? throw new ArgumentNullException(nameof(repository));
}
public IViewComponentResult Invoke(ModelExpression aspFor)
{
var sourceList = _repository.Get($"{aspFor.Metadata.Name}Model");
var model = new SelectViewModel()
{
Options = new SelectList(sourceList, "Id", "Name")
};
ViewData.TemplateInfo.HtmlFieldPrefix = ViewData.TemplateInfo.GetFullHtmlFieldName(modelMetadata.Name);
return View(model);
}
}
备注
- 使用
ModelExpression不仅允许我使用模型表达式调用视图组件,而且还通过反射为我提供了许多有用的元数据,例如验证参数。 - 参数名称
for在C# 中是非法的,因为它是保留关键字。因此,我改用aspFor,它将以asp-for的形式暴露给标签助手格式。这有点小技巧,但为开发人员提供了一个熟悉的界面。 - 显然,
_repository代码和逻辑将随实现而有很大差异。在我自己的用例中,我实际上是从一些自定义属性中提取参数。 -
GetFullHtmlFieldName()不构造 完整 HTML 字段名称;它总是返回我提交给它的任何值,这只是模型表达式名称。在下面的“问题”下对此进行了详细介绍。
/Models/SelectViewModel.cs
using Microsoft.AspNetCore.Mvc.Rendering;
public class SelectViewModel {
public SelectList Options { get; set; }
}
备注
- 从技术上讲,在这种情况下,我可以直接将
SelectList返回到视图,因为它将处理当前值。但是,如果您将模型绑定到<select>的asp-for标签助手,那么它将自动启用multiple,这是绑定到集合模型时的默认行为。
/Views/Shared/Select/Default.cshtml
@model SelectViewModel
<select asp-for=@Model asp-items="Model.Options">
<option value="">Select one…</option>
</select>
备注
- 从技术上讲,
@Model的值将返回SelectViewModel。如果这是<input />,那将是显而易见的。由于SelectList识别了正确的值,这个问题被掩盖了,大概来自ViewData.ModelMetadata。 - 我可以改为将
aspFor.Model设置为例如SelectViewModel上的UnderlyingModel属性。这将导致 HTML 字段名称为{HtmlFieldPrefix}.UnderlyingModel,并且仍然无法从原始属性中检索任何元数据(例如验证属性)。
变化
如果我不设置HtmlFieldPrefix,并将视图组件放置在例如上下文中<partial for /> 或 @Html.EditorFor() 那么字段名称将是正确的,因为 HtmlFieldPrefix 是在父上下文中定义的。但是,如果我将它直接放在顶级视图中,由于 HtmlFieldPrefix 未定义,我将收到以下错误:
ArgumentException:HTML 字段的名称不能为 null 或为空。而是使用带有非空 htmlFieldName 参数值的 Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor 或 Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper``1.EditorFor 方法。 (参数'表达式')
问题
-
HtmlFieldPrefix未正确填充完全限定值。例如,如果模型属性名称是Country,它将始终返回Country,即使实际模型路径是ShippingAddress.Country或Addresses[2].Country。 -
jQuery Validation Unobtrusive 功能未触发。例如,如果这个绑定的属性被标记为
[Required],那么这里就不会被标记。这大概是因为它被绑定到SelectViewModel,而不是父属性。 - 原始模型不会以任何方式传递到视图组件的视图;
SelectList能够从ViewData中推断出原始值,但这在视图中丢失了。我可以通过视图模型传递aspFor.Model,但它无法访问原始元数据(例如验证属性)。
备用选项
我考虑过的其他一些选项,但在我的用例中被拒绝了。
-
标签助手:这很容易通过标签助手实现。将依赖项(例如存储库)注入标签助手不太优雅,因为没有一种方法可以通过组合根实例化标签助手,例如
IViewComponentActivator。 -
控制器: 在这个简化的示例中,还可以在顶级视图模型上定义源集合,在实际属性旁边(例如,
Country用于值,CountryList用于选项)。在更复杂的示例中,这可能不实用或不优雅。 -
AJAX: 可以通过对 Web 服务的 JavaScript 调用检索值,将 JSON 输出绑定到客户端上的
<select>元素。我在其他应用程序中使用了这种方法,但在这里不受欢迎,因为我不想将所有潜在的查询逻辑暴露给公共接口。 -
显式值:我可以将 parent 模型与
ModelExpression一起显式传递,以便在视图组件下重新创建父上下文。这有点麻烦,所以我想先尝试一下ModelExpression方法。
以前的研究
之前有人问过(并回答过)这个问题:
然而,在这两种情况下,接受的答案(一个由 OP 提供)并没有完全探讨问题,而是决定标签助手更适合他们的场景。标签助手很棒,并且有其目的;但是,对于视图组件更合适的场景(例如依赖于外部服务),我想充分探讨原始问题。
我是在把兔子追到一个洞里吗?或者社区对模型表达式的更深入理解可以解决哪些选项?
【问题讨论】:
标签: c# asp.net-core asp.net-core-3.1 asp.net-core-tag-helpers asp.net-core-viewcomponent