简体   繁体   中英

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

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.

In my scenario, I also have a modal dialog with another form with another view model which shares a property with the same name. 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.

In my backend code, I want to be able to name my properties how I seem fit given the view model context. 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. I'd much rather use an autogenerated id that I can set via another attribute in the backend.
  • 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")] . I don't want to map a manually entered id back to my property.

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.

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.

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.

Then you should write a custom model binding in your project to bind the model according to the HtmlId attribute.

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.

The Generator.GenerateTextBox has five parameters, the third parameter expression is used to generate the input textbox's for attribute.

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.

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(); to get the htmlid in the input tag helper's GenerateTextBox method.

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

@addTagHelper *,[yournamespace]

Model exmaple:

    [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. About how to use custom model binding, you could refer to this article .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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