I tried to find a WPF datagrid that had a nice looking footer column that was both nice looking and functional. I couldn't find one, so I decided to try my hand at making my own.
Introduction
This will give you a nice datagrid
with the ability to add footer columns that will aggregate numeric values. I have also added a third click on the column headers that will remove sorting. When using the datagrid
, I default autogeneratecolumns
to false
because it is looking for a custom column type. If you want to change that, it is possible with some code changes.
Using the Code
Here are the three classes I use and the enum
for what type of aggregate you are trying to get.
public class CustomGrid : DataGrid
{
public CustomGrid()
{
this.AutoGenerateColumns = false;
}
private Grid FooterGrid;
private List<Border> FootersBlocks;
private Border FooterBorder;
private ScrollViewer ScrollViewer;
private DataGridColumnHeadersPresenter Headers;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.CanUserReorderColumns = false;
FootersBlocks = new List<Border>();
var temp = this.Template;
ScrollViewer = (ScrollViewer) temp.FindName("DG_ScrollViewer", this);
FooterBorder = (Border) temp.FindName("FooterBorder", this);
ScrollViewer.ApplyTemplate();
this.ApplyTemplate();
Headers = (DataGridColumnHeadersPresenter)
ScrollViewer.Template.FindName("PART_ColumnHeadersPresenter", ScrollViewer);
FooterGrid = (Grid)temp.FindName("FooterGrid", this);
this.LayoutUpdated += CustomGrid_LayoutUpdated;
this.PreviewMouseUp += CustomGrid_PreviewMouseUp;
}
protected override void OnSorting(DataGridSortingEventArgs eventArgs)
{
if (eventArgs.Column.SortDirection != null &&
eventArgs.Column.SortDirection == System.ComponentModel.ListSortDirection.Descending)
{
var view = CollectionViewSource.GetDefaultView(this.ItemsSource);
view?.SortDescriptions.Clear();
eventArgs.Column.SortDirection = null;
eventArgs.Handled = true;
return;
}
base.OnSorting(eventArgs);
}
private void CustomGrid_PreviewMouseUp(object sender,
System.Windows.Input.MouseButtonEventArgs e)
{
DependencyObject dep = (DependencyObject) e.OriginalSource;
while ((dep != null) && !(dep is DataGridCell) && !(dep is DataGridColumnHeader))
{
dep = VisualTreeHelper.GetParent(dep);
}
if (dep == null)
{
dep = (DependencyObject)e.OriginalSource;
while ((dep != null) && !(dep is DataGridRow))
{
dep = VisualTreeHelper.GetParent(dep);
}
if (dep == null)
return;
if (dep is DataGridRow)
{
SelectedIndex = this.ItemContainerGenerator.IndexFromContainer((DataGridRow) dep);
}
}
}
private void CustomGrid_LayoutUpdated(object sender, EventArgs e)
{
int index = 0;
double totalColumnWidth = 0;
foreach (CustomGridColumn item in this.Columns)
{
totalColumnWidth += item.ActualWidth;
if (FootersBlocks.Count > index)
{
FootersBlocks[index].Width = item.ActualWidth;
}
index++;
}
double test = Headers.ActualHeight;
double addForScrollBar = 0;
if (ScrollViewer.ComputedHorizontalScrollBarVisibility == Visibility.Visible)
{
addForScrollBar = 17;
}
FooterBorder.Margin = new Thickness(0, (this.ActualHeight -
(22 + Headers.ActualHeight)) + (ScrollViewer.VerticalOffset - 5) -
addForScrollBar, 0, -10);
}
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
base.OnItemsSourceChanged(oldValue, newValue);
if (oldValue != null)
{
foreach (var item in oldValue)
{
if (item is FcViewModelBase)
{
var bi = item as FcViewModelBase;
bi.PropertyChanged -= Con_PropertyChanged;
}
}
}
if (newValue != null)
{
foreach (var item in newValue)
{
if (item is FcViewModelBase)
{
var bi = item as FcViewModelBase;
bi.PropertyChanged += Con_PropertyChanged;
}
}
}
}
protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
{
base.OnItemsChanged(e);
Update_Footers();
}
public void Update_Footers()
{
FootersBlocks.Clear();
FooterGrid.Children.Clear();
FooterGrid.ColumnDefinitions.Clear();
int index = 0;
foreach (CustomGridColumn item in this.Columns)
{
decimal footerValue = 0;
var tb = new TextBlock();
FooterGrid.ColumnDefinitions.Add(new ColumnDefinition() { Width = GridLength.Auto });
if (item.AggregateType != AggregateType.None)
{
foreach (var r in this.Items)
{
System.Type type = r.GetType();
if (type.Name != "NamedObject")
{
string val = type.GetProperty(item.ColumnFooter).GetValue(r, null).ToString();
decimal.TryParse(val, out var fv);
footerValue += fv;
}
}
switch (item.AggregateType)
{
case AggregateType.None:
break;
case AggregateType.Sum:
tb.Text = string.Format(item.StringFormat, footerValue);
break;
case AggregateType.Count:
tb.Text = this.Items.Count.ToString(item.StringFormat);
break;
case AggregateType.Average:
decimal avg = Math.Round(footerValue / this.Items.Count, 2);
tb.Text = string.Format(item.StringFormat, avg);
break;
default:
break;
}
}
tb.VerticalAlignment = VerticalAlignment.Top;
tb.Margin = new Thickness(3);
var borderBrush = (SolidColorBrush)(new BrushConverter().ConvertFrom("#FFB1B1B1"));
var brd = new Border() { BorderThickness = new Thickness(0,0,1,0),
BorderBrush = borderBrush, Width = item.ActualWidth };
brd.Child = tb;
tb.HorizontalAlignment = item.HorizontalAlignment;
brd.SetValue(Grid.ColumnProperty, index);
FooterGrid.Children.Add(brd);
FootersBlocks.Add(brd);
index++;
}
}
private void Con_PropertyChanged(object sender,
System.ComponentModel.PropertyChangedEventArgs e)
{
Update_Footers();
}
}
public class CustomGridColumn : DataGridTextColumn
{
public CustomGridColumn()
{
this.AggregateType = AggregateType.None;
this.StringFormat = "{0}";
}
public static readonly DependencyProperty
ColumnFooterProperty = DependencyProperty.RegisterAttached
("ColumnFooter", typeof(string), typeof(CustomGridColumn));
public string ColumnFooter
{
get { return (string)GetValue(ColumnFooterProperty); }
set { SetValue(ColumnFooterProperty, value); }
}
public static readonly DependencyProperty HorizontalAlignmentProperty =
DependencyProperty.RegisterAttached("HorizontalAlignment",
typeof(HorizontalAlignment), typeof(CustomGridColumn));
public HorizontalAlignment HorizontalAlignment
{
get { return (HorizontalAlignment)GetValue(HorizontalAlignmentProperty); }
set { SetValue(HorizontalAlignmentProperty, value); }
}
public static readonly DependencyProperty StringFormatProperty =
DependencyProperty.RegisterAttached("StringFormat",
typeof(string), typeof(CustomGridColumn));
public string StringFormat
{
get { return (string)GetValue(StringFormatProperty); }
set { SetValue(StringFormatProperty, value); }
}
public static readonly DependencyProperty AggregateTypeProperty =
DependencyProperty.RegisterAttached("AggregateType",
typeof(AggregateType), typeof(CustomGridColumn));
public AggregateType AggregateType
{
get { return (AggregateType)GetValue(AggregateTypeProperty); }
set { SetValue(AggregateTypeProperty, value); }
}
}
public enum AggregateType
{
None, Sum, Count, Average
}
public class FcViewModelBase : INotifyPropertyChanged, IDisposable
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var propertyChanged = this.PropertyChanged;
if (propertyChanged != null)
{
propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
}
}
protected bool SetProperty<T>(ref T backingField, T Value,
Expression<Func<T>> propertyExpression)
{
var changed = !EqualityComparer<T>.Default.Equals(backingField, Value);
if (changed)
{
backingField = Value;
this.OnPropertyChanged(ExtractPropertyName(propertyExpression));
}
return changed;
}
private static string ExtractPropertyName<T>(Expression<Func<T>> propertyExpression)
{
var memberExp = propertyExpression.Body as MemberExpression;
if (memberExp == null)
{
throw new ArgumentException("Expression must be a MemberExpression.",
"propertyExpression");
}
return memberExp.Member.Name;
}
}
Here is my main window xaml that includes the style that I am using for the custom datagrid. You must use the style included because it contains the footer border and grid.
<Window x:Class="WpfApp2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp2"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<SolidColorBrush x:Key="ControlForeground_Normal" Color="#FF000000"/>
<SolidColorBrush x:Key="ControlOuterBorder_Normal" Color="#FFB2B2B2"/>
<SolidColorBrush x:Key="ControlInnerBorder_Normal" Color="#FFFFFFFF"/>
<LinearGradientBrush x:Key="ControlBackground_Normal"
EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="White"/>
<GradientStop Color="#FFE3E8EB" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="ControlBackground_MouseOver"
EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFD6E9F0"/>
<GradientStop Color="#FFA6C8D4" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="ControlBackground_Selected"
EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFD6E9F0"/>
<GradientStop Color="#FFD2EBF3" Offset="1"/>
</LinearGradientBrush>
<SolidColorBrush x:Key="ControlOuterBorder_MouseOver" Color="#FF6F9DB5"/>
<SolidColorBrush x:Key="ControlInnerBorder_MouseOver" Color="#FFFFFFFF"/>
<LinearGradientBrush x:Key="ControlBackground_Pressed"
EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FF8AAEBB" Offset="0"/>
<GradientStop Color="#FFBBDBE6" Offset="0.15"/>
<GradientStop Color="#FF7FBFD4" Offset="1"/>
</LinearGradientBrush>
<SolidColorBrush x:Key="ControlOuterBorder_Pressed" Color="#FF198FB0"/>
<LinearGradientBrush x:Key="ControlInnerBorder_Pressed"
EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FF65A5BB" Offset="0"/>
<GradientStop Color="#FFCCEDF8" Offset="0.143"/>
</LinearGradientBrush>
<SolidColorBrush x:Key="ControlInnerBorder_Disabled" Color="Transparent"/>
<LinearGradientBrush x:Key="ControlBackground_Disabled"
EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFE3E8EB" Offset="1"/>
<GradientStop Color="White"/>
</LinearGradientBrush>
<SolidColorBrush x:Key="ControlOuterBorder_Focused" Color="#FF00B1F0"/>
<SolidColorBrush x:Key="ControlInnerBorder_Focused" Color="Transparent"/>
<SolidColorBrush x:Key="ControlBackground_Focused" Color="Transparent"/>
<LinearGradientBrush x:Key="GridView_HeaderBackground"
EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FF0E7094" Offset="1"/>
<GradientStop Color="#FF1990B1"/>
</LinearGradientBrush>
<ControlTemplate x:Key="DataGridTemplate1" TargetType="{x:Type local:CustomGrid}">
<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="*"/>
<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}}}"/>
<ScrollContentPresenter x:Name="PART_ScrollContentPresenter"
Grid.ColumnSpan="2" CanContentScroll="{TemplateBinding CanContentScroll}"
Grid.Row="1"/>
<ScrollBar x:Name="PART_VerticalScrollBar" Grid.Column="2"
Maximum="{TemplateBinding ScrollableHeight}"
Orientation="Vertical" Grid.Row="1"
ViewportSize="{TemplateBinding ViewportHeight}"
Value="{Binding VerticalOffset, Mode=OneWay,
RelativeSource={RelativeSource TemplatedParent}}"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"/>
<Grid Grid.Column="1" Grid.Row="2">
<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>
<Grid>
<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
Margin="0,0,0,25"/>
<Border Height="25" VerticalAlignment="Top" x:Name="FooterBorder"
BorderBrush="#FFB1B1B1" BorderThickness="1">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="White"/>
<GradientStop Color="#FFE4E4E4" Offset="0.148"/>
</LinearGradientBrush>
</Border.Background>
<Grid x:Name="FooterGrid" Margin="5,0,0,0">
</Grid>
</Border>
</Grid>
</ScrollViewer>
</Border>
</ControlTemplate>
<Style TargetType="DataGridRow">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource ControlBackground_Selected}" />
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{StaticResource ControlBackground_MouseOver}" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="DataGridCell">
<Setter Property="Padding" Value="4"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type DataGridCell}">
<Border Padding="{TemplateBinding Padding}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}" SnapsToDevicePixels="True">
<ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{StaticResource ControlBackground_MouseOver}"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="BorderBrush" Value="Transparent" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Group By " VerticalAlignment="Center" />
<ComboBox DisplayMemberPath="StyleGroupItemName" Width="150" Height="30" Margin="2" />
<Button Content="Test" Width="150" Height="30" />
</StackPanel>
<local:CustomGrid ItemsSource="{Binding ItemsList}"
Template="{StaticResource DataGridTemplate1}" Grid.Row="1"
CanUserAddRows="False" SelectionMode="Single" SelectionUnit="FullRow"
HorizontalGridLinesBrush="{StaticResource ControlOuterBorder_Normal}"
VerticalGridLinesBrush="{StaticResource ControlOuterBorder_Normal}"
BorderBrush="Silver">
<local:CustomGrid.Columns>
<local:CustomGridColumn Binding="{Binding Item}" Header="Test Header" />
<local:CustomGridColumn Binding="{Binding TestItem}" Header="Test" />
<local:CustomGridColumn Binding="{Binding Total}" AggregateType="Sum"
HorizontalAlignment="Center" StringFormat="Total: {0:C2}"
ColumnFooter="Total" Header="Total" />
</local:CustomGrid.Columns>
</local:CustomGrid>
</Grid>
</Window>
In this example, I am just using the main window code behind for my DataContext
with a sample class to fill the datagrid with some test data.
public MainWindow()
{
InitializeComponent();
ItemsList = new List<TestClass>();
for (int i = 0; i < 50; i++)
{
var vm = new TestClass { Item = "Test", TestItem = "Test2", Total = 123.5m * i };
ItemsList.Add(vm);
}
this.DataContext = this;
}
public List<TestClass> ItemsList { get; set; }
}
public class TestClass : FcViewModelBase
{
public string Item { get; set; }
public string TestItem { get; set; }
private decimal _total;
public decimal Total
{
get { return _total; }
set { SetProperty(ref _total, value, () => Total); }
}
}
I hope this will make it easy for the next guy, trying to get my grid lines to look nice while also having my footer act proper when horizontal or vertical scrollbars appeared was difficult.
History
- 11th April, 2021: Initial version