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.
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.
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.
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"))
)
)
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"))
)
)
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
Ed