When there is some notification about data in a datagrid, I would like to show it like in the following image.
This is something quite common as a notification in windows, but not so common inside controls. I think WPF should be capable of doing this by using control templates, but I'm not very experienced on this topic yet. I need to jump right in this stuff so I need the basics of Datagrid templating.
You cannot do this with control templates alone. The control template defines the appreance and states of a control. In your case, you add functionality that requires some code. You could solve this using attached properties, triggers and an adapted control template, but writing a custom control seems to be more suitable.
Writing a custom control can be very complex, so I will focus on an easy solution to your problem. I assume that you want to display a single notification at a time. However, the follwing solution is easily extendable to support a collection of notifications, too.
To start, I create a custom control derived from DataGrid
. It provides dependency properties for the visibility of the notification, its text and the dismiss button text. It also exposes a RoutedUICommand
and registers a command binding to it. It will be used to dismiss notifications.
[TemplatePart(Name = NotificationBorderPart, Type = typeof(Border))]
[TemplatePart(Name = NotificationTextBlockPart, Type = typeof(TextBlock))]
[TemplatePart(Name = NotificationButtonPart, Type = typeof(Button))]
public class NotifyingDataGrid : DataGrid
{
private const string NotificationBorderPart = "PART_NotificationBorder";
private const string NotificationTextBlockPart = "PART_NotificationTextBlock";
private const string NotificationButtonPart = "PART_NotificationButton";
static NotifyingDataGrid()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(NotifyingDataGrid), new FrameworkPropertyMetadata());
}
public static readonly RoutedUICommand Dismiss = new RoutedUICommand("Dismisses a notification", "Dismiss", typeof(NotifyingDataGrid));
public static readonly DependencyProperty NotificationVisibilityProperty = DependencyProperty.Register(
nameof(NotificationVisibility), typeof(Visibility), typeof(NotifyingDataGrid), new PropertyMetadata(Visibility.Collapsed));
public static readonly DependencyProperty NotificationTextProperty = DependencyProperty.Register(
nameof(NotificationText), typeof(string), typeof(NotifyingDataGrid), new PropertyMetadata(string.Empty, OnNotificationTextChanged));
public static readonly DependencyProperty DismissTextProperty = DependencyProperty.Register(
nameof(DismissText), typeof(string), typeof(NotifyingDataGrid), new PropertyMetadata("Dismiss"));
public NotifyingDataGrid()
{
CommandBindings.Add(new CommandBinding(Dismiss, OnDismiss));
}
public Visibility NotificationVisibility
{
get => (Visibility)GetValue(NotificationVisibilityProperty);
set => SetValue(NotificationVisibilityProperty, value);
}
public string NotificationText
{
get => (string)GetValue(NotificationTextProperty);
set => SetValue(NotificationTextProperty, value);
}
public string DismissText
{
get => (string)GetValue(DismissTextProperty);
set => SetValue(DismissTextProperty, value);
}
private void OnDismiss(object sender, ExecutedRoutedEventArgs e)
{
NotificationVisibility = Visibility.Collapsed;
}
private static void OnNotificationTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var notifyingDataGrid = (NotifyingDataGrid)d;
notifyingDataGrid.NotificationVisibility = Visibility.Visible;
}
}
I created a copy of the original control template using Blend. I extended it with a Border
control that represents the notification header. In it there is a TextBlock
and a Button
. The Text
properties are bound to the dependency properties of our parent custom data grid. The command of the button uses the Dismiss
command, which will be handled by our command binding.
<Style x:Key="{ComponentResourceKey ResourceId=DataGridSelectAllButtonStyle, TypeInTargetAssembly={x:Type DataGrid}}" TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Grid>
<Rectangle x:Name="Border" Fill="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" SnapsToDevicePixels="True"/>
<Polygon x:Name="Arrow" Fill="Black" HorizontalAlignment="Right" Margin="8,8,3,3" Opacity="0.15" Points="0,10 10,10 10,0" Stretch="Uniform" VerticalAlignment="Bottom"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Stroke" TargetName="Border" Value="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Fill" TargetName="Border" Value="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Visibility" TargetName="Arrow" Value="Collapsed"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type local:NotifyingDataGrid}">
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
<Setter Property="BorderBrush" Value="#FF688CAF"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="RowDetailsVisibilityMode" Value="VisibleWhenSelected"/>
<Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
<Setter Property="ScrollViewer.PanningMode" Value="Both"/>
<Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:NotifyingDataGrid}">
<Border Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="True">
<ScrollViewer x:Name="DG_ScrollViewer" Focusable="false">
<ScrollViewer.Template>
<ControlTemplate TargetType="{x:Type ScrollViewer}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Button Command="{x:Static DataGrid.SelectAllCommand}" Focusable="false" Style="{DynamicResource {ComponentResourceKey ResourceId=DataGridSelectAllButtonStyle, TypeInTargetAssembly={x:Type DataGrid}}}" Visibility="{Binding HeadersVisibility, Converter={x:Static DataGrid.HeadersVisibilityConverter}, ConverterParameter={x:Static DataGridHeadersVisibility.All}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}" Width="{Binding CellsPanelHorizontalOffset, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/>
<DataGridColumnHeadersPresenter x:Name="PART_ColumnHeadersPresenter" Grid.Column="1" Visibility="{Binding HeadersVisibility, Converter={x:Static DataGrid.HeadersVisibilityConverter}, ConverterParameter={x:Static DataGridHeadersVisibility.Column}, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/>
<Border x:Name="PART_NotificationBorder" Grid.Row="1" Grid.Column="1" BorderBrush="Black" BorderThickness="1" Background="Yellow" Visibility="{Binding NotificationVisibility, RelativeSource={RelativeSource AncestorType={x:Type local:NotifyingDataGrid}}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock x:Name="PART_NotificationTextBlock" Grid.Column="0" Text="{Binding NotificationText, RelativeSource={RelativeSource AncestorType={x:Type local:NotifyingDataGrid}}}" FontStyle="Italic" Foreground="Gray" TextTrimming="CharacterEllipsis"/>
<Button x:Name="PART_NotificationButton" Grid.Column="1" BorderThickness="0" Background="Yellow" TextBlock.FontStyle="Italic" TextBlock.FontWeight="Bold" TextBlock.Foreground="Gray" Content="{Binding DismissText, RelativeSource={RelativeSource AncestorType={x:Type local:NotifyingDataGrid}}}" Command="local:NotifyingDataGrid.Dismiss"/>
</Grid>
</Border>
<ScrollContentPresenter x:Name="PART_ScrollContentPresenter" Grid.ColumnSpan="2" CanContentScroll="{TemplateBinding CanContentScroll}" Grid.Row="2"/>
<ScrollBar x:Name="PART_VerticalScrollBar" Grid.Column="2" Maximum="{TemplateBinding ScrollableHeight}" Orientation="Vertical" Grid.Row="2" ViewportSize="{TemplateBinding ViewportHeight}" Value="{Binding VerticalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"/>
<Grid Grid.Column="1" Grid.Row="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding NonFrozenColumnsViewportHorizontalOffset, RelativeSource={RelativeSource AncestorType={x:Type DataGrid}}}"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ScrollBar x:Name="PART_HorizontalScrollBar" Grid.Column="1" Maximum="{TemplateBinding ScrollableWidth}" Orientation="Horizontal" ViewportSize="{TemplateBinding ViewportWidth}" Value="{Binding HorizontalOffset, Mode=OneWay, RelativeSource={RelativeSource TemplatedParent}}" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"/>
</Grid>
</Grid>
</ControlTemplate>
</ScrollViewer.Template>
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsGrouping" Value="true"/>
<Condition Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="false"/>
</MultiTrigger.Conditions>
<Setter Property="ScrollViewer.CanContentScroll" Value="false"/>
</MultiTrigger>
</Style.Triggers>
</Style>
You can now bind the notification text and the dismiss button text, as well as the notification visibility, which comes in handy, when you want to hide it from a view model.
<local:NotifyingDataGrid ItemsSource="{Binding MyItems}"
NotificationText="{Binding MyNotificationText}"
DismissText="{Binding MyDismissText}"/>
Notice that I defined template parts here, although they are not directly used. This is because I want to focus on an easy solution using bindings. As you can see in the control template of the control, the notification header controls are nested inside another control template for ScrollViewer
and they cannot be accessed by GetTemplateChild
like you would do usually in custom controls. So consider this solution as a starting point.
If you want to display multiple notifcations, you could easily adapt this control. You would expose a dependency property of a collection type that holds all notification texts. In the control template, you would put an ItemsControl
as notification header that binds to this collection. To display each item as before, you would add a DataTemplate
as ItemTemplate
containing the Border
, TextBlock
and the Button
. Pass the current item as CommandParameter
, so that you can access and dismiss the focused notification.