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.
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
<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.
namespace DataGridUC1.Controls
{
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)
{
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.
private void LoadXML()
{
_ds.Clear();
_ds.ReadXml(_data.FullName);
}
private void WriteXML()
{
_ds.AcceptChanges();
_ds.WriteXml(path);
MessageBox.Show("xml data saved. ");
}
Logic for Filter and Text Search
private void CompleteFilter_Changed(object sender, RoutedEventArgs e)
{
if (sender != null)
{
cbCompleteFilter = (bool)((CheckBox)sender).IsChecked;
}
cvs.View.Refresh();
}
private void CollectionViewSource_Filter(object sender, FilterEventArgs e)
{
DataRowView drv = e.Item as DataRowView;
if (e.Item != null)
{
drv = (DataRowView)e.Item;
if (drv != null && cbCompleteFilter != null)
{
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;
}
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
.
<CollectionViewSource x:Key="cvsTasks" Source="{Binding Ds.Credits}"
CollectionViewType="ListCollectionView" >
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="Item"/>
<scm:SortDescription PropertyName="Check" />
</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
.
<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