Wednesday, January 23, 2013

Using Extensions to Make Code Simpler

Extension Methods were a new feature as of .NET 3.0. They are used extensively in Linq to SQL and have become one of my favorite tools. The premise is quite simple, as the C# Programming Guide explains:

Extension methods enable you to "add" methods to existing types without creating a new derived type, recompiling, or otherwise modifying the original type. Extension methods are a special kind of static method, but they are called as if they were instance methods on the extended type.

The main reason to use extensions is to make code more readable and to reuse logic, especially when dealing with classes that you cannot change. For example, consider the following snippet for the get and set accessors of a ComboBox in a Windows Form application:


public List<StateDataView> StateList
{
    get
    {
        List<StateDataView> states = (List<StateDataView>)this.statesComboBox.DataSource;
        states.RemoveAt(0);
        return states;
    }
    set
    {
        List<StateDataView> states = new List<StateDataView>(value);
        states.Insert(0, new StateDataView() { Code = "" });
        this.statesComboBox.DataSource = states;
    }
}
public StateDataView SelectedState
{
    get
    {
        if (this.statesComboBox.SelectedIndex == 0) return null;
        return (StateDataView)this.statesComboBox.SelectedItem;
    }
}

In the Set accessor, I did a little trick to make sure there was a blank entry in the list. This would be useful when created a lookup form that has a variety of options that may not always be used. When returning the selected object in the Get accessor, I have to handle the case when the blank entry is selected, in which case I want to return a null. This code isn't too bad, but imagine how ugly it would look if I had a lot of ComboBoxes. Wouldn't it be great if the could could be written like this?

public List<StateDataView> StateList
{
    get { return this.statesComboBox.GetDataSourceWithoutBlank<StateDataView>(); }
    set { this.statesComboBox.SetDataSourceWithBlank<StateDataView>(value); }
}
public StateDataView SelectedState
{
    get { return this.statesComboBox.GetSelectedItemUnlessFirst<StateDataView>(); }
}

Notice that there are three methods I am calling on the ComboBox control that do not exist in the framework. These are my own extension methods. It should be fairly clear by their names what they do.

If I only had one or two cases like this, I wouldn't bother with the extension method. However, I find this particular scenario to be quite common. Let's look at the extension methods that make it happen. They exist in a class I created specifically to extend the Windows Form ComboBox:

public static class ComboBoxExtensions
{
    public static void SetDataSourceWithBlank<T>(this ComboBox combo, List<T> list)
            where T : new()
    {
        List<T> newlist = new List<T>(list);
        newlist.Insert(0, new T());
        combo.DataSource = newlist;
    }
    public static List<T> GetDataSourceWithoutBlank<T>(this ComboBox combo)
    {
        List<T> list = (List<T>)combo.DataSource;
        list.RemoveAt(0);
        return list;
    }
    public static T GetSelectedItemUnlessFirst<T>(this ComboBox combo)
        where T : class
    {
        if (combo.SelectedIndex == 0) return null;
        return (T)combo.SelectedItem;
    }
}

There are a few important characteristics of extension methods:
  • They must exist in a static class.
  • They must themselves be static.
  • The first parameter must be prefixed with the keywork this and must be the type being extended.
I made liberal use of generics in this case, which is fairly ubiquitous in C# development these days. Because of that, I had to clarify a couple things for my methods:
  • Since I needed to create a new instance of an object in the SetDataSourceWithBlank method, I had to use the new() type constraint.
  • Since I needed to return a null in the GetSelectedItemUnlessFirst method, I had to use the class constraint.
I find using Extension Methods especially helpful in the MVP pattern because the view is mostly a set of properties that expose controls. The logic involved in setting the control properties and determining certain details, such as whether or not something is selected, is a concern of the view, so it is appropraite for the logic to be maintained there. By using extension methods, you can build a useful library that will not only make your code easier to read, it will make it easier to maintain and easier to write.

Some Useful Extensions

For the remainder of this article, I thought I'd share a few extensions that I've created that you might find useful.

GridView.SelectWhere<T>

Description: This method allows you to select rows in a DataGridView based on the properties of the bound object. I typically use this to select a specific row, but it could also be used to select a set or rows.

Usage: grid.SelectWhere<type>(x => x.property == value);

public static void SelectWhere<T>(this DataGridView grid,
     Expression<Func<T,bool>> testexpression)
{
    grid.ClearSelection();
    Func<T,bool> test = testexpression.Compile();
    foreach (DataGridViewRow row in grid.Rows)
    {
        T item = (T)row.DataBoundItem;
        if (test(item))
        {
            row.Selected = true;
        }
    }
}

IEnumerable<T>.Distinct

Description: Gets a distinct list of objects from an IEnumerable based on a filter expression. In this case, the key must be a nullable integer, but Icould easily create different overloads for different types.

Usage: list.Distinct(x => x.filterproperty);

public static IEnumerable<T> Distinct<T>(this IEnumerable<T> source,
     Expression<Func<T,int?>> key)
{
    List<T> items = new List<T>();
    Func<T,int?> keyprop = key.Compile();
    foreach (T sourceitem in source.ToList())
    {
        int? id = keyprop(sourceitem);
        if (items.Find(i => keyprop(i) == id) == null)
            items.Add(sourceitem);
    }
    return items.AsEnumerable();
}

Nullable<T>.Coalesce

Description: Just like the T-SQL equivalent, this method gives you an alternative value to return when a nullable type is null, e.g. if you want a nullable int to return a -1 when it's null.

Usage: value.Coalesce(x);
 
public static T Coalesce<T>(this Nullable<T> value, T nullvalue)
{
    if (value.HasValue) return value.Value;
    return nullvalue;
}

No comments:

Post a Comment