소스 검색

DynamicTreeGrid summary row things

Kenric Nugteren 7 달 전
부모
커밋
32a98a7b28
2개의 변경된 파일416개의 추가작업 그리고 93개의 파일을 삭제
  1. 356 67
      inabox.wpf/DynamicGrid/UIComponent/DynamicGridTreeUIComponent.cs
  2. 60 26
      inabox.wpf/WPFUtils.cs

+ 356 - 67
inabox.wpf/DynamicGrid/UIComponent/DynamicGridTreeUIComponent.cs

@@ -22,6 +22,7 @@ using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using Syncfusion.UI.Xaml.TreeGrid.Filtering;
 using Syncfusion.UI.Xaml.TreeGrid.Cells;
+using System.Windows.Controls.Primitives;
 
 namespace InABox.DynamicGrid;
 
@@ -67,6 +68,7 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
 
     private ContextMenu _menu;
     private SfTreeGrid _tree;
+    private Grid _summaryRow;
     private readonly ContextMenu ColumnsMenu;
 
     public event OnContextMenuOpening OnContextMenuOpening;
@@ -181,13 +183,6 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
     private DynamicGridCellStyleConverter<System.Windows.FontStyle?> CellFontStyleConverter;
     private DynamicGridCellStyleConverter<System.Windows.FontWeight?> CellFontWeightConverter;
 
-    protected virtual Brush? GetCellBackground(CoreRow row, DynamicColumnBase column) => null;
-    protected virtual Brush? GetCellForeground(CoreRow row, DynamicColumnBase column) => null;
-    protected virtual double? GetCellFontSize(CoreRow row, DynamicColumnBase column) => null;
-    protected virtual FontStyle? GetCellFontStyle(CoreRow row, DynamicColumnBase column) => null;
-    protected virtual FontWeight? GetCellFontWeight(CoreRow row, DynamicColumnBase column) => null;
-
-
     public DynamicGridTreeUIComponent(Expression<Func<T, Guid>> idColumn, Expression<Func<T, Guid>> parentIDColumn)
     {
         IDColumn = new Column<T>(CoreUtils.GetFullPropertyName(idColumn, "."));
@@ -216,6 +211,8 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         _tree.CellDoubleTapped += _tree_CellDoubleTapped;
         _tree.KeyUp += _tree_KeyUp;
 
+        _tree.Loaded += _tree_Loaded;
+
         _tree.CellToolTipOpening += _tree_CellToolTipOpening;
 
         _menu = new ContextMenu();
@@ -272,6 +269,90 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         _tree.SizeChanged += _tree_SizeChanged;
     }
 
+    private void _tree_Loaded(object sender, RoutedEventArgs e)
+    {
+        _summaryRow = new Grid();
+        _summaryRow.Visibility = Visibility.Collapsed;
+
+        var scroll = _tree.FindChild<ScrollViewer>("PART_ScrollViewer");
+        if(scroll is not null)
+        {
+            var grid = scroll.FindVisualChildren<Grid>(recursive: false).FirstOrDefault();
+            if(grid is not null)
+            {
+                var row1 = grid.RowDefinitions[0];
+                var row2 = grid.RowDefinitions[1];
+                grid.RowDefinitions.Clear();
+                grid.RowDefinitions.Add(row1);
+                var rowDef = grid.AddRow(GridUnitType.Auto);
+
+                grid.RowDefinitions.Add(row2);
+
+                foreach(var child in grid.Children.OfType<UIElement>())
+                {
+                    var row = Grid.GetRow(child);
+                    if(row >= 1)
+                    {
+                        Grid.SetRow(child, row + 1);
+                    }
+                    else
+                    {
+                        var rowSpan = Grid.GetRowSpan(child);
+                        if(row + rowSpan >= 1 && child is ScrollBar)
+                        {
+                            Grid.SetRowSpan(child, rowSpan + 1);
+                        }
+                    }
+                }
+                var horizontalScrollbar = grid.Children.OfType<ScrollBar>().First(x => x.Orientation == Orientation.Horizontal);
+
+                var treeGridPanel = scroll.FindChild<TreeGridPanel>("PART_TreeGridPanel");
+
+                var summaryScroll = new ScrollViewer
+                {
+                    VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
+                    HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden
+                };
+                summaryScroll.Content = _summaryRow;
+
+                scroll.ScrollChanged += (o, e) =>
+                {
+                    summaryScroll.ScrollToHorizontalOffset(scroll.HorizontalOffset);
+
+                    var panel = treeGridPanel;
+                };
+
+                var transform = new TranslateTransform();
+                summaryScroll.RenderTransform = transform;
+
+                void UpdateSize(double height)
+                {
+                    var desiredHeight = treeGridPanel.RowHeights.TotalExtent;
+                    if(desiredHeight < height)
+                    {
+                        var diff = height - desiredHeight;
+                        transform.Y = -diff - 1;
+                    }
+                    else
+                    {
+                        transform.Y = 0;
+                    }
+                }
+
+                treeGridPanel.SizeChanged += (o, e) =>
+                {
+                    UpdateSize(e.NewSize.Height);
+                };
+                _tree.FilterChanged += (o, e) =>
+                {
+                    UpdateSize(treeGridPanel.ActualHeight);
+                };
+
+                grid.AddChild(summaryScroll, 1, 0);
+            }
+        }
+    }
+
     private class TreeGridSelectionControllerExt(SfTreeGrid treeGrid, DynamicGridTreeUIComponent<T> grid) : TreeGridRowSelectionController(treeGrid)
     {
         private DynamicGridTreeUIComponent<T> Grid = grid;
@@ -496,6 +577,7 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         Parent.UIFilterChanged(this);
 
         UpdateRecordCount();
+        CalculateSummaries();
     }
 
     public void AddVisualFilter(string column, string value, FilterType filtertype = FilterType.Contains)
@@ -604,6 +686,88 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         );
     }
 
+    #region Styles
+
+    protected virtual Brush? GetCellBackground(CoreRow row, DynamicColumnBase column) => null;
+    protected virtual Brush? GetCellForeground(CoreRow row, DynamicColumnBase column) => null;
+    protected virtual double? GetCellFontSize(CoreRow row, DynamicColumnBase column) => null;
+    protected virtual FontStyle? GetCellFontStyle(CoreRow row, DynamicColumnBase column) => null;
+    protected virtual FontWeight? GetCellFontWeight(CoreRow row, DynamicColumnBase column) => null;
+
+    protected virtual Brush? GetCellSelectionForegroundBrush() => DynamicGridUtils.SelectionForeground;
+    protected virtual Brush? GetCellSelectionBackgroundBrush() => DynamicGridUtils.SelectionBackground;
+
+    protected virtual Style GetHeaderCellStyle(DynamicColumnBase column)
+    {
+        var headStyle = new Style(typeof(TreeGridHeaderCell));
+
+        headStyle.Setters.Add(new Setter(Control.BackgroundProperty, new SolidColorBrush(Colors.Gainsboro)));
+        headStyle.Setters.Add(new Setter(Control.ForegroundProperty, new SolidColorBrush(Colors.Black)));
+        headStyle.Setters.Add(new Setter(Control.FontSizeProperty, 12D));
+
+        if(column is DynamicActionColumn actionColumn)
+        {
+            headStyle.Setters.Add(new Setter(Control.BorderThicknessProperty, new Thickness(0.0)));
+            headStyle.Setters.Add(new Setter(Control.MarginProperty, new Thickness(0, 0, 1, 1)));
+            if(column is DynamicImageColumn imgCol)
+            {
+                if (imgCol.HeaderText.IsNullOrWhiteSpace())
+                {
+                    var image = imgCol.Image?.Invoke(null);
+                    if (image != null)
+                    {
+                        var template = new ControlTemplate(typeof(GridHeaderCellControl));
+                        var border = new FrameworkElementFactory(typeof(Border));
+                        border.SetValue(Border.BackgroundProperty, new SolidColorBrush(Colors.Gainsboro));
+                        border.SetValue(Border.PaddingProperty, new Thickness(4));
+                        var img = new FrameworkElementFactory(typeof(Image));
+                        img.SetValue(Image.SourceProperty, image);
+                        border.AppendChild(img);
+                        template.VisualTree = border;
+                        headStyle.Setters.Add(new Setter(Control.TemplateProperty, template));
+                    }
+                }
+            }
+            if (actionColumn.VerticalHeader && !actionColumn.HeaderText.IsNullOrWhiteSpace())
+            {
+                headStyle.Setters.Add(new Setter(Control.HorizontalContentAlignmentProperty, HorizontalAlignment.Left));
+                headStyle.Setters.Add(new Setter(Control.TemplateProperty,
+                    Application.Current.Resources["VerticalColumnHeader"] as ControlTemplate));
+            }
+        }
+        return headStyle;
+    }
+
+    protected virtual Style GetSummaryCellStyle(DynamicColumnBase column)
+    {
+        var style = new Style(typeof(SummaryCellControl));
+
+        style.Setters.Add(new Setter(Control.BackgroundProperty, new SolidColorBrush(Colors.Gainsboro)));
+        style.Setters.Add(new Setter(Control.ForegroundProperty, new SolidColorBrush(Colors.Black)));
+
+        if(column is DynamicGridColumn gridColumn)
+        {
+            style.Setters.Add(new Setter(Control.HorizontalContentAlignmentProperty,
+                column != null ? gridColumn.HorizontalAlignment(typeof(double)) : HorizontalAlignment.Right));
+        }
+        else
+        {
+            style.Setters.Add(new Setter(Control.HorizontalContentAlignmentProperty, HorizontalAlignment.Right));
+        }
+        style.Setters.Add(new Setter(Control.VerticalContentAlignmentProperty, VerticalAlignment.Center));
+
+        style.Setters.Add(new Setter(Control.BorderBrushProperty, new SolidColorBrush(Colors.Gray)));
+        style.Setters.Add(new Setter(Control.BorderThicknessProperty, new Thickness(0, 0.75, 0.75, 0.0)));
+        style.Setters.Add(new Setter(Control.FontSizeProperty, 12D));
+        style.Setters.Add(new Setter(Control.FontWeightProperty, FontWeights.DemiBold));
+
+        style.Setters.Add(new Setter(Control.PaddingProperty, new Thickness(4)));
+
+        return style;
+    }
+
+    #endregion
+
     #region Sizing
 
     public double RowHeight
@@ -754,6 +918,8 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
 
     private List<DynamicActionColumn> ActionColumns = new();
 
+    private List<Summary> Summaries = new();
+
     private readonly Dictionary<string, string> FilterPredicates = new();
 
     private DynamicColumnBase? GetColumn(int index) =>
@@ -812,6 +978,12 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
             {
                 var sColName = $"[_ActionColumn{i}]";
 
+                var summary = column.Summary();
+                if(summary is not null)
+                {
+                    Summaries.Add(new(column, summary, null, null));
+                }
+
                 if (column is DynamicImageColumn imgcol)
                 {
                     var newcol = new TreeGridTemplateColumn();
@@ -840,40 +1012,9 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                     newcol.ShowToolTip = column.ToolTip != null;
                     newcol.ShowHeaderToolTip = column.ToolTip != null;
 
-                    var headstyle = new Style(typeof(TreeGridHeaderCell));
-                    headstyle.Setters.Add(new Setter(Control.BackgroundProperty, new SolidColorBrush(Colors.Gainsboro)));
-                    headstyle.Setters.Add(new Setter(Control.ForegroundProperty, new SolidColorBrush(Colors.Black)));
-                    headstyle.Setters.Add(new Setter(Control.FontSizeProperty, 12D));
+                    var headstyle = GetHeaderCellStyle(column);
                     headstyle.Setters.Add(new EventSetter(Control.MouseLeftButtonUpEvent, new MouseButtonEventHandler(HeaderCell_LeftMouseButtonEvent)));
-                    if (!string.IsNullOrWhiteSpace(column.HeaderText))
-                    {
-                        //headstyle.Setters.Add(new Setter(LayoutTransformProperty, new RotateTransform(270.0F)));
-                        headstyle.Setters.Add(new Setter(Control.BorderThicknessProperty, new Thickness(0.0, 0.0, 0, 0)));
-                        headstyle.Setters.Add(new Setter(Control.MarginProperty, new Thickness(0, 0, 1, 1)));
-                        if (imgcol.VerticalHeader)
-                            headstyle.Setters.Add(new Setter(Control.TemplateProperty,
-                                Application.Current.Resources["VerticalColumnHeader"] as ControlTemplate));
-                    }
-                    else
-                    {
-                        var image = imgcol.Image?.Invoke(null);
-                        if (image != null)
-                        {
-                            var template = new DataTemplate(typeof(TreeGridHeaderCell));
-                            var border = new FrameworkElementFactory(typeof(Border));
-                            border.SetValue(Border.BackgroundProperty, new SolidColorBrush(Colors.Gainsboro));
-                            border.SetValue(Border.PaddingProperty, new Thickness(4));
-                            border.SetValue(Control.MarginProperty, new Thickness(0, 0, 1, 1));
-                            var img = new FrameworkElementFactory(typeof(Image));
-                            img.SetValue(Image.SourceProperty, image);
-                            border.AppendChild(img);
-                            template.VisualTree = border;
-                            headstyle.Setters.Add(new Setter(TreeGridHeaderCell.PaddingProperty, new Thickness(0)));
-                            headstyle.Setters.Add(new Setter(TreeGridHeaderCell.ContentTemplateProperty, template));
-                        }
-                    }
-
-                    newcol.HeaderStyle = headstyle;
+                    newcol.HeaderStyle = GetHeaderCellStyle(column);
 
                     _tree.Columns.Add(newcol);
                     ColumnList.Add(column);
@@ -905,20 +1046,8 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                     
                     ApplyFilterStyle(newcol, false, true);
 
-                    var headstyle = new Style(typeof(TreeGridHeaderCell));
-                    headstyle.Setters.Add(new Setter(Control.BackgroundProperty, new SolidColorBrush(Colors.Gainsboro)));
-                    headstyle.Setters.Add(new Setter(Control.ForegroundProperty, new SolidColorBrush(Colors.Black)));
-                    headstyle.Setters.Add(new Setter(Control.FontSizeProperty, 12D));
-                    headstyle.Setters.Add(new Setter(Control.MarginProperty, new Thickness(0, -0.75, 0, 0.75)));
-                    headstyle.Setters.Add(new Setter(Control.BorderThicknessProperty, new Thickness(0.75)));
+                    var headstyle = GetHeaderCellStyle(column);
                     headstyle.Setters.Add(new EventSetter(Control.MouseLeftButtonUpEvent, new MouseButtonEventHandler(HeaderCell_LeftMouseButtonEvent)));
-                    if (txtCol.VerticalHeader)
-                    {
-                        headstyle.Setters.Add(new Setter(Control.HorizontalContentAlignmentProperty, HorizontalAlignment.Left));
-                        headstyle.Setters.Add(new Setter(Control.TemplateProperty,
-                            Application.Current.Resources["VerticalColumnHeader"] as ControlTemplate));
-                    }
-
                     newcol.HeaderStyle = headstyle;
 
                     _tree.Columns.Add(newcol);
@@ -942,12 +1071,7 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                     
                     ApplyFilterStyle(newcol, false, true);
 
-                    var headstyle = new Style(typeof(TreeGridHeaderCell));
-                    headstyle.Setters.Add(new Setter(Control.BackgroundProperty, new SolidColorBrush(Colors.Gainsboro)));
-                    headstyle.Setters.Add(new Setter(Control.ForegroundProperty, new SolidColorBrush(Colors.Black)));
-                    headstyle.Setters.Add(new Setter(Control.FontSizeProperty, 12D));
-                    headstyle.Setters.Add(new Setter(Control.MarginProperty, new Thickness(0, -0.75, 0, 0.75)));
-                    headstyle.Setters.Add(new Setter(Control.BorderThicknessProperty, new Thickness(0.75)));
+                    var headstyle = GetHeaderCellStyle(column);
                     headstyle.Setters.Add(new EventSetter(Control.MouseLeftButtonUpEvent, new MouseButtonEventHandler(HeaderCell_LeftMouseButtonEvent)));
                     newcol.HeaderStyle = headstyle;
 
@@ -969,13 +1093,15 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
 
                 var newColumn = newcol.CreateTreeGridColumn();
 
+                var summary = newcol.Summary();
+                if(summary is not null)
+                {
+                    Summaries.Add(new(column, summary, null, null));
+                }
+
                 ApplyFilterStyle(newColumn, newcol.Filtered, false);
                 
-                var headstyle = new Style(typeof(TreeGridHeaderCell));
-                headstyle.Setters.Add(new Setter(Control.BackgroundProperty, new SolidColorBrush(Colors.Gainsboro)));
-                headstyle.Setters.Add(new Setter(Control.ForegroundProperty, new SolidColorBrush(Colors.Black)));
-                headstyle.Setters.Add(new Setter(Control.FontSizeProperty, 12D));
-                newColumn.HeaderStyle = headstyle;
+                newColumn.HeaderStyle = GetHeaderCellStyle(column);
 
                 var cellstyle = new Style();
                 if (Parent.IsDirectEditMode())
@@ -1086,6 +1212,7 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
 
         ColumnList.Clear();
         _tree.Columns.Clear();
+        Summaries.Clear();
 
         ActionColumns = actionColumns.ToList();
         
@@ -1130,12 +1257,164 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         grid.Dispatcher.BeginInvoke(() =>
         {
             foreach (var (index, size) in this.CalculateColumnSizes(width))
-                _tree.Columns[index].Width = Math.Max(0.0F, size);
+            {
+                var colSize = Math.Max(0.0F, size);
+                _tree.Columns[index].Width = colSize;
+            }
+
+            RebuildSummaryRow();
         });
     }
 
     #endregion
 
+    #region Summary
+
+    private class Summary(DynamicColumnBase column, IDynamicGridSummary summary, SummaryCellControl? control, object? data)
+    {
+        public DynamicColumnBase Column { get; set; } = column;
+
+        public IDynamicGridSummary SummaryDefinition { get; set; } = summary;
+
+        public SummaryCellControl? Control { get; set; } = control;
+
+        public object? Data { get; set; } = data;
+    }
+
+    protected class SummaryCellControl : ContentControl
+    {
+        public SummaryCellControl()
+        {
+            var template = new ControlTemplate(typeof(ContentControl));
+
+            var factory = new FrameworkElementFactory(typeof(Border));
+            factory.Bind<ContentControl, Brush>(Border.BorderBrushProperty, x => x.BorderBrush);
+            factory.Bind<ContentControl, Thickness>(Border.BorderThicknessProperty, x => x.BorderThickness);
+            factory.Bind<ContentControl, Brush>(Border.BackgroundProperty, x => x.Background);
+            factory.Bind<ContentControl, Thickness>(Border.PaddingProperty, x => x.Padding);
+
+            var content = new FrameworkElementFactory(typeof(ContentPresenter));
+            content.Bind<ContentControl, HorizontalAlignment>(ContentPresenter.HorizontalAlignmentProperty, x => x.HorizontalContentAlignment);
+            content.Bind<ContentControl, VerticalAlignment>(ContentPresenter.VerticalAlignmentProperty, x => x.VerticalContentAlignment);
+            factory.AppendChild(content);
+            template.VisualTree = factory;
+
+            Template = template;
+        }
+    }
+
+    private void RebuildSummaryRow()
+    {
+        _summaryRow.RowDefinitions.Clear();
+        _summaryRow.ColumnDefinitions.Clear();
+        _summaryRow.Children.Clear();
+
+        var row = _summaryRow.AddRow(GridUnitType.Auto);
+        row.MinHeight = _tree.RowHeight;
+
+        foreach(var (i, column) in _tree.Columns.WithIndex())
+        {
+            _summaryRow.AddColumn(column.ActualWidth);
+
+            var cell = new SummaryCellControl();
+            if(GetColumn(i) is DynamicColumnBase col)
+            {
+                cell.Style = GetSummaryCellStyle(col);
+
+                var summary = Summaries.FirstOrDefault(x => x.Column == col);
+                if(summary is not null)
+                {
+                    cell.Content = summary.Data;
+                    summary.Control = cell;
+                }
+            }
+            _summaryRow.AddChild(cell, 0, _summaryRow.ColumnDefinitions.Count - 1);
+        }
+    }
+
+    private void CalculateSummaries()
+    {
+        foreach(var column in ColumnList)
+        {
+            CalculateSummary(column);
+        }
+    }
+
+    private object? CalculateSummaryData(IDynamicGridSummary summary, DynamicColumnBase column)
+    {
+        if(summary is DynamicGridCountSummary count)
+        {
+            return string.Format("{0:N0}", _tree.View.Nodes.Count);
+        }
+        else if(summary is DynamicGridSumSummary sum)
+        {
+            if(column is DynamicGridColumn gridColumn)
+            {
+                var data = _tree.View.Nodes.Select(x => MapRow((x.Item as CoreTreeNode)?.Row)).NotNull()
+                    .Select(x => x[gridColumn.ColumnName]);
+
+                object? result;
+                if(sum.AggregateType == typeof(double))
+                {
+                    result = data.Sum(x => x is double d ? d : 0);
+                }
+                else if(sum.AggregateType == typeof(int))
+                {
+                    result = data.Sum(x => x is int i ? i : 0);
+                }
+                else if(sum.AggregateType == typeof(TimeSpan))
+                {
+                    result = data.Aggregate(TimeSpan.Zero, (cur, x) => x is TimeSpan ts ? cur + ts : cur);
+                }
+                else
+                {
+                    result = null;
+                }
+                if(result is not null)
+                {
+                    return string.Format($"{{0:{sum.Format}}}", result);
+                }
+            }
+        }
+        else if(summary is DynamicGridCustomSummary custom)
+        {
+            var data = _tree.View.Nodes.Select(x => MapRow((x.Item as CoreTreeNode)?.Row)).NotNull();
+            var result = custom.Aggregate(data);
+            if(result is not null)
+            {
+                return string.Format($"{{0:{custom.Format}}}", result);
+            }
+        }
+        else if(summary is DynamicGridTemplateSummary template)
+        {
+            return template.Template();
+        }
+        return null;
+    }
+
+    private void CalculateSummary(DynamicColumnBase column)
+    {
+        var (idx, summary) = Summaries.WithIndex().FirstOrDefault(x => x.Value.Column == column);
+        if(summary is null)
+        {
+            return;
+        }
+        var colIdx = ColumnList.IndexOf(summary.Column);
+        if(colIdx == -1)
+        {
+            return;
+        }
+
+        summary.Data = CalculateSummaryData(summary.SummaryDefinition, column);
+
+        if(summary.Control is not null)
+        {
+            summary.Control.Content = summary.Data;
+        }
+    }
+
+    #endregion
+
     #region Refresh
 
     public CoreTreeNodes Nodes { get; set; }
@@ -1145,8 +1424,12 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
 
     public void BeforeRefresh()
     {
-        _tree.SelectionForeground = DynamicGridUtils.SelectionForeground;
-        _tree.SelectionBackground = DynamicGridUtils.SelectionBackground;
+        if(_summaryRow is not null)
+        {
+            _summaryRow.Visibility = Visibility.Collapsed;
+        }
+        _tree.SelectionForeground = GetCellSelectionForegroundBrush();
+        _tree.SelectionBackground = GetCellSelectionBackgroundBrush();
     }
 
     public void RefreshData(CoreTable data)
@@ -1181,6 +1464,9 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         nodes.ColumnChanged += Nodes_ColumnChanged;
         Nodes = nodes;
         _tree.ItemsSource = nodes.Nodes;
+        _summaryRow.Visibility = Visibility.Visible;
+
+        CalculateSummaries();
 
         CalculateRowHeight();
         ResizeColumns(_tree, _tree.ActualWidth - 1, _tree.ActualHeight);
@@ -1203,6 +1489,7 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
             var _parent = row.Get<Guid>(ParentColumn.Property);
             Nodes.Add(_id, _parent, newRow);
         }
+        CalculateSummaries();
 
         CalculateRowHeight();
         UpdateRecordCount();
@@ -1456,6 +1743,8 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         if (col is null || dataCol is null)
             return;
 
+        CalculateSummary(col);
+
         if (col is DynamicGridCheckBoxColumn<T>)
         {
             EnsureEditingObject(row);

+ 60 - 26
inabox.wpf/WPFUtils.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Drawing;
+using System.Linq;
 using System.Linq.Expressions;
 using System.Windows;
 using System.Windows.Controls;
@@ -84,8 +85,8 @@ public static class WPFUtils
     public static Binding CreateBinding<T, TProperty>(
         T source,
         Expression<Func<T, TProperty>> expression,
-        IValueConverter? converter,
-        string? format)
+        IValueConverter? converter = null,
+        string? format = null)
     {
         return new Binding(CoreUtils.GetFullPropertyName(expression, "_"))
         {
@@ -97,15 +98,17 @@ public static class WPFUtils
 
     public static Binding CreateBinding<T, TProperty>(
         Expression<Func<T, TProperty>> expression, 
-        IValueConverter? converter,
-        BindingMode mode,
-        string? format)
+        IValueConverter? converter = null,
+        BindingMode mode = BindingMode.Default,
+        string? format = null,
+        RelativeSource? relativeSource = null)
     {
         return new Binding(CoreUtils.GetFullPropertyName(expression, "_"))
         {
             Converter = converter,
             StringFormat = format,
-            Mode = mode
+            Mode = mode,
+            RelativeSource = relativeSource
         };
     }
 
@@ -127,9 +130,10 @@ public static class WPFUtils
         TValue value,
         IValueConverter<TProperty, TValue>? converter,
         BindingMode mode = BindingMode.Default,
-        string? format = null)
+        string? format = null,
+        RelativeSource? relativeSource = null)
     {
-        trigger.Binding = CreateBinding(expression, converter, mode, format);
+        trigger.Binding = CreateBinding(expression, converter, mode, format, relativeSource: relativeSource);
         trigger.Value = value;
         return trigger;
     }
@@ -153,9 +157,10 @@ public static class WPFUtils
         TProperty value,
         IValueConverter? converter = null,
         BindingMode mode = BindingMode.Default,
-        string? format = null)
+        string? format = null,
+        RelativeSource? relativeSource = null)
     {
-        trigger.Binding = CreateBinding(expression, converter, mode, format);
+        trigger.Binding = CreateBinding(expression, converter, mode, format, relativeSource: relativeSource);
         trigger.Value = value;
         return trigger;
     }
@@ -180,12 +185,44 @@ public static class WPFUtils
         Expression<Func<T, TProperty>> expression, 
         IValueConverter? converter = null,
         BindingMode mode = BindingMode.Default,
-        string? format = null )
+        string? format = null,
+        RelativeSource? relativeSource = null)
+    {
+        element.SetBinding(
+            property,
+            CreateBinding(expression, converter, mode, format, relativeSource: relativeSource)
+        );
+    }
+
+    public static FrameworkElementFactory Bind<T, TProperty>(
+        this FrameworkElementFactory element,
+        DependencyProperty property,
+        T source,
+        Expression<Func<T, TProperty>> expression,
+        IValueConverter? converter = null,
+        string? format = null)
+    {
+        element.SetBinding(
+            property,
+            CreateBinding(source, expression, converter, format)
+        );
+        return element;
+    }
+
+    public static FrameworkElementFactory Bind<T, TProperty>(
+        this FrameworkElementFactory element, 
+        DependencyProperty property, 
+        Expression<Func<T, TProperty>> expression, 
+        IValueConverter? converter = null,
+        BindingMode mode = BindingMode.Default,
+        string? format = null,
+        RelativeSource? relativeSource = null)
     {
         element.SetBinding(
             property,
-            CreateBinding(expression, converter, mode, format)
+            CreateBinding(expression, converter, mode, format, relativeSource: relativeSource ?? RelativeSource.TemplatedParent)
         );
+        return element;
     }
 
     #endregion
@@ -210,20 +247,9 @@ public static class WPFUtils
         return parent as T;
     }
 
-    public static IEnumerable<T> FindVisualChildren<T>(this DependencyObject depObj)
+    public static IEnumerable<T> FindVisualChildren<T>(this DependencyObject depObj, bool recursive = true)
     {
         if (depObj != null)
-            //ContentControl cc = depObj as ContentControl;
-            //if (cc != null)
-            //{
-            //    if (cc.Content == null)
-            //        yield return null;
-            //    if (cc.Content is T)
-            //        yield return cc.Content as T;
-            //    foreach (var child in FindVisualChildren<T>(cc.Content as DependencyObject))
-            //        yield return child;
-            //}
-            //else
             for (var i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
             {
                 var child = VisualTreeHelper.GetChild(depObj, i);
@@ -233,11 +259,19 @@ public static class WPFUtils
                 if (child is T t)
                     yield return t;
 
-                foreach (var childOfChild in FindVisualChildren<T>(child))
-                    yield return childOfChild;
+                if (recursive)
+                {
+                    foreach (var childOfChild in FindVisualChildren<T>(child))
+                        yield return childOfChild;
+                }
             }
     }
 
+    public static T? FindChild<T>(this DependencyObject parent, string childName)
+        where T : FrameworkElement
+    {
+        return parent.FindVisualChildren<T>().FirstOrDefault(x => x.Name == childName);
+    }
 
     #region Grid Children