Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Sorting in WPF-MVVM

0.00/5 (No votes)
1 Jul 2015 2  
Sorting of simple ListView Elements in Ascending/Descending Order

Introduction

In this tip, I will implement simple WPF Sorting in ListView of Observable Collections by the Inputs provided from UI using MVVM arch.

For the UI, I will use several listView elements for only sorting purposes & I think we can do better.
The application is built with the aim to provide an overview of the many simple best practices used in .NET programming for the newbie developer.

I had read somewhere that the three essential character flaws of any good programmer were sloth, impatience and hubris.
Good programmers want to do the minimum amount of work (sloth).
They want their programs to run quickly (impatience).
They take inordinate pride in what they have written (hubris).
This application will encourage the vices of sloth and hubris, by allowing programmers to do far less work but still produce great looking results.

OverView

:::: MVVM Observable Collections ::::

This is one of the very useful features for collections in WPF applications. In this application example, I am making the collections of my Model Class in IObservable Collections. Here, I had taken the name of Model Class as "Model" and ViewModel class as "UserViewModel".

Where I will first make the IObservable Collections in this way.

C#
public ObservableCollection<Model> _UsersList;
        //Constructor for View Model Class 
        public UserViewModel()
        {
            _UsersList = new ObservableCollection<Model>();
        }

:::: Comparer Classes ::::

In this example, I'm using two Comparer classes PersonComparer & ListViewCustomComparer.
In PersonComparer.cs, as per the required ListView Elements (Tabs like SapId, Name, Dob & Gender in UI) condition are sorted with the param values.

C#
using Assignments;
using System;
using System.ComponentModel;

namespace Assignments
{
    public class PersonComparer : ListViewCustomComparer<Model>
    {
        /// <summary>
        /// Compares the specified x to y.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <param name="y">The y.</param>
        /// <returns></returns>
        public override int Compare(Model x, Model y)
        {
            try
            {
                String valueX = String.Empty, valueY = String.Empty;
                switch (SortBy)
                {
                   // We can add as many cases we want to sort the ListView Elements Tab
                    default:
                    case "Name":
                        valueX = x.Name;
                        valueY = y.Name;
                        break;
                    case "Gender":
                        valueX = x.Gender;
                        valueY = y.Gender;
                        break;
                    case "SapId":
                        if (SortDirection.Equals(ListSortDirection.Ascending)) 
				return x.SapId.CompareTo(y.SapId);
                        else return (-1) * x.SapId.CompareTo(y.SapId);

                    case "Num":
                        if (SortDirection.Equals(ListSortDirection.Ascending)) 
				return x.Num.CompareTo(y.Num);
                        else return (-1) * x.Num.CompareTo(y.Num);
                }

                if (SortDirection.Equals(ListSortDirection.Ascending)) 
				return String.Compare(valueX, valueY);
                else return (-1) * String.Compare(valueX, valueY);
            }
            catch (Exception)
            {
                return 0;
            }
        }
    }
}

Now, In ListViewCustomComparer.cs which is my abstract Generic class, I am comparing two params indicating whether less than, equal to, or greater than the other.

C#
using System;
using System.Collections;
using System.ComponentModel;

namespace Assignments
{
    public abstract class ListViewCustomComparer<T> : IComparer, 
			IListViewCustomComparer where T : class
    {
        #region [ Fields ]

        private String sortBy = String.Empty;
        private ListSortDirection direction = ListSortDirection.Ascending;

        #endregion

        #region [ Properties ]

        /// <summary>
        /// Gets or sets the sort by data column name.
        /// </summary>
        /// <value>The sort by.</value>
        public String SortBy
        {
            get { return sortBy; }
            set { sortBy = value; }
        }

        /// <summary>
        /// Gets or sets the sort direction.
        /// </summary>
        /// <value>The sort direction.</value>
        public ListSortDirection SortDirection
        {
            get { return direction; }
            set { direction = value; }
        }

        #endregion

        #region [ Methods ]

        /// <summary>
        /// Compares two objects and returns a value indicating whether one is less than, 
	/// equal to, or greater than the other.
        /// </summary>
        /// <param name="x">The first object to compare.</param>
        /// <param name="y">The second object to compare.</param>
        /// <returns>
        /// Value Condition Less than zero <paramref name="x"/> is less than 
        /// <paramref name="y"/>. Zero <paramref name="x"/> equals 
        /// <paramref name="y"/>. Greater than zero <paramref name="x"/> 
        /// is greater than <paramref name="y"/>.
        /// </returns>
        /// <exception cref="T:System.ArgumentException">Neither 
        /// <paramref name="x"/> nor <paramref name="y"/> 
        /// implements the <see cref="T:System.IComparable"/> interface.-or- 
        /// <paramref name="x"/> and <paramref name="y"/> 
        /// are of different types and neither one can handle comparisons with the other. </exception>
        public Int32 Compare(Object x, Object y)
        {
            T item1 = x as T;
            T item2 = y as T;

            if (item1 == null || item2 == null)
            {
                System.Diagnostics.Trace.Write("either x or y is null in compare(x,y)");
                return 0;
            }

            return Compare(item1, item2);
        }

        /// <summary>
        /// Compares the specified x to y.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <param name="y">The y.</param>
        /// <returns></returns>
        public abstract Int32 Compare(T x, T y);

        #endregion
    }
}

:::: Sorting Classes ::::

Apart from this, I am having two Sorting classes ListViewSorter & ListViewSortItem.
In ListViewSorter.cs, I'm sorting the ListView with binding the elements with each other in a column.

C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace Assignments
{
    public static class ListViewSorter
    {
        #region [ Fields ]

        private static Dictionary<String, ListViewSortItem> 
        _listViewDefinitions = new Dictionary<String, ListViewSortItem>();

        #endregion

        #region [ DependencyProperties ]

        public static readonly DependencyProperty 
		CustomListViewSorterProperty = DependencyProperty.RegisterAttached(
            "CustomListViewSorter",
            typeof(String),
            typeof(ListViewSorter),
            new FrameworkPropertyMetadata("", new PropertyChangedCallback(OnRegisterSortableGrid)));

        #region [ IsCustomListViewSorter get / set ]

        /// <summary>
        /// Gets the custom list view sorter.
        /// </summary>
        /// <param name="obj">The obj.</param>
        /// <returns></returns>
        public static String GetCustomListViewSorter(DependencyObject obj)
        {
            return (String)obj.GetValue(CustomListViewSorterProperty);
        }

        /// <summary>
        /// Sets the custom list view sorter.
        /// </summary>
        /// <param name="obj">The obj.</param>
        /// <param name="value">The value.</param>
        public static void SetCustomListViewSorter(DependencyObject obj, String value)
        {
            obj.SetValue(CustomListViewSorterProperty, value);
        }
        #endregion

        #endregion

        #region [ Public Methods ]

        /// <summary>
        /// Grids the view column header clicked handler.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="System.Windows.RoutedEventArgs"/> 
        instance containing the event data.</param>
        public static void GridViewColumnHeaderClickedHandler(Object sender, RoutedEventArgs e)
        {
            ListView view = sender as ListView;
            if (view == null) return;

            ListViewSortItem listViewSortItem = (_listViewDefinitions.ContainsKey(view.Name)) 
            ? _listViewDefinitions[view.Name] : null;
            if (listViewSortItem == null) return;

            GridViewColumnHeader headerClicked = e.OriginalSource as GridViewColumnHeader;
            if (headerClicked == null) return;

            ListCollectionView collectionView = 
		CollectionViewSource.GetDefaultView(view.ItemsSource) as ListCollectionView;
            if (collectionView == null) return;

            ListSortDirection sortDirection = GetSortingDirection(headerClicked, listViewSortItem);

            // get header name
            String header = (headerClicked.Column.DisplayMemberBinding as Binding).Path.Path as String;
            if (String.IsNullOrEmpty(header)) return;

            // sort listview
            if (listViewSortItem.Comparer != null)
            {
                listViewSortItem.Comparer.SortBy = header;
                listViewSortItem.Comparer.SortDirection = sortDirection;
                collectionView.CustomSort = listViewSortItem.Comparer;
                view.Items.Refresh();
            }
            else
            {
                view.Items.SortDescriptions.Clear();
                view.Items.SortDescriptions.Add(new SortDescription
                (headerClicked.Column.Header.ToString(), sortDirection));
                view.Items.Refresh();
            }

            // change datatemplate of previous and current column header
            headerClicked.Column.HeaderTemplate = 
            GetHeaderColumnsDataTemplate(view, listViewSortItem, sortDirection);

            // Set current sort values as last sort values
            listViewSortItem.LastColumnHeaderClicked = headerClicked;
            listViewSortItem.LastSortDirection = sortDirection;
        }

        #endregion

        #region [ Private Methods ]

        /// <summary>
        /// Called when [register sortable grid].
        /// </summary>
        /// <param name="obj">The obj.</param>
        /// <param name="args">The 
        /// <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> 
        /// instance containing the event data.</param>
        private static void OnRegisterSortableGrid(DependencyObject obj, 
		DependencyPropertyChangedEventArgs args)
        {
            // Check if we are in design mode, if so don't do anything.
            if ((Boolean)(DesignerProperties.IsInDesignModeProperty.GetMetadata
            	(typeof(DependencyObject)).DefaultValue)) return;

            ListView view = obj as ListView;

            if (view != null)
            {
                _listViewDefinitions.Add(view.Name, new ListViewSortItem
                	(System.Activator.CreateInstance(Type.GetType
                		(GetCustomListViewSorter(obj))) as IListViewCustomComparer, null, 
                			ListSortDirection.Ascending));
                view.AddHandler(GridViewColumnHeader.ClickEvent, 
                	new RoutedEventHandler(GridViewColumnHeaderClickedHandler));
            }
        }

        /// <summary>
        /// Gets the header columns data template.
        /// </summary>
        /// <param name="view">The view.</param>
        /// <param name="listViewSortItem">The list view sort item.</param>
        /// <param name="sortDirection">The sort direction.</param>
        /// <returns></returns>
        private static DataTemplate GetHeaderColumnsDataTemplate
        (ListView view, ListViewSortItem listViewSortItem, ListSortDirection sortDirection)
        {
            // remove mark from previous sort column
            if (listViewSortItem.LastColumnHeaderClicked != null)
                listViewSortItem.LastColumnHeaderClicked.Column.HeaderTemplate = 
                view.TryFindResource("ListViewHeaderTemplateNoSorting") as DataTemplate;

            // set correct mark to current column
            switch (sortDirection)
            {
                case ListSortDirection.Ascending:
                    return view.TryFindResource("ListViewHeaderTemplateAscendingSorting") 
			as DataTemplate;
                case ListSortDirection.Descending:
                    return view.TryFindResource("ListViewHeaderTemplateDescendingSorting") 
			as DataTemplate;
                default:
                    return null;
            }
        }

        /// <summary>
        /// Gets the sorting direction.
        /// </summary>
        /// <param name="headerClicked">The header clicked.</param>
        /// <param name="listViewSortItem">The list view sort item.</param>
        /// <returns></returns>
        private static ListSortDirection GetSortingDirection
        (GridViewColumnHeader headerClicked, ListViewSortItem listViewSortItem)
        {
            if (headerClicked != listViewSortItem.LastColumnHeaderClicked) 
		return ListSortDirection.Ascending;
            else
                return (listViewSortItem.LastSortDirection == ListSortDirection.Ascending) 
                ? ListSortDirection.Descending : ListSortDirection.Ascending;
        }

        #endregion
    }
}

In ListViewSortItem.cs, I'm just initializing a new instance of that class for compare, Last Sorted Direction & UI Tab Clicked.

C#
using System.ComponentModel;
using System.Windows.Controls;

namespace Assignments
{
    public class ListViewSortItem
    {
        #region [ Constructor ]

        /// <summary>
        /// Initializes a new instance of the <see cref="ListViewSortItem"/> class.
        /// </summary>
        /// <param name="comparer">The comparer.</param>
        /// <param name="lastColumnHeaderClicked">The last column header clicked.</param>
        /// <param name="lastSortDirection">The last sort direction.</param>
        public ListViewSortItem(IListViewCustomComparer comparer, 
        GridViewColumnHeader lastColumnHeaderClicked, ListSortDirection lastSortDirection)
        {
            Comparer = comparer;
            LastColumnHeaderClicked = lastColumnHeaderClicked;
            LastSortDirection = lastSortDirection;
        }

        #endregion

        #region [ Properties ]

        public IListViewCustomComparer Comparer { get; private set; }

        public GridViewColumnHeader LastColumnHeaderClicked { get; set; }

        public ListSortDirection LastSortDirection { get; set; }

        #endregion
    }
}

Using the Code

:::: Interfaces for ICommand ::::

Now I have Interface for Icommand as IListViewCustomComparer where I'm just declaring the SortBy & SortDirection with Get-Set Properties.

C#
using System;
using System.Collections;
using System.ComponentModel;

namespace Assignments
{
    public interface IListViewCustomComparer : IComparer
    {
        // Gets or sets the sort by column name.
        String SortBy { get; set; }

        // Gets or sets the sort direction the sort direction
        ListSortDirection SortDirection { get; set; }
    }
}

:::: MainWindow.xaml ::::

In this XAML part, we only have to do two things in our Window.Resources we have to add DataTemplates for arrows Buttons Styles.

XML
<DataTemplate x:Key="ListViewHeaderTemplateDescendingSorting">
          <DockPanel>
              <TextBlock Text="{Binding}"/>
              <Path x:Name="arrow"
              StrokeThickness = "2"
              Fill            = "Red"
              Data            = "M 5,10 L 15,10 L 10,5 L 5,10"/>
          </DockPanel>
      </DataTemplate>

      <DataTemplate x:Key="ListViewHeaderTemplateAscendingSorting">
          <DockPanel>
              <TextBlock Text="{Binding }"/>
              <Path x:Name="arrow"
              StrokeThickness = "2"
              Fill            = "Green"
              Data            = "M 5,5 L 10,10 L 15,5 L 5,5"/>
          </DockPanel>
      </DataTemplate>

      <DataTemplate x:Key="ListViewHeaderTemplateNoSorting">
          <DockPanel>
              <TextBlock Text="{Binding }"/>
          </DockPanel>
      </DataTemplate>

In our List View, add the following reference to Comparer class.

XML
<ListView Name="UserGrid" ItemsSource="{Binding _UsersList}"
                 RenderTransformOrigin="0.538,-1.94" Margin="73,285,141,19"
                  local:ListViewSorter.CustomListViewSorter="Assignments.PersonComparer" >

ScreenShots

This is how the application looks like on startup.

DescendingSorting- Red Arrow

Image 1

AscendingSorting - Green Arrow

Image 2

For a basically lazy developer, to avoid too much work, this article is to relieve the workload.
So if you loved the way I explained to you, then stayed tuned. I will soon be uploading more tips for you!!

I must Serve you to lead all ~ Sumit Anand

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here