In a recent post, I talked about generic lists and how we have been using them at work as a replacement for the DataTable class. For the most part, it has been a real pleasure getting rid of DataTable, but I will admit that there are certain situations in which a DataTable is easier to code for. This is primarily because DataTables are mature and robust, and several helper classes already exist in the .NET framework to process them. The Fill function for the SqlDataAdapter, for example, will automatically load a DataTable with the result of an SQL command for you, which is extremely convenient. There are other classes such as DataView that make manipulative tasks (like sorting and filtering) a piece of cake. For instance, say you have a DataTable populated with about 500 rows and you need to re-sort it. This can be achieved very easily with the DataView class like this…
Private Function SortDataTable(ByRef Table as DataTable, ByVal SortString as String) as DataTable Dim View as New DataView(Table) View.Sort = SortString Return View.ToTable() End Function
Man, that's almost too easy! It is capable of sorting by multiple fields in any order using a very simple syntax similiar to the "ORDER BY" clause in SQL. As you could probably guess, it is more difficult to sort a generic list. Thanks to the inherent flexibility of generics and reflection, however, it is possible to achieve identical functionality. And I've already done the work to make this happen. The class I created uses reflection, which does not perform fantastically, but it offers up unprecedented flexibility. If performance/efficiency is of noteworthy concern in your application, you might want to consider using a type-specific approach. Also, it was mentioned on Stack Overflow that C# developers can use inline delegates and VB9 developers can use lambda expressions, which I thought was an advanced yet elegant approach. For the sake of simplicity, I just created an instance class that inherits IComparer and will work with any type on the .NET 2.0 framework...
Imports System.Collections Imports System.Collections.Generic Imports System.Reflection Namespace Utility ''' <summary> ''' Sort order enumeration ''' </summary> Public Enum SortOrder Ascending Descending End Enum ''' <summary> ''' This instance class is used to sort a generic collection of object instances. ''' It automatically fetches the type and performs the necessary comparison(s) to sort. ''' ''' To use, instantiate this class, set the sort string property, and pass this ''' instance to the internal Sort() function of your generic collection. ''' ''' Example: ''' Dim MyList As List(Of MyClassType) = 'Populate the list somehow ''' Dim Sorter As New Sorter(Of MyClassType)
''' Sorter.SortString = "Field1 DESC, Field2" ''' MyList.Sort(Sorter) 'After this call, the list is sorted ''' </summary> Public Class Sorter(Of T) Implements IComparer(Of T) Private _Sort As String ''' <summary> ''' Instantiate the class. ''' </summary> Public Sub New() End Sub ''' <summary> ''' Instantiate the class, setting the sort string. ''' ''' Example: "LastName DESC, FirstName" ''' </summary> Public Sub New(ByVal SortString As String) _Sort = SortString End Sub ''' <summary> ''' The sort string used to perform the sort. Can sort on multiple fields. ''' Use the property names of the class and basic SQL Syntax. ''' ''' Example: "LastName DESC, FirstName" ''' </summary> Public Property SortString() As String Get If _Sort IsNot Nothing Then Return _Sort.Trim() End If Return Nothing End Get Set(ByVal value As String) _Sort = value End Set End Property ''' <summary> ''' This is an implementation of IComparer(Of T).Compare ''' Can sort on multiple fields, or just one. ''' </summary> Public Function Compare(ByVal x As T, ByVal y As T) As Integer Implements IComparer(Of T).Compare If Not String.IsNullOrEmpty(Me.SortString) Then Const ERR As String = "The property ""{0}"" does not exist in type ""{1}""" Dim Type As Type = GetType(T) Dim Comp As Comparer = Comparer.DefaultInvariant Dim Info As PropertyInfo For Each Expr As String In Me.SortString.Split(","c) Dim Dir As SortOrder = SortOrder.Ascending Dim Field As String Expr = Expr.Trim() If Expr.EndsWith(" DESC") Then Field = Expr.Replace(" DESC", String.Empty).Trim() Dir = SortOrder.Descending Else Field = Expr.Replace(" ASC", String.Empty).Trim() End If Info = Type.GetProperty(Field) If Info Is Nothing Then Throw New MissingFieldException(String.Format(ERR, Field, Type.ToString())) Else Dim Result As Integer = Comp.Compare(Info.GetValue(x, Nothing), Info.GetValue(y, Nothing)) If Result <> 0 Then If Dir = SortOrder.Descending Then Return Result * -1 Else Return Result End If End If End If Next End If Return 0 End Function End Class End Namespace
The meat of the class lies in the Compare function, which is the essential drivetrain of the IComparer interface. If you are not at all familiar with IComparer, you should check out this article on C# corner about comparing objects; it's quite a useful concept to grasp. The class is fully capable of sorting on one or multiple fields of any type of object. The "sort string" uses the same basic syntax as the ORDER BY clause in SQL, and the field names to use are actually the property names within your class. All you have to do is instantiate the Sorter class, set the type and the sort string, and then pass the class instance to the Sort() function of your generic collection. Following is a simple example (called ConsoleApplication149, for me!) that fully demonstrates the flexibility...
Module Module1 Sub Main() Dim People As List(Of Person) = GetPeople() Dim Sorter As New Sorter(Of Person) Sorter.SortString = "FirstName" People.Sort(Sorter) PrintResults(People, Sorter.SortString) Sorter.SortString = "LastName DESC, FirstName, MiddleName" People.Sort(Sorter) PrintResults(People, Sorter.SortString) Sorter.SortString = "DateOfBirth DESC" People.Sort(Sorter) PrintResults(People, Sorter.SortString) Console.ReadLine() End Sub Function GetPeople() As List(Of Person) Dim Result As New List(Of Person) With Result .Add(New Person("John", "M", "Doe", #1/1/1969#)) .Add(New Person("Jane", "A", "Doe", #3/4/1972#)) .Add(New Person("Paul", "L", "Smith", #2/1/1948#)) .Add(New Person("Janet", "A", "Doe", #9/16/1975#)) .Add(New Person("Patricia", "B", "Smith", #3/14/1952#)) .Add(New Person("John", "Matthew", "Doe", #12/21/1988#)) .Add(New Person("James", "L", "Doe", #6/19/1990#)) .Add(New Person("Patrick", "O", "Smith", #8/26/1993#)) End With Return Result End Function Sub PrintResults(ByRef People As List(Of Person), ByVal SortString As String) Console.WriteLine("Sort String: " & SortString) Console.WriteLine() For Each Per As Person In People Console.WriteLine(Per.ToString()) Next Console.WriteLine() Console.WriteLine() End Sub End Module Class Person Private _First As String Private _Middle As String Private _Last As String Private _Dob As Date Sub New() End Sub Sub New(ByVal FirstName As String, ByVal MiddleName As String, ByVal LastName As String, ByVal DateOfBirth As Date) _First = FirstName _Middle = MiddleName _Last = LastName _Dob = DateOfBirth End Sub Property FirstName() As String Get Return _First End Get Set(ByVal value As String) _First = value End Set End Property Property MiddleName() As String Get Return _Middle End Get Set(ByVal value As String) _Middle = value End Set End Property Property LastName() As String Get Return _Last End Get Set(ByVal value As String) _Last = value End Set End Property Property DateOfBirth() As Date Get Return _Dob End Get Set(ByVal value As Date) _Dob = value End Set End Property Overrides Function ToString() As String Return Me.FirstName.PadRight(15, " ") & _ Me.MiddleName.PadRight(10, " ") & _ Me.LastName.PadRight(15, " ") & _ Me.DateOfBirth.ToString("yyyy-MM-dd") End Function End Class
Simple as that! I've grown to love this class, and I hope you too find it useful!
Thanks for sharing!
Dim Comp As Comparer = Comparer.DefaultInvariant
saying:
Too few type arguments to System.Collections.Generic.Comparer(Of T)
System.Collections.Comparer is what it should use.
System.Collections.Generic.Comparer is what is was referencing instead.
DOH <smack>
I had to transform it into c#:
public enum SortOrder
{
Ascending,
Descending
}
public class Sorter<T> : IComparer<T>
{
string _SortString = String.Empty;
public string SortString
{
get { return _SortString.Trim(); }
set { _SortString = value; }
}
public Sorter() { }
public Sorter(string sortstring)
{
_SortString = sortstring;
}
#region IComparer<T> Members
public int Compare(T x, T y)
{
int result = 0;
if (!string.IsNullOrEmpty(SortString))
{
Type t = typeof(T);
Comparer c = Comparer.DefaultInvariant;
System.Reflection.PropertyInfo pi;
foreach (string expr in SortString.Split(new char[] {','}))
{
SortOrder dir = SortOrder.Ascending;
string field;
if (expr.EndsWith(" DESC"))
{
field = expr.Replace(" DESC", String.Empty).Trim();
dir = SortOrder.Descending;
}
else {
field = expr.Replace(" ASC", String.Empty).Trim();
}
pi = t.GetProperty(field);
if (pi != null)
{
result = c.Compare(pi.GetValue(x, null), pi.GetValue(y, null));
if (dir.Equals(SortOrder.Descending))
{
result = -result;
}
if (result != 0)
{
break;
}
}
}
return result;
}
return result;
}
Owe you a beer!
You get intellisense and compile-time checking of the the sort fields.
Check out Dynamic methods as a replacement for your Reflection.
LINQ is perfect in this scenario.
See how beautiful this is:
string[] words = { "aPPLE", "AbAcUs", "bRaNcH", "BlUeBeRrY", "ClOvEr", "cHeRry"};
var sortedWords =
words.OrderBy(a => a.Length)
.ThenBy(a => a, new CaseInsensitiveComparer());
Thanks for posting.
John Williams
http://johnwilliams.eu
thanks for sharing!
Wonderful effort
I have a DB of movies (from the "mvc tutorials"), and have been elaborating on that tutorial to learn mvc.
anyways, this is what I put in my Movie Controller (or home controller, depending on the way you did it):
public ActionResult Movies()
{
List<Movie> moviesTable = _db.MovieSet.ToList();
List<Movie> finalMoviesTable = new List<Movie>();
Movie curMovie = new Movie();
int? highScore = 0;
if (moviesTable.Count != 0)
{
AddNextHighestToList(moviesTable, finalMoviesTable, highScore, curMovie);
}
ViewData.Model = finalMoviesTable;
return View();
}
private void AddNextHighestToList(List<Movie> moviesTable, List<Movie> finalMoviesTable, int? highScore, Movie curMovie)
{
highScore = 0;
foreach (var mov in moviesTable)
{
int? curScore = mov.Score;
if (curScore > highScore)
{
highScore = curScore;
curMovie = mov;
}
}
finalMoviesTable.Add(curMovie);
moviesTable.Remove(curMovie);
if (moviesTable.Count != 0)
{
AddNextHighestToList(moviesTable, finalMoviesTable, highScore, curMovie);
}
else
{
return;
}
}
BASICALLY, moviesTable is the list of movies from the database and finalMoviesTable is what they are going to become, ie the table that will be sorted. The rest is self explanatory so long as you know what everything is.
Hope that helps someone, as it was a REAL PITA to find anywhere to do this (this was the closest solution I found then thought sod it, will come up with one myself).
I've made some changes to the list of objects I was sorting and I'm struggling to amend this code to cope with them. Basically I now have a list of objects - Pursuits, and each Pursuit has a PursuitDirector which is an Employee object. The Employee object has a property called Name and it's this that my sort is based on.
Is it possible to sort a list of objects based on this or is it too deeply nested?
Thanks
Imports System.Collections
Imports System.Collections.Generic
Imports System.Reflection
Public Class PropertySorter(Of T)
Implements IComparer(Of T)
Private m_compareType As Type = GetType(T)
Private m_comparer As Comparer = Comparer.DefaultInvariant
Private m_propertyInfo As PropertyInfo
Private m_sortParameters As List(Of KeyValuePair(Of PropertyInfo, SortOrder))
''' <summary>
''' Sort order enumeration
''' </summary>
Public Enum SortOrder
Ascending
Descending
End Enum
Public Sub New(ByVal sortString As String)
SetSortString(sortString)
End Sub
Public Sub SetSortString(ByVal sortString As String)
m_sortParameters = New List(Of KeyValuePair(Of PropertyInfo, SortOrder))
For Each propAndDirection As String In sortString.Split(","c)
Dim direction As SortOrder = SortOrder.Ascending
Dim propName As String
propAndDirection = propAndDirection.Trim()
If propAndDirection.EndsWith(" DESC") Then
propName = propAndDirection.Replace(" DESC", String.Empty).Trim()
direction = SortOrder.Descending
Else
propName = propAndDirection.Replace(" ASC", String.Empty).Trim()
End If
Dim propInfo As PropertyInfo = m_compareType.GetProperty(propName)
If propInfo Is Nothing Then
Throw New MissingFieldException(String.Format("The property ""{0}"" does not exist in type ""{1}""", propName, m_compareType.ToString()))
Else
' create ref to property and direction
m_sortParameters.Add(New KeyValuePair(Of PropertyInfo, SortOrder)(propInfo, direction))
End If
Next
End Sub
Public Function Compare(ByVal x As T, ByVal y As T) As Integer Implements IComparer(Of T).Compare
If m_sortParameters.Count < 1 Then
Return 0
Else
For Each pio As KeyValuePair(Of PropertyInfo, SortOrder) In m_sortParameters
Dim result As Integer = m_comparer.Compare(pio.Key.GetValue(x, Nothing), pio.Key.GetValue(y, Nothing))
If result <> 0 Then
'found difference return result
If pio.Value = SortOrder.Descending Then
result *= -1
End If
Return result
Else
'difference not yet found, keep checking the sort paramet
I can display my data in a datagridview from my database through "list". The problem is I cannot display only one field: "photo" in image type. the error is "unable to cast object of type 'system.byte[]' to type 'system.drawing.image'
My question is how to display my photo in my datagridview. For example in your tuturial in class person, I will add Property photo() As image.
I hope you answer my question.
thanks in advance
Guess What?
There are a few basic guidelines you should be aware of before leaving a comment…
- If you choose to display your email address, it will not be detected by spam bots
- Comments are limited to 3,000 characters; so far you have used none of them
- HTML will be encoded; links and line breaks will be converted automatically
- Comments containing five or more links will be subject to moderation
