我想将模型表达式(例如属性)绑定到视图组件——就像我使用 HTML 助手(例如,@Html.EditorFor()
)或标签助手(例如,<partial for />
)一样——并在视图中重用这个模型带有嵌套的 HTML 和/或标签助手。我能够将 a 定义ModelExpression
为视图组件上的参数,并从中检索许多有用的元数据。除此之外,我开始遇到障碍:
- 如何将底层源模型中继并绑定到例如
asp-for
标签助手? - 如何确保
ViewData.ModelMetadata
尊重属性元数据(例如验证属性)? - 我如何组装一个完全合格
HtmlFieldPrefix
的字段name
属性?
我在下面提供了一个(简化的)场景,其中包含代码和结果——但代码暴露的未知数多于答案。众所周知,许多代码是不正确的,但我将其包括在内,因此我们可以有一个具体的基线来评估和讨论替代方案。
设想
列表的值<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);
}
}
笔记
- using
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>
笔记
- 从技术上讲, for 的值
@Model
将返回SelectViewModel
。如果这是一个<input />
那将是显而易见的。由于SelectList
识别出正确的值(大概来自ViewData.ModelMetadata
. - 我可以改为将 设置为
aspFor.Model
例如UnderlyingModel
.SelectViewModel
这将导致 HTML 字段名称为{HtmlFieldPrefix}.UnderlyingModel
— 并且仍然无法从原始属性中检索任何元数据(例如验证属性)。
变化
如果我不设置HtmlFieldPrefix
, 并将视图组件放置在例如 a<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功能没有触发。例如,如果 this 绑定到的属性被标记为
[Required]
then 此处不会被标记。这大概是因为它被绑定到SelectViewModel
,而不是父属性。 - 原始模型不会以任何方式传递给视图组件的视图;
SelectList
能够从 中推断出原始值,ViewData
但这在视图中丢失了。我可以aspFor.Model
通过视图模型传递,但它无法访问原始元数据(例如验证属性)。
备用选项
我考虑过的其他一些选项,但在我的用例中被拒绝了。
- 标签助手:这很容易通过标签助手实现。将依赖项(例如存储库)注入标签助手不太优雅,因为没有一种方法可以通过组合根实例化标签助手,就像使用
IViewComponentActivator
. - 控制器:在这个简化的示例中,还可以在顶级视图模型上定义源集合,紧挨着实际属性(例如,
Country
对于值,CountryList
对于选项)。在更复杂的示例中,这可能不实用或不优雅。 - AJAX:可以通过对 Web 服务的 JavaScript 调用检索值,将 JSON 输出绑定到
<select>
客户端上的元素。我在其他应用程序中使用这种方法,但这里不希望这样做,因为我不想将所有潜在的查询逻辑暴露给公共接口。 - 显式值:我可以显式地传递父模型以及
ModelExpression
在视图组件下重新创建父上下文。这有点麻烦,所以我想先解决这个问题ModelExpression
。
以前的研究
这个问题之前已经被问过(并回答过):
然而,在这两种情况下,接受的答案(一个由 OP 提供)都没有完全探索问题,而是决定标签助手更适合他们的场景。标签助手很棒,并且有其目的;但是,对于视图组件更合适的场景(例如依赖于外部服务),我想充分探讨原始问题。
我在追兔子到洞里吗?或者社区对模型表达式的更深入理解可以解决哪些选项?