- Style-able footer
- Two way detection of horizontal scrolling
- Detection of DataSource change
- Simpler consumption
The examples are written in C# in Visual Studio 2015. I am targeting Framework 4 although it should work in earlier frameworks too.
Start a new Visual Studio Custom Control project and call it FooterGrid.
Then rename CustomControl1 to FooterGrid. Click Yes when prompted.
The custom control project creates a Themes\Generic.xaml file and a FooterGrid.cs file. Take a close look at the Generic.xaml file. It defines a default style for FooterGrid controls. The style defines a template. All we have to do is modify the template.
Our custom control will be a grid with two rows. The main grid goes in the top row and the footer grid goes in the bottom row.
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:FooterGrid">
<Style TargetType="{x:Type local:FooterGrid}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:FooterGrid}">
<Grid Name="xLayout">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<DataGrid Name="xMain" Grid.Row="0" HeadersVisibility="All" AutoGenerateColumns="False"
CanUserResizeRows="false" HorizontalScrollBarVisibility="Hidden"/>
<DataGrid Name="xFooter" Grid.Row="1" HeadersVisibility="Row" AutoGenerateColumns="False"
RowHeaderWidth="{Binding ElementName=xMain, Path=RowHeaderActualWidth}"
IsReadOnly="true" CanUserSortColumns="False" CanUserReorderColumns="False" CanUserResizeColumns="False" CanUserResizeRows="False"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Reflection;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Markup;
using System.IO;
using System.Xml;
namespace FooterGrid
{
public class FooterGrid : DataGrid
{
static FooterGrid()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(FooterGrid), new FrameworkPropertyMetadata(typeof(FooterGrid)));
}
}
}
public FooterGrid()
{
this.Loaded += FooterGrid_Loaded;
this.Unloaded += FooterGrid_Unloaded;
}
public void FooterGrid_Loaded(object sender, EventArgs e)
{
// Copy the FooterGrid's column collection to xMain and xFooter
FooterGrid dgOuter = sender as FooterGrid;
DataGrid dgMain = (DataGrid)dgOuter.GetTemplateChild("xMain");
DataGrid dgFooter = (DataGrid)dgOuter.GetTemplateChild("xFooter");
dgMain.RowHeaderTemplate = dgOuter.RowHeaderTemplate;
dgMain.IsReadOnly = dgOuter.IsReadOnly;
dgMain.ColumnHeaderStyle = dgOuter.ColumnHeaderStyle;
dgMain.HeadersVisibility = dgOuter.HeadersVisibility;
if (dgMain.HeadersVisibility == DataGridHeadersVisibility.All || dgMain.HeadersVisibility == DataGridHeadersVisibility.Row)
dgFooter.HeadersVisibility = DataGridHeadersVisibility.Row;
else
dgFooter.HeadersVisibility = DataGridHeadersVisibility.None;
foreach (DataGridBoundColumn dc in dgOuter.Columns)
{
dc.DisplayIndex = dgOuter.Columns.IndexOf(dc);
dgMain.Columns.Add(CloneMainColumn(dc));
dgFooter.Columns.Add(CloneFooterColumn(dc));
}
// Bind the Main and Footer columns together
BindFooterColumns(dgMain, dgFooter);
dgFooter.AddHandler(ScrollViewer.ScrollChangedEvent, new RoutedEventHandler(xDataGrid_ScrollChanged));
// Create the item sources
dgMain.ItemsSource = dgOuter.ItemsSource;
CreateFooterItemSource(dgMain, dgFooter);
// Populate the footer items source and set style
CalcFooterItemSource(dgMain, dgFooter);
dgFooter.RowStyle = (Style)dgOuter.GetValue(ColumnFooterStyleProperty);
// Trap future edits
dgMain.CurrentCellChanged += FooterGrid_CurrentCellsChanged;
// Trap changes to ItemsSource
DependencyPropertyDescriptor dpd = DependencyPropertyDescriptor.FromProperty(ItemsControl.ItemsSourceProperty, typeof(DataGrid));
if (dpd != null)
dpd.AddValueChanged(dgOuter, FooterGrid_ItemsSourceChanged);
}
The FooterGrid_Unloaded method removes all event handlers as the footer grid is unloaded. This reduces memory leaks.
private static void FooterGrid_Unloaded(object sender, EventArgs e)
{
FooterGrid dgOuter = sender as FooterGrid;
DataGrid dgMain = (DataGrid)dgOuter.GetTemplateChild("xMain");
DataGrid dgFooter = (DataGrid)dgOuter.GetTemplateChild("xFooter");
dgMain.CurrentCellChanged -= FooterGrid_CurrentCellsChanged;
}
private static void FooterGrid_ItemsSourceChanged(object sender, EventArgs e)
{
FooterGrid dgOuter = sender as FooterGrid;
DataGrid dgMain = (DataGrid)dgOuter.GetTemplateChild("xMain");
DataGrid dgFooter = (DataGrid)dgOuter.GetTemplateChild("xFooter");
dgMain.ItemsSource = dgOuter.ItemsSource;
if (dgFooter.ItemsSource == null)
CreateFooterItemSource(dgMain, dgFooter);
CalcFooterItemSource(dgMain, dgFooter);
}
private static DataGridBoundColumn CloneMainColumn(DataGridBoundColumn dc)
{
// Clone the column and return it. Modify properties as needed
string ColumnXaml = XamlWriter.Save(dc);
StringReader sr = new StringReader(ColumnXaml);
XmlReader xmlReader = XmlReader.Create(sr);
DataGridBoundColumn newDC = (DataGridBoundColumn)XamlReader.Load(xmlReader);
return newDC;
}
private static DataGridTextColumn CloneFooterColumn(DataGridBoundColumn dc)
{
// Create a new text column and return it. Modify properties as needed
DataGridTextColumn newDC = new DataGridTextColumn();
Binding b = (dc as DataGridBoundColumn).Binding as Binding;
bool HasAggregate = (dc.GetValue(AggregateProperty).ToString() != "");
newDC.Width = dc.Width;
newDC.DisplayIndex = dc.DisplayIndex;
newDC.CellStyle = dc.CellStyle;
if (b != null)
newDC.Binding = new Binding
{
Path = new PropertyPath(b.Path.Path),
ConverterParameter = b.ConverterParameter,
Converter = b.Converter
};
return newDC;
}
private static void BindFooterColumns(DataGrid dgMain, DataGrid dgFooter)
{
// Bind the width and DisplayIndex properties of the Main and Footer columns
try
{
int sourceColIndex = 0;
bool bAllowColumnDisplayIndexSynchronization = true;
for (int i = 0; i < dgFooter.Columns.Count; i++)
{
if (GetColumnSpan(dgFooter.Columns[i]) > 1)
{
bAllowColumnDisplayIndexSynchronization = false;
break;
}
}
for (int associatedColIndex = 0; associatedColIndex < dgFooter.Columns.Count; associatedColIndex++)
{
var colAssociated = dgFooter.Columns[associatedColIndex];
int columnSpan = GetColumnSpan(colAssociated);
if (sourceColIndex >= dgMain.Columns.Count) break;
if (columnSpan <= 1)
{
var colSource = dgMain.Columns[sourceColIndex];
Binding binding = new Binding();
binding.Mode = BindingMode.TwoWay;
binding.Source = colSource;
binding.Path = new PropertyPath(DataGridColumn.WidthProperty);
BindingOperations.SetBinding(colAssociated, DataGridColumn.WidthProperty, binding);
if (bAllowColumnDisplayIndexSynchronization)
{
binding = new Binding();
binding.Mode = BindingMode.TwoWay;
binding.Source = colSource;
binding.Path = new PropertyPath(DataGridColumn.DisplayIndexProperty);
BindingOperations.SetBinding(colAssociated, DataGridColumn.DisplayIndexProperty, binding);
}
sourceColIndex++;
}
else
{
MultiBinding multiBinding = new MultiBinding();
multiBinding.Converter = WidthConverter;
for (int i = 0; i < columnSpan; i++)
{
var colSource = dgMain.Columns[sourceColIndex];
Binding binding = new Binding();
binding.Source = colSource;
binding.Path = new PropertyPath(DataGridColumn.WidthProperty);
multiBinding.Bindings.Add(binding);
binding = new Binding();
binding.Source = colSource;
multiBinding.Bindings.Add(binding);
sourceColIndex++;
}
BindingOperations.SetBinding(colAssociated, InternalWidthOnColumnProperty, multiBinding);
}
}
}
catch (Exception ex)
{
throw new Exception("BindFooterColumns:" + ex.Message);
}
}
In CreateFooterItemSource we look at the xMain items source and create a DataColumn for each element. The xMain items source can be a DataItemView or an IEnumerable of objects. We end up with a DataTable containing DataColumns with the same names as the xMain items source but with a datatype of String because the aggregate may not have the same data type as the aggregee (I just made that word up).
Once we have the data table's columns defined, we add a single row and bind it to xFooter's ItemsSource.
private static void CreateFooterItemSource(DataGrid dgMain, DataGrid dgFooter)
{
// Create the ItemSource for the Footer based on the itemSource for Main
DataTable DT;
try
{
if (dgMain.ItemsSource == null) return;
DT = new DataTable();
if (dgMain.ItemsSource.GetType().Name == "DataView")
{ // ItemsSource is a DataView
foreach (DataColumn DC in (dgMain.ItemsSource as DataView).Table.Columns)
{
DT.Columns.Add(new DataColumn(DC.ColumnName, Type.GetType("System.String")));
}
DT.Rows.Add(DT.NewRow());
dgFooter.ItemsSource = DT.DefaultView;
}
else
{ // ItemsSource is a collection
Type itemType = dgMain.ItemsSource.GetType().GetGenericArguments()[0];
foreach (PropertyInfo pi in itemType.GetProperties())
{
if (pi.Name != "ExtensionData")
DT.Columns.Add(new DataColumn(pi.Name, Type.GetType("System.String")));
}
DT.Rows.Add(DT.NewRow());
dgFooter.ItemsSource = DT.DefaultView;
}
}
catch (Exception ex)
{
throw new Exception("CreateFooterItemSource:" + ex.Message);
}
}
private static void FooterGrid_CurrentCellsChanged(object sender, EventArgs e)
{
DataGrid dgMain = sender as DataGrid;
Grid gLayout = dgMain.Parent as Grid;
DataGrid dgFooter = gLayout.FindName("xFooter") as DataGrid;
CalcFooterItemSource(dgMain, dgFooter);
}
private static void CalcFooterItemSource(DataGrid dgMain, DataGrid dgFooter)
{
// Recalculate Footer values and force update. Call this on initial load and whenever a
// cell in Main might have changed
// Add support for different aggregates as required
IEnumerable ItemsSource;
String Aggregate = "";
String Function = "";
String Column = "";
Decimal Result = 0;
Object oResult = null;
String sResult = "";
Type t = null;
try
{
ItemsSource = dgMain.ItemsSource;
if (ItemsSource == null) return;
dgMain.CommitEdit(); // Ensure the ItemsSource is up to date
foreach (DataGridBoundColumn c in dgFooter.Columns)
{
Aggregate = dgMain.Columns.First((mc) => mc.DisplayIndex == c.DisplayIndex).GetValue(FooterGrid.AggregateProperty).ToString();
if (Aggregate != "")
{
Result = 0;
oResult = null;
try
{
Function = Aggregate.Split('(')[0];
Column = Aggregate.Replace(Function, "").Replace("(", "").Replace(")", "");
if (Column != "")
t = GetItemType(ItemsSource, Column);
switch (Function.ToUpper().Trim())
{
case "SUM":
// Assume the addends can be converted to decimals
foreach (var item in ItemsSource)
{
Result += Convert.ToDecimal(GetItemValue(item, Column));
}
sResult = Result.ToString();
break;
case "COUNT":
foreach (var item in ItemsSource)
{
if (GetItemValue(item, Column) != null)
Result += 1;
}
sResult = Result.ToString();
break;
case "MAX":
// Max could be any type
foreach (var item in ItemsSource)
{
if (Comparer.DefaultInvariant.Compare(GetItemValue(item, Column), oResult) == 1)
oResult = Convert.ChangeType(GetItemValue(item, Column), t);
}
sResult = (oResult == null) ? "" : oResult.ToString();
break;
default:
sResult = Aggregate;
break;
}
// Get the path.path of the footer column's binding
String p = ((c as DataGridBoundColumn).Binding as Binding).Path.Path;
(dgFooter.ItemsSource as DataView)[0][p] = sResult;
}
catch (Exception ex)
{
throw new Exception(Aggregate + ":" + ex.Message);
}
}
}
// Crude but effective
IEnumerable fis = dgFooter.ItemsSource;
dgFooter.ItemsSource = null;
dgFooter.ItemsSource = fis;
}
catch (Exception ex)
{
throw new Exception("CalcFooterItemSource:" + ex.Message);
}
}
public static Type GetItemType(object ItemsSource, string Column)
{
return (ItemsSource as IEnumerable).GetType().GetGenericArguments()[0].GetProperty(Column).PropertyType;
}
public static object GetItemValue(object Item, String Column)
{
Object Result = null;
if (Item.GetType().Name == "DataRowView")
{ // Item is a data view
if (!(Item as DataRowView).Row.Table.Columns.Contains(Column))
throw new Exception("DataView does not contain column " + Column);
Result = Convert.ChangeType((Item as DataRowView)[Column], (Item as DataRowView).Row.Table.Columns[Column].DataType);
}
else
{ // Item is a list
Type itemType = Item.GetType();
PropertyInfo pi = itemType.GetProperty(Column);
if (pi == null)
throw new Exception("List does not contain column " + Column);
Result = Convert.ChangeType(pi.GetValue(Item, null), pi.PropertyType);
}
return Result;
}
public static String GetAggregate(DependencyObject obj)
{
return (String)obj.GetValue(AggregateProperty);
}
public static void SetAggregate(DependencyObject obj, String value)
{
obj.SetValue(AggregateProperty, value);
}
public static readonly DependencyProperty AggregateProperty =
DependencyProperty.RegisterAttached("Aggregate", typeof(String), typeof(FooterGrid), new UIPropertyMetadata(""));
public static int GetColumnSpan(DependencyObject obj)
{
return (int)obj.GetValue(ColumnSpanProperty);
}
public static void SetColumnSpan(DependencyObject obj, int value)
{
obj.SetValue(ColumnSpanProperty, value);
}
public static readonly DependencyProperty ColumnSpanProperty =
DependencyProperty.RegisterAttached("ColumnSpan", typeof(int), typeof(FooterGrid), new UIPropertyMetadata(1));
private static Nullable<Double> GetInternalWidthOnColumn(DependencyObject obj)
{
return (double)obj.GetValue(InternalWidthOnColumnProperty);
}
private static void SetInternalWidthOnColumn(DependencyObject obj, Nullable<Double> value)
{
obj.SetValue(InternalWidthOnColumnProperty, value);
}
private static readonly DependencyProperty InternalWidthOnColumnProperty =
DependencyProperty.RegisterAttached("InternalWidthOnColumn", typeof(Nullable<Double>), typeof(FooterGrid), new UIPropertyMetadata(null, InternalWidthOnColumnPropertyChanged));
public static Style GetColumnFooterStyle(DependencyObject obj)
{
return (Style)obj.GetValue(ColumnFooterStyleProperty);
}
public static void SetColumnFooterStyle(DependencyObject obj, Style value)
{
obj.SetValue(ColumnFooterStyleProperty, value);
}
public static readonly DependencyProperty ColumnFooterStyleProperty =
DependencyProperty.RegisterAttached("ColumnFooterStyle", typeof(Style), typeof(DataGridRow), new PropertyMetadata(null));
private static IMultiValueConverter WidthConverter = new WidthConverterClass();
private class WidthConverterClass : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
double result = 0;
foreach (var value in values)
{
DataGridColumn column = value as DataGridColumn;
if (column != null)
{
result = result + column.ActualWidth;
}
}
return result;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Finally, horizontal scrolling is synchronized through event handlers.
private static void InternalWidthOnColumnPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (e.NewValue != null)
{
((DataGridColumn)sender).Width = ((Nullable<double>)e.NewValue).Value;
}
}
private const string ScrollViewerNameInTemplate = "DG_ScrollViewer";
private static void xDataGrid_ScrollChanged(object sender, RoutedEventArgs eBase)
{
// The footer scrolled so the main must scroll to match
try
{
ScrollChangedEventArgs e = (ScrollChangedEventArgs)eBase;
ScrollViewer sourceScrollViewer = (ScrollViewer)e.OriginalSource;
DataGrid Main = (DataGrid)((Grid)((DataGrid)sender).Parent).FindName("xMain");
SynchronizeScrollHorizontalOffset(Main, sourceScrollViewer);
}
catch (Exception ex)
{
throw new Exception("xDataGrid_ScrollChanged:" + ex.Message);
}
}
private static void SynchronizeScrollHorizontalOffset(DataGrid FooterGrid, ScrollViewer sourceScrollViewer)
{
try
{
if (FooterGrid != null)
{
ScrollViewer associatedScrollViewer = (ScrollViewer)FooterGrid.Template.FindName(ScrollViewerNameInTemplate, FooterGrid);
associatedScrollViewer.ScrollToHorizontalOffset(sourceScrollViewer.HorizontalOffset);
}
}
catch (Exception ex)
{
throw new Exception("SynchronizeScrollHorizontalOffset:" + ex.Message);
}
}
So that's the custom control. Let's take a look at how we will consume it.
Because we have written this as a DataGrid, the consumer gets all the properties of a DataGrid (we just have to remember to copy them to xMain in our code). Let's add a new WPF Application project to our solution and call it TestFooterGrid.
Set the new project as the startup project. Now add a reference to the FooterGrid project. In the TestFooterGrid project, right-click on References and select Add Reference...
Let's start with a minimal MainWindow.xaml that looks like this. We added a reference to the FooterGrid project and replaced the default Grid with a reference to our FooterGrid. We also set our DataContext to our code behind.
Because we have written this as a DataGrid, the consumer gets all the properties of a DataGrid (we just have to remember to copy them to xMain in our code). Let's add a new WPF Application project to our solution and call it TestFooterGrid.
Set the new project as the startup project. Now add a reference to the FooterGrid project. In the TestFooterGrid project, right-click on References and select Add Reference...
Let's start with a minimal MainWindow.xaml that looks like this. We added a reference to the FooterGrid project and replaced the default Grid with a reference to our FooterGrid. We also set our DataContext to our code behind.
<Window x:Class="TestFooterGrid.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:TestFooterGrid"
xmlns:cc="clr-namespace:FooterGrid;assembly=FooterGrid"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<cc:FooterGrid AutoGenerateColumns="False" ItemsSource="{Binding PIs}" IsReadOnly="False" HeadersVisibility="Column">
<DataGrid.Columns>
</DataGrid.Columns>
</cc:FooterGrid>
</Window>
Before we flesh out the XAML, lets write the code behind. We just need a public enumerable property called PIs. Let's populate it with some information about the DateTime class. This is an easy way to populate a list with different types of data.
using System.Collections.Generic;
using System.Data;
using System.Reflection;
using System.Windows;
namespace TestFooterGrid
{
public partial class MainWindow : Window
{
public List<cPI> PIs { get; set; }
public MainWindow()
{
PIs = new List<cPI>();
foreach (PropertyInfo pi in typeof(System.DateTime).GetProperties())
{
cPI PI = new cPI();
PI.Name = pi.Name;
PI.Type = pi.PropertyType.Name;
PI.CustomAttributeCount = pi.GetCustomAttributes(false).Length;
PI.CanRead = pi.CanRead;
PI.CanWrite = pi.CanWrite;
PIs.Add(PI);
}
InitializeComponent();
}
}
public class cPI
{
public string Name { get; set; }
public string Type { get; set; }
public int CustomAttributeCount { get; set; }
public bool CanRead { get; set; }
public bool CanWrite { get; set; }
}
}
Now let's return to MainWindow.xaml and add some columns. Put this XAML between the <DataGrid.Columns> tags.
If you run the project now you can see a functional data grid with footers. Note it supports all column types and can even perform aggregation on non numeric data types (boolean).
Now let's style the header and the footer. The header is styled through the existing ColumnHeaderStyle property but the footer is styled through the new ColumnFooterStyle property like this.
Make MainWindow.xaml look like this.
<DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
<DataGridTextColumn Header="Type" Binding="{Binding Type}" cc:FooterGrid.Aggregate="Total Custom"/>
<DataGridTextColumn Header="Custom Attributes" Binding="{Binding CustomAttributeCount}" cc:FooterGrid.Aggregate="SUM(CustomAttributeCount)"></DataGridTextColumn>
<DataGridCheckBoxColumn Header="CanRead" Binding="{Binding CanRead}" cc:FooterGrid.Aggregate="SUM(CanRead)"/>
<DataGridCheckBoxColumn Header="CanWrite" Binding="{Binding CanWrite}" cc:FooterGrid.Aggregate="SUM(CanWrite)"/>
Now let's style the header and the footer. The header is styled through the existing ColumnHeaderStyle property but the footer is styled through the new ColumnFooterStyle property like this.
Make MainWindow.xaml look like this.
<Window x:Class="TestFooterGrid.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:TestFooterGrid"
xmlns:cc="clr-namespace:FooterGrid;assembly=FooterGrid"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Window.Resources>
<Style TargetType="DataGridColumnHeader" x:Key="Header">
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Background" Value="Silver"/>
<Setter Property="Padding" Value="4"/>
</Style>
<Style TargetType="DataGridRow" x:Key="Footer">
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Background" Value="BlanchedAlmond"/>
</Style>
</Window.Resources>
<cc:FooterGrid AutoGenerateColumns="False" ItemsSource="{Binding PIs}" IsReadOnly="True" HeadersVisibility="Column" ColumnHeaderStyle="{StaticResource Header}" cc:FooterGrid.ColumnFooterStyle="{StaticResource Footer}">
<DataGridTextColumn Header="Name" Binding="{Binding Name}"/>
<DataGridTextColumn Header="Type" Binding="{Binding Type}" cc:FooterGrid.Aggregate="Total Custom"/>
<DataGridTextColumn Header="Custom Attributes" Binding="{Binding CustomAttributeCount}" cc:FooterGrid.Aggregate="SUM(CustomAttributeCount)"></DataGridTextColumn>
<DataGridCheckBoxColumn Header="CanRead" Binding="{Binding CanRead}" cc:FooterGrid.Aggregate="SUM(CanRead)"/>
<DataGridCheckBoxColumn Header="CanWrite" Binding="{Binding CanWrite}" cc:FooterGrid.Aggregate="SUM(CanWrite)"/>
</cc:FooterGrid>
</Window>
The result looks like this.
Let's right-align the custom attributes column. Change the column definition to look like this.
Let's right-align the custom attributes column. Change the column definition to look like this.
<DataGridTextColumn Header="Custom Attributes" Binding="{Binding CustomAttributeCount}" cc:FooterGrid.Aggregate="SUM(CustomAttributeCount)">
<DataGridTextColumn.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="HorizontalAlignment" Value="Right"/>
</Style>
</DataGridTextColumn.CellStyle>
</DataGridTextColumn>
Note that the footer changed alignment too because we are copying the xMain column's cell style to xFooter.
Is it possible to put 2 FooterGrids in a tab control? When the 2nd footer grid is being initialized, the dgMain variable is null.
ReplyDelete