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

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

假设我有一个像这样的视图模型

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

我的视图看起来像这样(也是我在这篇文章中省略的表单标签)

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

这会产生一个以 FooInternal 作为 id-attribute 的渲染文本框。

在我的场景中,我还有一个模态对话框,其中另一个窗体与另一个视图模型共享同名的属性。我知道 asp-for taghelper 从指定的 id 手动呈现 id 属性或从属性名称推断 id。

在我的后端代码中,我希望能够根据视图模型上下文命名我的属性。我不想重命名我的属性以使它们在全球范围内唯一。

我尽量避免两件事:

  • 在视图/输入元素中手动指定 id。我更愿意使用可以通过后端的另一个属性设置的自动生成的 id。
  • 鉴于我在帖子中使用带有 [FromBody] 的视图模型,我无法像使用 [FromRoute(Name="MyFoo")] 那样完全重命名该属性。我不想将手动输入的 id 映射回我的属性。

基本上,我正在寻找这样的东西:

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

其中 HtmlId 是一个属性,它与标签助手交互以进行渲染,也用于将视图模型重新绑定为 [HttpPost] 方法的参数。

也许另一种方法也是有效的,因为在同一页面上避免多个表单中的多个输入元素(具有相同标识符)对我来说似乎是一种常见情况。


根据你的描述,如果你想达到你的要求,你应该编写自定义模型绑定和自定义输入标签助手来实现你的要求。

由于asp.net core modelbinding会根据post back的表单数据绑定数据,所以需要先编写自定义input tag helper渲染input name属性使用HtmlId值。

那你应该在你的项目中编写自定义模型绑定,根据HtmlId属性绑定模型。

关于如何重写自定义输入标签助手,您可以参考以下步骤:

注意:由于输入标签助手有多种类型"文件、单选、复选框和其他",您应该根据源代码编写所有逻辑。

根据输入标签助手的源码,你可以发现标签助手会调用GenerateTextBox方法来生成输入标签的html内容。

GenerateTextBox有五个参数,第三个参数表达式用于生成输入文本框的for属性。

1
2
3
4
5
6
7
Generator.GenerateTextBox(
                ViewContext,
                modelExplorer,
                For.Name,
                modelExplorer.Model,
                format,
                htmlAttributes);

如果要将 HtmlId 值显示为 for 属性的名称,则应创建自定义输入 taghelper。

你应该首先创建一个自定义属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[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; }
    }
}

然后你可以使用 var re = ((Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.DefaultModelMetadata)For.ModelExplorer.Metadata).Attributes.PropertyAttributes.Where(x => x.GetType() == typeof(HtmlId)).FirstOrDefault(); 在输入标签助手的 GenerateTextBox 方法中获取 htmlid。

详情,您可以参考下面的自定义输入标签助手代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
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);
        }

    }
}

在 _ViewImports.cshtml

中引入这个 taghelper

1
@addTagHelper *,[yournamespace]

模型示例:

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

结果:

enter