7

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

}

笔记

  • usingModelExpression不仅允许我使用模型表达式调用视图组件,而且还通过反射为我提供了许多有用的元数据,例如验证参数。
  • 参数名称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…&lt;/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.CountryAddresses[2].Country
  • jQuery Validation Unobtrusive功能没有触发。例如,如果 this 绑定到的属性被标记为[Required]then 此处不会被标记。这大概是因为它被绑定到SelectViewModel,而不是父属性。
  • 原始模型不会以任何方式传递给视图组件的视图;SelectList能够从 中推断出原始值,ViewData但这在视图中丢失了。我可以aspFor.Model通过视图模型传递,但它无法访问原始元数据(例如验证属性)。

备用选项

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

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

以前的研究

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

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

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

4

1 回答 1

6

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

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

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

@model UserViewModel

<vc:select asp-for="Country" />

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

@model SelectViewModel

<select asp-for=@??? asp-items="Model.Options">
  <option value="">Select one…&lt;/option>
</select>

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

<select asp-for=@Model asp-items="Model.Options">
  <option value="">Select one…&lt;/option>
</select>

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

ModelExpression.Model类似地,您可以通过例如一个属性来传递向下消息SelectViewModel.Model……</p>

<select asp-for=@Model asp-items="Model.Options">
  <option value="">Select one…&lt;/option>
</select>

…但这也不能解决问题,因为很明显,传递不会传递源属性的属性。

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

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

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

于 2019-12-28T01:19:49.827 回答