WPF DataGrid Custom Paging and Sorting

By Michael Detras

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.

Popularity  (23482 Views)
Biography - Michael Detras
.NET developer. Interested in WPF, Silverlight, and XNA.
My blog
My FAQs
Create New Account
Article Discussion: WPF DataGrid Custom Paging and Sorting
Michael Detras posted at Sunday, September 27, 2009 3:05 AM
reply
Awesome contents,,,
Perry replied to Michael Detras at Saturday, October 16, 2010 7:17 AM
Hi Michael, I have been watching your WPF and WCF related articles. They are simply awesome. I have just started learning WPF and WCF. Could you please help me to start with these. What are the difference in these? Where I can use it? and very simple example of these application would really helpful for me.

Thanks.
Raj
reply
Thanks
Michael Detras replied to Perry at Saturday, October 16, 2010 7:17 AM

Hi Raj,

Thanks for reading some of my articles. If you just started learning these technologies, I suggest you get good books. There are also many sites out there providing tutorials on these topics. I believe that will answer most of your questions.

Best regards,

Michael

reply
Reza replied to Michael Detras at Saturday, October 16, 2010 7:17 AM
What is this RelayCommand? I am rather new to wpf and been looking so much for ways to create paging for a grid, so this could be very handy.... thanks
reply
Michael Detras replied to Reza at Saturday, October 16, 2010 7:17 AM
Hi Reza,

The RelayCommand is an implementation of the ICommand interface. This is usually used in WPF applications using MVVM design pattern. Instead of attaching event handlers to your controls, you can use commands to implement the application logic code. For example, a Button has a Command property. When you bind an ICommand object to the Command property and the Button gets clicked, then the ICommand's execute function is invoked.

Typically, you'll need to create a class that implements the ICommand interface for every command you need. Fortunately, the RelayCommand removes this burden by using delegates instead. You will only need to specify the methods for the Execute and CanExecute functions.

Hope this helps.
Mike
reply
Stanley replied to Michael Detras at Saturday, October 16, 2010 7:17 AM
Thanks!!!!!

this is a great example.   It is clear and easy to understand.    I couldn't get a sort to work on the datagrid and it took me a long while to find this article.  

I really appreciate it.
reply
jay panchali replied to Stanley at Saturday, October 16, 2010 7:17 AM
Hello ,
         First of all nice article of paging. I have go through your implementation,now  in my case i have datagrid having specific x:name also .I am not doing binding process onload . I have some search parameter and according to it  i have to bind data to datagrid .Here is code sample i how i am doing it ..please guide me to implement as i am new to WPF .
Guide me how can i bind data  runtime ?

XMAL:

<my:DataGrid  Background="White" VerticalGridLinesBrush="LightGray"  HorizontalGridLinesBrush="#B9B9B9" AutoGenerateColumns="True"  
                     IsReadOnly="True" Margin="5,44,7,0" Name="dtGrdSearchList" CanUserResizeRows="False" AreRowDetailsFrozen="True" 
                     SelectionUnit="Cell" IsTextSearchEnabled="True" VirtualizingStackPanel.IsVirtualizing="False" EnableRowVirtualization="False" Loaded="dtGrdSearchList_Loaded"
                      TargetUpdated="dtGrdSearchList_TargetUpdated" ItemsSource="{Binding}"
                      >


            <my:DataGrid.AlternatingRowBackground>
                <LinearGradientBrush StartPoint="0.5,0.0" EndPoint="0.5,1.0">
                    <GradientStop Color="#FFFEFEFF" Offset="0"/>
                    <GradientStop Color="#FFE4F0FC" Offset="1"/>
                </LinearGradientBrush>
            </my:DataGrid.AlternatingRowBackground>


            <my:DataGrid.Columns>
                <my:DataGridTextColumn Binding="{Binding FileID}" Header="File ID" Visibility="Collapsed"  CanUserResize="False"/>
                <my:DataGridTemplateColumn Header="Open" CanUserResize="True" MinWidth="40" Width="40">
                    <my:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <Grid>
                                <!--<Button Name="btnOpen" Content="Open" Click="btnOpen_Click" Tag="{Binding FileID}" Template="{DynamicResource GlassButton}" />-->
                                <Image Name="imgOpen" Source="/Images/getsubmission.png" Stretch="Fill" Width="20" Height="20" Tag="{Binding FileID}" MouseLeftButtonDown="btnOpen_Click" Cursor="Hand" ></Image>
                                <TextBlock Text="{Binding FileNumber}" Width="1"></TextBlock>
                            </Grid>
                        </DataTemplate>
                    </my:DataGridTemplateColumn.CellTemplate>
                </my:DataGridTemplateColumn>
                <my:DataGridTextColumn Binding="{Binding Cabinet}" Header="Cabinet" MinWidth="80"  />
                <my:DataGridTextColumn Binding="{Binding FileNumber}" Header="File Number" MinWidth="120"  />
                <my:DataGridTextColumn Binding="{Binding FileName}" Header="File Name" MinWidth="120"/>
                <my:DataGridTextColumn Binding="{Binding SubmissionNumber}" Header="Submission Number" MinWidth="120"/>
                <my:DataGridTextColumn Binding="{Binding InsuredName}" Header="Insured Name"  MinWidth="120"  />
                <my:DataGridTextColumn Binding="{Binding AgentName}" Header="Agent" MinWidth="100"/>
                <my:DataGridTextColumn Binding="{Binding CreatedDate}" CellStyle="{StaticResource LeftAlignedCellStyle}" Header="Created Date"  MinWidth="100" Width="*"/>




            </my:DataGrid.Columns>
       
        </my:DataGrid>

void objDocumentManager_SearchDocumentCompleted(object result)
        {

            drawerList = null;
            try
            {

//Data may be in form of different clss type like in this example is type of "File"
                drawerList = result as List<File>;
                var newList = (from list in drawerList
                              select new File
                              {
                                  FileID = list.FileID,
                                  FileName = list.FileName,
                                  FileNumber = list.FileNumber,
                                  InsuredName = list.InsuredName,
                                  SubmissionNumber = list.SubmissionNumber,
                                  FileDescription = list.FileDescription,
                                  AgentName = list.AgentName,
                                  DrawerName = list.DrawerName,
                                  CreatedDate = list.CreatedDate//.ToString("MM/dd/yyyy")
                              }) ;


             //newList.ToList<File>();
                //dataGrid1.DataContext = newList;
                //this._cview = new PagingCollectionView(newList.ToList(), 100);
                //this.Source = CollectionViewSource.GetDefaultView(newList);


                System.Collections.Generic.List<File> lstFile = newList.ToList<File>();
                int count = newList.Count();
                if (count == 0)
                    lblRecordInfo.Content = "No Record Found ! ";
                else
                    lblRecordInfo.Content = (count + " Records Found").ToString();

//Bind whole data to Datagrid
                //dtGrdSearchList.ItemsSource = newList;
//Need here some where to implement model...?????

                //********************************    R & D   ******************************************************************//
                DataContext = new MainViewModel(lstFile);
                //searchList = newList;
                //dataPager.ItemsSource = newList as List<File> ;
                //dtGrdSearchList.ItemsSource = this.Source;
                //dtGrdSearchList.ItemsSource = this._cview;
                //********************************    R & D   ******************************************************************//
                this.progressbar.Visibility = Visibility.Collapsed;
            }
            catch (Exception)
            {
                
                this.progressbar.Visibility = Visibility.Collapsed;
                lblRecordInfo.Content = "Search operation returned error ! ";
            } 
reply

WPF DataGrid Custom Paging and SortingThis article shows how to implement custom paging and sorting on a WPF DataGrid.