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" /> |
这会产生一个以
在我的场景中,我还有一个模态对话框,其中另一个窗体与另一个视图模型共享同名的属性。我知道
在我的后端代码中,我希望能够根据视图模型上下文命名我的属性。我不想重命名我的属性以使它们在全球范围内唯一。
我尽量避免两件事:
- 在视图/输入元素中手动指定 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 是一个属性,它与标签助手交互以进行渲染,也用于将视图模型重新绑定为
也许另一种方法也是有效的,因为在同一页面上避免多个表单中的多个输入元素(具有相同标识符)对我来说似乎是一种常见情况。
根据你的描述,如果你想达到你的要求,你应该编写自定义模型绑定和自定义输入标签助手来实现你的要求。
由于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; } } } |
然后你可以使用
详情,您可以参考下面的自定义输入标签助手代码:
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; } |
结果:
然后你可以为模型编写一个自定义模型绑定来根据htmlid绑定数据。关于如何使用自定义模型绑定,可以参考这篇文章。