I’ve recently been rediscovering all the input fields that HTML offers and how they can help developers build more straightforward user experiences. The library of native HTML controls is impressive, with 22 HTML input types as of this post.

Looking through ASP.NET Core’s InputTagHelper, I counted 14 supported input types based on the .NET common language runtime types you may use in your models. That’s over 8 controls missing from ASP.NET Core. The missing range input is one of the most valuable controls.

In this post, we’ll write an ASP.NET Core tag helper that piggybacks on the InputTagHelper and turns a number property into a range input.

What is the range Input?

The range input is as it sounds. Developers commonly refer to these elements as a “slider” since users typically slide an indicator to set a value. The input allows users to choose a value constrained by a minimum and maximum value. The limitation ensures that users can only choose valid values. When defining a range input, you may specify the max, min, and step attributes. You may also provide markers along the slider path in the form of a datalist. It is a very powerful control.

I highly recommend reading the MDN on the topic to learn more details.

TagHelper Piggyback to Slider

When working with ASP.NET Core MVC or Razor Pages, you’ll typically have a model and a razor view. Let’s examine both. First, the page model.

public class IndexModel(ILogger<IndexModel> logger) : PageModel
{
    [BindProperty]
    public int Value { get; set; }
    
    public string? Message { get; set; }
    
    public void OnGet()
    {
    }

    public void OnPost()
    {
        Message = $"You selected {Value}!";
    }
}

Next, the Razor view.

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}

@if (Model.Message is not null)
{
    <div class="alert alert-info">
        @Model.Message
    </div>
}

<form method="post" asp-page="Index">
    <div>
        <label class="form-label" asp-for="Value"></label>
        <input class="form-range" asp-for="Value" />
    </div>
    <button type="submit" class="btn btn-primary">
        Add
    </button>
</form>

So far, so good, but when you run this application, you’ll notice that the input type for our Value field is set to text. It’s not what we want, let’s fix that.

The first step is using the RangeAttribute and creating a derived RangeWithStepAttribute for additional metadata. You could also create separate attributes for additional metadata. The choice is yours.

public class RangeWithStepAttribute(int minimum, int maximum)
    : RangeAttribute(minimum, maximum)
{
    public double Step { get; set; } = 1;
} 

Next, let’s decorate our property with the new attribute.

[BindProperty, RangeWithStep(1, 5, Step = 1)]
public int Value { get; set; }

So far, so good; now let’s create a tag helper that recognizes the RangeAttribute on our models.

using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using OutputValues.Pages;

namespace OutputValues;

[HtmlTargetElement("input", 
    Attributes = ForAttributeName, 
    TagStructure = TagStructure.WithoutEndTag)]
public class RangeInputTagHelper : TagHelper
{
    private const string ForAttributeName = "asp-for";
    private const string TypeAttributeValue = "range";

    public override int Order { get; } = -999;

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

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        var metadata = For.Metadata;

        if (metadata is { ContainerType: not null, PropertyName: not null })
        {
            var attribute =
                metadata.ContainerType.GetProperty(metadata.PropertyName)
                ?.GetCustomAttributes(typeof(RangeAttribute), true)
                .FirstOrDefault();
            
            if (attribute is RangeAttribute range)
            {
                output.Attributes.SetAttribute("type", TypeAttributeValue);
                output.Attributes.SetAttribute("min", range.Minimum);
                output.Attributes.SetAttribute("max", range.Maximum);

                if (range is RangeWithStepAttribute rws)
                {
                    output.Attributes.SetAttribute("step", rws.Step);
                }
            }
        }
    }
}

All we need to do is register our custom tag helper in the _ViewImports.cshtml. Change the assembly name to match your assembly name.

@addTagHelper *, OutputValues

A note about the Order property on the RangeInputTagHelper and why it’s set to -999. The default InputTagHelper has a value of -1000, meaning it will likely run before all tag helpers you create. Typically, the order doesn’t matter too much, but in this case, we want our tag helper to do as little work as possible, letting the original tag helper do much of the work it typically does. Since HTML inputs share a lot of attributes and behaviors, this is ideal for this scenario. We set our tag helper to -999 to ensure it runs sometime right after the original tag helper.

After rerunning the application, you’ll see a slider with the values for min, max, and step set ready for your users. Awesome!

Conclusion

This technique can work with any HTML5 input you’d like to support that isn’t in ASP.NET Core. It also ensures that all HTML id and name values derive from your C# models and that the HTML and server-side handlers are in sync.

I hope you found this post helpful, and as always, thanks for reading. Cheers.