Updated Multi-Property Sorting with LINQ

Posted August 1, 2008 by Rich
Categories: .NET, C#, LINQ

In the quest to bring you, the consumer, an ever increasingly useful product, I present the new and improved ListSorter class!

During a peer review, a fellow developer asked if he could use this code to continue sorting on a previously sorted list. The answer was of course, “Erm…” So I took a look at what I had and did some refactoring. The new code (shown below) is now implemented as extension methods to the IEnumerable and IOrderedEnumerable interfaces. The Sort method will perform a primary ordering on an IEnumerable, whereas ContinueSort will perform subsequent ordering on an already sorted collection. Sort will call ContinueSort if there is more than one sort option passed in.

I’m still looking for a good way to supply a collection of IComparer objects to the sorting methods such that these comparers can be applied either to specific properties or to all properties of a given type. I may be able to pass the comparers along as simple objects and cast them to the appropriate type once the property is found, or the type comes up, but that just seems extra messy. I’m interested to hear any ideas on this, and will post an update if a solution is found.

In the meantime, enjoy!

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

namespace ESC.SV.UI.Win.RATA.Helpers
{
	public static class ListSorter
	{
		public enum SortingOrder
		{
			Ascending,
			Descending,
		};

		public static IOrderedEnumerable Sort(this IEnumerable toSort, IDictionary sortOptions)
		{
			IOrderedEnumerable orderedList = null;

			if (sortOptions != null && sortOptions.Count > 0)
			{
				//Create a copy of our dictionary to preserve the original.  We'll be removing items, so we don't want to mess up the reference.
				Dictionary sortCopy = sortOptions.Copy();

				//Get the primary sort option and remove it from the list
				//We will later check if there are additional elements in the list for further sorting
				KeyValuePair primarySort = sortCopy.ElementAt(0);
				sortCopy.Remove(primarySort.Key);

				orderedList = primarySort.Value == SortingOrder.Ascending ? toSort.ApplyOrder(primarySort.Key, "OrderBy") : toSort.ApplyOrder(primarySort.Key, "OrderByDescending");

				//Continue the sort if there are more options
				if (sortCopy.Count > 0)
				{
					orderedList = orderedList.ContinueSort(sortCopy);
				}
			}

			return orderedList;
		}

		public static IOrderedEnumerable ContinueSort(this IOrderedEnumerable orderedList, IDictionary sortOptions)
		{
			if (sortOptions != null)
			{
				foreach (KeyValuePair entry in sortOptions)
				{
					orderedList = entry.Value == SortingOrder.Ascending ? orderedList.ApplyOrder(entry.Key, "ThenBy") : orderedList.ApplyOrder(entry.Key, "ThenByDescending");
				}
			}

			return orderedList;
		}

		private static IOrderedEnumerable ApplyOrder(this IEnumerable source, string property, string methodName)
		{
			ParameterExpression param = Expression.Parameter(typeof(T), "x");
			Expression expr = param;

			//Create the right-hand part of the lambda expression based on the property provided
			foreach (string prop in property.Split('.'))
			{
				expr = Expression.PropertyOrField(expr, prop);
			}

			Type delegateType = typeof(Func).MakeGenericType(typeof(T), expr.Type);
			LambdaExpression lambda = Expression.Lambda(delegateType, expr, param);

			//Fetch the desired method
			MethodInfo mi = typeof(Enumerable).GetMethods().Single(method =>
				method.Name == methodName
				&& method.IsGenericMethodDefinition
				&& method.GetGenericArguments().Length == 2	// 
				&& method.GetParameters().Length == 2)		// source, keySelector
				.MakeGenericMethod(typeof(T), expr.Type);	// Substitute the appropriate types

			return (IOrderedEnumerable)mi.Invoke(null, new object[] { source, lambda.Compile() });
		}

		private static Dictionary Copy(this IDictionary toCopy)
		{
			Dictionary dictionaryCopy = new Dictionary();

			foreach (KeyValuePair item in toCopy)
			{
				dictionaryCopy.Add(item.Key, item.Value);
			}

			return dictionaryCopy;
		}
	}
}

Multi-Select Calendar with a Date Range

Posted July 25, 2008 by barnum004
Categories: .NET, C#

For a recent assignment I found myself desiring an Asp.Net calendar control with the ability to select multiple non-contiguous dates over a specific range of dates. Specifically, I needed only weekdays within the next two weeks to be available dates. I had a difficult time finding a whole lot of information on the subject so I’ve decided to throw this out there for all of you.

So without further ado, let’s jump into the code. We’ll start by defining our calendar.

<asp:Calendar ID="calExample" runat="server" 
CssClass="Calendar"
NextPrevStyle-CssClass="NextPrevStyle" 
DayStyle-CssClass="DayStyle"
TitleStyle-CssClass="Title" 
DayHeaderStyle-CssClass="DayHeader"
OtherMonthDayStyle-CssClass="OtherMonthDay" 
SelectedDayStyle-CssClass="DayStyle"
OnSelectionChanged="calExample_SelectionChanged" 
OnDayRender="calExample_DayRender" ></asp:Calendar>

The styles aren’t terribly important, except for:

SelectedDayStyle-CssClass="DayStyle"

This will be important later on when we want to use our own date selection rather than the default.

Next we’ll set up up the dates that we don’t want to selectable by using the OnDayRender event.

 DateTime MinDate = DateTime.Today;
 DateTime MaxDate = DateTime.Today.AddDays(14);
if ((e.Day.Date < MinDate) || (e.Day.Date > MaxDate) || (e.Day.IsWeekend))
{
    e.Day.IsSelectable = false;
    e.Cell.CssClass = "OtherMonthDay";
}

We set the IsSelectable property to false to prevent selection and we use the OtherMonthDay style to maintain consistency with other unselectable dates.

So far everything has been pretty painless right? Good, because this where things start to get a little wacky. Not too wacky mind you but we have to start being clever. The goal here is to be able to select a date by clicking on it, and deselect a date that is selected by clicking on it again. The first order of business is to make use of the OnSelectionChanged event Like so:

 protected void calExample_SelectionChanged(object sender, EventArgs e)
{
            int index = -1;
            List<DateTime> SelectedDates = ViewState["SelectedDates"] as List<DateTime>;

            if (SelectedDates == null)
            {
                SelectedDates = new List<DateTime>();
            }

            index = SelectedDates.IndexOf(calExample.SelectedDate);
            //select any unselected dates            
            if (index < 0)
            {
                SelectedDates.Add(calExample.SelectedDate);
            }
            //Remove any already selected dates
            else
            {
                SelectedDates.RemoveAt(index);
            }

            ViewState.Add("SelectedDates", SelectedDates);
}

As you see we are just retrieving our list of dates from the viewstate and adding or removing the selected dates as necessary. Finally we need to flesh out our OnDayRender event to display our dates properly.

protected void calExample_DayRender(object sender, DayRenderEventArgs e)
{
            List<DateTime> SelectedDates = ViewState["SelectedDates"] as List<DateTime>;

            if (SelectedDates == null)
            {
                SelectedDates = new List<DateTime>();
            }

            DateTime MinDate = DateTime.Today;
            DateTime MaxDate = DateTime.Today.AddDays(14);
            if ((e.Day.Date < MinDate) || (e.Day.Date > MaxDate) || (e.Day.IsWeekend))
            {
                e.Day.IsSelectable = false;
                e.Cell.CssClass = "OtherMonthDay";
            }

            if (SelectedDates.Contains(e.Day.Date))
            {
                e.Cell.CssClass = "SelectedDay";
            }

            calExample.SelectedDate = DateTime.MinValue;
}

This should be fairly self explanatory too. The only thing I’d like to highlight here is:

     calExample.SelectedDate = DateTime.MinValue;

This is important, because if we didn’t programatically reset the selected date, then susbsequent clicks on the most recently selected date would not fire the OnSelectionChanged event.

So there you have it, a multi-select calendar with a date range. I recognize that this is actually a relatively naive implementation. I am currently working on a refactoring this idea into a custom control or possibly an AJAX based server control that I’ll gladly discuss in a future post. Let me know in the comments if you have any preferences on which of those I post about. Also, I’ve got a sample project available if anyone is interested.

Generic multi-property sorting using LINQ and Reflection

Posted July 21, 2008 by Rich
Categories: .NET, C#, LINQ

Recently I was tasked with the creation of a summary grid for a list of data objects.  All of the objects would be retrieved when the form containing the list was loaded, and there would be a default multi-column sort applied to the list.  After the initial load, more items could be added to the list, and the current sort would need to be applied.

There was some discussion amongst the team about how and where to sort.  Do we perform our ordering in the SQL call?  Quick, but only works on that initial load.  Do we simply rely on the Infragistics grids we’re using?  They do multi-column sorting but as soon as you add multiple bands to the grid (for master/detail views) the sort no longer works, though why I couldn’t even begin to take a guess at.  That’s when I remembered the OrderBy and ThenBy LINQ extension methods provided in the 3.5 Framework.

OrderBy, ThenBy and their reverse order cousins, OrderByDescending and ThenByDescending all take lambda expressions to define their sort order.  Usually a property is chosen as the sort candidate and the line would look something like this:

dateList.OrderBy(d => d.Year);

However, we could be sorting by any property in our object and multiple properties to boot.  Creating a big list of OrderBy and ThenBy statements for each property would simply be madness.  A better method would be to have a string key for the column which matches the property name and use Reflection to identify the property.  A dictionary of keys and their matching sort direction could be iterated over to provide multi-property sorting by storing the IOrderedEnumerable<T> object returned from the initial OrderBy/Descending call to gain access to ThenBy/Descending.  After encapsulating the whole thing in its own class, I ended up with this:

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

namespace ESC.SV.UI.Win.RATA.Helpers
{
	public static class ListSorter
	{
		public enum SortingOrder
		{
			Ascending,
			Descending,
		};

		public static IOrderedEnumerable<T> Sort<T>(IEnumerable<T> toSort, Dictionary<string, SortingOrder> sortOptions)
		{
			IOrderedEnumerable<T> orderedList = null;

			foreach (KeyValuePair<string, SortingOrder> entry in sortOptions)
			{
				if (orderedList != null)
				{
					if (entry.Value == SortingOrder.Ascending)
					{
						orderedList = orderedList.ApplyOrder<T>(entry.Key, "ThenBy");
					}
					else
					{
						orderedList = orderedList.ApplyOrder<T>(entry.Key, "ThenByDescending");
					}
				}
				else
				{
					if (entry.Value == SortingOrder.Ascending)
					{
						orderedList = toSort.ApplyOrder<T>(entry.Key, "OrderBy");
					}
					else
					{
						orderedList = toSort.ApplyOrder<T>(entry.Key, "OrderByDescending");
					}
				}
			}

			return orderedList;
		}

		private static IOrderedEnumerable<T> ApplyOrder<T>(this IEnumerable<T> source, string property, string methodName)
		{
			ParameterExpression param = Expression.Parameter(typeof(T), "x");
			Expression expr = param;
			foreach (string prop in property.Split('.'))
			{
				// use reflection (not ComponentModel) to mirror LINQ
				expr = Expression.PropertyOrField(expr, prop);
			}
			Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), expr.Type);
			LambdaExpression lambda = Expression.Lambda(delegateType, expr, param);

			MethodInfo mi = typeof(Enumerable).GetMethods().Single(
					method => method.Name == methodName
							&& method.IsGenericMethodDefinition
							&& method.GetGenericArguments().Length == 2
							&& method.GetParameters().Length == 2)
					.MakeGenericMethod(typeof(T), expr.Type);
			return (IOrderedEnumerable<T>)mi.Invoke(null, new object[] { source, lambda.Compile() });
		}
	}
}

Edit: Thanks to Karl for pointing out earlier that the original version didn’t work.  The values of the properties being sorted by were not getting extracted through the lambda expression. I have updated the code above to fix this issue and extended the functionality to include nested property sorting, so you can sort on something like “Name.FirstName” for an object. Thanks to this post for pointing me in the right direction.  I had to call .Compile()  on the lambda expression before I wouldn’t receive a runtime error.

Hello world!

Posted July 21, 2008 by Rich
Categories: Uncategorized

Welcome, folks!  Step right up and gaze upon the likes of which you’ve never seen before!

Do you have threading anxiety?  Index out of bounds exceptions?  Troubles with your page state?  A general lack of creativity?  Ubernostrum can help!

You should take Ubernostrum, just in case.  =)

Listen to the Ubernostrum ad from the You Don’t Know Jack CD.


Follow

Get every new post delivered to your Inbox.