Click here to Skip to main content
Click here to Skip to main content
Go to top

MVC Html Table Helper (Part 1: Display Tables)

, 16 May 2014
Rate this:
Please Sign up or sign in to vote.
An MVC HtmlHelper to generate display tables.

Introduction

I am developing a complex LOB application that needs to display a lot of tabular information. I very quickly got sick of repeatedly doing this in my views:

<table>
  <thead>
    <tr>
      <th>Column 1 Heading</th>
      <!--More column headings-->
    </tr>
  </thead>
  <tbody>
    @foreach (var item in Model.MyCollection)
    {
      <tr>
        <td>@item.PropertyName</td>
        <!--More properties-->
      </tr>
    }
  </tbody>
  <!--Add footer for totals-->
</table>

and having to create extra properties in my view models for footer totals.

This article describes an MVC HtmlHelper extension method which generates tables using a single line in the view (where MyCollection implements IEnumerable:

@Html.TableDisplayFor(m => m.MyCollection)

in conjunction with attributes to determine which properties to display and how they are displayed.

This is the first in a two-part article. Part 2 will describe the TableEditorFor() helper which generates tables for inline editing (including numeric inputs, selects, datepickers and checkboxes) and allows dynamic addition and deletion of rows with full postback for saving in transactions.

Using the Code

Used without attributes, the TableDisplayFor() method will generate a table with columns for each property of a type (including recursively displaying the properties of complex types) except those marked with the System.Web.Mvc.HiddenInputAttribute.

There are three attributes that can be used to control the formatting of tables:

Table Display Attribute

The TableDisplayAttribute is applied at class level and is used to specify additional columns for row numbers, view links and edit links. The attribute contains the following properties:

  • IncludeRowNumbers (bool): A value indicating if an additional column showing row numbers should be rendered in the table (the default is false).
  • Controller (string): The name of the route controller if view or edit links are displayed.
  • ViewAction (string): The name of the route action method for view links.
  • EditAction (string): The name of the route action method for edit links
  • IDProperty (string): The name of the property that contains the value to pass as the parameter to the view or edit action methods (the default is "ID").

If the ViewAction and/or EditAction and Controller and IDProperty properties are specified, then additional columns with links are rendered in the table.

Typically these properties would be used if the table is displaying only a subset of properties (e.g. just the name, phone number and email address of contacts), and a link is required to detail and/or edit pages. An alternative to rendering these additional columns (and my personal preference) is to use the TableLinkAttribute described below.

Using all properties as follows:

using Sandtrap.Web.DataAnnotations;

[TableDisplay(IncludeRowNumbers = true, Controller = "Contact", 
  ViewAction = "Details", EditAction = "Edit", IDProperty = "ID")]
public class Contact
{
  [HiddenInput]
  public int ID { get; set; }
  // Other properties
}

will change this:

to this:

Table Column Attribute

The TableColumnAttribute is applied at property level. The attribute contains the following properties:

  • Exclude (bool): A value indicating if the property is excluded from the table (the default is false). Note: Any property with the System.Web.Mvc.HiddenInputAttribute is also excluded from the table.
  • NoRepeat (bool): A value indicating a property's value is repeated if it has the same value as the preceding row(s). The value is ignored unless it's the first column or the preceding column has NoRepeat=true.
  • IncludeTotal (bool): A value indicating if totals are rendered in the table footer. The value is ignored if the property is not numeric.
  • DisplayProperty (string): If applied to a complex type, sets the name of the property used to display the type. If not set, a column is rendered for each property of the type (except those with the HiddenInput or TableColumn.Exclude attributes.

Applying the attribute as follows (where Organisation is a complex type):

using Sandtrap.Web.DataAnnotations;

public class ConsultantFee
{
  [HiddenInput]
  public int? ID { get; set; }

  [TableColumn(DisplayProperty = "Name", NoRepeat = true)]
  public Organisation Consultant { get; set; }

  public string Phase { get; set; }

  [Display(Name = "Variation")]
  public bool IsVariation { get; set; }

  [TableColumn(IncludeTotal = true)]
  [DisplayFormat(DataFormatString = "{0:C}")]
  public decimal Amount { get; set; }

}

will render this:

Removing the TableColumnAttribute from Organisation will render this (where Organisation contains properties int ID, string Name and bool IsActive):

Table Link Attribute

The TableLinkAttribute is applied at either class or property level and is used to display the property as a hyperlink. The attribute contains the following properties:

  • Controller (string): The name of the route controller.
  • Action (string): The name of the route action method.
  • IDProperty (string): The name of the property that contains the value to pass as the parameter to action method (the default is "ID").
  • DisplayProperty (string): The name of the property that contains the value to display as the hyperlink text.

Applying the attribute as follows (using the above example)

  [TableColumn(NoRepeat = true)]
  [TableLink(Controller = "Organisation" Action = "Details", IDProperty = "ID")]
  public Organisation Consultant { get; set; }
}

will change the display to this:

System.ComponentModel.DataAnotations Attributes

ModelMetadata properties generated from attributes in the System.ComponentModel.DataAnotations namespace (at least the ones I am aware of) are respected when formatting the table including:

  • [Display(Name = "")]: If present, used as the column heading
  • [Display(Order = ##)]: Determines the order of columns
  • [DataType(DataType.EmailAddress)] to create mailto: links
  • [DisplayFormat(DataFormatString = "")]: For formatting property values
  • [DisplayFormat(NullDisplayText = "")]: For formatting null values

How it Works

The code is too long to reproduce here (and the download includes all the source code with comments) so I will just cover some key aspects.

Attributes

Each attribute inherits from System.Attribute and implements System.Web.Mvc.IMetadataAware.

Each property has an associated static property that defines the key used to add the property's value to ModelMetadata.AdditionalValues.

[AttributeUsage(AttributeTargets.Property)]
public class TableColumnAttribute : Attribute, IMetadataAware
{
  public bool IncludeTotal { get; set; }

  public static string IncludeTotalKey
  {
    get { return "TableColumnIncludeTotal"; }
  }

  // More properties

In the OnMetadataCreated method, validation checks are performed on each property, and if successful its value is added to ModelMetadata.AdditionalValues. In the case of the TableColumnAttribute (some code omitted):

  1. Check if the property should be excluded from the table, and if so add the metadata and exit (any other properties are irrelevant)
  2. If the property is a complex type and the Display property has been set, check that the type contains the property (and throw an exception if it doesn't).
  3. If IncludeTotal is true, check that the property is a numeric type (Note: IsNumeric() is an extension method in the Sandtrap assembly that uses System.TypeCode to check if its byte, decimal, double etc.)
  4. Finally, if NoRepeat is true, check that IncludeTotal is false (it wouldn't make much sense to total a column that had empty cells as a result of its value being the same as the value of the previous row).
  public void OnMetadataCreated(ModelMetadata metadata)
  {
    if (Exclude)
    {
      metadata.AdditionalValues[ExcludeKey] = true;
      return;
    }
    // Check for a Display property
    ModelMetadata propertyMetadata = null;
    if (DisplayProperty != null && metadata.IsComplexType)
    {
      propertyMetadata = metadata.Properties
       .FirstOrDefault(m => m.PropertyName == DisplayProperty);
      if (propertyMetadata == null)
      {
        // Throw ArgumentException
      }
      metadata.AdditionalValues[DisplayPropertyKey] = DisplayProperty;
    }
    // If rendering totals, check we can
    if (IncludeTotal)
    {
      if (metadata.ModelType.IsNumeric())
      {
        metadata.AdditionalValues[IncludeTotalKey] = true;
      }
      else
      {
        // Reset
        IncludeTotal = false;
      }
    }
    if (NoRepeat && !IncludeTotal)
    {
      metadata.AdditionalValues[NoRepeatKey] = true;
    }
  }
}

ModelMetadata extensions

The assembly includes numerous extensions methods that for the most part are just shortcuts to checking ModelMetadata.AdditionalValues, for example:

internal static bool ColumnIncludeTotals(this ModelMetadata metaData)
{
  return metaData.AdditionalValues
    .ContainsKey(TableColumnAttribute.IncludeTotalKey);
}

Html helper

The key steps in generating the html are:

  1. Get the ModelMetadata from the expression and check that the model is a collection (implements System.Collections.IEnumerable).
  2. Get the ModelMetadata of type in the collection and retrieve the table level properties used to determine what (if any) additional columns are rendered.
  3. Initialise an empty collection of List<TableColumn> (TableColumn is a helper class used to store column properties and update running totals as each row is created).
  4. Call the TableHeader() to generate the thead and its tr element, which in turn calls the TableHeaderRow() method to create each th element. The TableHeaderRow() method also creates a TableColumn for each property in the type, and sets its values based on the type metadata. If a property is a complex type the method calls itself (recursive).
  5. Call the ReadonlyTableBody() method to generate the tbody element, which loops each object in the collection to get its ModelMetadata, create a tr element and call the recursive ReadonlyTableBodyRow() method to create each td element. The associated TableColumn is checked to determine how to display the property (e.g. as formatted text, hyperlink etc.) and if TableColumn.IncludeTotal is true, running totals are updated.
  6. Finally, if any property has the TableColumn.IncludeTotal attribute property, call the TableFooter() method to generate the tfoot and its tr element, which in turn calls the recursive TableFooterRow() method to create each td element.

Note: When generating the html for each row, the ModelMetadata of the item is created from the type in the collection (step 2 above), not from the model itself.

foreach (var item in collection)
{
  //  ....
  ModelMetadata itemMetadata = ModelMetadataProviders.Current
    .GetMetadataForType(() => item, type);
  // ....
}

If the models ModelMetadata was used and the collection was (say) List<Organisation> but contained some items of type Engineer which inherits from Organisation, then additional columns would be generated for Engineer, completely screwing up the table layout.

Known issues

The code works with collections generated by linq expressions including .OrderBy() (OrderedEnumerable) and .Select() (Enumerable+WhereSelectListIterator), including anonymous types (although this would render a very basic table since Attributes, can’t be applied to anonymous types), but it will not work if the collection implements IGrouping. For instance:

var collection = myCollection.GroupBy(...);

will fail (which group should be rendered?), but this will work

var collection = myCollection.GroupBy(...).First(...);

Aknowledgements

Chis Sinclair for helping to resolve this question on Stack Overflow.

The download

The download (Visual Studio 2012, MVC 4.0) includes full source code and some examples of tables using various combinations of attributes. It also include the source code and examples from my previous article MVC Custom Select Control, and some (but not all) of the code associated with the TableEditorFor() method to be covered in my next article.

To use the code:

  1. Build the project (the download does not include the packages, obj and bin folders but these will be restored on first build)
  2. Add a reference to both Sandtrap.dll and Sandtrap.Web.dll in your project
  3. Add the namespace to your web.config files
    <system.web>
      <namespaces>
       <add namespace="Sandtrap.Web.Html">

The rest should be clear from examining the examples and help file in the download.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Stephen Muecke

Australia Australia
No Biography provided

Comments and Discussions

 
QuestionGreat piece of work PinmemberMember 1026955919-May-14 9:36 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.140916.1 | Last Updated 16 May 2014
Article Copyright 2014 by Stephen Muecke
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid