【问题标题】:How to bind a ModelExpression to a ViewComponent in ASP.NET Core?如何将 ModelExpression 绑定到 ASP.NET Core 中的 ViewComponent?
【发布时间】: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。如果这是&lt;input /&gt;,那将是显而易见的。由于SelectList 识别了正确的值,这个问题被掩盖了,大概来自ViewData.ModelMetadata
  • 我可以改为将aspFor.Model 设置为例如SelectViewModel 上的 UnderlyingModel 属性。这将导致 HTML 字段名称为 {HtmlFieldPrefix}.UnderlyingModel,并且仍然无法从原始属性中检索任何元数据(例如验证属性)。

变化

如果我不设置HtmlFieldPrefix,并将视图组件放置在例如上下文中&lt;partial for /&gt;@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 功能未触发。例如,如果这个绑定的属性被标记为[Required],那么这里就不会被标记。这大概是因为它被绑定到SelectViewModel,而不是父属性。
  • 原始模型不会以任何方式传递到视图组件的视图; SelectList 能够从 ViewData 中推断出原始值,但这在视图中丢失了。我可以通过视图模型传递aspFor.Model,但它无法访问原始元数据(例如验证属性)。

备用选项

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

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

以前的研究

之前有人问过(并回答过)这个问题:

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

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

【问题讨论】:

    标签: c# asp.net-core asp.net-core-3.1 asp.net-core-tag-helpers asp.net-core-viewcomponent


    【解决方案1】:

    以否定的方式回答我自己的问题:我最终得出的结论是,虽然就我们的父视图而言,这可能是直观且理想的功能,但它最终是一个混淆的概念我们的视图组件

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

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

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

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

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

    在我最初的问题中,我提出了@Model,这类似于您在例如通过@Html.EditorFor() 调用的编辑器模板

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

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

    同样,您可以通过例如转发ModelExpression.Model SelectViewModel.Model 属性…

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

    ...但这也不能解决问题,因为很明显,中继 不会中继源属性的 属性

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-06-11
      • 2019-10-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多