views:

188

answers:

2

I'm attempting to perform dynamic sorting of data that I'm putting into grids into our MVC UI. Since MVC is abstracted from everything else via WCF, I've created a couple utility classes and extensions to help with this. The two most important things (slightly simplified) are as follows:

    public static IQueryable<TModel> ApplySortOptions<TModel, TProperty>(this IQueryable<TModel> collection, IEnumerable<ISortOption<TModel, TProperty>> sortOptions) where TModel : class
    {
        var sortedSortOptions = (from o in sortOptions
                                 orderby o.Priority ascending
                                 select o).ToList();

        var results = collection;

        foreach (var option in sortedSortOptions)
        {
            var currentOption = option;
            var propertyName = currentOption.Property.MemberWithoutInstance();
            var isAscending = currentOption.IsAscending;

            if (isAscending)
            {
                results = from r in results
                          orderby propertyName ascending 
                          select r;
            }
            else
            {
                results = from r in results
                          orderby propertyName descending 
                          select r;
            }
        }

        return results;
    }


public interface ISortOption<TModel, TProperty> where TModel : class
{
    Expression<Func<TModel, TProperty>> Property { get; set; }
    bool IsAscending { get; set; }
    int Priority { get; set; }
}

I've not given you the implementation for MemberWithoutInstance() but just trust me in that it returns the name of the property as a string. :-)

Following is an example of how I would consume this (using a non-interesting, basic implementation of ISortOption<TModel, TProperty>):

var query = from b in CurrentContext.Businesses
            select b;

var sortOptions = new List<ISortOption<Business, object>>
                      {
                          new SortOption<Business, object>
                              {
                                  Property = (x => x.Name),
                                  IsAscending = true,
                                  Priority = 0
                              }
                      };

var results = query.ApplySortOptions(sortOptions);

As I discovered with this question, the problem is specific to my orderby propertyName ascending and orderby propertyName descending lines (everything else works great as far as I can tell). How can I do this in a dynamic/generic way that works properly?

+2  A: 

You should really look at using Dynamic LINQ for this. In fact, you may opt to simply list the properties by name instead of using an expression, making it somewhat easier to construct.

public static IQueryable<T> ApplySortOptions<T, TModel, TProperty>(this IQueryable<T> collection, IEnumerable<ISortOption<TModel, TProperty>> sortOptions) where TModel : class  
{    
    var results = collection;  

    foreach (var option in sortOptions.OrderBy( o => o.Priority ))  
    {  
        var currentOption = option;  
        var propertyName = currentOption.Property.MemberWithoutInstance();  
        var isAscending = currentOption.IsAscending;  

         results = results.OrderBy( string.Format( "{0}{1}", propertyName, !isAscending ? " desc" : null ) );
    }  

    return results;  
}
tvanfosson
I forgot about the Dynamic LINQ libraries (um, that was quite the accidental pun!)
Jaxidian
While I didn't go with this solution, I ended up using the Dynamic LINQ stuff to help me with some serialization issues I had.
Jaxidian
A: 

While I think @tvanfosson's solution will function perfectly, I'm also looking into this possibility:

    /// <summary>
    /// This extension method is used to help us apply ISortOptions to an IQueryable.
    /// </summary>
    /// <param name="collection">This is the IQueryable you wish to apply the ISortOptions to.</param>
    /// <param name="sortOptions">These are the ISortOptions you wish to have applied. You must specify at least one ISortOption (otherwise, don't call this method).</param>
    /// <returns>This returns an IQueryable object.</returns>
    /// <remarks>This extension method should honor deferred execution on the IQueryable that is passed in.</remarks>
    public static IOrderedQueryable<TModel> ApplySortOptions<TModel, TProperty>(this IQueryable<TModel> collection, IEnumerable<ISortOption<TModel, TProperty>> sortOptions) where TModel : class
    {
        Debug.Assert(sortOptions != null, "ApplySortOptions cannot accept a null sortOptions input.");
        Debug.Assert(sortOptions.Count() > 0, "At least one sort order must be specified to ApplySortOptions' sortOptions input.");

        var firstSortOption = sortOptions.OrderBy(o => o.Priority).First();
        var propertyName = firstSortOption.Property.MemberWithoutInstance();
        var isAscending = firstSortOption.IsAscending;

        // Perform the first sort action
        var results = isAscending ? collection.OrderBy(propertyName) : collection.OrderByDescending(propertyName);

        // Loop through all of the rest ISortOptions
        foreach (var sortOption in sortOptions.OrderBy(o => o.Priority).Skip(1))
        {
            // Make a copy of this or our deferred execution will bite us later.
            var currentOption = sortOption;

            propertyName = currentOption.Property.MemberWithoutInstance();
            isAscending = currentOption.IsAscending;

            // Perform the additional orderings.
            results = isAscending ? results.ThenBy(propertyName) : results.ThenByDescending(propertyName);
        }

        return results;
    }

using the code from this question's answer:

    public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string property)
    {
        return ApplyOrder(source, property, "OrderBy");
    }

    public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> source, string property)
    {
        return ApplyOrder(source, property, "OrderByDescending");
    }

    public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source, string property)
    {
        return ApplyOrder(source, property, "ThenBy");
    }

    public static IOrderedQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> source, string property)
    {
        return ApplyOrder(source, property, "ThenByDescending");
    }

    private static IOrderedQueryable<T> ApplyOrder<T>(IQueryable<T> source, string property, string methodName)
    {
        var props = property.Split('.');
        var type = typeof (T);
        var arg = Expression.Parameter(type, "x");
        Expression expr = arg;
        foreach (var prop in props)
        {
            // use reflection (not ComponentModel) to mirror LINQ
            var pi = type.GetProperty(prop);
            expr = Expression.Property(expr, pi);
            type = pi.PropertyType;
        }
        var delegateType = typeof (Func<,>).MakeGenericType(typeof (T), type);
        var lambda = Expression.Lambda(delegateType, expr, arg);

        var result = typeof (Queryable).GetMethods().Single(
            method => method.Name == methodName
                      && method.IsGenericMethodDefinition
                      && method.GetGenericArguments().Length == 2
                      && method.GetParameters().Length == 2)
            .MakeGenericMethod(typeof (T), type)
            .Invoke(null, new object[] {source, lambda});
        return (IOrderedQueryable<T>) result;
    }
Jaxidian
I actually used this reflection-based code to do this. Although funny - I ended up using Dynamic LINQ for "Expression Serialization" so I can get these SortOrders through WCF.
Jaxidian

related questions