WPF DataGrid Custom Paging and Sorting

This article shows how to implement custom paging and sorting on a WPF DataGrid.

Introduction

WPF DataGrid has built-in functionality for sorting its items by clicking on a column header. However, it only works for the current items in the DataGrid.  This becomes a problem when paging functionality is implemented. Paging is a must when items are so many that it should not be loaded into memory because it might affect performance.

Application

Let’s create an application like the one below.

The window is made up of a data grid which shows a list of products and buttons for moving through the list. The data grid is a WPF DataGrid. It is not yet a released product but is downloadable from CodePlex under WPF Toolkit.

Paging

To implement paging, a method must be created that retrieves items from data storage where the calling method can specify the range of items that should be returned. The following code listing shows a static class that has the said method.

/// <summary>

/// Class that simulates a DataAccess module.

/// </summary>

public static class DataAccess

{

    /// <summary>

    /// A list of products. This should be replaced by a database.

    /// </summary>

    private static ObservableCollection<Product> products = new ObservableCollection<Product>

    {

        new Product(1, "Book"),

        new Product(2, "Desktop Computer"),

        new Product(3, "Notebook"),

        new Product(4, "Netbook"),

        new Product(5, "Business Software"),

        new Product(6, "Antivirus Software"),

        new Product(7, "Game Console"),

        new Product(8, "Handheld Game Console"),

        new Product(9, "Mobile Phone"),

        new Product(10, "Multimedia Software"),

        new Product(11, "PC Game")           

    };

 

    /// <summary>

    /// Gets the products.

    /// </summary>

    /// <param name="start">Zero-based index that determines the start of the products to be returned.</param>

    /// <param name="itemCount">Number of products that is requested to be returned.</param>

    /// <param name="sortColumn">Name of column or member that is the basis for sorting.</param>

    /// <param name="ascending">Indicates the sort direction to be used.</param>

    /// <param name="totalItems">Total number of products.</param>

    /// <returns>List of products.</returns>

    public static ObservableCollection<Product> GetProducts(int start, int itemCount, string sortColumn, bool ascending, out int totalItems)

    {

        totalItems = products.Count;

 

        ObservableCollection<Product> sortedProducts = new ObservableCollection<Product>();

 

        // Sort the products. In reality, the items should be stored in a database and

        // use SQL statements for sorting and querying items.

        switch (sortColumn)

        {

            case ("Id"):

                sortedProducts = new ObservableCollection<Product>

                (

                    from p in products

                    orderby p.Id

                    select p

                );

                break;

            case ("Name"):

                sortedProducts = new ObservableCollection<Product>

                (

                    from p in products

                    orderby p.Name

                    select p

                );

                break;

        }

 

        sortedProducts = ascending ? sortedProducts : new ObservableCollection<Product>(sortedProducts.Reverse());

 

        ObservableCollection<Product> filteredProducts = new ObservableCollection<Product>();

 

        for (int i = start; i < start + itemCount &&  i < totalItems; i++)

        {

            filteredProducts.Add(sortedProducts[i]);

        }

 

        return filteredProducts;

    }

}

The GetProducts() determines the range of products to return by using the start and itemCount parameters. The sortColumn and ascending parameters are used in sorting the items, which will be discussed later. The totalItems is an out parameter that is set by the method to the total number of products. This can be more useful if there is a search function. The totalItems will be set to the number of products that matched the specified search criteria instead.

Notice that the GetProducts() method only gets the products from a static member defined in the class. This is for demonstration purposes only. Accessing items from a database is more desirable.

Now let’s take a look at XAML definition of the window that was shown earlier and make a way to call the GetProducts() method.

<Window

    x:Class="WPFApp.MainWindow"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:tk="http://schemas.microsoft.com/wpf/2008/toolkit"   

    Width="350"

    Height="190"

    Title="WPF DataGrid Paging and Sorting">

    <Grid>

        <Grid.RowDefinitions>

            <RowDefinition Height="*"/>

            <RowDefinition Height="Auto"/>

        </Grid.RowDefinitions>

        <tk:DataGrid

            AutoGenerateColumns="False"

            IsReadOnly="True"

            ItemsSource="{Binding Products}">

            <tk:DataGrid.Columns>

                <tk:DataGridTextColumn

                    Header="PRODUCT ID"

                    Binding="{Binding Id}"

                    Width="*"/>

                <tk:DataGridTextColumn

                    Header="PRODUCT NAME"

                    Binding="{Binding Name}"

                    Width="*"/>

            </tk:DataGrid.Columns>

        </tk:DataGrid>

        <StackPanel

            Margin="4"

            Grid.Row="1"

            Orientation="Horizontal"

            HorizontalAlignment="Center">

            <Button               

                Margin="4,0"

                Content="<<"

                Command="{Binding FirstCommand}"/>

            <Button

                Margin="4,0"

                Content="<"

                Command="{Binding PreviousCommand}"/>

            <StackPanel

                VerticalAlignment="Center"

                Orientation="Horizontal">

                <TextBlock

                    Text="{Binding Start}"/>

                <TextBlock

                    Text=" to "/>

                <TextBlock

                    Text="{Binding End}"/>

                <TextBlock

                    Text=" of "/>

                <TextBlock

                    Text="{Binding TotalItems}"/>

            </StackPanel>

            <Button

                Margin="4,0"

                Content=">"

                Command="{Binding NextCommand}"/>

            <Button

                Margin="4,0"

                Content=">>"

                Command="{Binding LastCommand}"/>           

        </StackPanel>

    </Grid>

</Window>

The DataGrid’s ItemsSource dependency property is bounded to the Products property in the window’s ViewModel. The Products property is set to a new object every time a user clicks on a navigation button. Each button has its Command property bounded to a property in the ViewModel. If you are unfamiliar with Model-View-ViewModel design pattern, you may look at my previous article entitled “WPF and the Model View View Model Pattern” or you could search for other resources.

 

Most of the logic is implemented in the window’s ViewModel. The following code listing shows the ViewModel.

 

/// <summary>

/// ViewModel of the MainWindow. This is assigned to the MainWindow's DataContext

/// property. Implements the INotifyPropertyChanged interface to notify the View

/// of property changes.

/// </summary>

public class MainViewModel : INotifyPropertyChanged

{

    #region INotifyPropertyChanged Members

 

    public event PropertyChangedEventHandler PropertyChanged;

 

    #endregion

 

    #region Private Fields

 

    private ObservableCollection<Product> products;

 

    private int start = 0;

 

    private int itemCount = 5;

 

    private string sortColumn = "Id";

 

    private bool ascending = true;

 

    private int totalItems = 0;

 

    private ICommand firstCommand;

 

    private ICommand previousCommand;

 

    private ICommand nextCommand;

 

    private ICommand lastCommand;

 

    #endregion

 

    /// <summary>

    /// Constructor. Initializes the list of products.

    /// </summary>

    public MainViewModel()

    {

        RefreshProducts();

    }

 

    /// <summary>

    /// The list of products in the current page.

    /// </summary>

    public ObservableCollection<Product> Products

    {

        get

        {

            return products;

        }

        private set

        {

            if (object.ReferenceEquals(products, value) != true)

            {

                products = value;

                NotifyPropertyChanged("Products");

            }

        }

    }

 

    /// <summary>

    /// Gets the index of the first item in the products list.

    /// </summary>

    public int Start { get { return start + 1; } }

 

    /// <summary>

    /// Gets the index of the last item in the products list.

    /// </summary>

    public int End { get { return start + itemCount < totalItems ? start + itemCount : totalItems ; } }

 

    /// <summary>

    /// The number of total items in the data store.

    /// </summary>

    public int TotalItems { get { return totalItems; } }

 

    /// <summary>

    /// Gets the command for moving to the first page of products.

    /// </summary>

    public ICommand FirstCommand

    {

        get

        {

            if (firstCommand == null)

            {

                firstCommand = new RelayCommand

                (

                    param =>

                    {

                        start = 0;

                        RefreshProducts();

                    },

                    param =>

                    {

                        return start - itemCount >= 0 ? true : false;

                    }

                );

            }

 

            return firstCommand;

        }

    }

 

    /// <summary>

    /// Gets the command for moving to the previous page of products.

    /// </summary>

    public ICommand PreviousCommand

    {

        get

        {

            if (previousCommand == null)

            {

                previousCommand = new RelayCommand

                (

                    param =>

                    {

                        start -= itemCount;

                        RefreshProducts();

                    },

                    param =>

                    {

                        return start - itemCount >= 0 ? true : false;

                    }

                );

            }

 

            return previousCommand;

        }

    }

 

    /// <summary>

    /// Gets the command for moving to the next page of products.

    /// </summary>

    public ICommand NextCommand

    {

        get

        {

            if (nextCommand == null)

            {

                nextCommand = new RelayCommand

                (

                    param =>

                    {

                        start += itemCount;

                        RefreshProducts();

                    },

                    param =>

                    {

                        return start + itemCount < totalItems ? true : false;

                    }

                );

            }

 

            return nextCommand;

        }

    }

 

    /// <summary>

    /// Gets the command for moving to the last page of products.

    /// </summary>

    public ICommand LastCommand

    {

        get

        {

            if (lastCommand == null)

            {

                lastCommand = new RelayCommand

                (

                    param =>

                    {

                        start = (totalItems / itemCount - 1) * itemCount;

                        start += totalItems % itemCount == 0 ? 0 : itemCount;

                        RefreshProducts();

                    },

                    param =>

                    {

                        return start + itemCount < totalItems ? true : false;

                    }

                );

            }

 

            return lastCommand;

        }

    }

 

    /// <summary>

    /// Refreshes the list of products. Called by navigation commands.

    /// </summary>

    private void RefreshProducts()

    {

        Products = DataAccess.GetProducts(start, itemCount, sortColumn, ascending, out totalItems);

 

        NotifyPropertyChanged("Start");

        NotifyPropertyChanged("End");

        NotifyPropertyChanged("TotalItems");

    }

 

    /// <summary>

    /// Notifies subscribers of changed properties.

    /// </summary>

    /// <param name="propertyName">Name of the changed property.</param>

    private void NotifyPropertyChanged(string propertyName)

    {

        if (PropertyChanged != null)

        {

            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));

        }

    }

}

 

The ViewModel contains the commands for navigating through the list of products. Basically, these commands just set the start variable then call the RefreshProducts() method. For example, the FirstCommand just sets the start variable to the value 0. Afterwards, it calls the RefreshProducts() method which in turn calls the DataAccess.GetProducts() method which uses the updated start variable.  

 

The itemCount value is not changed anywhere in the application. It is useful when a user wants to select the number of items that can be displayed. This is a common functionality in most applications that implement paging. This is not implemented in the example.

 

Sorting

 

Now that paging has been implemented, the only thing left is custom sorting. The following screenshot shows a sorted list of products if the data grid’s built-in sorting is used.

In the example, the product name is sorted. Notice that only the items that were sorted are the current items in the data grid. The items stored in our data store are not included in the sort. The following screenshot shows the sorted list of products where custom sorting was used.



To implement custom sorting, some code changes need to be done. The following code listing shows the updated data grid definition in the XAML file.

 

<tk:DataGrid

    AutoGenerateColumns="False"

    IsReadOnly="True"

    ItemsSource="{Binding Products, NotifyOnTargetUpdated=True}"

    Sorting="ProductsDataGrid_Sorting"

    TargetUpdated="ProductsDataGrid_TargetUpdated"

    Loaded="ProductsDataGrid_Loaded">

    <tk:DataGrid.Columns>

        <tk:DataGridTextColumn

            Header="PRODUCT ID"

            Binding="{Binding Id}"

            Width="*"

            SortDirection="Ascending"/>

        <tk:DataGridTextColumn

            Header="PRODUCT NAME"

            Binding="{Binding Name}"

            Width="*"/>

    </tk:DataGrid.Columns>

</tk:DataGrid>

 

Notice that the following event handlers are added: ProductsDataGrid_Sorting, ProductsDataGrid_TargetUpdated and ProductsDataGrid_Loaded. Also, the NotifyOnTargetUpdated property of the ItemsSource’s binding is set to true. The following code listing shows the code-behind file of the window. This shows the definition for the event handlers previously mentioned.

 

/// <summary>

/// Interaction logic for MainWindow.xaml

/// </summary>

public partial class MainWindow : Window

{

    private DataGridColumn currentSortColumn;

 

    private ListSortDirection currentSortDirection;

 

    public MainWindow()

    {

        InitializeComponent();

 

        DataContext = new MainViewModel();

    }

 

    /// <summary>

    /// Initializes the current sort column and direction.

    /// </summary>

    /// <param name="sender">The products data grid.</param>

    /// <param name="e">Ignored.</param>

    private void ProductsDataGrid_Loaded(object sender, RoutedEventArgs e)

    {

        DataGrid dataGrid = (DataGrid)sender;

 

        // The current sorted column must be specified in XAML.

        currentSortColumn = dataGrid.Columns.Where(c => c.SortDirection.HasValue).Single();

        currentSortDirection = currentSortColumn.SortDirection.Value;

    }

 

    /// <summary>

    /// Sets the sort direction for the current sorted column since the sort direction

    /// is lost when the DataGrid's ItemsSource property is updated.

    /// </summary>

    /// <param name="sender">The parts data grid.</param>

    /// <param name="e">Ignored.</param>

    private void ProductsDataGrid_TargetUpdated(object sender, DataTransferEventArgs e)

    {

        if (currentSortColumn != null)

        {

            currentSortColumn.SortDirection = currentSortDirection;

        }

    }

 

    /// <summary>

    /// Custom sort the datagrid since the actual records are stored in the

    /// server, not in the items collection of the datagrid.

    /// </summary>

    /// <param name="sender">The parts data grid.</param>

    /// <param name="e">Contains the column to be sorted.</param>

    private void ProductsDataGrid_Sorting(object sender, DataGridSortingEventArgs e)

    {

        e.Handled = true;

 

        MainViewModel mainViewModel = (MainViewModel)DataContext;

 

        string sortField = String.Empty;

 

        // Use a switch statement to check the SortMemberPath

        // and set the sort column to the actual column name. In this case,

        // the SortMemberPath and column names match.

        switch (e.Column.SortMemberPath)

        {

            case ("Id"):

                sortField = "Id";

                break;

            case ("Name") :

                sortField = "Name";

                break;

        }

 

        ListSortDirection direction = (e.Column.SortDirection != ListSortDirection.Ascending) ?

            ListSortDirection.Ascending : ListSortDirection.Descending;

 

        bool sortAscending = direction == ListSortDirection.Ascending;

 

        mainViewModel.Sort(sortField, sortAscending);

 

        currentSortColumn.SortDirection = null;

 

        e.Column.SortDirection = direction;

 

        currentSortColumn = e.Column;

        currentSortDirection = direction;

    }   

}   

 

First, the current data grid column and sort direction are stored in member variables because the current sort information is lost when the DataGrid’s ItemsSource property is set to another instance, which will be done every time the user sorts the list or navigates to other pages. These variables are initialized in the Loaded event handler of the window. Note that there must be one column that has its SortDirection property initialized by specifying it either in code or XAML.

 

To set the sort direction again when a user sorts the list or moves to another page, the current column’s SortDirection property should be set in the TargetUpdated event handler. The event is triggered when the DataGrid’s ItemsSource property is updated. The NotifyOnTargetUpdated property in the binding expression should be set to true. Take note that setting the sort direction does not sort the data grid. It just specifies how the sort arrow of the column should be displayed.

 

The Sorting event handler overrides the default sorting mechanism of the data grid. The Handled property of the DataGridSortingEventArgs parameter must be set to true so that the default sorting is not executed. This method calls the newly added Sort() method of the MainViewModel. It requires two parameters, the sort column and the sort direction. The application should know beforehand the possible values for the sort column. The possible values may be defined as an enumeration type in the DataAccess module. I just used a string for simplicity.

 

The direction variable is a local variable that stores the next sort direction. Basically, it toggles the sort direction for the column that is to be sorted. Meanwhile, the sort direction for the current column that is sorted is set to null. Finally, the currentSortColumn and currentSortDirection are set to their new values.

 

The Visual Studio 2008 example can be downloaded here.

By Michael Detras   Popularity  (33638 Views)
Biography - Michael Detras
.NET developer. Interested in WPF, Silverlight, and XNA.
My blog