将ModelExpression从标签助手传递至局部视图

3
我将使用此处的示例,允许在标签助手内呈现部分视图。我的目标是能够像这样定义标签助手:
<mydateinput for="@Model.StartDate" />

在标签助手的c#代码中,我需要定义“for”属性,从我所了解的情况来看,这需要定义为“ModelExpression”。

public class MyDateInputTagHelper : TagHelper
{
    public ModelExpression For { get; set; }
    ...
}

使用之前在这篇文章中提到的代码,我正在渲染一个部分视图,并将标签助手的类作为部分视图的模型传递。

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        base.Process(context, output);

        ((IViewContextAware)HtmlHelper).Contextualize(ViewContext);

        output.Content.SetHtmlContent(HtmlHelper.Partial("~/Views/Partials/TagHelpers/MyDateInput.cshtml", this));

    }

最后,我的局部视图定义如下:
<input asp-for="For" />

我遇到的问题是无法正确传递模型表达式“For”到部分视图中。当我查看HTML源代码时,只能看到input标签的id和name属性中字面上的名称“For”。此外,我在razor页面模型中设置的值也没有正确显示。
我希望发生的情况是,在提交页面后,我的razor页面模型将以所选值填充位于标签助手/部分视图下方的"StartDate"(在我的示例中),而不是"For"属性。
有人知道我做错了什么,以及我可以在这个示例中更改什么来正确地将ModelExpression传递到部分视图吗?
2个回答

6
使用InputTagHelper无法直接实现该功能。有关更多信息,请参见Razor/issues #926,以及SO上的类似问题解决方法 但是作为一种解决方法,您可以使用自定义的HtmlHelper<TModel>(就像@Html一样)来达到相同的目标。您的局部视图可能如下所示:
@model App.TagHelpers.PartialVM

@{ 
    var PartialHtml = Model.HtmlHelper;
}

@PartialHtml.Label(Model.NewFor.Name,Model.NewFor.Name) 
@PartialHtml.TextBox(Model.NewFor.Name, Model.NewModel)

即使用@Html.Label()@Html.TextBox()而不是<label><input>

这里的PartialVM是一个简单的类,它保存有关模型的元信息:

public class PartialVM
{

    public PartialVM(ModelExpression originalFor, IHtmlHelper htmlHelper)
    {
        var originalExplorer = originalFor.ModelExplorer;

        OriginalFor = originalFor;
        OriginalExplorer = originalExplorer;
        NewModel = originalExplorer.Model;
        NewModelExplorer = originalExplorer.GetExplorerForModel(NewModel);
        NewFor = new ModelExpression(OriginalFor.Name, NewModelExplorer);
        this.HtmlHelper = htmlHelper;
    }


    public IHtmlHelper HtmlHelper { get; set; }

    public ModelExpression OriginalFor { get; set; }
    public ModelExplorer OriginalExplorer { get; set; }
    public ModelExpression NewFor { get; set; }
    public ModelExplorer NewModelExplorer { get; set; }
    public Object NewModel { get; set; }
}

请注意,IHtmlHelper 实际上是一个带有泛型参数的 IHtmlHelper<TSomeDynamicModel>,而不是一个普通的 IHtmlHelper

最后,按以下方式更改您的 TagHelper

    [HtmlTargetElement("my-custom-input")]
    public class MyCustomInputTagHelper : TagHelper
    {
        private readonly IServiceProvider _sp;

        [ViewContext]
        public ViewContext ViewContext { set; get; }

        public ModelExpression For { get; set; }

        public MyCustomInputTagHelper(IServiceProvider sp)
        {
            this._sp = sp;
        }

        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            base.Process(context, output);

            var originExplorer = For.ModelExplorer;
            var newModel = originExplorer.Model;
            var newExplorer = originExplorer.GetExplorerForModel(newModel);
            var newFor = new ModelExpression(For.Name, newExplorer);
            var ModelType = originExplorer.Container.Model.GetType();

            var htmlHelperType = typeof(IHtmlHelper<>).MakeGenericType(ModelType);
            var htmlHelper = this._sp.GetService(htmlHelperType) as IHtmlHelper;   // get the actual IHtmlHelper<TModel>
            (htmlHelper as IViewContextAware).Contextualize(ViewContext);

            var vm = new PartialVM(For, htmlHelper);

            var writer = new StringWriter();
            var content = await htmlHelper.PartialAsync("~/Views/Partials/TagHelpers/MyDateInput.cshtml", vm);
            output.TagName = "div";
            output.TagMode = TagMode.StartTagAndEndTag;
            output.Content.SetHtmlContent(content);
        }
    }

现在,您可以将任何 asp-for 表达式字符串传递给您的 TagHelperFor 属性,并且它应该按预期工作。

测试用例:

假设我们有一个 Dto 模型:

public class XModel {
    public int Id { get; set; }
    public DateTime StartDate { get; set; }
    public DateTime EndDate { get; set; }
    public string ActiveStatus{ get; set; }
}

您可以按照以下方式呈现它:
/// the action method looks like:
///    var model = new XModel {
///        StartDate = DateTime.Now,
///        EndDate = DateTime.Now.AddYears(1),
///        ActiveStatus = "Active",
///    };
///     return View(model);


@model XModel

<my-custom-input For="StartDate" />
<my-custom-input For="EndDate" />
<my-custom-input For="ActiveStatus" />

这是渲染时的截图: enter image description here

非常棒的回答,感谢您抽出时间回答并给我提供了继续前进的选择! - raterus
这是为了使一些微不足道的 UI 从一个部分可重用而进行的惊人工作量。在2022年,这仍然是Razor最好的处理方式吗?我们能否通过ViewComponent实现类似的功能?我已经尝试过了,但无法让它正常工作。 - Neutrino

0

或者在您的部分视图中使用Html Helpers,这样它就可以工作了。

// _MovieField.cshtml
@{
   string propertyName = (string)ViewData["PropertyName"];
}

<div class="form-group">

   @Html.Label(propertyName, null, new{ @class = "control-label" })
   @Html.Editor(propertyName, new { htmlAttributes = new { @class = "form-control" } })
   @Html.ValidationMessage(propertyName, null, new{ @class = "text-danger" })

</div>

并像这样使用它

// EditMovie.cshtml
<form method="post">
   <div asp-validation-summary="ModelOnly" class="text-danger"></div>
   <input type="hidden" asp-for="Movie.Id" />

   <partial name="_MovieField" for="Movie"
            view-data='new ViewDataDictionary(ViewData){ {"PropertyName", "Title" }}' />

   <partial name="_MovieField" for="Movie"
            view-data='new ViewDataDictionary(ViewData){ {"PropertyName", "ReleaseDate" }}' />

   <partial name="_MovieField" for="Movie"
            view-data='new ViewDataDictionary(ViewData){ {"PropertyName", "Genre" }}' />

   <partial name="_MovieField" for="Movie"
            view-data='new ViewDataDictionary(ViewData){ {"PropertyName", "Price" }}' />

   <partial name="_MovieField" for="Movie"
            view-data='new ViewDataDictionary(ViewData){ {"PropertyName", "Rating" }}' />

   <div class="form-group">
      <input type="submit" value="Save" class="btn btn-primary" />
   </div>

</form>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接