Click here to Skip to main content
11,412,578 members (72,862 online)
Click here to Skip to main content

Implementing a Sortable BindingList Very, Very Quickly

, 1 Dec 2008 CPOL
Rate this:
Please Sign up or sign in to vote.
A custom implementation of BindingList that provides sorting for every property of type T.

SortableBindingList

Introduction

Implementing parent-child hierarchies (for example, a Sale object and the SaleDetails associated with it) is one of the most common scenarios encountered when modeling the entities in a business domain. If you implement the business objects using classes, a collection of child objects will typically be stored in a List<T>. However, List<T>, will prove anemic should you require a rich user interface built on top of the .NET framework's support for data binding.

The typical solution will be to wrap the List<T> in a BindingSource in order to take advantage of its design time support for data binding. That road will only take you so far as a critical feature will be absent - support for sorting.

This article will seek to remedy that by providing a custom implementation of a BindingList<T> that will automatically provide the methods required to provide sorting capability on every property defined in type T.

Implementation Objectives

  • Support sorting on all properties by instantiating an instance of the custom implementation of the BindingList<T>. E.g., write:
  • MySortableBindingList<SaleDetails>sortableSaleDetails = 
                          new MySortableBindingList<SaleDetail>();

    and get the sorting functionality.

Motivating Example

To illustrate this approach, we shall model two classes, Sale and SaleDetail, as follows:

public class Sale {

    public Sale() {
        SaleDate = DateTime.Now;
    }

    public MySortableBindingList<SaleDetail> SaleDetails { get; set; }
    public string Salesman { get; set; }
    public string Client { get; set; }
    public DateTime SaleDate { get; set; }

    public decimal TotalAmount {
        get {
            Debug.Assert(SaleDetails != null);
            return SaleDetails.Sum(a => a.TotalAmount);
        }
    }
}

public class SaleDetail {

    public string Product { get; set; }
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }

    public decimal TotalAmount {
        get {
            return UnitPrice * Quantity;
        }
    }
}

The above classes are just simple enough to illustrate the main concepts behind the article, as validation, persistence, error handling etc., are beyond the scope of the article.

Subclassing BindingList<T>

First, the code:

public class MySortableBindingList<T> : BindingList<T> {

    // reference to the list provided at the time of instantiation
    List<T> originalList;
    ListSortDirection sortDirection;
    PropertyDescriptor sortProperty;

    // function that refereshes the contents
    // of the base classes collection of elements
    Action<MySortableBindingList<T>, List<T>> 
                   populateBaseList = (a, b) => a.ResetItems(b);

    // a cache of functions that perform the sorting
    // for a given type, property, and sort direction
    static Dictionary<string, Func<List<T>, IEnumerable<T>>> 
       cachedOrderByExpressions = new Dictionary<string, Func<List<T>, 
                                                 IEnumerable<T>>>();

    public MySortableBindingList() {
        originalList = new List<T>();
    }

    public MySortableBindingList(IEnumerable<T> enumerable) {
        originalList = enumerable.ToList();
        populateBaseList(this, originalList);
    }

    public MySortableBindingList(List<T> list) {
        originalList = list;
        populateBaseList(this, originalList);
    }

    protected override void ApplySortCore(PropertyDescriptor prop, 
                            ListSortDirection direction) {
        /*
         Look for an appropriate sort method in the cache if not found .
         Call CreateOrderByMethod to create one. 
         Apply it to the original list.
         Notify any bound controls that the sort has been applied.
         */

        sortProperty = prop;

        var orderByMethodName = sortDirection == 
            ListSortDirection.Ascending ? "OrderBy" : "OrderByDescending";
        var cacheKey = typeof(T).GUID + prop.Name + orderByMethodName;

        if (!cachedOrderByExpressions.ContainsKey(cacheKey)) {
            CreateOrderByMethod(prop, orderByMethodName, cacheKey);
        }

        ResetItems(cachedOrderByExpressions[cacheKey](originalList).ToList());
        ResetBindings();
        sortDirection = sortDirection == ListSortDirection.Ascending ? 
                        ListSortDirection.Descending : ListSortDirection.Ascending;
    }


    private void CreateOrderByMethod(PropertyDescriptor prop, 
                 string orderByMethodName, string cacheKey) {

        /*
         Create a generic method implementation for IEnumerable<T>.
         Cache it.
        */

        var sourceParameter = Expression.Parameter(typeof(List<T>), "source");
        var lambdaParameter = Expression.Parameter(typeof(T), "lambdaParameter");
        var accesedMember = typeof(T).GetProperty(prop.Name);
        var propertySelectorLambda =
            Expression.Lambda(Expression.MakeMemberAccess(lambdaParameter, 
                              accesedMember), lambdaParameter);
        var orderByMethod = typeof(Enumerable).GetMethods()
                                      .Where(a => a.Name == orderByMethodName &&
                                                   a.GetParameters().Length == 2)
                                      .Single()
                                      .MakeGenericMethod(typeof(T), prop.PropertyType);

        var orderByExpression = Expression.Lambda<Func<List<T>, IEnumerable<T>>>(
                                    Expression.Call(orderByMethod,
                                            new Expression[] { sourceParameter, 
                                                               propertySelectorLambda }),
                                            sourceParameter);

        cachedOrderByExpressions.Add(cacheKey, orderByExpression.Compile());
    }

    protected override void RemoveSortCore() {
        ResetItems(originalList);
    }

    private void ResetItems(List<T> items) {

        base.ClearItems();

        for (int i = 0; i < items.Count; i++) {
            base.InsertItem(i, items[i]);
        }
    }

    protected override bool SupportsSortingCore {
        get {
            // indeed we do
            return true;
        }
    }

    protected override ListSortDirection SortDirectionCore {
        get {
            return sortDirection;
        }
    }

    protected override PropertyDescriptor SortPropertyCore {
        get {
            return sortProperty;
        }
    }

    protected override void OnListChanged(ListChangedEventArgs e) {
        originalList = base.Items.ToList();
    }
}

In a Nutshell

If, for instance, you create a MySortableBindingList<Sale> and sort on the Customer property, an expression that conceptually looks something like Enumerable.OrderBy<Sale>(originalList, a => a.Customer) will be created and used to do the sorting.

The code to create the sample data and set up the data binding:

public void OnLoad(object source, EventArgs e) {

    var sales = new[] {
        new Sale(){

            Client = "Jahmani Mwaura",
            SaleDate = new DateTime(2008,1,1),
            Salesman = "Gachie",

            SaleDetails = new MySortableBindingList<SaleDetail>(){

                new SaleDetail(){
                    Product = "Sportsman",
                    Quantity = 1,
                    UnitPrice = 80
                },

                 new SaleDetail(){
                    Product = "Tusker Malt",
                    Quantity = 2,
                    UnitPrice = 100
                },

                new SaleDetail(){
                    Product = "Alvaro",
                    Quantity = 1,
                    UnitPrice = 50
                }
            }
        },

        new Sale(){

            Client = "Ben Kones",
            SaleDate = new DateTime(2008,1,1),
            Salesman = "Danny",

            SaleDetails = new MySortableBindingList<SaleDetail>(){

                new SaleDetail(){
                    Product = "Embassy Kings",
                    Quantity = 1,
                    UnitPrice = 80
                },

                 new SaleDetail(){
                    Product = "Tusker",
                    Quantity = 5,
                    UnitPrice = 100
                },

                new SaleDetail(){
                    Product = "Novida",
                    Quantity = 3,
                    UnitPrice = 50
                }
            }
        },

        new Sale(){

            Client = "Tim Kim",
            SaleDate = new DateTime(2008,1,1),
            Salesman = "Kiplagat",

            SaleDetails = new MySortableBindingList<SaleDetail>(){

                new SaleDetail(){
                    Product = "Citizen Special",
                    Quantity = 10,
                    UnitPrice = 30

                },

                new SaleDetail(){
                    Product = "Burn",
                    Quantity = 2,
                    UnitPrice = 100
                }
            }
        }
    };

    saleBindingSource.DataSource = new MySortableBindingList<Sale>(sales);
}

Seeing it at work

You can download the samples at the top of the page and see it at work for yourself. I hope you enjoy.

Cheers!

History

  • December 2, 2008: Article posted.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Muigai Mwaura
Technical Lead Olivine Technology
Kenya Kenya
Technical Lead, Olivine Technology - Nairobi, Kenya.

"The bane of productivity: confusing the rituals of work (sitting at your desk by 8:00am, wearing a clean and well pressed business costume etc.) with actual work that produces results."

Watch me!
Follow on   Twitter

Comments and Discussions

 
QuestionI have Second Level Properties, This is not working. Help is appreciated. Thanks! Pin
AshokDNet at 26-Feb-15 13:49
memberAshokDNet26-Feb-15 13:49 
QuestionWow - since 7 years. Pin
snoopy001 at 3-Feb-15 13:02
membersnoopy0013-Feb-15 13:02 
QuestionPerfect - thanks! Pin
scott.leckie at 12-Nov-14 7:01
memberscott.leckie12-Nov-14 7:01 
SuggestionMore lightweigt and speeded up for multicore CPU [modified] Pin
Lim6us at 7-Apr-13 6:49
memberLim6us7-Apr-13 6:49 
GeneralRe: More lightweigt and speeded up for multicore CPU Pin
Muigai Mwaura at 19-Apr-13 15:50
memberMuigai Mwaura19-Apr-13 15:50 
GeneralRe: More lightweigt and speeded up for multicore CPU Pin
Member 8056028 at 29-Apr-13 2:59
memberMember 805602829-Apr-13 2:59 
GeneralRe: More lightweigt and speeded up for multicore CPU Pin
dedlok at 9-Apr-14 6:24
memberdedlok9-Apr-14 6:24 
SuggestionRe: More lightweigt and speeded up for multicore CPU [modified] Pin
yesilbasmurat at 3-Feb-15 11:49
memberyesilbasmurat3-Feb-15 11:49 
SuggestionDataGridView connected with Linq Pin
Dennnis at 19-Mar-13 8:39
memberDennnis19-Mar-13 8:39 
GeneralRe: DataGridView connected with Linq Pin
Muigai Mwaura at 19-Mar-13 22:54
memberMuigai Mwaura19-Mar-13 22:54 
SuggestionOnListChanged Function Pin
Mathew Crothers at 17-Oct-12 16:07
memberMathew Crothers17-Oct-12 16:07 
GeneralRe: OnListChanged Function Pin
Jahmani at 9-Nov-12 7:17
memberJahmani9-Nov-12 7:17 
QuestionThanks. Pin
Brady Kelly at 25-Sep-12 3:54
memberBrady Kelly25-Sep-12 3:54 
AnswerRe: Thanks. Pin
Jahmani at 9-Nov-12 7:13
memberJahmani9-Nov-12 7:13 
SuggestionSpeeding up loading and sorting... Pin
OICU812 at 13-Apr-12 7:38
memberOICU81213-Apr-12 7:38 
GeneralRe: Speeding up loading and sorting... Pin
Jahmani at 19-Jun-12 9:49
memberJahmani19-Jun-12 9:49 
GeneralMuch more simple realization Pin
killmeplease at 16-Apr-11 0:10
memberkillmeplease16-Apr-11 0:10 
GeneralRe: Much more simple realization Pin
dreamgarden at 29-Sep-13 9:52
memberdreamgarden29-Sep-13 9:52 
NewsHere is another more general way [modified] Pin
kkyriako@yahoo.com at 30-Jan-11 9:36
memberkkyriako@yahoo.com30-Jan-11 9:36 
GeneralRe: Here is another more general way Pin
Jahmani at 3-Feb-11 22:22
memberJahmani3-Feb-11 22:22 
QuestionRe: Here is another more general way Pin
Michael Ehrt at 20-Feb-11 21:11
memberMichael Ehrt20-Feb-11 21:11 
GeneralRe: Here is another more general way Pin
Gianni Gardini at 21-Feb-11 1:30
memberGianni Gardini21-Feb-11 1:30 
GeneralRe: Here is another more general way [modified] Pin
BigPilot96 at 28-Nov-11 23:57
memberBigPilot9628-Nov-11 23:57 
GeneralRe: Here is another more general way Pin
Sonam K at 27-Dec-12 23:58
memberSonam K27-Dec-12 23:58 
GeneralMy vote of 5 Pin
_clem at 25-Jan-11 7:36
member_clem25-Jan-11 7:36 
GeneralRe: My vote of 5 Pin
Jahmani at 27-Jan-11 7:37
memberJahmani27-Jan-11 7:37 
GeneralNot perfect Pin
Member 7409541 at 17-Nov-10 1:51
memberMember 740954117-Nov-10 1:51 
1) If you provide a sample, put the interesting class in it's own file.
2) As pointed out, some things like the glyph were still missing.
3) It's loading my DataGridView slowly as it doesn't temporarily disable the ListChanged event.
4) Use IList instead of List.
I know how difficult these dataclasses are. If they're deep inside the framework, it's hard to figure out what they do internally.
But thanks anyway.
[anonymous coward]

GeneralRe: Not perfect Pin
Jahmani at 27-Jan-11 7:38
memberJahmani27-Jan-11 7:38 
Generalmy vote of 5 Pin
unami82 at 24-Sep-10 6:09
memberunami8224-Sep-10 6:09 
GeneralRe: my vote of 5 Pin
Jahmani at 24-Sep-10 7:38
memberJahmani24-Sep-10 7:38 
NewsCodeplex Pin
Jahmani at 8-Aug-10 21:32
memberJahmani8-Aug-10 21:32 
QuestionIs this a bug? Pin
Jason Brown at 26-Jul-10 7:28
memberJason Brown26-Jul-10 7:28 
AnswerRe: Is this a bug? Pin
Jahmani at 27-Jul-10 2:23
memberJahmani27-Jul-10 2:23 
GeneralRe: Is this a bug? Pin
Daniel Castenholz at 5-Aug-10 8:35
memberDaniel Castenholz5-Aug-10 8:35 
GeneralRe: Is this a bug? Pin
Jahmani at 5-Aug-10 23:12
memberJahmani5-Aug-10 23:12 
GeneralMy vote of 5 Pin
Jason Brown at 26-Jul-10 1:46
memberJason Brown26-Jul-10 1:46 
GeneralRe: My vote of 5 Pin
Jahmani at 27-Jul-10 2:21
memberJahmani27-Jul-10 2:21 
GeneralMy vote of 5 Pin
Member 1599945 at 4-Jul-10 4:35
memberMember 15999454-Jul-10 4:35 
GeneralRe: My vote of 5 Pin
Jahmani at 20-Jul-10 1:02
memberJahmani20-Jul-10 1:02 
GeneralPlease Fix yours amazing class Pin
minskowl at 25-Sep-09 4:47
memberminskowl25-Sep-09 4:47 
GeneralRe: Please Fix yours amazing class Pin
Jahmani at 30-Sep-09 21:37
memberJahmani30-Sep-09 21:37 
AnswerFixes Pin
jjbongo at 16-Feb-10 1:55
memberjjbongo16-Feb-10 1:55 
GeneralRe: Fixes Pin
Jahmani at 16-Feb-10 4:25
memberJahmani16-Feb-10 4:25 
GeneralOrdering glyph not displayed correctly Pin
Tzvi_r at 27-Jul-09 5:13
memberTzvi_r27-Jul-09 5:13 
GeneralRe: Ordering glyph not displayed correctly Pin
Jahmani at 30-Sep-09 21:38
memberJahmani30-Sep-09 21:38 
GeneralI like this! Pin
JohnStodden at 24-Jul-09 2:08
memberJohnStodden24-Jul-09 2:08 
GeneralRe: I like this! Pin
Jahmani at 24-Jul-09 22:58
memberJahmani24-Jul-09 22:58 
GeneralSimply amazing Pin
tigerfist555 at 23-Jul-09 11:46
membertigerfist55523-Jul-09 11:46 
GeneralRe: Simply amazing Pin
Jahmani at 23-Jul-09 21:29
memberJahmani23-Jul-09 21:29 
GeneralSeems to break automatic list change notifications... Pin
Sly2aar at 27-Jan-09 1:15
memberSly2aar27-Jan-09 1:15 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.150427.1 | Last Updated 2 Dec 2008
Article Copyright 2008 by Muigai Mwaura
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid