Refactoring Data Grids with C# Extension Methods

Extension methods were added in C# 3.0 and ever since they have been an indispensable part of how I write code. In this post, we'll look where extension methods are used, the details of what makes up an extension method, and how to create them. We'll wrap things up with some refactoring tips you can use with Telerik UI for ASP.NET MVC data grids.

Extension Methods in The Wild

One of the fundamental reasons that extension methods exist is to support Language Integrated Query (LINQ). If you're using LINQ methods such as .Select or .Where you might not know you're already utilizing extension methods. These LINQ methods extend the types IEnumerable<T> and IQueryable<T> giving us powerful functional programming operators that make short work of iterating collections and interacting with Entity Framework.

ASP.NET MVC is another popular place you may have encountered extension methods. Extension methods are used throughout MVC to provide what is affectionately been named syntactic sugar. This is when a method is created to make the API easier to use or understand. Extension methods are found in view code as HTML Helpers and also found in application code like middleware. Often times these methods are abstractions built upon more complex methods with many parameters. The abstractions allow developers to use an easy to read, sometimes chain-able API call, while behind the scenes complex operations are handled.

@Html.LabelFor(parameters)

app.UseSomeMiddleware()

Now that we know how to recognize extension methods, let's understand how they work. With a clear understanding of how they work we can begin creating our own.

The Anatomy of an Extension Method

Like most methods, extension methods have a return type, a method name, and can accept parameters. The extension method uses a this keyword in the parameter declaration.

The this keyword in an extension method carries a special importance. The this keyword and the type that follows identifies which type will be extended. For example, if we wanted to extend a String object, than the parameter declaration would be (this String s).

public static class StringExtensionMethods {
    public static String UppercaseFirst(this String s) {
        // Make the first letter of the string capital
        return string.IsNullOrEmpty(s) ? string.Empty :
        char.ToUpper(s[0]) + s.Substring(1);
     }
}

//usage
var message = "hello world";
message.UppercaseFirst(); // result => "Hello world"

The return type of an extension method can be of any type. Often extension methods will return a specific type that allows additional methods to be chained.

Refactoring Common Grid Elements

If you're building business applications, it's quite common to need data grids displayed within the app. Often times these grids have several common elements among them. Features like paging, sorting and filtering are likely standardized across many views. In the following example, we'll use extension methods to create default settings for a Telerik UI for ASP.NET grid. These default settings can be helpful in cutting down the time needed to replicate similar views in an application.

In this example, we have a grid that shares a common set of paging and filtering options. The properties are set with the methods HtmlAttributes, Pageable, Sortable, Scrollable and Filterable which are all part of a GridBuilder. By extending the GridBuilder object, we can create a collection of default grid settings. Abstracting these settings out into a separate method allows us to control the default behavior of multiple grids in one place. Not only is this a time saver, but extremely handy when new features are added to the grid control. Any grid using the default settings can be updated in one place to take advantage of a new feature without the need to find every instance of the grid in our application.

@(Html.Kendo().Grid<OrderViewModel>()
        .Name("grid")
        .HtmlAttributes(new { style = "height:550px;" })
        .Pageable()
        .Sortable()
        .Scrollable()
        .Filterable()
        .Columns(columns =>
        {
            columns.Bound(p => p.OrderID).Filterable(false);
            columns.Bound(p => p.Freight);
            columns.Bound(p => p.ShipName);
            columns.Bound(p => p.ShipCity);
            columns.Bound(p => p.OrderDate).Format("{0:MM/dd/yyyy}");
        })
        .DataSource(dataSource => dataSource
            .Ajax()
            .PageSize(20)
            .Read(read => read.Action("Orders_Read", "Grid"))
        )
)

a Telerik UI for ASP.NET MVC Grid

We'll factor out common grid properties that are used throughout the application using extension methods. Here we extend the GridBuilder object that is used to define the grid's features. Since the GridBuilder uses C# generics T we'll create an extension method DefaultSettings<T> where T will represent any class. Once the extension method is defined, we can copy in our properties that are commonly found in many of our application's grids.

public static class GridHelpers
{
    public static GridBuilder<T> DefaultSettings<T>(this GridBuilder<T> builder) where T : class =>
            builder
            .HtmlAttributes(new { style = "height:550px;" })
            .Pageable()
            .Sortable()
            .Scrollable()
            .Filterable();
}

Now we can replace all of those settings with our new DefaultSettings extension method.

@(Html.Kendo().Grid<OrderViewModel>()
        .Name("grid")
        .DefaultSettings()  
        .Columns(columns =>
        {
            columns.Bound(p => p.OrderID).Filterable(false);
            columns.Bound(p => p.Freight);
            columns.Bound(p => p.ShipName);
            columns.Bound(p => p.ShipCity);
            columns.Bound(p => p.OrderDate).Format("{0:MM/dd/yyyy}");
        })
        .DataSource(dataSource => dataSource
            .Ajax()
            .PageSize(20)
            .Read(read => read.Action("Orders_Read", "Grid"))
        )
)

Another common scenario is that an application may have multiple grids with the same core set of columns. Let's assume that our application has several instances of a Customer Orders grid, where many of the grids share the same columns OrderID, Freight, ShipName and ShipCity. We would like to have this common set of grid columns be the default, but still retain the ability to add new columns as needed.

Once again, we can use an extension method to accomplish this. Since the defaults this time will be specific to the OrderViewModel object, we'll ignore the generic <T> and instead us the type OrderViewModel. By using OrderViewModel in the method signature we archive two things, the grid settings will be aware of the object's properties allowing us to specify the column names, and the method will only be available when our grid is using the OrderViewModel type.

public static class GridHelpers
{
    public static GridBuilder<T> ...

    public static GridBuilder<OrderViewModel> DefaultCustomerOrdersColumns(this GridBuilder<OrderViewModel> builder) =>
        builder.Columns(columns =>
        {
            columns.Bound(p => p.OrderID).Filterable(false);
            columns.Bound(p => p.Freight);
            columns.Bound(p => p.ShipName);
            columns.Bound(p => p.ShipCity);
        });
}

Now we have defaults that can be used throughout the application giving us a single point of configuration.

@(Html.Kendo().Grid<GridRefactor.Models.OrderViewModel>()
        .Name("grid")
        .DefaultSettings()
        .DefaultCustomerOrdersColumns()
        .Columns(columns =>
        {
            columns.Bound(p => p.OrderDate).Format("{0:MM/dd/yyyy}").;
        })
        .DataSource(dataSource => dataSource
            .Ajax()
            .PageSize(20)
            .Read(read => read.Action("Orders_Read", "Grid"))
        )
)

Wrapping up

Extension methods are a powerful tool in C#. This is just one of many ways they can be used to refactor code in an application. Using extension methods to create common configurations for grids to reduce development time and create a single reference point to update many similar grids in one place.

Additional Resources

Comments