ASP.NET Core asp-for attribute not working in tag helper

103 Views Asked by At

I have a custom tag helper and I want to use the asp-for attribute in it, but it renders something else:

public class FormerTagHelper : TagHelper
{
    [HtmlAttributeName("asp-for")]
    public ModelExpression For { get; set; }
    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = "div";
        output.Content.AppendHtml($@"<input class=""form-control"" asp-for=""{For}""> </input>");
    }
}
<former asp-for="Name"></former>

It renders following in the HTML:

<div>
    <input class="form-control" asp-for="Microsoft.AspNetCore.Mvc.ViewFeatures.ModelExpression">
</div>

Edit: My goal is to achieve this cshtml with the custom tag

<form method="post">
    <div class="mt-2">
        <label class="form-label">Name</label>
        <input class="form-control" asp-for="Name" />
        <span asp-validation-for="Name" class="text-danger"></span> <br />
    </div>
</form>

<div class="mt-3">
    <button type="submit" class="btn-primary">Send</button>
</div>

Which renders following HTML in the browser:

Your provided code worked right out of the box but looking at these questions I might wonder if it can be simplier:

Adding default tag ('asp-for') in custom tag helper
Resolve asp-for in custom tag helper
ASP.NET Core creating custom input tag helper
ASP.NET Core MVC : using custom tag helpers

Edit2 My goal is to achieve this cshtml with the custom tag

<form method="post">
    <div class="mt-2">
        <label class="form-label">Name</label>
        <input class="form-control" asp-for="Name" />
        <span asp-validation-for="Name" class="text-danger"></span> <br />
    </div>
</form>

<div class="mt-3">
    <button type="submit" class="btn-primary">Send</button>
</div>

Which renders following HTML in the browser:

<div class="mt-3"
<label class="form-label" for="Name">Name</label>
<input class="form-control" type="text" data-val="true" data-val-length="The field Name must be a string with a maximum length of 255." data-val-length-max="255" data-val-required="The Name field is required." id="Name" name="Name" value="">
<span class="field-validation-valid" data-valmsg-for="Name" data-valmsg-replace="true">
</span>
</div>
2

There are 2 best solutions below

2
Yong Shun On

To dynamically generate the <input> element with asp-for attribute you need the InputTagHelper.

  1. Register the IHtmlGenerator service into the DI container by adding to Startup.cs or Program.cs.
using Microsoft.AspNetCore.Mvc.ViewFeatures;

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IHtmlGenerator, DefaultHtmlGenerator>();
}
  1. To generate the <input> element, you need InputTagHelper.

  2. Render inputTagHelperOutput in <div>.

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

[HtmlTargetElement("former", Attributes = ForAttributeName)]
public class FormerTagHelper : TagHelper
{
    private const string ForAttributeName = "asp-for";

    [HtmlAttributeName(ForAttributeName)]
    public ModelExpression For { get; set; }

    [ViewContext]
    [HtmlAttributeNotBound]
    public ViewContext ViewContext { get; set; }

    IHtmlGenerator Generator { get; set; }

    public FormerTagHelper(IHtmlGenerator generator)
    {
        Generator = generator;
    }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var inputTagHelper = new InputTagHelper(Generator)
        {
            For = this.For,
            ViewContext = this.ViewContext,
        };

        var attributes = new TagHelperAttributeList();

        var inputTagHelperContext = new TagHelperContext("input",
            attributes,
            new Dictionary<object, object> { },
            Guid.NewGuid().ToString("N"));

        TagHelperOutput inputTagHelperOutput = new TagHelperOutput(
            tagName: "input",
            attributes: attributes,
            getChildContentAsync: (useCachedResult, encoder) =>
                 Task.Factory.StartNew<TagHelperContent>(
                        () => new DefaultTagHelperContent()
                 )
            )
        {
            TagMode = TagMode.SelfClosing,
        };

        await inputTagHelper.ProcessAsync(inputTagHelperContext, inputTagHelperOutput);
        inputTagHelperOutput.Attributes.Add("class", "form-control");

        output.TagName = "div";
        output.Content.AppendHtml(inputTagHelperOutput);
    }
}
1
Yuning Duan On

For is a ModelExpression object, Name is the name of the model attribute, you should bind the corresponding attribute asp-for=""{For.Name}"":

public class FormerTagHelper : TagHelper
{
     [HtmlAttributeName("asp-for")]
     public ModelExpression For { get; set; }
     public override void Process(TagHelperContext context, TagHelperOutput output)
     {
         output.TagName = "div";
         output.Content.AppendHtml($@"<input class=""form-control"" asp-for=""{For.Name}""> </input>");
         output.Content.AppendHtml($@"<span asp-validation-for=""{For.Name}"" class=""text-danger""></span>");
     }
}

enter image description here

But we usually use name instead of asp-for in HTML to bind the attributes

name=""{For.Name}""