Part of what makes ASP.NET Core so powerful is the C# language and the ability to generate UI based on object metadata. The ability to define metadata as a single source of truth helps reduce repetition and makes refactoring UI more straightforward. When defining metadata on your ASP.NET Core models, the System.ComponentModel.DataAnnotations namespace is the most commonly used attribute library across the .NET ecosystem.

In this short post, you’ll learn how to decorate your models with the DisplayAttribute and read the Description to output to HTML using a custom ASP.NET Core tag helper.

What Is DisplayAttribute

You can find the DisplayAttribute in the System.ComponentModel.DataAnnotations namespace, and you can use it to define user-facing text when generating user interfaces. For example, some properties include Name, ShortName, Description, and ResourceType. The metadata provided in the attribute can help UI generators create the correct UI elements for the particular property.

To use the DisplayAttribute, you’ll need to decorate your model properties as shown in the following Razor Pages page model.

[BindProperty,
 Display(
     Name = "Profile Url",
     Description = "Your profile url, ex. @user@instance.com"),
 Required(AllowEmptyStrings = false),
 RegularExpression(Mastodon.ProfileLinkRegularExpression)]
public string? ProfileUrl { get; set; }

The asp-for tag helper then uses this Name metadata from the DisplayAttribute to display the human-readable name rather than the property name.

For example, the following Razor syntax will produce a label with the Profile Url value.

<label asp-for="ProfileUrl"></label>

Processing the Razor syntax will produce the following HTML output.

<label for="ProfileUrl">Profile Url</label>

Pretty sweet! Now, how do we get the description into an HTML element?

Description TagHelper

Unfortunately, there is no asp-description-for tag helper. Luckily, it’s pretty straightforward to write yourself.

We aim to take the Razor syntax and produce our description as an HTML element’s value.

<span asp-description-for="ProfileUrl" class="text-muted ps-2"></span>

We’ll read the Description property from our Display attribute. In another stroke of luck, that information is already parsed for us using the ModelExpression property type. Let’s see what the TagHelper implementation is.

using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;

[HtmlTargetElement("div", Attributes = ForAttributeName)]
[HtmlTargetElement("p", Attributes = ForAttributeName)]
[HtmlTargetElement("span", Attributes = ForAttributeName)]
public sealed class DescriptionForTagHelper : TagHelper
{
    private const string ForAttributeName = "asp-description-for";

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

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (output == null)
        {
            throw new ArgumentNullException(nameof(output));
        }

        var description = For.Metadata.Description;
        if (description != null)
        {
            // Do not update the content if another tag helper
            // targeting this element has already done so.
            if (!output.IsContentModified)
            {
                var childContent = await output.GetChildContentAsync();
                if (childContent.IsEmptyOrWhiteSpace)
                {
                    output.Content.SetHtmlContent(description);
                }
                else
                {
                    output.Content.SetHtmlContent(childContent);
                }
            }
        }
    }
}

I’ve limited the scope of this particular tag helper to span, p, and div elements, but you could as easily target all HTML elements by adding a * option. So, for example, when we run our ASP.NET Core application, we should get the following HTML output.

<span class="text-muted ps-2">Your profile url, ex. @user@instance.com</span>

Awesome!

Conclusion

Tag helpers are fantastic, and this tag helper can make it easier to maintain UI text in a single location on your view models. The ModelExpression type does most of the heavy lifting and has more information you could take advantage of to build more UI helpers.

If you don’t feel like adding this type to your class, I have submitted the tag helper to Damian Edwards TagHelperPack NuGet package, hoping he’ll add it to the existing collection he’s managing.

I hope you found this post helpful, and feel free to reach out to me at any of the social media channels I have available. Cheers!