简体   繁体   English

如何在渲染具有相同属性名称的多个输入元素时使用替代的自动生成标识符?

[英]How to use an alternative auto-generated identifier upon rendering multiple input elements with the same property name?

Let's assume that I have a view model like this假设我有这样的视图 model

public class ExampleVM
{
    [Display(Name = "Foo")]
    public Nullable<decimal> FooInternal { get; set; }
}

My view looks like this (also a form tag which I omitted in this post)我的视图看起来像这样(也是我在这篇文章中省略的表单标签)

@model ExampleVM
....
<input asp-for="FooInternal" class="form-control" type="number" />

This results in a rendered text box with FooInternal as id-attribute.这会生成一个以FooInternal作为 id 属性的渲染文本框。

In my scenario, I also have a modal dialog with another form with another view model which shares a property with the same name.在我的场景中,我还有一个带有另一个表单的模式对话框,另一个视图 model 共享一个同名的属性。 I know that the asp-for taghelper renders the id-attribute either a manually from a specified id or infers the id from the property name.我知道asp-for taghelper 从指定的 id 手动呈现 id 属性或从属性名称推断 id。

In my backend code, I want to be able to name my properties how I seem fit given the view model context.在我的后端代码中,我希望能够在给定视图 model 上下文的情况下命名我的属性。 I don't want to rename my properties to make them globally unique.我不想重命名我的属性以使它们在全球范围内独一无二。

I try to avoid two things:我尽量避免两件事:

  • To manually specify the id in the view / the input element.在视图/输入元素中手动指定 id。 I'd much rather use an autogenerated id that I can set via another attribute in the backend.我更愿意使用可以通过后端的另一个属性设置的自动生成的 id。
  • Given that I use the view model with [FromBody] in a post, I can't exactly rename the property as it would be with [FromRoute(Name="MyFoo")] .鉴于我在帖子中使用带有[FromBody]的视图 model ,我不能像使用[FromRoute(Name="MyFoo")]那样完全重命名该属性。 I don't want to map a manually entered id back to my property.我不想 map 手动输入的 id 回到我的财产。

Basically, I'm looking for something like this:基本上,我正在寻找这样的东西:

public class ExampleVM
{
    [Display(Name = "Foo")]
    [HtmlId(Name = "MyUniqueFooName")]
    public Nullable<decimal> FooInternal { get; set; }
}

where HtmlId would be an attribute that interacts with the tag-helper for rendering and also for rebinding the view model as parameter from a [HttpPost] method.其中 HtmlId 是一个属性,它与标签助手交互以进行渲染,也用于重新绑定视图 model 作为[HttpPost]方法的参数。

Maybe another approach is also valid since avoiding multiple input elements (with the same identifier) in multiple forms on the same page seems like a common situation to me.也许另一种方法也是有效的,因为在同一页面上的多个 forms 中避免多个输入元素(具有相同的标识符)对我来说似乎很常见。

According to your description, if you want to achieve your requirement, you should write custom modelbinding and custom input tag helper to achieve your requirement.根据您的描述,如果您想实现您的要求,您应该编写自定义模型绑定和自定义输入标签助手来实现您的要求。

Since the asp.net core modelbinding will bind the data according to the post back's form data, you should firstly write the custom input tag helper to render the input name property to use HtmlId value.由于 asp.net 核心模型绑定会根据回传的表单数据绑定数据,因此您应该首先编写自定义输入标签助手以渲染输入名称属性以使用 HtmlId 值。

Then you should write a custom model binding in your project to bind the model according to the HtmlId attribute.然后您应该在您的项目中编写自定义 model 绑定,以根据 HtmlId 属性绑定 model。

About how to re-write the custom input tag helper, you could refer to below steps:关于如何重写自定义输入标签助手,您可以参考以下步骤:

Notice: Since the input tag helper has multiple type "file, radio,checkbox and else", you should write all the logic based on the source codes .注意:由于输入标签助手有多种类型“文件,单选,复选框和其他”,您应该根据源代码编写所有逻辑。

According to the input taghelper source codes, you could find the tag helper will call the Generator.GenerateTextBox method to generate the input tag html content.根据输入标签助手源码,可以发现标签助手会调用Generator.GenerateTextBox方法生成输入标签html内容。

The Generator.GenerateTextBox has five parameters, the third parameter expression is used to generate the input textbox's for attribute. Generator.GenerateTextBox 有五个参数,第三个参数表达式用于生成输入文本框的 for 属性。

Generator.GenerateTextBox(
                ViewContext,
                modelExplorer,
                For.Name,
                modelExplorer.Model,
                format,
                htmlAttributes);

If you want to show the HtmlId value as the name for the for attribute, you should create a custom input taghelper.如果要将 HtmlId 值显示为 for 属性的名称,则应创建自定义输入 taghelper。

You should firstly create a custom attribute:您应该首先创建一个自定义属性:

[System.AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
public class HtmlId : Attribute
{
    public string _Id;
    public HtmlId(string Id) {

        _Id = Id;
    }

    public string Id
    {
        get { return _Id; }
    }
}

Then you could use var re = ((Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DefaultModelMetadata)For.ModelExplorer.Metadata).Attributes.PropertyAttributes.Where(x => x.GetType() == typeof(HtmlId)).FirstOrDefault();然后你可以使用var re = ((Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DefaultModelMetadata)For.ModelExplorer.Metadata).Attributes.PropertyAttributes.Where(x => x.GetType() == typeof(HtmlId)).FirstOrDefault(); to get the htmlid in the input tag helper's GenerateTextBox method.在输入标签助手的 GenerateTextBox 方法中获取 htmlid。

Details, you could refer to below custom input tag helper codes:详细信息,您可以参考下面的自定义输入标签助手代码:

using Microsoft.AspNetCore.Mvc.TagHelpers;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace SecurityRelatedIssue
{
    [HtmlTargetElement("input", Attributes = ForAttributeName, TagStructure = TagStructure.WithoutEndTag)]
    public class CustomInputTagHelper: InputTagHelper
    {
        private const string ForAttributeName = "asp-for";
        private const string FormatAttributeName = "asp-format";
        public override int Order => -10000;
        public CustomInputTagHelper(IHtmlGenerator generator)
       : base(generator)
        {
        }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (output == null)
            {
                throw new ArgumentNullException(nameof(output));
            }

            // Pass through attributes that are also well-known HTML attributes. Must be done prior to any copying
            // from a TagBuilder.
            if (InputTypeName != null)
            {
                output.CopyHtmlAttribute("type", context);
            }

            if (Name != null)
            {
                output.CopyHtmlAttribute(nameof(Name), context);
            }

            if (Value != null)
            {
                output.CopyHtmlAttribute(nameof(Value), context);
            }

            // Note null or empty For.Name is allowed because TemplateInfo.HtmlFieldPrefix may be sufficient.
            // IHtmlGenerator will enforce name requirements.
            var metadata = For.Metadata;
            var modelExplorer = For.ModelExplorer;
            if (metadata == null)
            {
                throw new InvalidOperationException();
            }

            string inputType;
            string inputTypeHint;
            if (string.IsNullOrEmpty(InputTypeName))
            {
                // Note GetInputType never returns null.
                inputType = GetInputType(modelExplorer, out inputTypeHint);
            }
            else
            {
                inputType = InputTypeName.ToLowerInvariant();
                inputTypeHint = null;
            }

            // inputType may be more specific than default the generator chooses below.
            if (!output.Attributes.ContainsName("type"))
            {
                output.Attributes.SetAttribute("type", inputType);
            }

            // Ensure Generator does not throw due to empty "fullName" if user provided a name attribute.
            IDictionary<string, object> htmlAttributes = null;
            if (string.IsNullOrEmpty(For.Name) &&
                string.IsNullOrEmpty(ViewContext.ViewData.TemplateInfo.HtmlFieldPrefix) &&
                !string.IsNullOrEmpty(Name))
            {
                htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase)
                {
                    { "name", Name },
                };
            }

            TagBuilder tagBuilder;
            switch (inputType)
            {
                //case "hidden":
                //    tagBuilder = GenerateHidden(modelExplorer, htmlAttributes);
                //    break;

                //case "checkbox":
                //    tagBuilder = GenerateCheckBox(modelExplorer, output, htmlAttributes);
                //    break;

                //case "password":
                //    tagBuilder = Generator.GeneratePassword(
                //        ViewContext,
                //        modelExplorer,
                //        For.Name,
                //        value: null,
                //        htmlAttributes: htmlAttributes);
                //    break;

                //case "radio":
                //    tagBuilder = GenerateRadio(modelExplorer, htmlAttributes);
                //    break;

                default:
                    tagBuilder = GenerateTextBox(modelExplorer, inputTypeHint, inputType, htmlAttributes);
                    break;
            }

            if (tagBuilder != null)
            {
                // This TagBuilder contains the one <input/> element of interest.
                output.MergeAttributes(tagBuilder);
                if (tagBuilder.HasInnerHtml)
                {
                    // Since this is not the "checkbox" special-case, no guarantee that output is a self-closing
                    // element. A later tag helper targeting this element may change output.TagMode.
                    output.Content.AppendHtml(tagBuilder.InnerHtml);
                }
            }
        }


        private TagBuilder GenerateTextBox(
     ModelExplorer modelExplorer,
     string inputTypeHint,
     string inputType,
     IDictionary<string, object> htmlAttributes)
        {
            var format = Format;
            if (string.IsNullOrEmpty(format))
            {
                if (!modelExplorer.Metadata.HasNonDefaultEditFormat &&
                    string.Equals("week", inputType, StringComparison.OrdinalIgnoreCase) &&
                    (modelExplorer.Model is DateTime || modelExplorer.Model is DateTimeOffset))
                {
                   // modelExplorer = modelExplorer.GetExplorerForModel(FormatWeekHelper.GetFormattedWeek(modelExplorer));
                }
                else
                {
                    //format = GetFormat(modelExplorer, inputTypeHint, inputType);
                }
            }

            if (htmlAttributes == null)
            {
                htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
            }

            htmlAttributes["type"] = inputType;
            if (string.Equals(inputType, "file"))
            {
                htmlAttributes["multiple"] = "multiple";
            }

            var re = ((Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DefaultModelMetadata)For.ModelExplorer.Metadata).Attributes.PropertyAttributes.Where(x => x.GetType() == typeof(HtmlId)).FirstOrDefault();

 
            return Generator.GenerateTextBox(
                ViewContext,
                modelExplorer,
                ((HtmlId)re).Id,
                modelExplorer.Model,
                format,
                htmlAttributes);
        }

    }
}

Improt this taghelper in _ViewImports.cshtml在 _ViewImports.cshtml 中引入这个 taghelper

@addTagHelper *,[yournamespace]

Model exmaple: Model 示例:

    [Display(Name = "Foo")]
    [HtmlId("test")]
    public string str { get; set; }

Result:结果:

在此处输入图像描述

Then you could write a custom model binding for the model to bind the data according to the htmlid.然后,您可以为 model 编写自定义 model 绑定,以根据 htmlid 绑定数据。 About how to use custom model binding, you could refer to this article .关于如何使用自定义 model 绑定,可以参考这篇文章

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM