ASP.NET MVC Tabular Display Template

asp.net mvc, asp.net, code 0 comments suggest edit

The ASP.NET MVC2 templates feature is a pretty nice way to quickly scaffold objects at runtime. Be sure to read Brad Wilson’s fantastic series on this topic starting at ASP.NET MVC 2 Templates, Part 1: Introduction.

As great as this feature is, there is one template that’s conspicuously missing. ASP.NET MVC does not include a template for displaying a list of objects in a tabular format. Earlier today, ScottGu forwarded an email from Daniel Manes (what?! no blog! ;) with a question on how to accomplish this. Daniel had much of it implemented, but was trying to get over the last hurdle.

With Brad’s help, I was able to give him a boost over that hurdle. Let’s walk through the scenario.

First, we need a model.

zoolander

No, not that kind of model. Something more along the lines of a C# variety.

public class Book
{
    public int Id { get; set; }

    public string Title { get; set; }

    public string Author { get; set; }

    [DisplayName("Date Published")]
    public DateTime PublishDate { get; set; }
}

Great, now lets add a controller action to the default HomeController which will create a few books and pass them to a view.

public ActionResult Index()
{
    var books = new List<Book>
    {
        new Book { 
            Id = 1, 
            Title = "1984", 
            Author = "George Orwell", 
            PublishDate = DateTime.Now 
        },
        new Book { 
            Id = 2, 
            Title = "Fellowship of the Ring", 
            Author = "J.R.R. Tolkien", 
            PublishDate = DateTime.Now 
        },
        //...
    };
    return View(books);
}

Now we’ll create a strongly typed view we’ll use to display a list of such books.

<% @Page MasterPageFile="~/Views/Shared/Site.Master"
  Language="C#"
  Inherits="ViewPage<IEnumerable<TableTemplateWeb.Models.Book>>" %>

<asp:Content ContentPlaceHolderID="TitleContent" runat="server">
    Home Page
</asp:Content>

<asp:Content ContentPlaceHolderID="MainContent" runat="server">
    <h2>All Books</h2>
    <p>
        <%: Html.DisplayForModel("Table") %>
    </p>
</asp:Content>

If you run the code right now, you won’t get a very useful display. Also, notice that we pass in the string “Table” to the DisplayForModel method. That’s a hint to the template method which tells it, “Hey! If you see a template named ‘Table’, tell him he owes me money! Oh, and use it to render the model. Otherwise, if he’s not around fallback to your normal behavior.”

Since we don’t have a Table template yet, this code is effectively the same as if we didn’t pass anything to DisplayForModel.

What we need to do now is create the Table template. To do so, create a DisplayTemplates folder within the Views/Shared directory. Then right click on that folder and select Add | View.

This brings up the Add View dialog. Enter Table as the view name and make sure check Create a partial view. Also, check Create a strongly-typed view and type in IList as the View Data Class.

Add View
Dialog

When you click Add, you should see the new template in the DisplayTemplates folder like so.

solution
explorer

Here’s the code for the template. Note that there’s some code in here that I could refactor into a helper class in order to clean up the template a bit, but I wanted to show the full template code here in one shot.

<% @Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IList>" %>
<script runat="server">
  public static bool ShouldShow(ModelMetadata metadata,       ViewDataDictionary viewData) {
    return metadata.ShowForDisplay
      && metadata.ModelType != typeof(System.Data.EntityState)
      && !metadata.IsComplexType
      && !viewData.TemplateInfo.Visited(metadata);
  }
</script>
<%
  var properties = ModelMetadata.FromLambdaExpression(m => m[0], ViewData)
    .Properties
    .Where(pm => ShouldShow(pm, ViewData));
%>
<table>
  <tr>
    <% foreach(var property in properties) { %>        
    <th>
      <%= property.GetDisplayName() %>
    </th>
    <% } %>
  </tr>
    <% for(int i = 0; i < Model.Count; i++) {
    var itemMD = ModelMetadata.FromLambdaExpression(m => m[i], ViewData); %>
    <tr>
      <% foreach(var property in properties) { %>
      <td>
        <% var propertyMetadata = itemMD.Properties
              .Single(m => m.PropertyName == property.PropertyName); %>  
          <%= Html.DisplayFor(m => propertyMetadata.Model) %>
        </td>
      <% } %>
    </tr>
    <% } %>
</table>

Explanation {.clear}

There’s a lot going on in here, but I’ll try to walk through it bit by bit. If you’d rather skip this part and just take the code and run, I won’t hold it against you.

In the first section, we define a ShouldShow method which is pulled right out of the logic for our default Object template. You’ll notice there’s mention of System.Data.EntityState (defined in the System.Data.Entity.dll) which is used to filter out certain Entity Framework properties. If you aren’t using Entity Framework you can safely delete that line.You’ll know you don’t need that line if you aren’t referencing System.Data.Entity.dll which will cause this code to blow up like aluminum foil in a microwave.

In the next code block, we grab all the property ModelMetadata for the first item in the list. Remember, the current model in this template is a list, but we need the metadata for an item in this list, not the list itself. That’s why we have this odd bit of code here. Once we grab this metadata, we can iterate over it and display the column headers.

In the final block of code, we iterate over every item in the list and use this handy dandy FromLambdaExpression method to grab the ModelMetadata for an individual item.

Then we grab the property metadata for that item and iterate over that so that we can display each property in its own column. Notice that we call DisplayFor on each property rather than simply spitting out propertyMetadata.Model.

Usage

Now that you’ve created this Table.ascx template and placed it in the Shared/DisplayTemplates folder, it is available any time you’re using a display template to render a list. Simply supply a hint to use the table template. For example:

<%: Html.DisplayForModel("Table") %>

or

<%: Html.DisplayFor(m => m.SomeList, "Table") %>

Download the sample

As I typically do, I’ve written up a sample project you can try out in case you run into problems getting this to work. Note this sample was built for Visual Studio 2010 targetting ASP.NET 4. If you are running ASP.NET MVC 2 on Visual Studio 2008 SP1, just copy the Table.ascx into your own project but replace the Html encoding code nuggets <%: … %> to <%= Html.Encode(…) %>.

Here’s the link to the sample.

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

39 responses

  1. Avatar for shiju varghese
    shiju varghese May 5th, 2010

    Thanks for sharing this very useful post.

  2. Avatar for Lohith
    Lohith May 5th, 2010

    Excellent post Phil...Thanks for sharing the info.

  3. Avatar for Raj Kaimal
    Raj Kaimal May 5th, 2010

    Phil,
    The MVCContrib Grid makes it much easier to do this.
    You add this (can be customized too):
    <%= Html.Grid(Model).AutoGenerateColumns()%>
    and add an attribute on the PublishDate property
    [DisplayFormat(DataFormatString= "{0:dddd MMMM dd, yyyy}")]
    Raj

  4. Avatar for Artiom
    Artiom May 5th, 2010

    Just a quick note:
    1) You're saying to type IView in the View Data Class but on the screenshot you've typed IList...
    2) In the next screenshot, the big red arrow isn't pointing to the right file ;)
    Pretty cool article! :)

  5. Avatar for Binky
    Binky May 5th, 2010

    This will render divs in the td. We need more templates if we want the label in one td and the input field in another td.

  6. Avatar for Artiom
    Artiom May 5th, 2010

    Umm. Hey Phil, why is your blog loading 50px gravatars, but then stretching them with css to 80px? That sounds kind of wrong, and makes the avatars look... ugly :P
    Is that a bug? ^_^

  7. Avatar for Nick Reeve
    Nick Reeve May 5th, 2010

    Hey Phil,
    You used typeof(System.Data.EntityState) to filter out the Entity Framework properties but that only filters out the EntityState property. There is also EntityKey and references to foreign key objects in my entity. I can filter out the EntityKey property by using typeof(System.Data.EntityKey) but can't seem to remove the foreign key reference properties. Is there a way of filtering these out here? The only way I have managed this is by creating a partial metadata class and adding '[ScaffoldColumn(false)]' to those fields.
    Cheers,
    Nick

  8. Avatar for Erik Forbes
    Erik Forbes May 5th, 2010

    In your example you're hiding the loop inside the template, but what if I do it the other way around and call DisplayForModel inside a loop which requires IDs on form elements? Will the IDs generated by such helpers as EditorFor be unique, or would I need to manage that myself somehow? And if that's the case, what would be the recommended method of dealing with this?

  9. Avatar for Ron Krauter
    Ron Krauter May 5th, 2010

    Nick,
    You could check for the following types:
    System.Data.Objects.DataClasses.EntityReference
    System.Data.Objects.DataClasses.EntityObject
    System.Data.Objects.DataClasses.RelatedEnd
    Although your approach of scaffolding is much better.
    Raj,
    Is the MVC contrib Grid in Beta?
    Regards,
    Ron

  10. Avatar for Eric Hexter
    Eric Hexter May 5th, 2010

    @haacked This is nice.. alot better than what this looked like in some of the mvc 2 betas. So Combine this with your shared\display shared\edit and your golden. I think you have runtime scafolding done!.
    @Raj - This has an advantage over the MvcContrib Grid in that the actual template does not have to be strongly typed. The Grid works on the Generic type of the Model and cannot auto generate columns without being strongly types.. So this can support a generic implementation rather than having duplicate views to create the grid. I think this approach is great for a general table rendering. The MvcContrib Grid is for a scenerio when you want some very specific formating.

  11. Avatar for haacked
    haacked May 5th, 2010

    @Artiom, thanks! I've fixed the issues you identified. How sloppy of me! ;)

  12. Avatar for Bret Ferrier
    Bret Ferrier May 5th, 2010

    Wouldn't it be cool if we had a project on CodePlex with a bunch of these display templates?

  13. Avatar for Raj Kaimal
    Raj Kaimal May 5th, 2010

    Good point Eric. Thanks!

  14. Avatar for Felipe Lima
    Felipe Lima May 5th, 2010

    Phil, I never understood why the method ShouldShow checks if the model is not a "complex type". I'm having problems today because of this check, so I'm workarounding by adding a fake string type converter to my model, so it is "masked as a simple type". However, this has caused me other side effects as well, so I'm a bit lost on this. Could you explain why EditorForModel() won't render the templates for "complex types"?
    Thanks,
    Felipe

  15. Avatar for McConnell Group
    McConnell Group May 5th, 2010

    Thank you for the sample, I will do some testing this weekend with it

  16. Avatar for Bikal Gurung
    Bikal Gurung May 5th, 2010

    Phil,
    Helpful article but shouldn't the 'ShouldShow()' method be refactored into a Helper class as a - maybe - an extension method of IQueryable<> ? So that we remove the tag soup and promote better readability, testability and maintainability of the MVC codebase?
    Bikal

  17. Avatar for Justin
    Justin May 6th, 2010

    This is a useful post but I agree with Raj, MvcContrib's Grid is much cleaner and easier than this. I think I would prefer a helper method over template files.

  18. Avatar for Erik
    Erik May 6th, 2010

    Phil,
    This is still losing some Matadata. If you change you Display Line to <%= Html.DisplayFor(m => propertyMetadata.Model, propertyMetadata.TemplateHint) %>, It will it least carry along any Template hint applied to the Model.
    Erik

  19. Avatar for Dan M.
    Dan M. May 6th, 2010

    Phil, this is great! You even managed to "genericize" it. Thanks again for all your help.

  20. Avatar for Kyle Nunery
    Kyle Nunery May 6th, 2010

    Templates is the coolest feature of MVC 2 and hope you expand upon this greatly in MVC 3. Is there a performance concern of rendering the UI in this manner? Have you done any tests with regards to performance? I am hoping to use this feature heavily in some projects.

  21. Avatar for Vampal
    Vampal May 6th, 2010

    useful post,thanks

  22. Avatar for Sam
    Sam May 6th, 2010

    If the IList is empty does this code fail with an out of bounds exception?
    <%
    var properties = ModelMetadata.FromLambdaExpression(m => m[0], ViewData)
    .Properties
    .Where(pm => ShouldShow(pm, ViewData));
    %>

  23. Avatar for Thomas Payne
    Thomas Payne May 6th, 2010

    If you really want to be cool, you can rename Table.ascx to List`1.ascx. Then you can replace Html.DisplayForModel("Table") with just Html.DisplayForModel().
    It will also automatically use the template for any properties of type List on any of your view models.
    Also your code dies on an empty list. I used Model.GetType().GetGenericArguments()[0] to build my ModelMetaData so that I could still show the header row for the empty list.

  24. Avatar for Parag
    Parag May 6th, 2010

    Awesome article. We can adapt it to use jQGrid or some other tabular format.

  25. Avatar for Arnis L.
    Arnis L. May 8th, 2010

    This was first thing i was looking for when introduced to myself mvc2 templates. Didn't find it back then and was too lazy to figure it out myself.
    Thanks. Might be useful. :)

  26. Avatar for Asif
    Asif May 9th, 2010

    Nice one...
    It help me a lot..

  27. Avatar for NC
    NC May 11th, 2010

    This is such a terrible article. WTF is the point in using the MVC pattern if you move all your logic down to the view?

  28. Avatar for Hal
    Hal May 25th, 2010

    I'm not sure I agree with @NC - what logic besides display logic is loving in the view?

  29. Avatar for femi
    femi May 27th, 2010

    how can this be extended to edit and create views?

  30. Avatar for Bret Ferrier
    Bret Ferrier June 14th, 2010

    How is Model.GetType().GetGenericArguments()[0] used on an empty List?

  31. Avatar for Roby
    Roby July 13th, 2010

    Great article!
    But i can't create an helper method for do this...can someone post the code?

  32. Avatar for Jake
    Jake July 19th, 2010

    All metadata (DisplayFormat, UIHint, etc.) for the properties is lost when DisplayFor is called so the code isn't very useful as it is. After playing around with generics and reflection I was able to come up with a solution that preserves the metadata, but I wonder if there's an easier way to do it.
    Anyway, here it is:

    <% foreach (var property in properties)
    { %>
    <td>
    <%
    var parameters = new ParameterExpression[] { Expression.Parameter(typeof(IList), "m") };
    var memberExpression = Expression.MakeMemberAccess(Expression.Constant(Model[i]), Model[i].GetType().GetProperty(property.PropertyName));
    Type[] typeArgs = { typeof(IList), property.ModelType };
    var finalExpression = Expression.Lambda(typeof(Func<,>).MakeGenericType(typeArgs), memberExpression, parameters);
    %>
    <%=(MvcHtmlString)typeof(DisplayExtensions).GetMethods().
    First(method => method.Name == "DisplayFor" && method.IsGenericMethodDefinition)
    .MakeGenericMethod(typeArgs).Invoke(Html, new object[] { Html, finalExpression })%>
    </td>
    <% } %>

  33. Avatar for Eric Walch
    Eric Walch October 29th, 2010

    For anyone still having an issue with getting column names from an empty list:

    IList CurrentList = ViewData.Model;
    Type ListType = CurrentList.GetType().GetProperties()[2].PropertyType;
    ModelMetadata TypeTMetaData = ModelMetadataProviders.Current.GetMetadataForType( null, ListType);
    var properties = TypeTMetaData
    .Properties
    .Where(pm => ShouldShow(pm, ViewData) );
    %>
    <table>
    <tr>
    <% foreach (var property in properties)

    ....

  34. Avatar for baio
    baio May 2nd, 2011

    Hey Jake! Great remark!

  35. Avatar for Maxime Rouiller
    Maxime Rouiller May 25th, 2011

    I'm at the same point than Jake except that I'm displaying a generic IEnumerable and now I get "Late bound operations cannot be performed on types or methods for which ContainsGenericParameters is true."
    I guess I'll have to change type.

  36. Avatar for Javaman
    Javaman November 28th, 2011

    This is great, but is there an example of using this with Razor and MVC3? The code shown does not directly translate to Razor.
    The code below does not compile. Any suggestions?
    @{
    var properties = ModelMetadata.FromLambdaExpression(m => m[0], ViewData)
    .Properties
    .Where(pm => ShouldShow(pm, ViewData));
    }

  37. Avatar for John Reilly
    John Reilly March 9th, 2012

    Hi Guys,
    I happened upon this blog by accident when researching how to get metadata (display names) from an empty list on an object. The Eric Walch comment got me 99% of the way there. Thanks Eric!!!
    I ended up turning it into an HTML extension and thought I'd share it in case anyone else is in the same boat as me.
    Here's the extension:

    public static MvcHtmlString GetDisplayNameForPropertyOfList<TModel, TProperty>(
    this HtmlHelper<TModel> htmlHelper,
    Expression<Func<TModel, TProperty>> expression,
    string propertyName
    )
    {
    //Get the current list from the expression
    var currentList = expression.Compile().Invoke(htmlHelper.ViewData.Model);
    //Determine list type
    var listType = currentList.GetType().GetProperties()[2].PropertyType;
    //Get metadata for list type
    var typeTMetaData = ModelMetadataProviders.Current.GetMetadataForType(null, listType);
    //Get the relevant property using the supplied propertyName
    var property = typeTMetaData.Properties.Where(x => x.PropertyName == propertyName).SingleOrDefault();
    string value = (property == null) ? propertyName : property.DisplayName;
    return MvcHtmlString.Create(value);
    }

    And it can be used in views as follows:

    @Html.GetDisplayNameForPropertyOfList(vm => vm.MyList, "ListPropertyName")

  38. Avatar for andy
    andy June 28th, 2014

    thanks for sharing,but I have a problem, when the list is empty, the viewmodel would be error

  39. Avatar for nikolai
    nikolai November 13th, 2014

    Very useful article!
    I see only one issue: If the list with books is empty you get an exception.
    Better alternative to get all model metadata properties:

    ModelMetadataProviders.Current.GetMetadataForType(null, typeof(Book)).Properties

    This works for empty lists too!