如何将ModelExpression绑定到ASP.NET Core中的ViewComponent? [英] How to bind a ModelExpression to a ViewComponent in ASP.NET Core?

本文介绍了如何将ModelExpression绑定到ASP.NET Core中的ViewComponent?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想将模型表达式(例如属性)绑定到视图组件,就像使用HTML帮助器(例如 @ Html.EditorFor())或标签帮助程序(例如,< /> 的部分)— 在具有嵌套HTML和/或标记助手。我可以在视图组件上定义 ModelExpression 作为参数,并从中检索很多有用的元数据。除此之外,我开始遇到障碍:




  • 如何中继和绑定到底层源模型,例如 asp-for 标记帮助器?

  • 如何确保 ViewData中的属性元数据(例如,验证属性) .ModelMetadata 是荣幸的吗?

  • 如何组装完全合格的 HtmlFieldPrefix 对于字段 name 属性?



我提供了一个( )中包含以下代码和结果的场景-但是代码暴露的未知数多于答案。众所周知,许多代码都是不正确的,但我将其包括在内,因此我们可以有一个具体的基准来评估和讨论替代方案。



场景



< select> 列表的值需要通过数据存储库填充。假设将可能的值作为例如值的一部分来填充是不切实际或不希望的。原始视图模型(请参见下面的替代选项。)



示例代码



/Components/SelectListViewComponent.cs



 使用系统; 
使用Microsoft.AspNetCore.Mvc.Rendering;

公共类SelectViewComponent
{

private只读IRepository _repository;

公共SelectViewComponent(IRepository存储库)
{
_repository =存储库???抛出新的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 不仅允许我使用模型调用视图组件表达式,但也通过反射为我提供了很多有用的元数据,例如验证参数。

  • 在C#中,参数名称 for 是非法的,因为它是保留关键字。因此,我改用 aspFor ,它将以 asp-for 的形式显示在标签帮助程序格式中。 b

  • 显然, _repository 的代码和逻辑将有很大的不同。与实施。在我自己的用例中,实际上是从一些自定义属性中提取参数。

  • GetFullHtmlFieldName()不会构造完整 HTML字段名称;它总是返回我提交给它的任何值,即模型表达式名称。



/Models/SelectViewModel.cs



 使用Microsoft.AspNetCore.Mvc.Rendering; 

公共类SelectViewModel {
公共SelectList选项{组; }
}

注释




  • 从技术上讲,在这种情况下,我可以直接将 SelectList 返回给视图,因为它将处理当前值。但是,如果将模型绑定到< select> asp-for 标记帮助器,则它将自动启用 multiple ,这是绑定到收集模型时的默认行为。



/Views/Shared/Select/Default.cshtml



  @model SelectViewModel 

<选择asp-for = @ Model asp-items = Model.Options>
< option value =>选择一个…< / option>
< / select>

注释




  • 从技术上讲, @Model 的值将返回 SelectViewModel 。如果这是< input /> ,那将是显而易见的。由于 SelectList 标识了正确的值(可能来自 ViewData.ModelMetadata ),因此掩盖了此问题。

  • 我可以将 aspFor.Model 设置为例如 SelectViewModel 上的 UnderlyingModel 属性。这将导致HTML字段名称为 {HtmlFieldPrefix} .UnderlyingModel ,并且仍然无法从原始属性中检索任何元数据(例如验证属性)。 / li>


变化



如果我未设置 HtmlFieldPrefix ,并将视图组件放置在例如<部分为/> @ 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

  • The jQuery Validation不干扰功能未启用。例如,如果此属性绑定到的属性被标记为 [必需] ,则此处不会被标记。大概是因为它绑定到 SelectViewModel 而不是父属性。

  • 原始模型没有以任何方式中继到视图组件的视图; SelectList 能够从 ViewData 推断出原始值,但是该值丢失了。我可以通过视图模型中继 aspFor.Model ,但它无法访问原始元数据(例如验证属性)。



备用选项



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




  • 标签助手:这很容易通过标签助手来实现。将依赖项(例如存储库)注入到标签助手中并不那么优雅,因为没有一种方法可以通过组合根实例化标签助手,例如, IViewComponentActivator

  • 控制器:在此简化示例中,还可以在顶级视图模型上的实际属性(例如, Country (值), CountryList (选项)。在更复杂的示例中,这可能不切合实际。

  • AJAX:可以通过对Web服务的JavaScript调用(绑定JSON输出)来检索值。到客户端上的< select> 元素。我在其他应用程序中使用了这种方法,但是这里不希望这样做,因为我不想将所有潜在的查询逻辑公开给公共接口。

  • 显式值:我可以将 parent 模型与 ModelExpression 一起显式中继,以便在视图组件下重新创建父上下文。这有点矛盾,所以我想先尝试使用 ModelExpression 方法。



先前的研究



此问题已在以下情况问过(并回答过):





但是,在两种情况下,被接受的答案(OP均接受)并没有完全探讨问题,而是决定标签助手更适合他们的情况。标记助手很棒,并且有其目标。但是,对于视图组件更合适的情况(例如,取决于外部服务),我想全面探讨原始问题。



我正在追逐兔子掉进洞里?还是社区可以更深入地理解模型表达式?

解决方案

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

即使您解决了技术问题,也可以从 ModelExpression中提取完全合格的 HtmlFieldPrefix ,更深层次的问题是概念上的。大概,视图组件将组装其他数据,并通过新的视图模型(例如,问题中建议的 SelectViewModel )将其中继到视图。否则,使用视图组件并没有真正的好处。 但是,在视图组件的视图中,没有逻辑方法将 child 视图模型的属性映射回 parent 视图模型。



例如,假设我们在父视图中将视图组件绑定到 UserViewModel.Country 属性:

  @model UserViewModel 

< vc:select asp- for = Country />

然后,您在子视图中绑定了哪些属性? / p>

  @model SelectViewModel 

<选择asp-for = @? ?? asp-items = Model.Options>
< option value =>选择一个…< / option>
< / select>

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

 <选择asp-for = @ Model asp-items = Model.Options> 
< option value =>选择一个…< / option>
< / select>

这可能会返回正确的 id name 属性,因为它回溯到 ViewData HtmlFieldPrefix >。但是,它将无法访问任何数据验证属性,因为它绑定到 SelectViewModel ,而不是对原始 UserViewModel.Country 属性的引用



类似地,您可以中继 ModelExpression.Model 通过例如下来 SelectViewModel.Model 属性…

  <选择asp-for = @ Model asp-items = Model.Options> 
< option value =>选择一个…< / option>
< / select>

...但这并不能解决问题,因为显然是中继 value 不会中继源属性的属性



最终,您想要的是绑定 asp-for 到您的 ModelExpression 解析为的原始对象的原始属性。尽管您可以从 ModelExpression 描述该属性和对象中获取元数据,但似乎没有一种中继引用的方法的方式,使 asp-for 标签助手可以识别。



显然,构想Microsoft将低级工具构建到 ModelExpression 以及 asp-for 标记帮助器的核心实现中一直沿线中继 ModelExpression 对象。或者,他们可以建立一个关键字,例如 @ParentModel ,该关键字允许从父视图引用模型。



我不会将其标记为答案,希望有人在某个时候找到我缺少的东西。但是,如果其他任何人都在尝试做这些工作并记录我自己的结论,我想把这些笔记留在这里。


I would like to bind a model expression (such as a property) to a view component—much like I would with an HTML helper (e.g., @Html.EditorFor()) or a tag helper (e.g., <partial for />)—and reuse this model in the view with nested HTML and/or tag helpers. I am able to define a ModelExpression as a parameter on a view component, and retrieve a lot of useful metadata from it. Beyond this, I start running into roadblocks:

  • How do I relay and bind to the underlying source model to e.g. an asp-for tag helper?
  • How do I ensure property metadata (e.g. validation attributes) from ViewData.ModelMetadata are honored?
  • How do I assemble a fully qualified HtmlFieldPrefix for the field name attribute?

I've provided a (simplified) scenario with code and outcomes below—but the code exposes more unknowns than answers. Much of the code is known to be incorrect, but I'm including it so we can have a concrete baseline to evaluate and discuss alternatives to.

Scenario

The values of a <select> list need to be populated via a data repository. Assume it is impractical or undesirable to populate the possible values as part of e.g. the original view model (see "Alternate Options" below).

Sample Code

/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);
  }

}

Notes

  • Using ModelExpression not only allows me to call the view component with a model expression, but also gives me a lot of useful metadata via reflection such as validation parameters.
  • The parameter name for is illegal in C#, since it's a reserved keyword. As such, I'm instead using aspFor, which will be exposed to the tag helper format as asp-for. This is a bit of a hack, but yields a familiar interface for developers.
  • Obviously, the _repository code and logic will vary considerably with implementation. In my own use case, I actually pull the arguments from some custom attributes.
  • The GetFullHtmlFieldName() doesn't construct a full HTML field name; it always returns whatever value I submit to it, which is just the model expression name. More on this under "Issues" below.

/Models/SelectViewModel.cs

using Microsoft.AspNetCore.Mvc.Rendering;

public class SelectViewModel {
  public SelectList Options { get; set; }
}

Notes

  • Technically, in this case, I could just return the SelectList directly to the view, since it will handle the current value. However, if you bind your model to your <select>'s asp-for tag helper, then it will automatically enable multiple, which is the default behavior when binding to a collection model.

/Views/Shared/Select/Default.cshtml

@model SelectViewModel

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

Notes

  • Technically, the value for @Model will return SelectViewModel. If this were an <input /> that would be obvious. This issue is obscured due to the SelectList identifying the correct value, presumably from the ViewData.ModelMetadata.
  • I could instead set the aspFor.Model to e.g. an UnderlyingModel property on the SelectViewModel. That would result in an HTML field name of {HtmlFieldPrefix}.UnderlyingModel—and would still fail to retrieve any of the metadata (such as validation attributes) from the original property.

Variations

If I don't set the HtmlFieldPrefix, and place the view component within the context of e.g. a <partial for /> or @Html.EditorFor() then the field names will be correct, as the HtmlFieldPrefix is getting defined in a parent context. If I place it directly in a top-level view, however, I will get the following error due to the HtmlFieldPrefix not being defined:

ArgumentException: The name of an HTML field cannot be null or empty. Instead use methods Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper.Editor or Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper``1.EditorFor with a non-empty htmlFieldName argument value. (Parameter 'expression')

Issues

  • The HtmlFieldPrefix doesn't get properly populated with a fully qualified value. E.g., if the model property name is Country it will always return Country, even if the actual model path is, say, ShippingAddress.Country or Addresses[2].Country.
  • The jQuery Validation Unobtrusive functionality isn't firing. For instance, if the property this is bound to is marked as [Required] then that's not getting flagged here. That's presumably because it's being bound to the SelectViewModel, not the parent property.
  • The original model isn't being relayed in any way to the view component's view; the SelectList is able to infer the original value from ViewData, but that is lost to the view. I could relay the aspFor.Model via the view model, but it won't have access to the original metadata (such as validation attributes).

Alternate Options

Some other options I've considered, and rejected for my use cases.

  • Tag Helpers: This is easy to achieve via tag helpers. Injecting dependencies, such as a repository, into a tag helper is less elegant since there isn't a way to instantiate a tag helper via the composition root, as one can do with e.g. IViewComponentActivator.
  • Controllers: In this simplified example, it is also possible to define the source collection on the top-level view model, next to the actual property (e.g., Country for the value, CountryList for the options). That may not be practical or elegant in more sophisticated examples.
  • AJAX: The values could be retrieved via a JavaScript call to a web service, binding the JSON output to the <select> element on the client. I use this approach in other applications, but it's undesirable here since I don't want to expose the full range of potential query logic to a public interface.
  • Explicit Values: I could explicitly relay the parent model along with the ModelExpression in order to recreate the parent context under the view component. That's a bit of a kludge, so I'd like to game out the ModelExpression approach first.

Previous Research

This question has been asked (and answered) before:

In both cases, however, the accepted answer (one by the OP) doesn't fully explore the question, and instead decides that a tag helper is more suitable for their scenarios. Tag helpers are great, and have their purpose; I'd like to fully explore the original questions, however, for the scenarios where view components are more appropriate (such as depending on an external service).

Am I chasing a rabbit down a hole? Or are there options that the community's deeper understanding of model expressions can resolve?

解决方案

To answer my own question in the negative: I ultimately came to the conclusion that while this may well be intuitive and desirable functionality in terms of our parent views, it's ultimately a confused concept in terms of our view components.

Even if you resolve the technical issue with extracting the fully-qualified HtmlFieldPrefix from ModelExpression, the deeper issue is conceptual. Presumably, the view component will assemble additional data, and relay it down to the view via a new view model—e.g., the SelectViewModel proposed in the question. Otherwise, there's no real benefit to using a view component. In the view component's view, however, there's no logical way to map properties of the child view model back to the parent view model.

So, for example, let us say that in your parent view you bind the view component to a UserViewModel.Country property:

@model UserViewModel

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

Then, what properties do you bind to in the child view?

@model SelectViewModel

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

In my original question, I proposed @Model, which is similar to what you would do in e.g. an editor template called via @Html.EditorFor():

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

That might return the correct id and name attributes, since it's falling back to the HtmlFieldPrefix of the ViewData. But, it's not going to have access to any e.g. data validation attributes, since it's binding to a SelectViewModel and not a reference to the original UserViewModel.Country property, as it would in an editor template.

Similarly, you could relay the ModelExpression.Model down via e.g. a SelectViewModel.Model property…

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

…but that doesn't solve the problem either since, obviously, relaying a value doesn't relay the attributes of the source property.

Ultimately, what you want is to bind your asp-for to the original property on the original object that your ModelExpression is resolving to. And while you can get metadata from ModelExpression describing that property and object, there doesn't seem to be a way to relay a reference to it in a way that the asp-for tag helpers recognize.

Obviously, one could conceive of Microsoft building in lower-level tooling into ModelExpression and the core implementations of the asp-for tag helpers which allow relaying ModelExpression objects all the way down the line. Alternatively, they might establish a keyword—such as @ParentModel—which allows a reference to the model from the parent view. In absence of that, however, this doesn't seem feasible.

I'm not going to mark this as the answer in hopes that someone, at some point, finds something I'm missing. I wanted to leave these notes here, however, in case anyone else is trying to make this work, and to document my own conclusions.

这篇关于如何将ModelExpression绑定到ASP.NET Core中的ViewComponent?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆