Saturday, November 18, 2017

MIT App Inventor on iOS

A few years ago, when I wanted to get started on writing mobile apps, I came across MIT App Inventor which is a click and drag interface for building Android programs. I was very impressed with their offering and wrote several simple apps including "Find my car" and a battery monitor.

I recently received an email from them saying they are crowd funding an effort to extend their development platform to iOS. I thought this was such a good idea, I sent them some money.


Monday, November 13, 2017

Editable DataGrid with footers as a custom control

In an earlier post I showed how to implement an editable DataGrid with fixed footers as a user control. In this post I will implement the same data grid as a custom control and add several new features such as

  • 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>

We want our custom control to act like a DataGrid. It will have an ItemsSource, column definitions, etc so we will inherit a DataGrid. Change FooterGrid.cs so it looks like this.

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)));
        }
    }
}

The first thing we need to do is set up a Loaded and Unloaded event handler while in the constructor. The Loaded event handler will do most of the hard work, and the Unloaded event handler will remove event handlers to avoid memory leaks. The constructor looks like this.

public FooterGrid()
{
    this.Loaded += FooterGrid_Loaded;
    this.Unloaded += FooterGrid_Unloaded;
}

The Footer_Loaded method does all the work. It copies all the properties from the custom control to the xMain control. I have only copied a few properties. Add code to copy any other properties you might need similar to ColumnHeaderStyle.

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;
}

We need to detect changes to the ItemsSource and recalculate the footer values.

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);
}

You cannot simply copy a DataGridColumn from one data grid to another. You have to create a new DataGridColumn with the same properties and then add the new DataGridColumn to the xMain column collection. For xMain, we do this by serializing the old column and deserializing into a new column. For xFooter, we know we are creating a DataGridTextColumn. Note the use of DataGridBoundColumn which is the base class for all bound datagrid columns.

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;
}

The BindFooterColumns method will cause the order and width of the xFooter columns to match those of the xMain columns. It is unchanged from the user version of the control.

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);
    }
}

On initial load, whenever the user changes the current cell, or whenever the ItemsSource is changed, we need to recalculate the footer values. Because the underlying data can be of any type, we use a lot of Reflection in the footer calculation code.

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;
}


Now for some dependency properties. You can add more if you want to add more new properties such as a footer cell style.

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));

 Now we need a width converter that sums column widths for a footer that spans multiple columns.

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.

<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.


<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)"/>


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.


<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.

<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.


The grid is editable. Try changing one of the Custom Attributes to another number (you have to click on the number to edit it) and tab off. You will see the total is recalculated immediately. It also works if you add or delete lines.