Kendo Grid MVC Wrapper Automatic Column Configuration

The Telerik Kendo Grid control is really powerful, especially when combined with the MVC wrappers. One of the things that make the MVC wrapper so useful is the ability to automatically (and easily) generate data-bound columns. It’s a single line of code:

.Columns(columns => columns.AutoGenerate(true))

The code behind AutoGenerate(true) understands some of the System.ComponentModel.DataAnnotations attributes. Specifically, it knows how to automatically configure the grid column for these attributes:

Atribute Description
Compare Compares two properties.
CreditCard Specifies that a data field value is a credit card number.
CustomValidation Specifies a custom validation method that is used to validate a property.
DataType Specifies the name of an additional type to associate with a data field.
Display Provides a general-purpose attribute that lets you specify localizable strings for types and members of entity partial classes.
DisplayColumn Specifies the column that is displayed in the referred table as a foreign-key column.
DisplayFormat Specifies how data fields are displayed and formatted by ASP.NET Dynamic Data.
Editable Indicates whether a data field is editable.
EmailAddress Validates an email address.
EnumDataType Enables a .NET Framework enumeration to be mapped to a data column.
FileExtensions Validates file name extensions.
FilterUIHint Represents an attribute that is used to specify the filtering behavior for a column.
MaxLength Specifies the maximum length of array or string data allowed in a property.
MinLength Specifies the minimum length of array or string data allowed in a property.
Phone Specifies that a data field value is a well-formed phone number using a regular expression for phone numbers.
Range Specifies the numeric range constraints for the value of a data field.
RegularExpression Specifies that a data field value in ASP.NET Dynamic Data must match the specified regular expression.
Required Specifies that a data field value is required.
ScaffoldColumn Specifies whether a class or data column uses scaffolding.
StringLength Specifies the minimum and maximum length of characters that are allowed in a data field.
UIHint Specifies the template or user control that Dynamic Data uses to display a data field.
Url Provides URL validation.

What’s nice about this support is that you can use these attributes to adorn your model properties and declaratively describe some of the metadata about the column.

The problem, though, is that you can’t also set the Kendo Grid specific properties, such as column width, if the column is locked or not, and if it should be included in the column menu (to name just a few).

Fortunately, we can hook into the AdditionalValues dictionary of the Metadata property on a data-bound column (which is of type Kendo.Mvc.UI.GridBoundColumn<TModel, TValue>). This property holds an instance of a System.Web.Mvc.ModelMetadata (more specifically an instance of a CachedModelMetadata<TPrototypeCache>) object, which has all of the metadata related attributes defined on the properties of the model and is the key to our solution of providing automatic column configuration based on data annotation attributes. To do this, we simply define our own attribute and implement the IMetadataAware interface. The runtime will handle everything for us and our new attribute will be added to the AdditionalValues dictionary.

I created a GridColumnAttribute to allow all of the additional Kendo specific properties to be set.

using System;
using System.Web.Mvc;

namespace Cadru.Web.KendoExtensions
{
    public class GridColumnAttribute : Attribute, IMetadataAware
    {
        public const string Key = "GridColumnMetadata";

        public bool Hidden { get; set; }

        public bool IncludeInMenu { get; set; }

        public bool Lockable { get; set; }

        public bool Locked { get; set; }

        public int MinScreenWidth { get; set; }

        public string Width { get; set; }

        public void OnMetadataCreated(ModelMetadata metadata)
        {
            metadata.AdditionalValues[GridColumnAttribute.Key] = this;
        }
    }
}

Now, we can decorate our model with the new attribute:

public class EmployeeModel
{
    [Editable(false)]
    [GridColumn(Width = "100px", Locked = true)]
    public string EmployeeID { get; set; }

    [GridColumn(Width = "200px", Locked = true)]
    public string EmployeeName { get; set; }

    [GridColumn(Width = "100px")]
    public string EmployeeFirstName { get; set; }

    [GridColumn(Width = "100px")]
    public string EmployeeLastName { get; set; }
}

However, that’s only part of the solution. We still need to tell the Kendo Grid that it needs to do something with this new attribute. To do this we can use the overload for the AutoGenerate method which takes an Action:

.Columns(columns => columns.AutoGenerate(c => GridColumnHelpers.ConfigureColumn(c)))

The GridColumnHelpers.ConfigureColumn<T> method looks like

using Kendo.Mvc.UI;
using System;
using System.Web.Mvc;

namespace Cadru.Web.KendoExtensions
{
    public static class GridColumnHelpers
    {
        public static void ConfigureColumn<T>(GridColumnBase<T> column) where T : class
        {
            CachedDataAnnotationsModelMetadata metadata = ((dynamic)column).Metadata;
            object attributeValue = null;
            if (metadata.AdditionalValues.TryGetValue(GridColumnAttribute.Key, out attributeValue))
            {
                var attribute = (GridColumnAttribute)attributeValue;
                column.Width = attribute.Width;
                column.Locked = attribute.Locked;
                column.Hidden = attribute.Hidden;
                column.IncludeInMenu = attribute.IncludeInMenu;
                column.Lockable = attribute.Lockable;
                column.MinScreenWidth = attribute.MinScreenWidth;
            }
        }
    }
}

This takes advantage of the fact that the method is being called in the context of automatically generating data-bound columns, so it’s able to take the column and cast it to a dynamic object in order to reference the Metadata property. We have to do this because the IGridBoundColumn doesn’t expose the Metadata property and we can’t cast it directly to a GridBoundColumn<TModel, TValue> because (among other reasons) we don’t know the type for TValue. That leaves us with casting it to dynamic and letting the dynamic dispatcher figure out how to give us back the requested property. From there, we look to see if the AdditionalValues dictionary contains our attribute, and if it does we then set the column properties to their respective values as specified by the attribute. We now have the ability to automatically configure the additional column properties using metadata specified in our model while still automatically generating the data-bound columns.