(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 = }) .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.