Click here to Skip to main content
15,885,662 members
Articles / Programming Languages / C#

Generic Multi-Field/Property Sorting for Lists of Business Objects

Rate me:
Please Sign up or sign in to vote.
4.89/5 (13 votes)
13 Feb 2008CPOL8 min read 75.2K   496   72  
This article presents a simple and flexible way to sort strongly-typed lists of business objects using multiple properties or fields.
using System;
using System.Threading;
using System.Diagnostics;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Reflection;
using BinaryNorthwest;

// Source code by Owen Emlen (owene_1998@yahoo.com, owen@binarynorthwest.com)

namespace MultiSortDemo
{
    public partial class FormMultiSortDemo : Form
    {
        private int nSortedColumn = int.MinValue;
        private bool fDescending = false;
        private int nStartingItems = 4096;        

        /// <summary>
        /// Stores a potentially large list of work items for sorting
        /// </summary>
        public List<WorkItem> AllWorkItems;

        /// <summary>
        /// Stores sort order and direction for multiple sort criteria
        /// </summary>
        public List<SortPropOrFieldAndDirection> rgSortBy;

        /// <summary>
        /// Stores bosses and workers, used for creating dummy "assigned by" & "assigned to" values
        /// </summary>
        public string[] rgBosses = new string[5];
        public string[] rgWorkers = new string[5];

        /// <summary>
        /// Stores the property names for class "WorkItem".  Could be constructed dynamically via reflection.
        /// </summary>
        public List<WorkItemPropertyName> AllPropertyNames = new List<WorkItemPropertyName>(16);

        /// <summary>
        /// Standard component initialization / Constructor
        /// </summary>
        public FormMultiSortDemo()
        {
            InitializeComponent();
        }

        private void FormMultiSortDemo_Load(object sender, EventArgs e)
        {
            // Allocate basic lists that will be used throughout the demo
            rgSortBy = new List<SortPropOrFieldAndDirection>(5);

            // Set up property<->field links
            WorkItem.SetupQuickAndDirtyPropertyToFieldLinkLookup();

            // Initialize dummy names
            InitializeNames();
            txtItemCount.Text = nStartingItems.ToString();

            // Generate test data
            GenerateRealWorkItems(nStartingItems);

            // Add a blank property name entry
            AllPropertyNames.Add(new WorkItemPropertyName(""));

            // Add property names via reflection            
            PropertyInfo[] rgProperties = typeof(WorkItem).GetProperties();
            for (int i = 0; i < rgProperties.Length; i++)
            {
                string sPropertyName = rgProperties[i].Name;
                AllPropertyNames.Add(new WorkItemPropertyName(sPropertyName));                
            }                       

            // Add property names.  This could also be done via reflection, but is currently hardcoded for the demo
            //AllPropertyNames.Add(new WorkItemPropertyName(""));
            //AllPropertyNames.Add(new WorkItemPropertyName("AssignedBy"));
            //AllPropertyNames.Add(new WorkItemPropertyName("AssignedTo"));
            //AllPropertyNames.Add(new WorkItemPropertyName("DateAssigned"));
            //AllPropertyNames.Add(new WorkItemPropertyName("DateFinished"));
            //AllPropertyNames.Add(new WorkItemPropertyName("ItemFinished"));
            //AllPropertyNames.Add(new WorkItemPropertyName("Priority"));
            //AllPropertyNames.Add(new WorkItemPropertyName("TaskBoredom"));

            // Bind the sample data to the grid
            workItemBindingSource.DataSource = AllWorkItems;
            workItemPropertyNameBindingSource1.DataSource = AllPropertyNames;
            workItemPropertyNameBindingSource2.DataSource = AllPropertyNames;
            workItemPropertyNameBindingSource3.DataSource = AllPropertyNames;
            workItemPropertyNameBindingSource4.DataSource = AllPropertyNames;
            workItemPropertyNameBindingSource5.DataSource = AllPropertyNames;

            cb1.SelectedIndex = 6;
        }

        /// <summary>
        /// When a column header is clicked, sort by the property associated with the header.  
        /// Also handles glyph display (momentarily - glyphs disappear after rebind) and reversal of sort direction.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void dgvList_ColumnHeaderMouseClick(object sender, System.Windows.Forms.DataGridViewCellMouseEventArgs e)
        {
            this.Cursor = Cursors.WaitCursor;
            try
            {
                // Retrieve the property name associated with the header
                string sPropertyName = dgvList.Columns[e.ColumnIndex].DataPropertyName;

                // Clear previous sort glyph
                if (nSortedColumn != int.MinValue)
                {
                    dgvList.Columns[nSortedColumn].HeaderCell.SortGlyphDirection = SortOrder.None;
                }

                dgvList.Columns[e.ColumnIndex].SortMode = DataGridViewColumnSortMode.Programmatic;

                // Determine if the user is re-clicking on the same column header
                if (nSortedColumn == e.ColumnIndex)
                {
                    dgvList.Columns[e.ColumnIndex].HeaderCell.SortGlyphDirection =
                      fDescending ? SortOrder.Ascending : SortOrder.Descending;

                    // Reverse the sort direction
                    fDescending = !fDescending;
                }
                else
                {
                    // User clicked on a new column header.  Sort Ascending.
                    fDescending = false;
                    nSortedColumn = e.ColumnIndex;
                    dgvList.Columns[e.ColumnIndex].HeaderCell.SortGlyphDirection = SortOrder.Ascending;
                }

                SortPropOrFieldAndDirection sortBy;

                // Determine if the user is requesting the sort via use of field values or property values
                bool fUseFieldLookup = rbFields.Checked;
                if (fUseFieldLookup)
                {
                    // Sort using direct field lookup
                    sortBy = new SortFieldAndDirection(
                      WorkItem.htPropertyToFieldNameMapping.GetFieldNameForProperty(sPropertyName), fDescending
                      );
                }
                else
                {
                    // Sort using property lookup
                    sortBy = new SortPropertyAndDirection(sPropertyName, fDescending);
                }

                // Suspend binding and layout for more accurate timing of the actual sort
                workItemBindingSource.SuspendBinding();
                dgvList.SuspendLayout();
                {
                    Stopwatch t = new Stopwatch();
                    t.Start();
                    {
                        // Perform the actual (in-place) sort
                        Sorting.SortInPlace<WorkItem>(AllWorkItems, sortBy);
                    }
                    t.Stop();

                    lblResults.Text = "Sorted " + AllWorkItems.Count + " items by " + sPropertyName + " using " +
                      (fUseFieldLookup ? "field value lookups " : "property get ") + " in " + t.ElapsedMilliseconds.ToString() + "ms";

                    Application.DoEvents();
                    workItemBindingSource.ResetBindings(false);
                }
                dgvList.ResumeLayout(false);
                workItemBindingSource.ResumeBinding();
            }
            finally
            {
                this.Cursor = Cursors.Default;
            }
        }

        /// <summary>
        /// Initialize some dummy boss and worker names (include null for demonstration purposes)
        /// </summary>
        public void InitializeNames()
        {
            rgBosses[0] = "Frankenstein";
            rgBosses[1] = "JoMomma";
            rgBosses[2] = "Bob in accounting";
            rgBosses[3] = null;
            rgBosses[4] = "Your wife";

            rgWorkers[0] = SystemInformation.UserName;
            rgWorkers[1] = "Programmer using computer " + SystemInformation.ComputerName;
            rgWorkers[2] = "Fred";
            rgWorkers[3] = "JoMomma Jr.";
            rgWorkers[4] = "Will Pastabuck Tuyu";
        }

        /// <summary>
        /// We need something to sort.  Set up some "real" work items...
        /// </summary>
        /// <param name="nItems"></param>
        public void GenerateRealWorkItems(int nItems)
        {
            Random rnd = new Random();

            AllWorkItems = new List<WorkItem>(nItems);
            for (int i = 0; i < nItems; i++)
            {
                WorkItem item = new WorkItem();

                // Select a random boss (assigned by)
                item.AssignedBy = rgBosses[rnd.Next(0, 5)];

                // Select a random worker (assigned to)
                item.AssignedTo = rgWorkers[rnd.Next(0, 5)];

                // Select a random priority
                item.Priority = rnd.Next(1, 10);

                // Select a random task interest level
                item.TaskBoredom = (BoredomRating)rnd.Next(
                    (int)BoredomRating.EvenWorseThanCodingASPWebPages,
                    (int)BoredomRating.MaxExcitement);
                item.FoundOutAboutWorkItemDaysLater(rnd.Next(0, 20));
                item.PretendCompleted(rnd.Next(0, 20));

                AllWorkItems.Add(item);
            }
        }

        /// <summary>
        /// Creates a SortPropertyAndDirection instance or a SortFieldAndDirection instance, 
        /// depending on whether the user wants value retrieval by Property or Field
        /// </summary>
        /// <param name="sPropertyName"></param>
        /// <param name="fDescending"></param>
        /// <returns></returns>
        public SortPropOrFieldAndDirection CreateSortBy(string sPropertyName, bool fDescending)
        {
            if (rbFields.Checked)
            {
                return new SortFieldAndDirection(
                    WorkItem.htPropertyToFieldNameMapping.GetFieldNameForProperty(sPropertyName), 
                    fDescending);
            }
            else
            {
                return new SortPropertyAndDirection(sPropertyName, fDescending);
            }
        }

        /// <summary>
        /// Handle the main Sort button.  Sort by (potentially) multiple field values or property values.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnSort_Click(object sender, EventArgs e)
        {
            string sWindowTitle = this.Text;
            this.Cursor = Cursors.WaitCursor;
            Application.DoEvents();
            try
            {
                rgSortBy.Clear();

                // Retrieve sort parameters from the UI (combo boxes) and sort direction from check boxes
                if (!string.IsNullOrEmpty(cb1.Text))
                {
                    rgSortBy.Add(CreateSortBy(cb1.Text, chk1.Checked));
                }
                if (!string.IsNullOrEmpty(cb2.Text))
                {
                    rgSortBy.Add(CreateSortBy(cb2.Text, chk2.Checked));
                }
                if (!string.IsNullOrEmpty(cb3.Text))
                {
                    rgSortBy.Add(CreateSortBy(cb3.Text, chk3.Checked));
                }
                if (!string.IsNullOrEmpty(cb4.Text))
                {
                    rgSortBy.Add(CreateSortBy(cb4.Text, chk4.Checked));
                }
                if (!string.IsNullOrEmpty(cb5.Text))
                {
                    rgSortBy.Add(CreateSortBy(cb5.Text, chk5.Checked));
                }

                // Suspend data binding and layout to obtain a more accurate sort timing
                workItemBindingSource.SuspendBinding();
                dgvList.SuspendLayout();
                {
                    Stopwatch t = new Stopwatch();
                    t.Start();

                    // Perform the actual multi-sort.  Sort the work items given a list of sort criteria
                    List<WorkItem> sortedList = Sorting.MultiSort<WorkItem>(AllWorkItems, rgSortBy);

                    t.Stop();

                    // Replace the old work item list with the new, sorted list, using an atomic operation for thread safety
                    Interlocked.Exchange<List<WorkItem>>(ref AllWorkItems, sortedList);

                    lblResults.Text = "Sorted " + AllWorkItems.Count + " items based on your selected criteria using " +
                      (rbFields.Checked ? "field value lookups " : "property get ") + "in " + t.ElapsedMilliseconds.ToString() + "ms";

                    this.Text = "Rebinding grid...";
                    Application.DoEvents();

                    if (!chkHideGrid.Checked) workItemBindingSource.DataSource = AllWorkItems;
                }
                dgvList.ResumeLayout(false);
                workItemBindingSource.ResumeBinding();                
            }
            finally
            {
                this.Cursor = Cursors.Default;
                this.Text = sWindowTitle;
            }
        }

        /// <summary>
        /// Generate a number (user-specified) of new "dummy" items
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnGenerateItems_Click(object sender, EventArgs e)
        {
            string sWindowTitle = this.Text;
            this.Cursor = Cursors.WaitCursor;
            Application.DoEvents();
            try
            {
                int nItems = 0;

                // Check user input for number of new items to create
                try
                {
                    nItems = Convert.ToInt32(txtItemCount.Text);
                }
                catch { }

                // Limit number from 64 to 100,000 items
                if (nItems < 64 || nItems > 100000)
                {
                    lblResults.Text = "Please enter a number between 64 and 100000";
                }
                else
                {
                    dgvList.SuspendLayout();
                    {
                        // Generate the items
                        GenerateRealWorkItems(nItems);
                        this.Text = "Created " + nItems.ToString() + " work items.  Rebinding grid...";
                        Application.DoEvents();
                        if (!chkHideGrid.Checked) workItemBindingSource.DataSource = AllWorkItems;
                    }
                    dgvList.ResumeLayout(false);
                }
            }
            finally
            {
                this.Text = sWindowTitle;
                this.Cursor = Cursors.Default;
            }
        }

        /// <summary>
        /// Hide or show the grid of items
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void chkHideGrid_CheckedChanged(object sender, EventArgs e)
        {
            this.Cursor = Cursors.WaitCursor;
            try
            {
                if (!chkHideGrid.Checked)
                {
                    workItemBindingSource.DataSource = AllWorkItems;
                }
                else
                {
                    dgvList.SuspendLayout();
                    {
                        workItemBindingSource.DataSource = new List<WorkItem>();
                        workItemBindingSource.ResetBindings(false);
                    }
                    dgvList.ResumeLayout(false);
                }
            }
            finally
            {
                this.Cursor = Cursors.Default;
            }
        }
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

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


Written By
Software Developer (Senior) Troppus Software
United States United States
Currently working as a Senior Silverlight Developer with Troppus Software in Superior, CO. I enjoy statistics, programming, new technology, playing the cello, and reading codeproject articles. Smile | :)

Comments and Discussions