Site Meter
 
 

(Server controls vs. Inline code) = (What vs. How)

When each developer first learns about ASP.NET MVC, their immediate reaction to MVC view pages is to be shocked and appalled by the mixture of HTML markup and C# code (“inline code”).

What about separating presentation from logic??” they’ll cry, and of course you’ll explain the bigger picture of separation of concerns: domain logic from application logic from UI logic, and how MVC adds scope for testability and cleaner code and how foreach loops beat databinding any day, and so on, mercilessly kicking WebForms’s elderly butt.

And of course you’re right. But each time I have to type out view code like this:

<table cellspacing="0">
    <thead>
        <tr>
            <th>ID</th>
            <th>Product</th>
            <th>Category</th>
            <th align="right">Price</th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        <% foreach(var item in ViewData.Products) { %>
            <tr>
                <td><%= item.ProductID %></td>
                <td><%= item.Name %></td>
                <td><%= item.Category %></td>
                <td align="right"><%= item.Price.ToString("c") %></td>
                <td><%= Html.ActionLink("Details", "Show", new { ProductID = item.ProductID }) %></td>
            </tr>
        <% } %>
    </tbody>
</table>

… I can’t help thinking that something’s slightly not right here (and honestly, I try to suppress that dark thought!). Oh don’t get me wrong, I’m a big fan of the new MVC framework, and I’d take it rather than WebForms any day, but software development technology normally progresses like this:

  • Old technology: Specify exactly what you want the computer to do
  • New technology: Specify what end result you want, but let the computer work out the details

Great examples of the latter bullet point are query languages like SQL or even LINQ, and many of C#’s newer features (e.g. type inference). And how about:

  • Classic ASP: Specify exactly what HTML markup you want
  • WebForms: Specify what end result you want (e.g. a grid), and let the framework figure out what HTML markup is required
  • ASP.NET MVC: Um, specify exactly what HTML markup you want

My experience is that WebForms was a great idea, for its time, but in practice it just didn’t work so well. For demoware, it’s an awesome platform, but try to build a 3-person-year project with it and it will all end in tears (but not tiers).

So, with MVC, we think again and take stock of where we are. HTML is still a real concern, WebForms’s abstraction layer *didn’t* save us from browser incompatibilities, and HTTP is still a stateless protocol. Time for a change of plan, back to basics and all that, but do we really have to go back to writing <TABLE><THEAD><TR><TH>… etc.? It’s not hard, it’s just verbose. Do we need a better DSL for HTML? Is that what alternate view engines could provide?

The framework’s built-in HTML helpers really do help us, but they only produce individual HTML tags, not related groups of tags. Maybe we should add some new ones, e.g. Html.Table, so you could just write:

<%= Html.Table(ViewData.Products, new { cellspacing = 0 })
    .AddColumn("ID", i => i.ProductID)
    .AddColumn("Product", i => i.Name)
    .AddColumn("Category", i => i.Category)
    .AddColumn("Price", i => i.Price.ToString("c"), new { align = "right" })
    .AddColumn("", i => Html.ActionLink("Details", "Show", new { ProductID = i.ProductID }))
%>

In case you do think that’s a good idea, here’s the code needed to implement it. Sorry it’s a bit long, but each method has a few overrides to give you more flexibility in how you supply parameters:

namespace StevenSanderson.Mvc.HtmlHelpers
{
    public static class TableHelper
    {
        public static HtmlTable<t> Table<t>(this HtmlHelper html, IEnumerable<t> data)
        {
            return html.Table<t>(data, null);
        }
        public static HtmlTable<t> Table<t>(this HtmlHelper html, IEnumerable<t> data, object htmlAttributes)
        {
            return html.Table<t>(data, htmlAttributes.ToPropertyList());
        }
        public static HtmlTable<t> Table<t>(this HtmlHelper html, IEnumerable<t> data, IDictionary<string, string> htmlAttributes)
        {
            return new HtmlTable<t> { Data = data, HtmlAttributes = htmlAttributes };
        }
 
        public class HtmlTable<t>
        {
            public IEnumerable<t> Data;
            public IDictionary<string, string> HtmlAttributes;
            private IList<htmlTableColumn> columns = new List<htmlTableColumn>();
 
            public HtmlTable<t> AddColumn(string headerText, Func<t, object> itemText)
            {
                return this.AddColumn(headerText, itemText, null);
            }
 
            public HtmlTable<t> AddColumn(string headerText, Func<t, object> itemText, object htmlAttributes)
            {
                return this.AddColumn(headerText, itemText, htmlAttributes.ToPropertyList());
            }
 
            public HtmlTable<t> AddColumn(string headerText, Func<t, object> itemText, IDictionary<string, string> htmlAttributes)
            {
                columns.Add(new HtmlTableColumn
                {
                    HeaderText = headerText,
                    ItemText = itemText,
                    HtmlAttributes = htmlAttributes
                });
                return this;
            }
 
            private void RenderAttributes(HtmlTextWriter writer, IDictionary<string, string> attribs)
            {
                if (attribs != null)
                    foreach (var attrib in attribs)
                        writer.AddAttribute(attrib.Key, attrib.Value.ToString(), true);
            }
 
            public override string ToString()
            {
                using (StringWriter sw = new StringWriter())
                {
                    HtmlTextWriter writer = new HtmlTextWriter(sw);
 
                    // <table>
                    RenderAttributes(writer, this.HtmlAttributes);
                    writer.RenderBeginTag(HtmlTextWriterTag.Table);
 
                    // Headers
                    writer.RenderBeginTag(HtmlTextWriterTag.Thead);
                    RenderTableRow(writer, HtmlTextWriterTag.Th, col => col.HeaderText);
                    writer.RenderEndTag(); // </thead>
 
                    // Rows
                    writer.RenderBeginTag(HtmlTextWriterTag.Tbody);
                    foreach (T row in Data)
                        RenderTableRow(writer, HtmlTextWriterTag.Td, col => col.ItemText(row).ToString());
                    writer.RenderEndTag(); // </tbody>
 
                    writer.RenderEndTag(); // </table>
                    return sw.ToString();
                }
            }
 
            private void RenderTableRow(HtmlTextWriter writer, HtmlTextWriterTag cellTag, Func<htmlTableColumn, string> cellValue)
            {
                writer.RenderBeginTag(HtmlTextWriterTag.Tr);
                foreach (var col in columns)
                {
                    RenderAttributes(writer, col.HtmlAttributes);
                    writer.RenderBeginTag(cellTag);
                    writer.Write(cellValue(col));
                    writer.RenderEndTag(); // </th>
                }
                writer.RenderEndTag(); // </tr>
            }
 
            class HtmlTableColumn
            {
                public string HeaderText { get; set; }
                public Func<t, object> ItemText { get; set; }
                public IDictionary<string, string> HtmlAttributes { get; set; }
            }
        }
 
        private static IDictionary<string, string> ToPropertyList(this object obj)
        {
            return obj == null ? null
                : obj.GetType().GetProperties().ToDictionary(p => p.Name, p => p.GetValue(obj, null).ToString());
        }
    }
}

Note that, to use this, you need to add <%@ Import Namespace="StevenSanderson.Mvc.HtmlHelpers" %> to the top of your view page (probably changing the namespace to something more suitable for your project), or add the namespace to your web.config in the system.web/pages/namespaces node.

10 Responses to (Server controls vs. Inline code) = (What vs. How)

  1. Matt

    Seems like this would fit well in the MVCContrib project. Are usercontrols/servercontrols the closet thing we have to the notion of Tag Libraries?

  2. Pingback: Links Today (2008-04-14)

  3. Steve

    Matt, I’m not really sure what you mean by a Tag Library – I think that’s a term from the JSP world (of which I know nothing).

    With ASP.NET MVC, you have a range of different methods to reuse UI code:

    [1] HTML helper methods, which are C# functions that return strings. These just provide a simpler syntax for getting a ready-made block of HTML markup, usually for single tags. The Html.Table example I provided above fits into this group.

    [2] MVC View user controls / WebForms server controls, which let you define a reusable block of HTML markup via an ASPX template rather than as pure C# code. They shouldn’t contain any logic except as needed to transform their input into HTML.

    [3] Components, which can contain more meaningful logic (e.g. obtaining data from your domain model), and run their own mini-MVC pipeline, possibly using a view template to render some arbitrary HTML.

    It’s up to you to pick one of these, trading off simplicity and power, according to each piece of reusable UI that you want.

  4. Sruly Taber

    Can you compile and publish this on codeplex it will be very helpful

  5. Steve

    Hi Sruly, I’ve provided the code in full so you should have no problems compiling it yourself in your own project. If you want to add it to a Codeplex project then please feel free to do so!

  6. The problem that I think WebForms solved and MVC.NET hasn’t yet is that of enabling non-programmer humans (i.e. designers) and tools to work with markup. Dropping code in there is not a good thing.

  7. Steve

    Michael – isn’t that what CSS is for? :)

  8. Robert Dean

    Steve,
    Thanks for this great concept.
    I, like many others, have an allergic reaction to tags. Nothing wrong with it, I just get nervous letting our designer near a page that has them:).
    I think that at least conceptually, translating your code into something like this would help developer/designer:

    I’ve been trying to create something like this but I haven’t had much luck.
    What is your opinion of this approach?

  9. Robert Dean

    Oops, guess your site didn’t like my markup. Sorry about that.

  10. Steve

    Hi Robert – might be easier if you email me whatever it was you were suggesting. WordPress doesn’t handle HTML characters well.