c# - 如何在渲染具有相同属性名称的多个输入元素时使用替代的自动生成标识符?
问题描述
假设我有一个像这样的视图模型
public class ExampleVM
{
[Display(Name = "Foo")]
public Nullable<decimal> FooInternal { get; set; }
}
我的视图看起来像这样(也是我在这篇文章中省略的表单标签)
@model ExampleVM
....
<input asp-for="FooInternal" class="form-control" type="number" />
这会产生一个带有FooInternal
id 属性的渲染文本框。
在我的场景中,我还有一个模态对话框,其中另一个窗体与另一个视图模型共享同名的属性。我知道asp-for
taghelper 从指定的 id 手动呈现 id 属性或从属性名称推断 id。
在我的后端代码中,我希望能够在给定视图模型上下文的情况下命名我的属性。我不想重命名我的属性以使它们在全球范围内独一无二。
我尽量避免两件事:
- 在视图/输入元素中手动指定 id。我更愿意使用可以通过后端的另一个属性设置的自动生成的 id。
- 鉴于我
[FromBody]
在帖子中使用了视图模型,我无法像使用[FromRoute(Name="MyFoo")]
. 我不想将手动输入的 id 映射回我的属性。
基本上,我正在寻找这样的东西:
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 属性绑定模型。
关于如何重写自定义输入标签助手,您可以参考以下步骤:
注意:由于输入标签助手有多种类型“文件,单选,复选框和其他”,您应该根据源代码编写所有逻辑。
根据输入标签助手源码,可以发现标签助手会调用Generator.GenerateTextBox方法生成输入标签html内容。
Generator.GenerateTextBox 有五个参数,第三个参数表达式用于生成输入文本框的 for 属性。
Generator.GenerateTextBox(
ViewContext,
modelExplorer,
For.Name,
modelExplorer.Model,
format,
htmlAttributes);
如果要将 HtmlId 值显示为 for 属性的名称,则应创建自定义输入 taghelper。
您应该首先创建一个自定义属性:
[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。
详细信息,您可以参考下面的自定义输入标签助手代码:
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
@addTagHelper *,[yournamespace]
型号示例:
[Display(Name = "Foo")]
[HtmlId("test")]
public string str { get; set; }
结果:
然后您可以为模型编写自定义模型绑定,以根据 htmlid 绑定数据。关于如何使用自定义模型绑定,可以参考这篇文章。
推荐阅读
- javascript - JavaScript:如果 CSS 是内联的还是内联的,会有不同的行为
- matlab - 如何在MATLAB中高效实现以下代码
- c# - 在数据网格 wpf 中绑定嵌套列表
- django - 在单行中以多对多关系获取数据
- javascript - 错误:警告:在循环依赖中访问模块导出的不存在属性“findOne”
- node.js - 以编程方式在 mongodb 中插入 JSON 文件
- wordpress - 如何通过 npm 安装的 wordpress 脚本激活 GitHub 插件?
- javascript - 字符串在 javascript 中是如何工作的?
- r - 如何使用 R 中的绘图以仅 10 次方的对数刻度显示 y 轴
- node.js - 在 throw new Error('record not found') 语句中出现 Nodejs 和 Mongoose 错误