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

WPF DataGrid UC with RowDetails, Grouping, Filter and more!

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
5 May 2024CPOL3 min read 2.9K   76   5   2
An overview how to use RowDetails, Grouping and Filter for a DataGrid.
Grouping within a DataGrid was always of interest for me and when I found a MS Learn example about it, making this bigger was the consequent next step.

 

Image 1

 

Introduction

This article and code snippets show how RowDetails, Grouping and Filter for a DataGrid work with a xml file as data source.

Background

This project is based on a MS Learn example.

Using the code

Overview

Grouping and Filter

are presented and explained in detail within the MS Learn example [1].

I've added Simple Text Search and an Add New Row button.

RowDetails area

This is defined in a ResourceDictionary

XML
<ResourceDictionary 
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:local="clr-namespace:DataGridUC1"
                xmlns:local2="clr-namespace:DataGridUC1.Controls" 
                xmlns:b="http://schemas.microsoft.com/xaml/behaviors" 
                xmlns:ViewModel="clr-namespace:DataGridUC1.ViewModel">

    <Style x:Key="DataGridCellStyle" 
         TargetType="{x:Type DataGridCell}">
        <Style.Triggers>
            <Trigger Property="IsSelected"
               Value="True">
                <Setter Property="BorderBrush" Value="Transparent"/>
                <Setter Property="Foreground"
                Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
                <Setter Property="Background" Value="Yellow"/>
            </Trigger>
        </Style.Triggers>
    </Style>

    <DataTemplate x:Key="DataGridPlusRowDetailsTemplate">
        <StackPanel HorizontalAlignment="Stretch" 
                    Height="225" Orientation="Vertical" Width="NaN" Margin="31,0,0,0" 
                    Background="{DynamicResource {x:Static SystemColors.InfoBrushKey}}">
            
            <Label Content="Group" HorizontalAlignment="Left" 
                   FontSize="14" FontWeight="Bold" />
            <TextBox x:Name="Item"
                   Text="{Binding Item, Mode=TwoWay, 
                            UpdateSourceTrigger=PropertyChanged}"
                    Margin="5,1,3,2"
                    IsEnabled="True" ToolTip="Item" 
                    HorizontalAlignment="Left" MinWidth="50" />
            <Label Content="Note" HorizontalAlignment="Left" 
                    FontSize="14" FontWeight="Bold" />
            <TextBox x:Name="Note"
                    Margin="5,1,3,2"
                    Text="{Binding Note, Mode=TwoWay, 
                            UpdateSourceTrigger=PropertyChanged}"
                    ToolTip="Note" IsEnabled="True" MinWidth="50" />

            <StackPanel Orientation="Horizontal" Height="32" 
                    VerticalAlignment="Stretch" 
                    HorizontalAlignment="Left" Width="500" >
                <Label Content="Check" HorizontalAlignment="Left" 
                        VerticalAlignment="Bottom" HorizontalContentAlignment="Center" 
                        VerticalContentAlignment="Center" 
                        FontSize="14" FontWeight="Bold" />
                <CheckBox
                        Margin="10,8,3,2"
                        IsChecked="{Binding Check, Mode=TwoWay, 
                                UpdateSourceTrigger=PropertyChanged}"
                        ToolTip="CheckBox" IsEnabled="True" 
                        AutomationProperties.HelpText="Check" 
                        HorizontalAlignment="Left" VerticalAlignment="Center" 
                        VerticalContentAlignment="Center" />

                <Label Content="Rating" HorizontalAlignment="Center" 
                       VerticalAlignment="Bottom" HorizontalContentAlignment="Right" 
                       VerticalContentAlignment="Center" Width="184" 
                       FontSize="14" FontWeight="Bold" />
                <ComboBox
                        Margin="22,10,3,2"
                        Text="{Binding Rating, Mode=TwoWay, 
                                UpdateSourceTrigger=PropertyChanged}"
                        ToolTip="ComboBox" 
                        IsEnabled="True" 
                        HorizontalAlignment="Right" VerticalAlignment="Bottom" 
                        Width="88" HorizontalContentAlignment="Center" 
                        VerticalContentAlignment="Center" >
                        <ComboBoxItem Content="Average"/>
                        <ComboBoxItem Content="Good"/>
                        <ComboBoxItem Content="Excellent"/>
                </ComboBox>
            </StackPanel>

            <Label Content="Link" HorizontalAlignment="Left" 
                   FontSize="14" FontWeight="Bold" />

             <TextBlock FontFamily="Segoe UI" FontSize="16">
                <Hyperlink NavigateUri="{Binding Text, ElementName=LinkTB, 
                    Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
                           local2:HyperlinkExtensions.IsExternal="true">
                           --> Click here to fire the hyperlink 
                </Hyperlink>
            </TextBlock>

            <TextBox x:Name="LinkTB"
                Margin="5,1,3,2"
                Text="{Binding Link, Mode=TwoWay, 
                        UpdateSourceTrigger=PropertyChanged}"
                ToolTip="Link" IsEnabled="True" 
                Foreground="{DynamicResource {x:Static 
                        SystemColors.InfoTextBrushKey}}" 
                Background="{DynamicResource {x:Static 
                        SystemColors.ControlLightLightBrushKey}}" />

        </StackPanel>
    </DataTemplate>

</ResourceDictionary>

The Row Details contain TextBoxes for Editing the current Row and a Test Button for fire the related Hyperlink.

Hyperlink Extensions

This extension is based on [3] and used in the Row Details area as described above.

C#
namespace DataGridUC1.Controls
{
    // https://stackoverflow.com/questions/10238694/example-using-hyperlink-in-wpf

    public static class HyperlinkExtensions
    {
        public static bool GetIsExternal(DependencyObject obj)
        {
            return (bool)obj.GetValue(IsExternalProperty);
        }

        public static void SetIsExternal(DependencyObject obj, bool value)
        {
            obj.SetValue(IsExternalProperty, value);
        }
        public static readonly DependencyProperty IsExternalProperty =
            DependencyProperty.RegisterAttached("IsExternal", typeof(bool), 
                typeof(HyperlinkExtensions), 
                new UIPropertyMetadata(false, OnIsExternalChanged));

        private static void OnIsExternalChanged(object sender, 
            DependencyPropertyChangedEventArgs args)
        {
            var hyperlink = sender as Hyperlink;

            if ((bool)args.NewValue)
                hyperlink.RequestNavigate += Hyperlink_RequestNavigate;
            else
                hyperlink.RequestNavigate -= Hyperlink_RequestNavigate;
        }

        private static void Hyperlink_RequestNavigate(object sender, 
            System.Windows.Navigation.RequestNavigateEventArgs e)
        {
            //Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri));
            //e.Handled = true;

// https://www.codeproject.com/Questions/5380961/How-do-I-fix-net-8-process-start-url-issue
            Hyperlink link = (Hyperlink)e.OriginalSource;
            Process? process = Process.Start(new ProcessStartInfo(link.NavigateUri.AbsoluteUri)
            {
                UseShellExecute = true
            });

            process!.WaitForExit();
            e.Handled = true;
        }
    }
}

 

Text Search and Filter

The method that the MS Learn example uses to filter checked/unchecked tasks, inspired me to create the following super easy Text Search.
With the FilterEventArgs we get the DataRowView for each task/row what allows us to use simple if statements for this method. The logic therefor lives in the VM.

 

MVVM

To pass objects/controls from the View to the ViewModel we use Interaction.Triggers and ParameterCommand.

The ViewModel also contains logic to read/write XML Data.

C#
private void LoadXML()
{
    _ds.Clear();
    _ds.ReadXml(_data.FullName);
}

// ds.WriteXml(path);
private void WriteXML()
{
    _ds.AcceptChanges();

    _ds.WriteXml(path);
MessageBox.Show("xml data saved. ");
}

Logic for Filter and Text Search

C#
private void CompleteFilter_Changed(object sender, RoutedEventArgs e)
{
    if (sender != null)
    {
        cbCompleteFilter = (bool)((CheckBox)sender).IsChecked;
    }
    // Refresh the view to apply filters.
    cvs.View.Refresh();
}

private void CollectionViewSource_Filter(object sender, FilterEventArgs e)
{
    //Task t = e.Item as Task;
    DataRowView drv = e.Item as DataRowView;

    if (e.Item != null)
    {
        drv = (DataRowView)e.Item;

        if (drv != null && cbCompleteFilter != null)
        // If filter is turned on, filter completed items.
        {
            // if (this.cbCompleteFilter.IsChecked == true && t.Complete == true)
            if (this.cbCompleteFilter == true && (bool)drv.Row["Check"] == true)
            {
                e.Accepted = false;
            }
            else
                e.Accepted = true;
        }
    }
}

private void SearchBox_Changed(object sender, RoutedEventArgs e)
{
    if (sender != null)
    {
        searchBox = (TextBox)sender;
    }
    // Refresh the view to apply filters.
    cvs.View.Refresh();
}

private void CollectionViewSource_Search(object sender, FilterEventArgs e)
{
    DataRowView drv = e.Item as DataRowView;
    if (e.Item != null)
    {
        drv = (DataRowView)e.Item;

        if (drv != null && searchBox != null
            && this.cbCompleteFilter == false)
        {
            if (drv.Row["Item"].ToString().ToLower().Contains(searchBox.Text.ToLower()) == false
                && drv.Row["Note"].ToString().ToLower().Contains(searchBox.Text.ToLower()) == false)
            {
                e.Accepted = false;
            }
            else
                e.Accepted = true;
        }

        if (drv != null && searchBox != null
            && this.cbCompleteFilter == true)
        {
            if (drv.Row["Item"].ToString().ToLower().Contains(searchBox.Text.ToLower()) == false
                && drv.Row["Note"].ToString().ToLower().Contains(searchBox.Text.ToLower()) == false
                || (bool)drv.Row["Check"] == true)
            {
                e.Accepted = false;
            }
            else
                e.Accepted = true;
        }
    }
}

The Properties, ICommands and ParameterCommands always work in the same way, so showing one example below, should be enough.

Collection View and Parameter Command example

We use the Text Search feature to make it clearer.

We use Interaction.Triggers in the XAML file. This means that every time when the CollectionView is Refreshed, the filter is handled.

And we get the data from the XML file when we bind Ds.Credits (the table name) as CollectinViewSource.

The CollectionViewType is ListCollectionView.

XML
<CollectionViewSource x:Key="cvsTasks" Source="{Binding Ds.Credits}"
                      CollectionViewType="ListCollectionView" >

    <CollectionViewSource.SortDescriptions>

        <scm:SortDescription PropertyName="Item"/>
        <scm:SortDescription PropertyName="Check" />
        <!--<scm:SortDescription PropertyName="DueDate" />-->

    </CollectionViewSource.SortDescriptions>

    <CollectionViewSource.GroupDescriptions>
        <PropertyGroupDescription PropertyName="Item"/>
        <PropertyGroupDescription PropertyName="Check"/>
    </CollectionViewSource.GroupDescriptions>

    <b:Interaction.Triggers>
        <b:EventTrigger EventName= "Filter">
            <b:InvokeCommandAction Command="{Binding
                        ParameterCmdFilter, Mode=OneWay}"
                CommandParameter="{Binding cvsTasks,
                            RelativeSource={RelativeSource
                                AncestorType={x:Type CollectionViewSource}}}"
                            PassEventArgsToCommand="True" />
        </b:EventTrigger>
        <b:EventTrigger EventName= "Filter">
            <b:InvokeCommandAction Command="{Binding
                        ParameterCmdSearch, Mode=OneWay}"
                CommandParameter="{Binding cvsTasks,
                            RelativeSource={RelativeSource
                                AncestorType={x:Type CollectionViewSource}}}"
                            PassEventArgsToCommand="True" />
        </b:EventTrigger>
    </b:Interaction.Triggers>

</CollectionViewSource>

For the Text Search, with EventTrigger EventName= "Filter", the  Command ParameterCmdSearch is called and CommandParameter is cvsTasks (the key for source Ds.Credits).

ParameterCmdSearch then calls CollectionViewSource_Search. Thus we get each DataRow as parameter.

The content of the Search TextBox is passed with another Parameter Command.

The Search string is converted ToLower, thus we ignore UpperCase.

XML
<TextBox x:Name="SearchBox"
    Margin="5,1,3,2"
    IsEnabled="True" ToolTip="Item" HorizontalAlignment="Left"
        MinWidth="120" FontSize="14" AcceptsReturn="True"
        MaxLines="1" >

    <b:Interaction.Triggers>
        <b:EventTrigger EventName= "TextChanged">
            <b:InvokeCommandAction Command="{Binding
                      ParameterCmdSearchBox, Mode=OneWay}"
                CommandParameter="{Binding ElementName= SearchBox,
                    Mode=OneWay}"/>
        </b:EventTrigger>
    </b:Interaction.Triggers>

</TextBox>

Using the App

When you start the App the DataGrid should be filled with Credits.

As soon as you select a row, the RowDetails expand.

You can edit the current row within the RowDetails area and test firing a Hyperlink.

The Buttons below the DataGrid are:

With Remove Groups you get the normal DataGrid outfit.

Grouping by Group/Status restores the Grouping outfit.

Add New Row and Save Credits do what it's name indicates.

It is possible to use Text Search Box and Filter out checked Items (for Checked/Unchecked rows) at the same time.

Copy and Paste is possible by using the Context Menu.

 

Credits/References

History

  • 3rd May, 2024: Initial version
  • 5th May, 2024: Version 1.1 fixes two smaller issues

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Engineer
Germany Germany

Comments and Discussions

 
GeneralMy vote of 5 Pin
Member 1587078816-May-24 17:24
Member 1587078816-May-24 17:24 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA4-May-24 7:06
professionalȘtefan-Mihai MOGA4-May-24 7:06 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.