Removing Strings in INotifyPropertyChanged and OData Expands

Removing Strings in INotifyPropertyChanged and OData Expands

Ok, so this is a rather random post but I wanted to jot down a couple of scenarios where I often see string literals in code.

Scenario 1: String Literals in INotifyPropertyChanged Implementation

The first is the default implementation of INotifyPropertyChanged. If you’ve done data binding (for example with WPF or Silverlight) you’ll be familiar with this interface – when a source property value changes if you raise the PropertyChanged event the UI gets an opportunity to refresh. The standard implementation looks a bit like this:

public class MainPageViewModel: INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private string _Title;
    public string Title
    {
        get { return _Title; }
        set
        {
            if (Title == value) return;
            _Title = value;
            RaisePropertyChanged("Title");
        }
    }
    private void RaisePropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

I always cringe when I see this because the name of the property, ie “Title” is passed into the RaisePropertyChanged method. This wouldn’t be so bad except this code block gets repeated over and over and over again – for pretty much any property you end up data binding to. Having string literals littered through your code is BAD as it makes your code incredibly brittle and susceptible to errors (for example you change the property name, without changing the string literal). A while ago I adopted the following version of the RaisePropertyChanged method which accepts an Expression and extracts the property name:

public void RaisePropertyChanged<TValue>(Expression<Func<TValue>> propertySelector)
{
    var memberExpression = propertySelector.Body as MemberExpression;
    if (memberExpression != null)
    {
        RaisePropertyChanged(memberExpression.Member.Name);
    }
}

The only change you need to make in your properties is to use a lambda expression instead of the string literal, for example:

private string _Title;
public string Title
{
    get { return _Title; }
    set
    {
        if (Title == value) return;
        _Title = value;
        RaisePropertyChanged(()=>Title);
    }
}

Scenario 2: String Literals in the Expands method for OData

Let’s set the scene – you’re connecting to an OData source using either the desktop or the new WP7 (Mango) OData client library. Your code might look something similar to the following – the important thing to note is that we’re writing a strongly typed LINQ statement to retrieve the list of customers.

NorthwindEntities entities = new NorthwindEntities(new Uri("http://services.odata.org/Northwind/Northwind.svc"));
private void LoadCustomers()
{
    var customerQuery = from c in entities.Customers
                                           select c;

    var customers = new DataServiceCollection<Customer>();
    customers.LoadCompleted += LoadCustomersCompleted;
    customers.LoadAsync(customerQuery);
}

private void LoadCustomersCompleted(object sender, LoadCompletedEventArgs e)
{
    var customers = sender as DataServiceCollection<Customer>;
    foreach (var customer in customers)
    {
        var name = customer.CompanyName;
    }
}

By default the LINQ expression will only retrieve the Customer objects themselves. If you wanted to not only retrieve the Customer but also their corresponding Orders then you’d have to change the LINQ to use the Expand method:

var customerQuery = from c in entities.Customers.Expand("Orders")
                                       select c;

Now, if you wanted to be more adventurous you could extend this to include the OrderDetails (for each Order) and subsequent Product (1 for each OrderDetails record) and Category (each Product belongs to a category).

var customerQuery = from c in entities.Customers.Expand("Orders/OrderDetails/Product/Category")
                                       select c;

The Order is also connected to the Shipper and Employee tables, so you might want to also bring back the relevant data from those tables too:

var customerQuery = from c in entities.Customers
                                                                     .Expand("Orders/OrderDetails/Product/Category")
                                                                     .Expand("Orders/Employee")
                                                                     .Expand("Orders/Shipper")
                                       select c;

The result is that you have a number of string literals defining which relationships you want to traverse and bring back. Note that you only need to do this if you want to eager load this information. If you want your application to lazy load the related data you don’t require the Expand method.

The work around for this isn’t as easy as the RaisePropertyChanged method used to eliminate string literals for the INotifyPropertyChanged scenario. However, we essentially use the same technique – we replace the string literal with an expression that makes the necessary traverses. For example:

var customerQuery = from c in entities.Customers
                                    .Expand(c=>c.Orders[0].Order_Details[0].Product.Category)
                                    .Expand(c => c.Orders[0].Employee)
                                    .Expand(c => c.Orders[0].Shipper)
                    select c;

You’ll notice that in this case where we traverse from Orders to Order_Details we need to specify an array index. This can actually be any number as it is completely ignored – it’s just required so that we can reference the Order_Details property which exists on an individual Order object.

Ok, but how is this going to work? Well we’ve simply created another extension method for the DataServiceQuery class, also called Expand but accepts an Expression instead of a string literal. This method expands out the Expression and converts it to a string, which is passed into the original Expand method. I’m not going to step through the following code – it essentially traverses the Expression tree looking for MemberAccess nodes (ie property accessors) which it adds to the expand string. It also detect Call nodes (which typically corresponds to the array index accessor eg get_item( 0 )) which is skipped to move on to the next node in the tree via the Object property.

public static class ODataExtensions
{
    public static DataServiceQuery<TElement> Expand<TElement, TValue>(this DataServiceQuery<TElement> query, Expression<Func<TElement, TValue>> expansion)
    {
        var expand = new StringBuilder();
        var expression = expansion.Body as Expression;
        while (expression.NodeType != ExpressionType.Parameter)
        {
            if (expression.NodeType == ExpressionType.MemberAccess)
            {
                if (expand.Length > 0)
                {
                    expand.Insert(0, "/");
                }
                var mex = (expression as MemberExpression);
                expand.Insert(0, mex.Member.Name);
                expression = mex.Expression;
            }
            else if (expression.NodeType == ExpressionType.Call)
            {
                var method = (expression as System.Linq.Expressions.MethodCallExpression);
                if (method != null)
                {
                    expression = method.Object as MemberExpression;
                }
            }
        }
        return query.Expand(expand.ToString());
    }
}

And there you have it – you can now effectively remove string literals from the Expand method. Be warned though: using the Expand method can result in a large quantity of data being retrieved from the server in one hit. Alternatively if the server has paging enabled you will need to ensure that you traverse any Continuations throughout the object graph (more on that in a subsequent post).

Leave a Reply

Your email address will not be published. Required fields are marked *