This website is no longer actively supported
Written by John DeVight on 30 Jun 2011 19:43
Download ASPX Source Code
Download RAZOR Source Code
Overview
In the article, ASP.NET MVC 3 : Creating Custom HTML Helpers I was asked about being able to "create a Telerik style HtmlHelper…" where the developer wanted to "…declare a model and have the generic types be inferred rather than have to implicitly declare them in the view". I was directed to a stack overflow question MVC3 HtmlHelpers using Generics and LINQ. The article says, "I am trying to create an HtmlHelper Extension that has the ability to take a collection of TModels. Then set an Expression that will grab the declared property foreach item in the collection and print them out."
I decided to see if I could create an HTML Helper that takes a collection of model objects and renders a table. I spent some time looking at how Telerik implements their GridBuilder using Telerik JustDecompile (Telerik's FREE decompiler) and got some ideas of how I could do this. I created an HtmlHelper extension method called TableFor that creates an instance of a TableBuilder class. The TableBuilder class contains an enumerable list of model objects to be rendered as a table and a list of lambda expressions, managed in the TableBuilder class as TableColumn objects, that identify which model properties are rendered in the table. The TableColumn objects get created from the ColumnBuilder class which creates the TableColumn objects and adds them to the TableBuilder class. The TableBuilder.ToHtml method renders the enumerable list of model objects as a table.
In the end, I was able to render a table from an enumerable list of model objects with the following code:
Html.TableFor<MvcRazorApp.Models.Person>() .Columns(column => { column.Expression(p => p.FirstName).Title("Given Name"); column.Expression(p => p.LastName); column.Expression(p => p.Title); column.Expression(p => p.Episodes); }) .DataSource(this.Model) .ToHtml()
Creating the TableColumn Class
The table column class contains the information about a column in the table. Each lambda expression that is passed in gets compiled and stored in an instance of a TableColumn class. The constructor also creates the default title for the column using a regular expression to add a space in between lowercase and uppercase letters. So, for example, FirstName becomes First Name. TableColumn has a Title method that allows the title to be set to something other than the default title defined in the constructor. The TableColumn class has an Evaluate method. This method evaluates the compiled lambda expression against an instance of a model and returns the property value. If the property value has not been set, an empty string is returned.
Here is the code:
/// <summary> /// Represents a column in a table. /// </summary> /// <typeparam name="TModel">Class that is rendered in a table.</typeparam> /// <typeparam name="TProperty">Class property that is rendered in the column.</typeparam> public class TableColumn<TModel, TProperty> : ITableColumn, ITableColumnInternal<TModel> where TModel : class { /// <summary> /// Column title to display in the table. /// </summary> public string ColumnTitle { get; set; } /// <summary> /// Compiled lambda expression to get the property value from a model object. /// </summary> public Func<TModel, TProperty> CompiledExpression { get; set; } /// <summary> /// Constructor. /// </summary> /// <param name="expression">Lambda expression identifying a property to be rendered.</param> public TableColumn(Expression<Func<TModel, TProperty>> expression) { string propertyName = (expression.Body as MemberExpression).Member.Name; this.ColumnTitle = Regex.Replace(propertyName, "([a-z])([A-Z])", "$1 $2"); this.CompiledExpression = expression.Compile(); } /// <summary> /// Set the title for the column. /// </summary> /// <param name="title">Title for the column.</param> /// <returns>Instance of a TableColumn.</returns> public ITableColumn Title(string title) { this.ColumnTitle = title; return this; } /// <summary> /// Get the property value from a model object. /// </summary> /// <param name="model">Model to get the property value from.</param> /// <returns>Property value from the model.</returns> public string Evaluate(TModel model) { var result = this.CompiledExpression(model); return result == null ? string.Empty : result.ToString(); } }
Creating the ITableColumn Interface
The TableColumn class implements the ITableColumn interface. This interface defines the properties and methods used by the developer to configure the TableColumn.
Here is the code:
/// <summary> /// Properties and methods used by the consumer to configure the TableColumn. /// </summary> public interface ITableColumn { ITableColumn Title(string title); }
Creating the ITableColumnInternal Interface
The TableColumn class also implements the ITableColumnInternal interface. The reason for this is to allow the TableBuilder class to maintain an IList of TableColumn objects. Since the TableBuilder doesn't know anything about the TProperty type, an error occurs. I therefore created an interface called ITableColumnInternal that only defines the TModel type (since it is used by the Evaluate method).
Here is the code:
/// <summary> /// Properties and methods used within the TableBuilder class. /// </summary> public interface ITableColumnInternal<TModel> where TModel : class { string ColumnTitle { get; set; } string Evaluate(TModel model); }
Creating the TableBuilder Class
The TableBuilder class contains a Columns method that creates an instance of the ColumnBuilder class. The ColumnBuilder class creates instances of the TableColumn class and adds them to the TableBuilder class by calling the TableBuilder.AddColumn method. The DataSource property is the enumerable list of models to be rendered as a table. The TableBuilder.ToHtml method renders the TableBuilder as HTML. I found it easy to simply build the table HTML code using an XmlDocument. I loop through the TableBuilder.TableColumns and render the column headings in the table header. I then loop though the enumerable list of model objects in TableBuilder.Data and for each model, I loop though the TableBuilder.TableColumns and call TableColumn.Evaluate for the model object to get the property value. When I am all done, I return a new instance of MvcHtmlString that contains the XmlDocument.OuterXml.
Here is the code:
/// <summary> /// Build a table based on an enumerable list of model objects. /// </summary> /// <typeparam name="TModel">Type of model to render in the table.</typeparam> public class TableBuilder<TModel> : ITableBuilder<TModel> where TModel : class { private HtmlHelper HtmlHelper { get; set; } private IEnumerable<TModel> Data { get; set; } /// <summary> /// Default constructor. /// </summary> private TableBuilder() { } /// <summary> /// Constructor. /// </summary> internal TableBuilder(HtmlHelper helper) { this.HtmlHelper = helper; this.TableColumns = new List<ITableColumnInternal<TModel>>(); } /// <summary> /// Set the enumerable list of model objects. /// </summary> /// <param name="dataSource">Enumerable list of model objects.</param> /// <returns>Reference to the TableBuilder object.</returns> public TableBuilder<TModel> DataSource(IEnumerable<TModel> dataSource) { this.Data = dataSource; return this; } /// <summary> /// List of table columns to be rendered in the table. /// </summary> internal IList<ITableColumnInternal<TModel>> TableColumns { get; set; } /// <summary> /// Add an lambda expression as a TableColumn. /// </summary> /// <typeparam name="TProperty">Model class property to be added as a column.</typeparam> /// <param name="expression">Lambda expression identifying a property to be rendered.</param> /// <returns>An instance of TableColumn.</returns> internal ITableColumn AddColumn<TProperty>(Expression<Func<TModel, TProperty>> expression) { TableColumn<TModel, TProperty> column = new TableColumn<TModel, TProperty>(expression); this.TableColumns.Add(column); return column; } /// <summary> /// Create an instance of the ColumnBuilder to add columns to the table. /// </summary> /// <param name="columnBuilder">Delegate to create an instance of ColumnBuilder.</param> /// <returns>An instance of TableBuilder.</returns> public TableBuilder<TModel> Columns(Action<ColumnBuilder<TModel>> columnBuilder) { ColumnBuilder<TModel> builder = new ColumnBuilder<TModel>(this); columnBuilder(builder); return this; } /// <summary> /// Convert the TableBuilder to HTML. /// </summary> public MvcHtmlString ToHtml() { XmlDocument html = new XmlDocument(); XmlElement table = html.CreateElement("table"); html.AppendChild(table); table.SetAttribute("border", "1px"); table.SetAttribute("cellpadding", "5px"); table.SetAttribute("cellspacing", "0px"); XmlElement thead = html.CreateElement("thead"); table.AppendChild(thead); XmlElement tr = html.CreateElement("tr"); thead.AppendChild(tr); foreach (ITableColumnInternal<TModel> tc in this.TableColumns) { XmlElement td = html.CreateElement("td"); td.SetAttribute("style", "background-color:Black; color:White;font-weight:bold;"); td.InnerText = tc.ColumnTitle; tr.AppendChild(td); } XmlElement tbody = html.CreateElement("tbody"); table.AppendChild(tbody); int row = 0; foreach (TModel model in this.Data) { tr = html.CreateElement("tr"); tbody.AppendChild(tr); foreach (ITableColumnInternal<TModel> tc in this.TableColumns) { XmlElement td = html.CreateElement("td"); td.InnerText = tc.Evaluate(model); tr.AppendChild(td); } row++; } return new MvcHtmlString(html.OuterXml); } }
Creating the ColumnBuilder Class
The ColumnBuilder class contains a single method called Expression that creates instances of the TableColumn class for the lambda expression passed in and returns an instance of the TableColumn class so that additional properties in the TableColumn class can be set.
Here is the code:
/// <summary> /// Create instances of TableColumns. /// </summary> /// <typeparam name="TModel">Type of model to render in the table.</typeparam> public class ColumnBuilder<TModel> where TModel : class { public TableBuilder<TModel> TableBuilder { get; set; } /// <summary> /// Constructor. /// </summary> /// <param name="tableBuilder">Instance of a TableBuilder.</param> public ColumnBuilder(TableBuilder<TModel> tableBuilder) { TableBuilder = tableBuilder; } /// <summary> /// Add lambda expressions to the TableBuilder. /// </summary> /// <typeparam name="TProperty">Class property that is rendered in the column.</typeparam> /// <param name="expression">Lambda expression identifying a property to be rendered.</param> /// <returns>An instance of TableColumn.</returns> public ITableColumn Expression<TProperty>(Expression<Func<TModel, TProperty>> expression) { return TableBuilder.AddColumn(expression); } }
Creating the ITableBuilder Interface
The TableBuilder class implements the ITableBuilder interface. This interface defines the properties and methods used by the developer to configure the TableBuilder. The TableFor HTML extension method returns an instance of the TableBuilder class as an ITableBuilder interface.
Here is the code:
/// <summary> /// Properties and methods used by the consumer to configure the TableBuilder. /// </summary> public interface ITableBuilder<TModel> where TModel : class { TableBuilder<TModel> DataSource(IEnumerable<TModel> dataSource); TableBuilder<TModel> Columns(Action<ColumnBuilder<TModel>> columnBuilder); }
Creating the TableFor HtmlHelper Extension Method
The TableFor HtmlHelper extension returns an instance of a TableBuilder class an ITableBuilder interface.
Here is the code:
public static class MvcHtmlTableExtensions { /// <summary> /// Return an instance of a TableBuilder. /// </summary> /// <typeparam name="TModel">Type of model to render in the table.</typeparam> /// <returns>Instance of a TableBuilder.</returns> public static ITableBuilder<TModel> TableFor<TModel>(this HtmlHelper helper) where TModel : class { return new TableBuilder<TModel>(helper); } }
Implementing HTML Helper in a View
The Person Model
I created a model class called Person and gave it four properties, FirstName, LastName, Title and Episodes. Here is the code:
public class Person { public string FirstName { get; set; } public string LastName { get; set; } public string Title { get; set; } public int Episodes { get; set; } }
The Controller
I created a controller and defined a method called TableBuilderSample that creates a list of Person objects and return a ViewResult with the Person list. Here is the code:
public ActionResult TableBuilderSample() { IList<Person> list = new List<Person>(); list.Add(new Person { FirstName = "William", LastName = "Adama", Title ="Commander", Episodes = 73 }); list.Add(new Person { FirstName = "Laura", LastName = "Roslin", Title = "President", Episodes = 73 }); list.Add(new Person { FirstName = "Gaius", LastName = "Baltar", Episodes = 73 }); list.Add(new Person { FirstName = "Lee", LastName = "Adama", Episodes = 73 }); list.Add(new Person { FirstName = "Kara", LastName = "Thrace", Episodes = 71 }); return View(list); }
The View
I then created a strongly typed view that took an IEnumerable list of Person objects. I implemented my Html.TableFor extension method where I defined the table using the TableBuilder class. Here is the code:
@model IList<MvcRazorApp.Models.Person> @{ ViewBag.Title = "TableBuilderSample"; } <h2>TableBuilderSample</h2> @(Html.TableFor<MvcRazorApp.Models.Person>() .Columns(column => { column.Expression(p => p.FirstName).Title("Given Name"); column.Expression(p => p.LastName); column.Expression(p => p.Title); column.Expression(p => p.Episodes); }) .DataSource(this.Model) .ToHtml() )
Conclusion
It is possible to create an HtmlHelper extension method that has the ability to take a collection of TModels and render them. What I did discover while exploring this was that Telerik has put a lot of work into figuring all this out. I'm impressed! I could keep going with this, and I just might do that so that I have a nice HtmlHelper extension method for rendering static tables, but if I want a table that is editable, sortable, filterable and all the other functionality that goes into making a nice control, I'll stick with Telerik's Grid.
References
- ASP.NET MVC 3 : Creating Custom HTML Helpers
- MVC3 HtmlHelpers using Generics and LINQ
- Telerik JustDecompile (Telerik's FREE decompiler)
- Telerik Extensions for ASP.NET MVC Grid