HelpMe
HelpMe

Reputation: 21

Asp.net Core Razor Pages - Custom TagHelpers & Boiler plate CRUD code

I'm sure this problem has been faced many times before.

I have a lot of CRUD forms, so I have a lot of repeated boiler plate code.

e.g.

<div class="form-group">
    <label asp-for="MyModel.Property1" class="control-label"></label>
    <input asp-for="MyModel.Property1" class="form-control" />
    <span asp-validation-for="MyModel.Property1" class="text-danger"></span>
</div>

The above code being repeated time and time again per property, per form. Always looks the same (except if it's a dropdown for example).

I have managed to get something repeatable running with EditorTemplates, but this feels clunky. I'd need a different template per property type etc. This requires running: @Html.EditorFor(m => m.MyModel.Property1, "_MyString") I feel @Html. is almost discouraged in Razor Pages and the solution should be elsewhere.

I've spent far too long looking into getting a custom TagHelper setup and working, and I don't think it's possible. You can create your own TagHelpers and use TagBuilder, but you then lose all of the logic which is inside the InputTagHelper etc. https://github.com/dotnet/aspnetcore/blob/main/src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs

It needs to utilize the Input and Label TagHelpers as there is a lot of logic for example around property type in the InputTagHelper as per the above link. You can't seemingly use these.

I'm looking for a really generic way to cut down on my boiler plate code, and was hoping there might be a magic pill?

Edited to say - you also used to be able to do this in MVC which you can't do via Razor Pages, without creating per model.

public static IHtmlContent MyEditorFor<TModel, TResult>(
    this IHtmlHelper<TModel> htmlHelper,
    Expression<Func<TModel, TResult>> expression,
    string templateName = "MyDefaultTemplateName")
{
    return htmlHelper.EditorFor(expression, templateName);
}

Upvotes: 1

Views: 171

Answers (1)

Rena
Rena

Reputation: 36655

Here is a whole working demo you could follow:

Create the Custom Tag Helper

[HtmlTargetElement("div-group", Attributes = "asp-for")]
public class DivGroupTagHelper : TagHelper
{
    private const string ForAttributeName = "asp-for";
    private IHtmlGenerator _htmlGenerator;
    public DivGroupTagHelper(IHtmlGenerator htmlGenerator)
    {
        _htmlGenerator = htmlGenerator;
    }
    [HtmlAttributeName(ForAttributeName)]
    public ModelExpression For { get; set; }

    [HtmlAttributeName("class")]
    public string CssClass { get; set; }

    [HtmlAttributeName("label-class")]
    public string LabelCssClass { get; set; }

    [HtmlAttributeName("input-class")]
    public string InputCssClass { get; set; }

    [HtmlAttributeName("validation-class")]
    public string ValidationCssClass { get; set; }
    [HtmlAttributeNotBound]

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

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = "div";
        output.Attributes.SetAttribute("class", "form-group " + CssClass);

        // Create a context and output for the label
        var labelContext = new TagHelperContext(new TagHelperAttributeList(), new Dictionary<object, object>(), Guid.NewGuid().ToString("N"));
        var labelOutput = new TagHelperOutput("label", new TagHelperAttributeList(), (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));

        var labelTagHelper = new LabelTagHelper(_htmlGenerator)
        {
            For = For,
            ViewContext = ViewContext
        };

        await labelTagHelper.ProcessAsync(labelContext, labelOutput);
        labelOutput.AddClass("control-label", HtmlEncoder.Default);
        if (!string.IsNullOrEmpty(LabelCssClass)) labelOutput.AddClass(LabelCssClass, HtmlEncoder.Default);

        //Create a context and output for the input

        var inputContext = new TagHelperContext(new TagHelperAttributeList(), new Dictionary<object, object>(), Guid.NewGuid().ToString("N"));
        var inputOutput = new TagHelperOutput("input", new TagHelperAttributeList(), (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));

        var inputTagHelper = new InputTagHelper(_htmlGenerator)
        {
            For = For,
            ViewContext = ViewContext
        };

        await inputTagHelper.ProcessAsync(inputContext, inputOutput);
        inputOutput.AddClass("form-control", HtmlEncoder.Default);
        if (!string.IsNullOrEmpty(InputCssClass)) inputOutput.AddClass(InputCssClass, HtmlEncoder.Default);

        // Create a context and output for the validation message
        var validationContext = new TagHelperContext(new TagHelperAttributeList(), new Dictionary<object, object>(), Guid.NewGuid().ToString("N"));
        var validationOutput = new TagHelperOutput("span", new TagHelperAttributeList(), (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));

        var validationTagHelper = new ValidationMessageTagHelper(_htmlGenerator)
        {
            For = For,
            ViewContext = ViewContext
        };

        await validationTagHelper.ProcessAsync(validationContext, validationOutput);
        validationOutput.AddClass("text-danger", HtmlEncoder.Default);
        if (!string.IsNullOrEmpty(ValidationCssClass)) validationOutput.AddClass(ValidationCssClass, HtmlEncoder.Default);

        // Append the generated content to the output
        output.Content.AppendHtml(labelOutput);
        output.Content.AppendHtml(inputOutput);
        output.Content.AppendHtml(validationOutput);
    }      
}

Add the Tag Helper registration in your _ViewImports.cshtml file

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, YourNamespace

Use the custom Tag Helper in your Razor Pages

@page
@model IndexModel

<form method="post">
    <div-group asp-for="Password"></div-group>
    <div-group asp-for="Email"></div-group>
    <input type="submit" value="Create"/>
</form>
@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

Upvotes: 1

Related Questions