DEV Community

Cover image for Reusable MVC Razor Components with HTML Extension Methods
Michael Kennedy
Michael Kennedy

Posted on

Reusable MVC Razor Components with HTML Extension Methods

Component reuse is one of the intrinsic features of modern JavaScript frameworks. React, Angular and their counterparts all guide us toward the idea of creating small, reusable elements in order to maintain a clean codebase.

MVC performs a similar task, but the architecture doesn't make it as intuitive. As the name suggests, it provides reusable components as standard; unfortunately these components can be relatively unsophisticated making easy to fall into the trap of creating massive views and forgetting about reusability.

On occasion, the time required to migrate existing solutions to new frameworks is prohibitive, but that doesn't mean that we can't provide an easy to use component library within the confines of MVC Razor.

By simplifying the our source code we can take back control of styling, reduce time to onboard new team members and standardise component usage to prevent subtle differences seeping through in different product areas.

Approaches will vary depending on the complexity of your components, but we can take inspiration from providers of third party libraries and make our own HTML helpers accessible directly through Visual Studio IntelliSense.

What are HTML Helpers?

For anyone not familiar with the concept, we can use extension methods to expand on the functionality of any data type. This Microsoft document explains it in more detail, but essentially we declare something like this:

namespace MyExtensionMethods
{
    public static class MyExtensions
    {
        public static int WordCount(this string str)
        {
            return str.Split(new char[] { ' ', '.', '?' },
                StringSplitOptions.RemoveEmptyEntries).Length;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Whenever the namespace is available we can execute the WordCount function against any string type:

var stringValue = "There are four words";
var wordCount = stringValue.WordCount(); // outputs 4.
Enter fullscreen mode Exit fullscreen mode

This same concept can be applied to the HTML helpers made available by Razor, we can leverage the same approach to make it easier to create & recreate common components; improving discoverability for new and existing developers alike.

@(Html.MyHtmlHelper()
    .GenerateThisControl("controlId")
    .EnableOptions(true)
);
Enter fullscreen mode Exit fullscreen mode

Basic Usage

A basic example might be something like this, which expands on the functionality of Html.Raw() to escape quotation marks for inclusion in DOM attributes:

using System.Web;
using System.Web.Mvc;

namespace MyExtensionMethods
{
    public static class HtmlStringHelper
    {
        public static IHtmlString RawHtmlString(
            this HtmlHelper htmlHelper, string value)
        {
            return !string.IsNullOrWhiteSpace(value) 
                ? htmlHelper.Raw(EscapeQuotes(value))
                : "";
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

An example like this is of course quite limited. We can use it to perform basic operations in day-to-day work, but it's not something which will reduce code complexity by any measurable amount.

But we can expand on this to return entire views:

namespace MyExtensionMethods
{
    public static class HtmlStringHelper
    {
        public IHtmlString LoadingAnimation(
            this HtmlHelper htmlHelper)
        {
            return _htmlHelper.Partial(
                "~/Views/HtmlHelpers/LoadingAnimation.cshtml");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

There's still not much benefit to this last example, certainly nothing that we can't achieve by documenting the location of the loading animation view, but this shows us that we can:

  • Use HTML Helpers to store content outside of the core solution. We could make this available as part of a DLL and perhaps directly render the HTML output from an embedded file. This would let us simplify create a component library which can be accessed by multiple projects.

  • Guide developers through the generation of HTML content by providing additional configuration options without the need to go digging for the right model or view name.

Helper Factory

To provide more advanced functionality, we'll have to expose additional options via helper factories.

This approach follows previous examples, except this time we're returning a class which implements IHtmlString instead of a partial view or string.

First, we add something like this to our existing helper:

public static HtmlHelperFactory MyProductName(this HtmlHelper helper)
{
    return new HtmlHelperFactory(helper);
}
Enter fullscreen mode Exit fullscreen mode

Which is implements a HtmlHelperFactory class:

namespace MyExtensionMethods.HtmlHelpers.Factory
{
    public class HtmlHelperFactory
    {
        private readonly HtmlHelper _htmlHelper;

        public HtmlHelperFactory(HtmlHelper htmlHelper)
        {
            _htmlHelper = htmlHelper;
        }

        public TooltipBuilder Tooltip(string id, string text)
        {
            return new TooltipBuilder(_htmlHelper, id, text);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

What we have here will allow us to generate a new tooltip component within our CSHTML files by entering something like @Html.MyProductName().Tooltip("id", "This is my tooltip").

We can imagine that this would perhaps generate a little "i" icon, with the specified tooltip text automatically escaped and assigned within the title attribute, perhaps using a third party component to make the tooltip look pretty.

Instead of creating the HTML content within the helper, we can shift it to a builder class, provide it with some additional options and create an appropriate view model.

public class TooltipBuilder : IHtmlString
{
    private readonly HtmlHelper _htmlHelper;
    private readonly TooltipModel _model = new TooltipModel();

    public TooltipBuilder(
        HtmlHelper htmlHelper, 
        string id = "", 
        string tooltipText = "")
    {
        _model.Id = id;
        _model.Text = tooltipText;
        _htmlHelper = htmlHelper;
    }

    public TooltipBuilder Colour(string value)
    {
        _model.Colour = value;
        return this;
    }

    public TooltipBuilder Mode(TooltipBuilderMode value)
    {
        _model.Mode = value;
        return this;
    }

    public string ToHtmlString()
    {
        var returnData = _htmlHelper
            .Partial(
                "~/Views/HtmlHelpers/Tooltip.cshtml", 
                _model
            )
            .ToHtmlString();

        return returnData;
    }
}
Enter fullscreen mode Exit fullscreen mode

We can expect the generated tooltip to be pretty simplistic, perhaps a couple of <span>'s and a custom font used to generate an appropriate icon.

But what we've done here is to standardise this simple element and ensure that every instance is rendered in the same way. Should we ever want to change the icon then we'll only ever need to update a single line of code.

We've also given other developers a list of possible options in a manner which lets us chain operations together.

@(Html.MyProductName()
    // provide required fields
    .Tooltip("id", "This is my tooltip")
    // provide optional parameters
    .Colour("#575757")
    .Mode(TooltipBuilderMode.FloatBottom)
)
Enter fullscreen mode Exit fullscreen mode

Html Attributes

While the tooltip example is pretty basic, we might not want every instance of the same object to behave in exactly the same way, perhaps we want to allow users to specify some additional classes on the parent container, or maybe we want to bind the tooltip value to a model.

In which case we might expand on TooltipBuilder by introducing some extra operations and enhanced processing:

public TooltipBuilder(
    HtmlHelper htmlHelper, 
    string id = "", 
    string defaultTooltipText = "")
{
    _model.Id = id;
    _model.Text = defaultTooltipText;
    _htmlHelper = htmlHelper;
}

// provide generic attribute options
public TooltipBuilder HtmlAttributes(
    IDictionary<string,object> value)
{
    if (value != null)
    {
        value.ToList().ForEach(v => SetHtmlAttribute(v));
    }
    return this;
}

// provide more specific guided attribute options
public TooltipBuilder TitleBindingAttribute(string value)
{
    SetHtmlAttribute(new KeyValuePair<string,object>(
        "data-bind", 
        $"attr: {{ title: {value} }}"
    ));

    return this;
}

// combine lists into a single property for processing
private void SetHtmlAttribute(
    KeyValuePair<string, object> attribute)
{
    if (!string.IsNullOrWhiteSpace(attribute.Value?.ToString() ?? ""))
    {
        if (_model.HtmlAttributes.ContainsKey(attribute.Key))
        {
            _model.HtmlAttributes[attribute.Key] = attribute.Value;
        }
        else
        {
            _model.HtmlAttributes.Add(attribute.Key, attribute.Value);
        }
    }
}

public string ToHtmlString()
{
    var returnData = _htmlHelper
        .Partial(
            "~/Views/HtmlHelpers/Tooltip.cshtml", 
            _model
        )
        .ToHtmlString();
    return returnData;
}
Enter fullscreen mode Exit fullscreen mode

With this, we can allow more in-depth customisation while still retaining control over the appearance and behaviour of the tooltip:

@(Html.MyProductName()
    .Tooltip("id", "This is my tooltip")
    .SetHtmlAttribute(
        [{@class = "myClassName"}] 
    )
    .TitleBindingAttribute("tooltipTextField")
)
Enter fullscreen mode Exit fullscreen mode

Grouping Commands

Finally, we may find ourselves in a situation where we have so many options within our helper that we want to group them by function to make them easier to work with. (Perhaps hard to believe within the confines of the tooltip example)

We can do this by creating a configurator class and using an Action delegate to preserve the simplicity of creating our control within a single razor command.

First we need a basic option factory:

public class TooltipOptionFactory
{
    internal TooltipOptions Options = 
        new TooltipOptions();

    public TooltipOptionFactory Disabled(bool value)
    {
        Options.Disabled = value;
        return this;
    }

    public TooltipOptionFactory Visible(bool value)
    {
        Options.Visible = value;
        return this;
    }
}
Enter fullscreen mode Exit fullscreen mode

Then we need to integrate it with our TooltipBuilder class:

public TooltipBuilder Options(
    Action<TooltipOptionFactory> configurator)
{
    var optionFactory = new TooltipOptionFactory();
    configurator.Invoke(optionFactory);
    _model.Options = optionFactory.Options;
    return this;
}
Enter fullscreen mode Exit fullscreen mode

Which will allow us to use it within our CSHTML like this:

@(Html.MyProductName()
    .Tooltip("id", "This is my tooltip")
    .Options(o => {
        o.Visible(true);
        o.Disabled(false);
    })
)
Enter fullscreen mode Exit fullscreen mode

These are just the first stepping stones in creating a library of reusable Razor components. While there's clearly some overhead in creating a full library of these components the future time savings from taking this approach are immeasurable.

As with many approaches, it's much easier to begin a project with something like this in mind rather than shoehorning it into an established product; as someone who recently spent two weeks fixing issues caused by the upgrade of a third party component library - I really wish our development team had taken this approach 10 years ago, but now we have the tools to correct that mistake.

Top comments (0)