2

I have an ObservableCollection which I want to sort, not in place but I want to create a new sorted copy.

There's lots of examples on how to sort lists using nifty lambda expressions or using LINQ, but I can't hardcode the fields I want to sort by into code.

I have an array of NSSortDescription which work kinda like SortDescription. There's a string with the name of the property but the direction is specified by a bool (true = ascending). The first value in the array should be the primary sorting field, when the values in that field match, the second sort descriptor should be used, etc.

Example:

Artist: Bob Marley, Title: No Woman No Cry
Artist: Bob Marley, Title: Could You Be Loved
Artist: Infected Mushroom, Title: Converting Vegetarians
Artist: Bob Marley, Title: One Love
Artist: Chemical Brothers, Title: Do It Again

Sort descriptor: Artist descending, Title ascending.

Result:

Artist: Infected Mushroom, Title: Converting Vegetarians
Artist: Chemical Brothers, Title: Do It Again
Artist: Bob Marley, Title: Could You Be Loved
Artist: Bob Marley, Title: No Woman No Cry
Artist: Bob Marley, Title: One Love

Any suggestions on how to accomplish this?

Christoffer Reijer
  • 1,925
  • 2
  • 21
  • 40

4 Answers4

4

UPDATE: Change Sort to OrderBy as Sort is unstable sort algorithm

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.ComponentModel;

namespace PNS
{
    public class SortableList<T> : List<T>
    {
        private string _propertyName;
        private bool _ascending;

        public void Sort(string propertyName, bool ascending)
        {
            //Flip the properties if the parameters are the same
            if (_propertyName == propertyName && _ascending == ascending)
            {
                _ascending = !ascending;
            }
            //Else, new properties are set with the new values
            else
            {
                _propertyName = propertyName;
                _ascending = ascending;
            }

            PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(typeof(T));
            PropertyDescriptor propertyDesc = properties.Find(propertyName, true);

            // Apply and set the sort, if items to sort
            PropertyComparer<T> pc = new PropertyComparer<T>(propertyDesc, (_ascending) ? ListSortDirection.Ascending : ListSortDirection.Descending);
            //this.Sort(pc); UNSTABLE SORT ALGORITHM
            this.OrderBy(t=>t, pc);
        }
    }

    public class PropertyComparer<T> : System.Collections.Generic.IComparer<T>
    {

        // The following code contains code implemented by Rockford Lhotka:
        // http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnadvnet/html/vbnet01272004.asp

        private PropertyDescriptor _property;
        private ListSortDirection _direction;

        public PropertyComparer(PropertyDescriptor property, ListSortDirection direction)
        {
            _property = property;
            _direction = direction;
        }

        public int Compare(T xWord, T yWord)
        {
            // Get property values
            object xValue = GetPropertyValue(xWord, _property.Name);
            object yValue = GetPropertyValue(yWord, _property.Name);

            // Determine sort order
            if (_direction == ListSortDirection.Ascending)
            {
                return CompareAscending(xValue, yValue);
            }
            else
            {
                return CompareDescending(xValue, yValue);
            }
        }

        public bool Equals(T xWord, T yWord)
        {
            return xWord.Equals(yWord);
        }

        public int GetHashCode(T obj)
        {
            return obj.GetHashCode();
        }

        // Compare two property values of any type
        private int CompareAscending(object xValue, object yValue)
        {
            int result;

            if (xValue == null && yValue != null) return -1;
            if (yValue == null && xValue != null) return 1;
            if (xValue == null && yValue == null) return 0;
            // If values implement IComparer
            if (xValue is IComparable)
            {
                result = ((IComparable)xValue).CompareTo(yValue);
            }
            // If values don't implement IComparer but are equivalent
            else if (xValue.Equals(yValue))
            {
                result = 0;
            }
            // Values don't implement IComparer and are not equivalent, so compare as string values
            else result = xValue.ToString().CompareTo(yValue.ToString());

            // Return result
            return result;
        }

        private int CompareDescending(object xValue, object yValue)
        {
            // Return result adjusted for ascending or descending sort order ie
            // multiplied by 1 for ascending or -1 for descending
            return CompareAscending(xValue, yValue) * -1;
        }

        private object GetPropertyValue(T value, string property)
        {
            // Get property
            PropertyInfo propertyInfo = value.GetType().GetProperty(property);

            // Return value
            return propertyInfo.GetValue(value, null);
        }
    }
}
Paul Sullivan
  • 2,865
  • 2
  • 19
  • 25
  • This should give you a good start to using reflection and generics to accomplish this task. It is by no means the only way but I have used this extensively and never had a problem with it – Paul Sullivan Jul 28 '13 at 20:26
  • Should also note that this probably will not give you the full desired behaviour but it can with a little tweaking. It is more as a 'starting point'. – Paul Sullivan Jul 28 '13 at 20:29
  • I have an issue: it seems the sorting is not stable (previous sorting is not maintained when two values are equal). If I first sort on Title and then on Artist, I don't get all songs with the same artist sorted by title. The order of songs with the same artist does depend on the sorting of the second field but not in a "correct" way (different order if I first sort on Title:asc then Artist, then when I sort on Title:desc then Artist, or Length:asc then Artist, etc.) – Christoffer Reijer Jul 30 '13 at 16:22
  • The way I use your code is to put all my songs into a SortableList and then call Sort() for each field I want to sort on. `list.Sort("Title", false); list.Sort("Artist", true);` – Christoffer Reijer Jul 30 '13 at 16:29
  • @ChristofferBrodd-Reijer Can you update your question with example of what you want (preferably with table of data as it is expected to appear after second sort and I will sort out the issue. Regards :) – Paul Sullivan Jul 30 '13 at 20:37
  • @ChristofferBrodd-Reijer happy to fix it by the way just example (or description of expected output and Ill mod the code for you (also found odd behaviour which I may have built in for previous project and forgot the reasoning) – Paul Sullivan Jul 30 '13 at 21:02
  • Think I know where you are getting the 'instability' from by the way ;) – Paul Sullivan Jul 30 '13 at 21:08
  • .Sort is an unstable algorithm (does not preserve sort order on equal items) and I need to use OrderBy (which will take a bit of time to rework though am working on it now). Suggest unaccepting this answer so maybe you get other posters @ChristofferBrodd-Reijer – Paul Sullivan Jul 30 '13 at 21:36
  • Almost there. OrderBy doesn't do in-place sort. I could go the route of making the Sort() method return a copy and have the caller replace the instance. But it would be nicer if the Sort() method itself could update the list after the call to OrderBy. But I can't just do `this = this.OrderBy(...)`. Is there any other way to have a the list instance replace itself? – Christoffer Reijer Jul 31 '13 at 07:32
2

You could dynamically create the OrderBy predicate based on string properties.

Func<MyType, object> firstSortFunc = null;
Func<MyType, object> secondSortFunc = null;

//these strings would be obtained from your NSSortDescription array
string firstProp = "firstPropertyToSortBy";
string secondProp = "secondPropertyToSortBy";
bool isAscending = true;

//create the predicate once you have the details
//GetProperty gets an object's property based on the string
firstSortFunc = x => x.GetType().GetProperty(firstProp).GetValue(x);
secondSortFunc = x => x.GetType().GetProperty(secondProp).GetValue(x);

List<MyType> ordered = new List<MyType>();

if(isAscending)
   ordered = unordered.OrderBy(firstSortFunc).ThenBy(secondSortFunc).ToList();
else
   ordered = unordered.OrderByDescending(firstSortFunc).ThenBy(secondSortFunc).ToList();
keyboardP
  • 68,824
  • 13
  • 156
  • 205
  • This has the downside of hardcoding in the number of properties so sort on (using OrderBy().ThenBy()). I don't know the number or properties as they change during runtime. – Christoffer Reijer Jul 30 '13 at 20:53
  • @ChristofferBrodd-Reijer - Ah fair enough. I thought it was just the two. If there can be a chain of OrderBy then this may be a bit convoluted for that. – keyboardP Jul 30 '13 at 21:01
2

You could ceate a class named e.g. DynamicProperty which does retrieve the requested value. I do assume that the returned values do implement IComparable which should not be a too harsh limitation since you do want to compare the values anyway.

using System;
using System.Linq;
using System.Reflection;

namespace DynamicSort
{
    class DynamicProperty<T>
    {
        PropertyInfo SortableProperty;

        public DynamicProperty(string propName)
        {
            SortableProperty = typeof(T).GetProperty(propName);
        }

        public IComparable GetPropertyValue(T obj)
        {
            return (IComparable)SortableProperty.GetValue(obj);
        }
    }

    class Program
    {
        class SomeData
        {
            public int X { get; set; }
            public string Name { get; set; }
        }

        static void Main(string[] args)
        {
            SomeData[] data = new SomeData[]
            {
                new SomeData { Name = "ZZZZ", X = -1 },
                new SomeData { Name = "AAAA", X = 5 },
                new SomeData { Name = "BBBB", X = 5 },
                new SomeData { Name = "CCCC", X = 5 }
            };


            var prop1 = new DynamicProperty<SomeData>("X");
            var prop2 = new DynamicProperty<SomeData>("Name");

            var sorted = data.OrderBy(x=> prop1.GetPropertyValue(x))
                             .ThenByDescending( x => prop2.GetPropertyValue(x));

            foreach(var res in sorted)
            {
                Console.WriteLine("{0} X: {1}", res.Name, res.X);
            }

        }
    }
}
Alois Kraus
  • 13,229
  • 1
  • 38
  • 64
  • Thanks. The take away here is the GetProperty and GetValue which is really what I was looking for (still learning reflection in C#). But I still have the problem with the sorting not being stable as in another comment. It seems the OrderBy doesn't maintain order when values are equal, at least not for me. Not sure why... – Christoffer Reijer Jul 30 '13 at 20:56
1

I once wrote the following extension methods, which basically have the effect of either OrderBy or ThenBy, depending on whether the source is already ordered:

public static class Extensions {
    public static IOrderedEnumerable<TSource> OrderByPreserve<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer, bool descending) {
        var orderedSource = source as IOrderedEnumerable<TSource>;
        if (orderedSource != null) {
            return orderedSource.CreateOrderedEnumerable(keySelector, comparer, descending);
        }
        if (descending) {
            return source.OrderByDescending(keySelector, comparer);
        }
        return source.OrderBy(keySelector, comparer);
    }

    public static IOrderedEnumerable<TSource> OrderByPreserve<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) {
        return source.OrderByPreserve(keySelector, null, false);
    }

    public static IOrderedEnumerable<TSource> OrderByPreserve<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer) {
        return source.OrderByPreserve(keySelector, comparer, false);
    }

    public static IOrderedEnumerable<TSource> OrderByDescendingPreserve<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector) {
        return source.OrderByPreserve(keySelector, null, true);
    }

    public static IOrderedEnumerable<TSource> OrderByDescendingPreserve<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer) {
        return source.OrderByPreserve(keySelector, comparer, true);
    }
}

The interface is the same as OrderBy / OrderByDescending (alternatively you can pass descending as a boolean). You can write:

list.OrderByPreserve(x => x.A).OrderByPreserve(x => x.B)

which has the same effect as:

list.OrderBy(x => x.A).ThenBy(x => x.B)

Thus you could easily use keyboardP's solution with an arbitrary list of property names:

public static IEnumerable<TSource> OrderByProperties<TSource>(IEnumerable<TSource> source, IEnumerable<string> propertyNames) {
    IEnumerable<TSource> result = source;
    foreach (var propertyName in propertyNames) {
        var localPropertyName = propertyName;
        result = result.OrderByPreserve(x => x.GetType().GetProperty(localPropertyName).GetValue(x, null));
    }
    return result;
}

(the localPropertyName variable is used here because the iteration variable will have changed by the time the query is executed -- see this question for details)


A possible issue with this is that the reflection operations will be executed for each item. It may be better to build a LINQ expression for each property beforehand so they can be called efficiently (this code requires the System.Linq.Expressions namespace):

public static IEnumerable<TSource> OrderByProperties<TSource>(IEnumerable<TSource> source, IEnumerable<string> propertyNames) {
    IEnumerable<TSource> result = source;
    var sourceType = typeof(TSource);
    foreach (var propertyName in propertyNames) {
        var parameterExpression = Expression.Parameter(sourceType, "x");
        var propertyExpression = Expression.Property(parameterExpression, propertyName);
        var castExpression = Expression.Convert(propertyExpression, typeof(object));
        var lambdaExpression = Expression.Lambda<Func<TSource, object>>(castExpression, new[] { parameterExpression });
        var keySelector = lambdaExpression.Compile();
        result = result.OrderByPreserve(keySelector);
    }
    return result;
}

Essentially what those Expression lines are doing is building the expression x => (object)x.A (where "A" is the current property name), which is then used as the ordering key selector.

Example usage would be:

var propertyNames = new List<string>() { "Title", "Artist" };
var sortedList = OrderByProperties(list, propertyNames).ToList();

You just need to add the ascending / descending logic.

Community
  • 1
  • 1
nmclean
  • 7,564
  • 2
  • 28
  • 37