• Sorting GridView in WPF & C#

    Posted on April 11, 2012 by in C#, Dotnet, WPF

    We use ListView control to display data items.  The presentation of the data items in a ListView is defined by its view mode, which is specified by the View property. Windows Presentation Foundation (WPF) provides a GridView view mode that partitions the ListView data item content into columns. The properties and methods on GridView and its related classes style and specify the content of the columns. We can also define custom views by creating a class that derives from ViewBase, but in this article I am going to concentrate on sorting grid view columns. If you are new to grid view, I recommend you reading my other article article “Using GridView in WPF & C#”.

    Example:
    In this example, we fetch list of products from Production.Product table of Adventure Works database and will bind to the ListView.  When user clicks on any column, we sort the items in the grid in ascending or descending order.

    I have created a new “WPF Application” project. I have added a new application configuration file to the project and the following application settings under configuration element.

    <appSettings>
        <add key="Sql" value="Data Source=(local);Initial
                     Catalog=AdventureWorks;User=testuser;Password=testuser;"/>
    </appSettings>
    

     
    I have set the view mode of the ListView to GridView.  I have added four grid view columns to the gridview to render Product ID, Product Name, Product Number & List Price attributes of the Product.  I  wanted to format the List Price differently from other attributes so that I can demonstrate about custom formatting using celltemplate.  I have set the event handler for the GridViewColumnHeader click event to GridViewSortClick.

    <ListView Name="lvProducts" GridViewColumnHeader.Click="GridViewSortClick">
        <ListView.ItemContainerStyle>
            <Style TargetType="{x:Type ListViewItem}">
                <Setter Property="HorizontalContentAlignment" Value="Stretch" />
            </Style>
        </ListView.ItemContainerStyle>
        <ListView.View>
            <GridView>
                <GridViewColumn Header="Product ID" Width="120"
                             DisplayMemberBinding="{Binding Path=ProductID}"/>
                <GridViewColumn Header="Product Number" Width="120"
                           DisplayMemberBinding="{Binding Path=ProductNumber}"/>
                <GridViewColumn Header="Name" Width="200"
                           DisplayMemberBinding="{Binding Path=Name}"/>
                <GridViewColumn Header="List Price" Width="120"  >
                    <GridViewColumn.CellTemplate>
                      <DataTemplate>
                        <TextBlock Name="txtListPrice"
                        Text="{Binding Path=ListPrice , StringFormat='{}{0:C}'}"
                             HorizontalAlignment="Right"/>
                        </DataTemplate>
                    </GridViewColumn.CellTemplate>
                </GridViewColumn>
            </GridView>
        </ListView.View>
    </ListView>
    

     
    I have created couple of data templates which would be appled to column headers based on if they are in ascending or descending mode. I am using Path class to draw those UpArrow and DownArrow shapes. I am usin geometry objects to describe curves and shapes.

    • Stroke: Describes how the shape’s outline is painted.
    • StrokeThickness: Describes the thickness of the shape’s outline.
    • Fill: Describes how the interior of the shape is painted.
    • Data properties to specify coordinates and vertices, measured in device-independent pixels.

    For example, setting data property to me “M 5,10 L 15,10 L 10,5 L 5,10″ means,
    start at coordinates (5,10),
    draw a line from 5,10 to 15,10,
    draw another line from 15,10 to 10,5
    draw line from 10,5 to 5,10

    <Window.Resources>
        <DataTemplate x:Key="ArrowUp">
            <DockPanel>
                <TextBlock HorizontalAlignment="Center" Text="{Binding}"/>
                <Path x:Name="arrow"
           		StrokeThickness = "2"
           		Fill            = "Orange"
           		Data            = "M 5,10 L 15,10 L 10,5 L 5,10"/>
            </DockPanel>
        </DataTemplate>
        <DataTemplate x:Key="ArrowDown">
            <DockPanel>
                <TextBlock HorizontalAlignment="Center" Text="{Binding}"/>
                <Path x:Name="arrow"
              		StrokeThickness = "2"
              		Fill            = "Orange"
              		Data            = "M 5,5 L 10,10 L 15,5 L 5,5"/>
            </DockPanel>
        </DataTemplate>
    </Window.Resources>
    

     
    I have added a simple button to the grid. When the user clicks on the Load button, we fetch products from Production.Product table using ADO.NET API and will bind them to the ListView control.

    <Button Name="btnLoad" Content="Load Products" Grid.Row="1"
          Click="btnLoad_Click" Width="100"/>
    
    private void btnLoad_Click(object sender, RoutedEventArgs e)
    {
        lvProducts.ItemsSource=RetrieveProducts().Tables[0].DefaultView;
    }
    public DataSet RetrieveProducts()
    {
        //fetch the connection string from app.config
        string connString = ConfigurationManager.AppSettings["Sql"];
    
        //SQL statement to fetch entries from products
        string sql = @"Select top 10  P.ProductID, P.Name,
                    P.ProductNumber, ListPrice from Production.Product P
                    where ProductSubcategoryID is not null";
    
        DataSet dsProducts = new DataSet();
        //Open SQL Connection
        using (SqlConnection conn = new SqlConnection(connString))
        {
            conn.Open();
            //Initialize command object
            using (SqlCommand cmd = new SqlCommand(sql, conn))
            {
                SqlDataAdapter adapter = new SqlDataAdapter(cmd);
                //Fill the result set
                adapter.Fill(dsProducts);
            }
        }
        return dsProducts;
    }
    

    Since the header name of the column header may not be the same as the column name, we get the binding path information from the column header and will use that information for sorting. If no special formatting or template is applied to the column, we can get it from the DisplayMemberBinding property. But if we specified the template like in the case of “ListPrice”, we have to iterate through the visual tree of the ListView to fetch required information.

    private string RetrieveSortName(GridViewColumnHeader columnHeader)
    {
        GridView gridview = lvProducts.View as GridView;
        string datacolumn = String.Empty;
    
        if (columnHeader.Column.DisplayMemberBinding != null)
            datacolumn = (columnHeader.Column.DisplayMemberBinding as
    Binding).Path.Path;
        else
        {
            TextBlock txtListPrice = FindChild<TextBlock>(lvProducts,
    "txtListPrice");
            Binding bind = BindingOperations.GetBinding(txtListPrice,
    TextBlock.TextProperty);
            datacolumn = bind.Path.Path;
        }
    
        return datacolumn;
    
    }
    public static T FindChild<T>(DependencyObject parent, string childName)
      where T : DependencyObject
    {
        // Confirm parent and childName are valid.
        if (parent == null) return null;
    
        T foundChild = null;
    
        int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
        for (int i = 0; i < childrenCount; i++)
        {
            var child = VisualTreeHelper.GetChild(parent, i);
            // If the child is not of the request child type child
            T childType = child as T;
            if (childType == null)
            {
                // recursively drill down the tree
                foundChild = FindChild<T>(child, childName);
    
                // If the child is found, break so we do not
    //overwrite the found child.
                if (foundChild != null) break;
            }
            else if (!string.IsNullOrEmpty(childName))
            {
                var frameworkElement = child as FrameworkElement;
                // If the child's name is set for search
                if (frameworkElement != null && frameworkElement.Name ==
                                                                     childName)
                {
                    // if the child's name is of the request name
                    foundChild = (T)child;
                    break;
                }
            }
            else
            {
                // child element found.
                foundChild = (T)child;
                break;
            }
        }
        return foundChild;
    }
    

     
    I have created couple of class variables to store the information of the last header that was clicked along with its sort direction.

    GridViewColumnHeader currSortHeader = null;
    ListSortDirection currSortDirection = ListSortDirection.Ascending;
    

     
    When the user clicks on a column header, GridViewSortClick event handler is invoked. If this is the first time, user clicked on any column, we simply set the currSortHeader to the source column header and will sort the data by that column in ascending order. If the user clicks on the same column again, we sort it in descending order. If user clicks on a different column header, we sort the data by that column in ascending order.

    protected void GridViewSortClick(object sender,RoutedEventArgs e)
    {
        ListSortDirection newSortDirection;
    
        //get the reference to the header clicked by the user
        GridViewColumnHeader headerClicked =
              e.OriginalSource as GridViewColumnHeader;
    
        if (headerClicked != null)
        {
            if (headerClicked.Role != GridViewColumnHeaderRole.Padding)
            {
                //if the user trying to sort a different column, default
                // the sort direction to ascending
                if (headerClicked != currSortHeader)
                    newSortDirection = ListSortDirection.Ascending;
                else
                {
                    //figure out the new sort direction depending upon
                    // the current sort direction
                    if (currSortDirection == ListSortDirection.Ascending)
                        newSortDirection = ListSortDirection.Descending;
                    else
                        newSortDirection = ListSortDirection.Ascending;
                }
    
                //header name could be different from the column name
                //get the actual column from the binding path
                string header = RetrieveSortName(headerClicked);
                Sort(header, newSortDirection);
    
                //show up or down arrow according to sort direction
                if (newSortDirection == ListSortDirection.Ascending)
                {
                    headerClicked.Column.HeaderTemplate =
                      Resources["ArrowUp"] as DataTemplate;
                }
                else
                {
                    headerClicked.Column.HeaderTemplate =
                      Resources["ArrowDown"] as DataTemplate;
                }
    
                // Remove arrow from previously sorted header
                if (currSortHeader != null && currSortHeader != headerClicked)
                {
                    currSortHeader.Column.HeaderTemplate = null;
                }
    
                currSortHeader = headerClicked;
                currSortDirection = newSortDirection;
            }
        }
    }
    

     
    The actual sorting of data is pretty straight forward. We get the default view from the item source and will apply the sorting description to the view.

    private void Sort(string sortByColName, ListSortDirection sortDirection)
    {
        ICollectionView dataView =
          CollectionViewSource.GetDefaultView(lvProducts.ItemsSource);
    
        dataView.SortDescriptions.Clear();
        SortDescription sd = new SortDescription(sortByColName, sortDirection);
        dataView.SortDescriptions.Add(sd);
        dataView.Refresh();
    }
    

     
    Snapshots:
    Click on “product ID” for the first time. Data will be sorted in ascending order by ProductID

    Click on “Product ID” again. Data will now be sorted in descending order by “Product ID”

    Click on “List Price” column header. The sorting will be performed now using ListPrice.

    Be Sociable, Share!
      Post Tagged with , ,

    Written by

    Software architect with over 10 years of proven experience in designing & developing n-tier and web based software applications, for Finance, Telecommunication, Manufacturing, Internet and other Commercial industries. He believes that success depends on one's ability to integrate multiple technologies to solve a simple as well as complicated problem.

    View all articles by

    Email : [email protected]

    Leave a Reply