Tuesday, April 30, 2013

ASP.NET MVC - @, meet Fluidity

As far as Razor syntax goes, '@' is king. It does just about everything you need it to do.  Even when it is not solely responsible, '@' manages to get the assist. It protects us from harm in the way of HTML Encoding.  It maintains readability as we cut in and out of content with dynamic code.  It parties with asterix to enable comments. It pairs up with colon to designate the start of a new line. It starts conditional blocks.  It partners with open-bracket to start new code blocks. It slices! It designates models. It includes namespaces. It dices! It starts sections. It defines HtmlHelpers.  It enables fluid configuration of re-usable HTML Helpers...

Wait, what was that last one? I suppose that final point begs for further explanation. 

Full disclosure:  I'm a big fan of fluid implementations via method-chaining. Many well known and widely used libraries have some version of a fluid api. Automapper allows you to quickly set up mappings between types. Same goes for nHibernate and mappings between tables. jQuery anybody? Anyone who has fully configured a new dom element in 1 line of jQuery can attest to how useful this style of programming can be.  Let's not forget the amazing Linq IQueryable. That alone should give you an idea how powerful fluidity can be.  With those examples in mind, surely we can find a way to let the Master Symbol of Razor (@) in on the action.

Let's start with the problem and work our way towards the solution. If there's one thing I don't like about Razor, its the continued use of dynamic types to define html attributes. I dislike this for a few reasons:  class names are not separated from attributes and you need to escape the class with '@', it doesn't read well, you must use underscores instead of dashes in attribute name, and it's not reusable.  You could argue that it *is* reusable because you can pass in anything to the attributes parameter, but stay with me here and I'll explain how there's a better way.

For example, lets say I want a textbox for the FirstName property of my model.  I want it to have the "wide" and "full" classes, I want an extra attribute data-val='test', and I want it to be disabled.  In Razor I'd use the following syntax:

   @Html.TextBoxFor(m => m.FirstName, new { @class="wide full", data_val = 'test', disabled="disabled" })

Wouldn't it be cool to do something like this?

   @Html.For(attr => Html.TextBoxFor(m => m.FirstName, attr)).AddClass("wide","full").MergeAttribute("data-val","test").Disable();

Well thanks to Extension methods, generics, and Razor, we can do just that.

The @ Symbol is going to set the stage for us by providing us with an entry point into the view creation process. It's common knowledge that using @ will Html encode anything after it, but what isn't so clear is that it does so by using the IHtmlString interface (MvcHtmlString implementation), callings its "ToHtmlString" method. The trick here is that in a razor view, if whatever comes after the @ returns a type that implements IHtmlString, it will automatically call the ToHtmlString method and insert the results into the document as pre-encoded text.  This is exactly what we need to implement our example above.

Let's lay out the skeleton for our helper, and then fill it in piece-by-piece.

public class HtmlFor : IHtmlString {

      public HtmlFor Disable();

      public HtmlFor DisableIf(bool condition); //useful if we are basing our decision on some other piece of information.

      public HtmlFor MergeAttribute(string key, object value);

      public HtmlFor AddClass(params string[] classes);



      public string ToHtmlString();

}

The idea here is to set up all of the config via the chaining methods and then apply the config right before writing out the html in ToHtmlString.  So let's add some config to our class, and be sure to return ourself in each chained call.

 

   private bool IsEnabled { get; set; }

   private Dictionary<string,object> Attributes { get; set; }

   private HashSet<string> ClassNames { get; set; }



   public HtmlFor Disable() {

         this.IsEnabled = false;

         return this;

   }

   public HtmlFor DisableIf(bool condition) { 

         this.IsEnabled = !condition;

         return this;

   }

   public HtmlFor MergeAttribute(string key, object value) {

         this.Attributes[key] = value;

         return this;

   }

   public HtmlFor AddClass(params string[] classes) {

         classes.ToList().ForEach(c => {

               this.ClassNames.Add(c);

         });

         return this;

   }

The last piece is the connection from the view's HtmlHelper to our helper. We'll accomplish this using an extension method and a lambda expression that we'll use to write to the view. To do this we'll supply our helper with a function that takes some attributes and returns to us an MvcHtmlString.  Most of the Html.SomethingFor helpers return MvcHtmlStrings so it should work well for our common case.

So we'll add a constructor and an extra configuration option:

   private Func<IDictionary<string,object>,MvcHtmlString> FnFor { get; set; }

	

   public HtmlFor(Func<IDictionary<string,object>,MvcHtmlString> fnFor) {

         this.FnFor = fnFor;

         this.IsEnabled = true;  //enabled by default;

         this.Attributes = new Dictionary<string,object>();

         this.ClassNames = new HashSet<string>();

   }

Then we'll hook it up with an extension method on HtmlHelper

   public static class Extensions {

      public static HtmlFor For<M>(this HtmlHelper<M> Html, Func<IDictionary<string,object>,MvcHtmlString> fnFor) {

            return new HtmlFor(fnFor);

      }

    }

Last thing to do would be to write out our desired output in IHtmlString.ToHtmlString() method. First we'll apply our configuration, then write it out using our caller's supplied lambda:

   public string ToHtmlString() {

      if (!this.IsEnabled) 

            this.Attributes["disabled"] = "disabled";

      if (this.ClassNames.Count > 0) 

            this.Attributes["class"] = string.Join(" ", this.ClassNames);

      return FnFor(this.Attributes).ToString();

   }

Here's the full source:

   public class HtmlFor : IHtmlString {

	

	    private bool IsEnabled { get; set; }

	    private Dictionary<string,object> Attributes { get; set; }

	    private HashSet<string> ClassNames { get; set; }

	    private Func<IDictionary<string,object>,MvcHtmlString> FnFor { get; set; }

	

	    public HtmlFor(Func<IDictionary<string,object>,MvcHtmlString> fnFor) {

		    this.FnFor = fnFor;

		    this.IsEnabled = true;  //enabled by default;

		    this.Attributes = new Dictionary<string,object>();

		    this.ClassNames = new HashSet<string>();

	    }

    

	    //chaining methods

	    public HtmlFor Disabled() {

		    this.IsEnabled = false;

		    return this;

	    }

	    public HtmlFor DisabledIf(bool condition) {

		    this.IsEnabled = !condition;

		    return this;

	    }

	    public HtmlFor MergeAttribute(string key, object value) {

		    this.Attributes[key] = value;

		    return this;

	    }

	    public HtmlFor AddClass(params string[] classNames) {

		    foreach (var cls in classNames) {

			    this.ClassNames.Add(cls);

		    }

		    return this;

	    }

	

	    public string ToHtmlString() {

		    if (!this.IsEnabled) 

			    this.Attributes["disabled"] = "disabled";

		    if (this.ClassNames.Count > 0) 

			    this.Attributes["class"] = string.Join(" ", this.ClassNames);

		    return FnFor(this.Attributes).ToString();

	    }

    }



    public static class Extensions {

	    public static HtmlFor For<M>(this HtmlHelper<M> Html, Func<IDictionary<string,object>,MvcHtmlString> fnFor) {

		    return new HtmlFor(fnFor);

	    }

    }

Hopefully this gave you some ideas on how to apply @ and IHtmlString in your own projects. This handy helper will work with TextBoxFor, RadioButtonFor, CheckboxFor, TextAreaFor, etc. etc. I think its way cool, and this merely scratches the surface of what you can do.