Friday, December 8, 2017

WPF Password control with bindable text

If you have included a password control in a MVVM solution you know that Microsoft has not made the password property a dependency property. This makes the password control pretty much useless.

It occurred to me that a regular text box control with a custom font would work as long as I prevented cut and paste operations on it. For the purpose of this demonstration I also limit the password to digits because that's the requirement I have to satisfy for my current project.

Let's start off by creating a new WPF application project called "Password".


I got my font from https://github.com/davidagraf/passwd/blob/master/public/ttf/password.ttf, thank you David. Create a folder in the project called Fonts and drop the ttf file into the folder. Make sure  Build Action = Resource and Copy to Output Directory = Do not copy.

I'm going to show you a window with several components. The interesting one is the TextBox. The FontFamily path is important to get right. The PreviewExecuted event handler suppresses edit keyboard shortcuts, the PreviewKeyDown suppresses non digit key presses, and the ContextMenu suppresses the text box's default context sensitive menu (because it contains shortcuts to edit commands).

<Window x:Class="Password.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:Password"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <StackPanel Orientation="Horizontal" Height="20">
        <TextBlock Text="Password: "/>
        <TextBox FontFamily="pack://application:,,,/Fonts/#Password" Text="{Binding Password, UpdateSourceTrigger=PropertyChanged}"
                 Width="100" CommandManager.PreviewExecuted="TextBox_PreviewExecuted" PreviewKeyDown="TextBox_PreviewKeyDown"
                 ContextMenu="{x:Null}" MaxLength="6"/>
        <TextBlock Text="{Binding Password}" Margin="10,0,0,0"/>
    </StackPanel>
</Window>


It's very important we include the font in the application. If we depended on the user having the font installed, and they didn't, then the password would appear in clear text.

The code behind implements INotifyPropertyChanged and defines a password property. It also contains the event handlers referenced in the XAML.

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;

namespace Password
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private String _Password;
        public String Password
        {
            get { return _Password; }
            set
            {
                if (!value.Equals(_Password))
                {
                    _Password = value;
                    NotifyPropertyChanged("Password");
                }
            }
        }

        public MainWindow()
        {
            InitializeComponent();
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged(String propertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
        private void TextBox_PreviewExecuted(object sender, ExecutedRoutedEventArgs e)
        {
            // Suppress copy and paste
            if (e.Command == ApplicationCommands.Cut ||
                e.Command == ApplicationCommands.Copy ||
                e.Command == ApplicationCommands.Paste)
                e.Handled = true;
        }

        private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)
        {
            // Allow cursor movement and backspace
            if (e.Key == Key.Back || e.Key == Key.Left || e.Key == Key.Right)
                return;

            String c = e.Key.ToString().Replace("NumPad", "").Replace("D", "");

            // Suppress any keypress that is not a digit
            if (c.Length != 1)
                e.Handled = true;
            else
             if (!char.IsDigit(Convert.ToChar(c)))
                e.Handled = true;
        }
    }
}


Thursday, December 7, 2017

Footer grid with custom aggregates

Developing on my DataGrid with footers custom control at http://wpfthoughts.blogspot.com/2017/11/editable-datagrid-with-footers-as.html I decided to implement custom aggregates. Basically this is a way to allow the grid's host to implement custom aggregates by allowing the grid to ask the host what the footer's value should be.

The obvious way to implement this is to create a event the control can raise when it sees a column's aggregate is custom. The page can subscribe to the event and provide a value.

The custom aggregate will be triggered when the column's aggregate has the form "CUSTOM(stringvalue)". The event will use a custom event args that includes an Aggregate property that will contain stringvalue. The event will pass a reference to the grid in the sender argument.

If you follow through the blog entry referenced above you will end up with a solution that displays the page below.


We will alter the aggregate for the CanRead and CanWrite columns to display the text "Found" in the appropriate column footer if any properties can be read or written.

Let's start by defining our custom event args in FooterGrid.cs before the definition of the FooterGrid class.

namespace FooterGrid
{
    public class FooterValueArgs : EventArgs
    {
        public string Aggregate { get; set; }
        public FooterValueArgs(String Aggregate)
        {
            this.Aggregate = Aggregate;
        }
    }

    public class FooterGrid : DataGrid



We need a delegate because we want the event to return the value to be displayed. We could pass this value back in the custom event argument, but I prefer the approach I'm taking here. Let's add that delegate, the event definition, and the event handler just after the declaration of the FooterGrid class.

    public class FooterGrid : DataGrid
    {
        public delegate String FooterValueEventHandler(object sender, FooterValueArgs args);
        public event FooterValueEventHandler FooterValueEvent;
        public String OnFooterValueEvent(FooterValueArgs e)
        {
            if (FooterValueEvent != null)
                return FooterValueEvent(this, e);
            else
                return "";
        }


Now our event is defined we just have to raise it when the column's aggregate starts with CUSTOM. Find CalcFooterItemSource and add a new case. The Column variable simply contains the part of the aggregate between parentheses. ie if the aggregate is "CUSTOM(Age)" then Column contains "Age".

case "CUSTOM":
    sResult = OnFooterValueEvent(new FooterValueArgs(Column));
    break;


That's all we have to do to enhance the FooterGrid. Now we need to consume this new feature.

In MainWindow.xaml we need to tell the footer grid which event handler will process custom aggregates and also create a custom aggregate. Make the following changes...

<cc:FooterGrid AutoGenerateColumns="False" ItemsSource="{Binding PIs}" IsReadOnly="False" HeadersVisibility="Column" ColumnHeaderStyle="{StaticResource Header}" cc:FooterGrid.ColumnFooterStyle="{StaticResource Footer}" FooterValueEvent="FooterGrid_FooterValueEvent">
    <DataGrid.Columns>
        <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.CellStyle>
                <Style TargetType="DataGridCell">
                    <Setter Property="HorizontalAlignment" Value="Right"/>
                </Style>
            </DataGridTextColumn.CellStyle>
        </DataGridTextColumn>
        <DataGridCheckBoxColumn Header="CanRead" Binding="{Binding CanRead}" cc:FooterGrid.Aggregate="CUSTOM(CanRead)"/>
        <DataGridCheckBoxColumn Header="CanWrite" Binding="{Binding CanWrite}" cc:FooterGrid.Aggregate="CUSTOM(CanWrite)"/>
    </DataGrid.Columns>
</cc:FooterGrid>

Finally we have to write FooterGrid_FooterValueEvent in MainWindow.xaml.cs. Just add the following method in the class.

private string FooterGrid_FooterValueEvent(object sender, FooterGrid.FooterValueArgs args)
{
    FooterGrid.FooterGrid fg = sender as FooterGrid.FooterGrid;
    switch(args.Aggregate.ToUpper())
    {
        case "CANREAD":
            return (fg.ItemsSource as List<cPI>).Find((pi) => pi.CanRead) == null ? "" : "Found";
        case "CANWRITE":
            return (fg.ItemsSource as List<cPI>).Find((pi) => pi.CanWrite) == null ? "" : "Found";
        default:
            return "";
    }
}


Because the entire footer grid is passed as the sender, you can get access to the main grid and footer grid via their template names xMain and xFooter. You can then get to the grids' item sources. The only thing you can't get to is footer values to the right of the footer you are processing because they have not been calculated yet.

The result of our work looks like this.


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.