Reputation: 1985
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?
Upvotes: 2
Views: 1233
Reputation: 7724
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.
Upvotes: 1
Reputation: 2875
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);
}
}
}
Upvotes: 4
Reputation: 13535
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);
}
}
}
}
Upvotes: 2
Reputation: 69362
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();
Upvotes: 2