Saturday, 19 November 2011

Fun with Fubu's Html Conventions


I recently blogged about being impressed with conventions in Fubu MVC that let you decide the rules that govern how actions are discovered, how routes are constructed and displayed, and how output/view selection occurs.
Whilst on my travels in fubu land, the next feature that left me in awe (of the fanboy variety [of awe]) were the html conventions. Read on to learn how they out-muscle and bully ASP.NET MVC ’s display/editor templates for all their dinner money.
As a quick caveat: I am not an expert – these are just my initial findings.
The following example comes from one of my current personal projects.

Establish Context () =>

My first opportunity to apply html conventions came on the create book page hidden in site administration. Take a look at the CreateBookInputModel’s public properties:
public class CreateBookInputModel
 {
     public String Title { getset; }

     public String Genre { getset; }

     public String Description_BigText { getset; }

     public BookStatus BookStatus { getset; }

     public IList<StringWrapper> Authors { getset; }

     public HttpPostedFileBase Image { getset; }
 }

This public interface would make an excellent contrived example, as it demonstrates a number of different opportunities which get progressively more interesting. But this is a naturally-occurring story.
My goal is to keep the corresponding Spark view very simple – here’s an idyllic vision minus the fluff:
<p>
    ${this.LabelFor(m => m.Title)} 
    ${this.InputFor(m => m.Title)}
</p>
<p>
    ${this.LabelFor(m => m.Description_BigText)}
    ${this.InputFor(m => m.Description_BigText).Attr("rows", "5")}
</p>
<p>
    ${this.LabelFor(m => m.BookStatus)}
    ${this.InputFor(m => m.BookStatus)}
</p>
<p>
    ${this.LabelFor(m => m.Genre)}
    ${this.InputFor(m => m.Genres)}
</p>
<p>
    ${this.LabelFor(m => m.Authors)}
    ${this.InputFor(m => m.Authors)}
</p>
<p>
     ${this.LabelFor(m => m.Image)}
     ${this.InputFor(m => m.Image)}
</p>

Let’s start at the Top and Work Down 

Strings

public String Title { getset; }
 
public String Genre { getset; }
Simple – strings work straight out of the box with a nicely-named html input as you would expect.

Special Strings

public String Description_BigText { getset; }

Question: When is a string not a string?
Answer: When I suffix the property name with “_BigText”.
So what is it now then? It’s…..a big blob of text that needs to be represented by a text area – not a text box. To make this happen, head over to my FubuRegistry.
First we need a label:
HtmlConvention(x =>
     x.Labels
      .If(e => e.Accessor.Name.EndsWith("_BigText"))
            .BuildBy(er => new HtmlTag("label").Text(er.Accessor.Name.Replace("_BigText""")        ))
);

Here, the variable x is the registry, which I’m adding a new label convention to. What I’m saying is, if the property name ends with “_BigText”, build the view portion by using a html label whose text will be the property name, minus the “_BigText” marker (e.g. “Description”).
Now we need an editor to go with the label:
HtmlConvention(x =>
    x.Editors
      .If(e => e.Accessor.Name.EndsWith("_BigText"))
                .BuildBy(er => new HtmlTag("textarea"))

Pretty simple, eh? I’ve just told it to return a text area when the property name ends with our special marker – it is smart enough to give the text area the correct name so you can post values.

Enums

Here’s a favourite, and something I dreamt of doing with ASP.NET MVC, but was not possible with editor templates.
public BookStatus BookStatus { getset; }

Dear gods of web programming, every time you see an enum to be posted back please be so kind and give me a drop down with each enum member.
HtmlConvention(x =>
     x.Editors
      .If(e => e.Accessor.PropertyType.IsEnum)
           .BuildBy(er =>
                   {
                      var tag = new HtmlTag("select");
                      var enumValues = Enum.GetValues(er.Accessor.PropertyType);
                      foreach (var enumValue in enumValues)
                      {
                         tag.Children.Add(new HtmlTag("option").Text(enumValue.ToString()));
                      }
                      return tag;
                    })

Once you work out which variables correspond to the property type (er.Accessor.PropertyType) it’s pretty simple to get all the values and make a drop down. Again, the tag you return will automatically have the property name applied, so you can bind the selected value to it nicely.

String Collections

Caveat: fubu mvc doesn’t support binding to a collection of strings yet or IEnumerables (you can add your own, but it’s a bit of work). I just use StringWrapper  – which has one string property, “Text”; and ILIst<T> which is supported for binding collections to.
public IList<StringWrapper> Authors { getset; }

Whenever I see a collection of strings I always want a text box and add button, down which lets you type in text items and build up a list to be posted back. By using some class-name conventions, you can write the JQuery once and it works wherever you use the class name.
But by using fubu-mvc conventions – I write the JQuery once and the class once – the rest is taken care of:
HtmlConvention(x =>
    x.Editors
     .If(e => e.Accessor.PropertyType.IsAssignableFrom(typeof (IList<StringWrapper>)))
               .BuildBy(er =>
                       {
                          var tag = new HtmlTag("div").AddClass("hasHiddenGroup");
                                                                                            
                          tag.Children.Add(
                                       new HtmlTag("input")
                                           .Attr("type""text")
                                           .Attr("name", er.Accessor.Name)
                                          );
                                                    
                          tag.Children.Add(
                                       new HtmlTag("a")
                                            .Attr("href""#")
                                            .Text("add")
                                            .AddClass("addItem")
                                           );
 
                          tag.Children.Add(new HtmlTag("ul"));
 
                          return tag;
                        }));

Start with a div – and give it the magic sauce – “hasHiddenGroup” class (don’t know why I called it that – blame the magic sauce) – then add a text box with the property’s name. Next to that text box, add a link called add.
To clarify, the conventions will build this:
<div class="hasHiddenGroup">
    <input type="text" name="Authors">
    <a href="#" class="addItem">add</a>
   <ul></ul>
</div>

..which means that dash of JQuery can be applied site-wide to wire up the add button - allowing that empty to list to be built up. It then renames each item in the list so they are posted back and can be used to model-bind to.
You can see the simple JQuery code here.

File Uploads

Here’s an easy one:
HtmlConvention(x =>
    x.Editors
      .If(e => e.Accessor.PropertyType.IsAssignableFrom(typeof(HttpPostedFileBase)))
                 .BuildBy(er => new HtmlTag("input").Attr("type""file"))
);

Closing Thoughts

No doubt about it, the Fubu MVC html conventions go a lot deeper, and are less than brittle than ASP.NET MVC’s Display/Editor templates. When applied to a real world project, the time-savings and bug-reduction have massive potential.
But that’s a production app - this example was based on a small toy project. What may happen in the wild is a mystery
·         What happens if the markup is complex and is not so easily created in code?
·         What happens if need to apply specific classes?
·         What about when I want to override conventions – what pitfalls await me?
I look forward to finding out the answer to these questions, and striving to maintain my intellectually-crippled views.
On a final note you can also use fubu mvc’s html conventions in ASP.NET MVC.

0 comments:

Post a Comment