如何将 ModelExpression 绑定到 ASP.NET Core 中的 ViewComponent?

Jer*_*ney 7 c# asp.net-core asp.net-core-tag-helpers asp.net-core-viewcomponent asp.net-core-3.1

我想将模型表达式(例如属性)绑定到视图组件——就像我使用 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);
  }

}
Run Code Online (Sandbox Code Playgroud)

笔记

  • 使用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; }
}
Run Code Online (Sandbox Code Playgroud)

笔记

  • 从技术上讲,在这种情况下,我可以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>
Run Code Online (Sandbox Code Playgroud)

笔记

  • 从技术上讲, 的值@Model将返回SelectViewModel。如果这是一个<input />,那将是显而易见的。由于SelectList识别了正确的值(大概来自ViewData.ModelMetadata.
  • 我可以改为设置aspFor.Model到例如UnderlyingModel对物业SelectViewModel。这将导致 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验证不显眼的功能不点火。例如,如果绑定到的属性被标记为[Required]那么这里就不会被标记。这大概是因为它被绑定到SelectViewModel,而不是父属性。
  • 原始模型不会以任何方式传递到视图组件的视图;在SelectList能够推断从原来的值ViewData,但毕竟是输给了看法。我可以aspFor.Model通过视图模型中继,但它无法访问原始元数据(例如验证属性)。

替代选项

我考虑过的其他一些选择,但在我的用例中被拒绝。

  • 标签助手:这很容易通过标签助手实现。将依赖项(例如存储库)注入标签助手不太优雅,因为没有一种方法可以通过组合根实例化标签助手,例如可以使用IViewComponentActivator.
  • 控制器:在这个简化的例子中,还可以在顶级视图模型上定义源集合,紧挨着实际属性(例如,Country对于值,CountryList对于选项)。在更复杂的示例中,这可能不实用或不优雅。
  • AJAX:可以通过对 Web 服务的 JavaScript 调用检索值,将 JSON 输出绑定到<select>客户端上的元素。我在其他应用程序中使用了这种方法,但在这里不受欢迎,因为我不想将所有潜在的查询逻辑暴露给公共接口。
  • 显式值:我可以显式地将父模型与 一起中继ModelExpression,以便在视图组件下重新创建父上下文。这有点麻烦,所以我想先玩弄这个ModelExpression方法。

以前的研究

这个问题之前曾被问过(并回答过):

然而,在这两种情况下,接受的答案(OP 的一个答案)并没有完全探索这个问题,而是决定标签助手更适合他们的场景。标签助手很棒,有他们的目的;但是,对于视图组件更合适的场景(例如依赖于外部服务),我想充分探讨原始问题。

我是在追兔子吗?或者社区对模型表达式的更深入理解可以解决哪些选项?

Jer*_*ney 5

否定地回答我自己的问题:我最终得出的结论是,虽然就我们的父视图而言,这可能是直观和理想的功能,但就我们的视图组件而言,它最终是一个令人困惑的概念。

即使您通过提取完全限定的HtmlFieldPrefixfrom解决了技术问题ModelExpression,更深层次的问题是概念性的。据推测,视图组件将组装额外的数据,并通过新的视图模型将其传递给视图——例如,SelectViewModel问题中提出的。否则,使用视图组件没有真正的好处。然而,在视图组件的视图中,没有将子视图模型的属性映射回父视图模型的逻辑方法。

因此,例如,让我们说在您的父视图中,您将视图组件绑定到一个UserViewModel.Country属性:

@model UserViewModel

<vc:select asp-for="Country" />
Run Code Online (Sandbox Code Playgroud)

那么,你在子视图中绑定了哪些属性?

@model UserViewModel

<vc:select asp-for="Country" />
Run Code Online (Sandbox Code Playgroud)

在我最初的问题中,我提出了@Model,这与您在例如名为 via的编辑器模板中所做的类似@Html.EditorFor():

@model SelectViewModel

<select asp-for=@??? asp-items="Model.Options">
  <option value="">Select one…</option>
</select>
Run Code Online (Sandbox Code Playgroud)

这可能会返回正确的id和name属性,因为它回落至HtmlFieldPrefix的ViewData。但是,它不会访问任何例如数据验证属性,因为它绑定到 aSelectViewModel而不是对原始UserViewModel.Country属性的引用,就像在编辑器模板中一样。

类似地,您可以ModelExpression.Model通过例如SelectViewModel.Model属性来传递向下的信息……

<select asp-for=@Model asp-items="Model.Options">
  <option value="">Select one…</option>
</select>
Run Code Online (Sandbox Code Playgroud)

......但是,这并不解决任何问题,因为,很明显,中继一个值不中继的属性的源属性。

最终,您想要的是将您绑定asp-for到您ModelExpression正在解析的原始对象上的原始属性。虽然您可以从ModelExpression 描述该属性和对象中获取元数据,但似乎没有一种方法可以以标签助手识别的方式传递对它的引用asp-for。

显然,人们可以设想 Microsoft 将底层工具构建到标签助手ModelExpression的核心实现中,asp-for从而允许从头到尾中继ModelExpression对象。或者,他们可能会建立一个关键字——例如@ParentModel——允许从父视图引用模型。然而,如果没有这一点,这似乎是不可行的。

我不会将此标记为答案,希望有人在某个时候找到我遗漏的东西。然而,我想把这些笔记留在这里,以防其他人试图完成这项工作,并记录我自己的结论。

  • 我遇到了同样的问题。那么,在 Angular、React、Vue 中可以轻松实现的视图组件化,在 Razor 中如何实现呢?似乎所有 UI 重用模式(基本上是局部视图和 ViewComponents)都会遇到相同的问题,即绑定到 ModelExpressions 的标签助手无法访问使其有用的属性。开始感觉 Razor 中实际上根本没有有用的 UI 重用机制! (2认同)