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.
Context Menu
The Context Menu of the DataGrid offers some additional commands/features like Toggle 'Group' Column Visibility or Undo/Redo for edits made in the current row.
The RichTextBox
on the RowDetails has it's own Context Menu, which appears on right click.
RichTextBoxFormatBar
The following text is taken from RichTextBoxFormatBar · xceedsoftware/wpftoolkit Wiki · GitHub
"The RichTextBoxFormatBar is a contextual formatting toolbar that mimics the behavior of the Microsoft Office 2010 formatting bar. It can be attached to any Richtextbox control by using the RichTextBoxFormatBarManager. You can even create your own formatting bar and use it instead, but still have all the functionality the RichTextboxFormatBarManager provides."
"The RichTextBoxFormatBar is a contextual text formatting toolbar that will apply text transformations to the selected text of a RichTextBox control. When the user is in the process of a selection, the RichTextBoxFormatBar will appear when the mouse is released after the selection. The RichTextBoxFormatBar will also appear during the last click of a "double-click" selection. While the RichTextFormatBar is shown, you may click on any number of text transformations to be applied to the selected text."
Credits/References
History
-
23th May, 2024: Source Version 1.4 adds images to RTB context menu and article lists the Xceed Community License agreement.
-
20th May, 2024: Version 1.3 adds RichTextBoxFormatBar
- 18th May, 2024: Version 1.2 adds Rich TextBox and Context Menu
- 3rd May, 2024: Initial version
- 5th May, 2024: Version 1.1 fixes two smaller issues