MVC Html Table Helper (Part 1: Display Tables)






4.95/5 (14 votes)
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 isfalse
).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 linksIDProperty
(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 isfalse
). Note: Any property with theSystem.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 hasNoRepeat=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 theHiddenInput
orTableColumn.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):
- Check if the property should be excluded from the table, and if so add the metadata and exit (any other properties are irrelevant)
- 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). - If
IncludeTotal
is true, check that the property is a numeric type (Note:IsNumeric()
is an extension method in theSandtrap
assembly that usesSystem.TypeCode
to check if itsbyte
,decimal
,double
etc.) - Finally, if
NoRepeat
is true, check thatIncludeTotal
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:
- Get the
ModelMetadata
from the expression and check that the model is a collection (implementsSystem.Collections.IEnumerable
). - Get the
ModelMetadata
of type in the collection and retrieve the table level properties used to determine what (if any) additional columns are rendered. - 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). - Call the
TableHeader()
to generate thethead
and itstr
element, which in turn calls theTableHeaderRow()
method to create eachth
element. TheTableHeaderRow()
method also creates aTableColumn
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). - Call the
ReadonlyTableBody()
method to generate thetbody
element, which loops each object in the collection to get itsModelMetadata
, create atr
element and call the recursiveReadonlyTableBodyRow()
method to create eachtd
element. The associatedTableColumn
is checked to determine how to display the property (e.g. as formatted text, hyperlink etc.) and ifTableColumn.IncludeTotal
is true, running totals are updated. - Finally, if any property has the
TableColumn.IncludeTotal
attribute property, call theTableFooter()
method to generate thetfoot
and itstr
element, which in turn calls the recursiveTableFooterRow()
method to create eachtd
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:
- Build the project (the download does not include the packages, obj and bin folders but these will be restored on first build)
- Add a reference to both Sandtrap.dll and Sandtrap.Web.dll in your project
- 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.