Click here to Skip to main content
15,867,594 members
Articles / Desktop Programming / WPF
Tip/Trick

Sorting in WPF-MVVM

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
1 Jul 2015CPOL2 min read 127.3K   204   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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
India India
All about me is that i am Quick Learner,Love to work in free Environment.
Core focus is to meet clients real business needs through initial consultation, followed by a communicative and collaborative approach.
Delivers applications using C# .net and MS SQL Server. Comfortable working independently, in a team, or mentoring to meets deadlines irrelevant of pressure

Comments and Discussions

 
-- There are no messages in this forum --