Search code examples
c#asp.net-mvchtml-helper

Elegant approach to handle dynamic HTML classes in MVC


I have a custom MVC HTML Helper method, like so:

public static MvcHtmlString Div (this HtmlHelper htmlHelper,
VehicleStatusViewModel vehicleStatus, string classType)

I call this helper method to help me choose the right bootstrap class to use for a div in my view. Inside the method, I use a switch to iterate through the possible states of vehicleStatus and assign the according bootstrap class. I.E. panel panel-success if vehicleStatus = 1 or panel panel-danger if vehicleStatus = 2.

However, I may want to assign a particular glyphicon, again, based on a certain returned status of VehicleStatus. In the future, I may have even more additional class types.

In my view, when I call this Helper method, I want to pass a classType (i.e panel or glyphicon). And in my Helper method, without having to duplicate my switch code for each possible class-type, I want an elegant solution to render the particular classType my View is requesting and return a TagBuilder div with the appropriately assigned class/class-type, such that:

@Html.Div(vehicleStatus, "panel")
// Would resolve and return, if vehicleStatus was 1: 
<div class="panel panel-success"> </div>

// Calling the same method, vehicleStatus = 1 but expecting a respective
// glyphicon:
@Html.Div(vehicleStatus, "glyphicon")
<div class="glyphicon glyphicon-remove-circle"> </div>

Solution

  • Well, your description of the problem sounds like a perfect example of either a base-class or interface setup (the possibility to add additional class types in the future by overriding behavior of a base class and/or implementing the interface differently).

    This may or may not be what your looking for. I've used similar patterns before, typically in a domain, but sometimes for Razor purposes. In this case, a base class could be used. After finishing, I realized I slightly over-engineered this example. You could probably easily get away with 2 levels until you reach a point you desire more.

    public abstract class HtmlTag
    {
        private readonly String _tag;
    
        //allow different type of tags to be rendered
        protected HtmlTag(String tag)
        {
            if (String.IsNullOrWhiteSpace(tag)) 
                throw new ArgumentException("Value cannot be null or whitespace.", "tag");
            _tag = tag;
        }
        //require a VehicleStatus for rendering to an HtmlString
        public String ToHtmlString( VehicleStatus vehicleStatus )
        {
            var tag = new TagBuilder( _tag );
            this.CreateTag( tag, vehicleStatus );
            return tag.ToString();
        }
        //require subclasses to implement the CreateTag method
        //this method will allow subclasses to modify the tag builder as needed
        //depending on the vehicle status
        protected abstract void CreateTag( TagBuilder tagBuilder, VehicleStatus vehicleStatus );
    }
    

    And, then if you specifically wanted a div or a span, for example:

    public abstract class Div : HtmlTag
    {
        protected Div( )
            : base( "div" )
        {
        }
    }
    public abstract class Span : HtmlTag
    {
        protected Span()
            : base("span")
        {
        }
    }
    

    And finally, all you need to do is create classes that implment the CreateTag method:

    public class PanelDiv : Div
    {
        protected override void CreateTag( TagBuilder tagBuilder, VehicleStatus vehicleStatus )
        {
            tagBuilder.AddCssClass( "panel" );
            tagBuilder.AddCssClass(vehicleStatus == VehicleStatus.Success 
                ? "panel-success" 
                : "panel-danger");
        }
    }
    public class GlyphiconDiv : Div
    {
        protected override void CreateTag( TagBuilder tagBuilder, VehicleStatus vehicleStatus )
        {
            tagBuilder.AddCssClass( "glyphicon" );
            tagBuilder.AddCssClass( vehicleStatus == VehicleStatus.Success 
                ? "glyphicon-remove-circle" 
                : "glyphicon-add-circle" );
        }
    }
    

    If you wanted to add another type, all you'd need to do is:

    public class MyOtherDiv : Div
    {
        protected override void CreateTag( TagBuilder tagBuilder, VehicleStatus vehicleStatus )
        {
            //adjust the tag here based on vehicle status
        }
    }
    

    And lastly, your HtmlHelper:

    public static class DivHtmlHelperExtensions
    {
        public static IHtmlString Div( this HtmlHelper htmlHelper, VehicleStatus vehicleStatus, HtmlTag htmlTag )
        {
            //simply return the HtmlString representation of your HtmlTag you passed in
            return new HtmlString( htmlTag.ToHtmlString( vehicleStatus ) );
        }
        //call this method likeso (from a view):
        //@Html.Div(vehicleStatus, new PanelDiv());
        //@Html.Div(vehicleStatus, new GlyphiconDiv());
    }
    

    If you find yourself doing the same conditions over and over again, you could slightly refactor the parent class to handle more of the responsibility:

    public abstract class Div : HtmlTag
    {
        private readonly String _className;
        private readonly String _trueCssClass;
        private readonly String _falseCssClass;
    
        protected Div( String className, String trueCssClass, String falseCssClass )
            : base( "div" )
        {
            _className = className;
            _trueCssClass = trueCssClass;
            _falseCssClass = falseCssClass;
        }
        //the div will use the constructing parameters to populate the classes instead
        protected override void CreateTag( TagBuilder tagBuilder, VehicleStatus vehicleStatus )
        {
            tagBuilder.AddCssClass( _className );
            tagBuilder.AddCssClass( vehicleStatus == VehicleStatus.Success 
                ? _trueCssClass 
                : _falseCssClass );
        }
    }
    //and your subclass:
    public class PanelDiv : Div
    {
        public PanelDiv()
            : base( "panel", "panel-success", "panel-danger" )
        {
        }
    }
    

    If this isn't what your looking for, let me know.