瀏覽代碼

Merge remote-tracking branch 'origin/kenric' into frank

# Conflicts:
#	inabox.wpf/DynamicGrid/DynamicGrid.cs
frogsoftware 1 年之前
父節點
當前提交
f3d327e7de

+ 11 - 0
InABox.Core/CoreTable/CoreRow.cs

@@ -335,5 +335,16 @@ namespace InABox.Core
         {
             return rows.Select(x => x.ToObject<T>());
         }
+
+        public static T[] ToArray<T>(this IList<CoreRow> rows)
+            where T : BaseObject, new()
+        {
+            var result = new T[rows.Count];
+            for (var i = 0; i < rows.Count; ++i)
+            {
+                result[i] = rows[i].ToObject<T>();
+            }
+            return result;
+        }
     }
 }

+ 21 - 2
InABox.Core/CoreUtils.cs

@@ -369,7 +369,7 @@ namespace InABox.Core
             return result;
         }
 
-        public static bool TryGetEntity(string entityname, [NotNullWhen(true)] out Type? type)
+        public static bool TryGetEntity(string? entityname, [NotNullWhen(true)] out Type? type)
         {
             if (string.IsNullOrWhiteSpace(entityname))
             {
@@ -409,7 +409,7 @@ namespace InABox.Core
                 return type;
             throw new Exception($"Entity {entityname} does not exist!");
         }
-        public static Type? GetEntityOrNull(string entityname)
+        public static Type? GetEntityOrNull(string? entityname)
         {
             TryGetEntity(entityname, out var type);
             return type;
@@ -2798,6 +2798,15 @@ namespace InABox.Core
             return (trueResult, falseResult);
         }
 
+        /// <summary>
+        /// Get the value in <paramref name="dictionary"/> for the given <paramref name="key"/>, adding a new instance of <typeparamref name="TValue"/>
+        /// if the value was not found.
+        /// </summary>
+        /// <typeparam name="TKey"></typeparam>
+        /// <typeparam name="TValue"></typeparam>
+        /// <param name="dictionary"></param>
+        /// <param name="key"></param>
+        /// <returns></returns>
         public static TValue GetValueOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key)
             where TValue : new()
         {
@@ -2856,6 +2865,16 @@ namespace InABox.Core
             }
         }
 
+        public static IEnumerable<KeyValuePair<int, T>> WithIndex<T>(this IEnumerable<T> enumerable)
+        {
+            int i = 0;
+            foreach(var obj in enumerable)
+            {
+                yield return new KeyValuePair<int, T>(i, obj);
+                ++i;
+            }
+        }
+
         #endregion
 
     }

+ 12 - 0
InABox.Core/Editors/Utils/EditorButton.cs

@@ -3,6 +3,7 @@
     public delegate void EditorButtonClick(object editor, object? item);
     public delegate void EditorButtonClick<T>(object editor, T? item) where T : class;
     public delegate void EditorButtonEnabled(bool enabled);
+    public delegate void EditorButtonVisible(bool visible);
 
     public class EditorButton
     {
@@ -14,6 +15,7 @@
             Item = item;
             DisableEditor = disableeditor;
             IsEnabled = true;
+            IsVisible = true;
         }
 
         public int Width { get; }
@@ -26,8 +28,12 @@
 
         public bool IsEnabled { get; private set; }
 
+        public bool IsVisible { get; private set; }
+
+
         public event EditorButtonClick OnClick;
         public event EditorButtonEnabled OnEnabled;
+        public event EditorButtonEnabled OnVisible;
 
         public void Click(object editor)
         {
@@ -42,5 +48,11 @@
             IsEnabled = enabled;
             OnEnabled?.Invoke(enabled);
         }
+
+        public void SetVisible(bool visible)
+        {
+            IsVisible = visible;
+            OnVisible?.Invoke(visible);
+        }
     }
 }

+ 226 - 227
inabox.scripting/ScriptDocument.cs

@@ -17,301 +17,300 @@ using Microsoft.CodeAnalysis.Scripting;
 using Microsoft.CodeAnalysis.Scripting.Hosting;
 using RoslynPad.Roslyn;
 
-namespace InABox.Scripting
+namespace InABox.Scripting;
+
+public class ScriptProperty : Dictionary<string, object>
 {
-    public class ScriptProperty : Dictionary<string, object>
+    public ScriptProperty(string name, object? value)
     {
-        public ScriptProperty(string name, object? value)
-        {
-            Name = name;
-            Value = value;
-        }
-
-        public string Name { get; set; }
-        public object? Value { get; set; }
+        Name = name;
+        Value = value;
     }
 
-    public class CompileException : Exception
-    {
-        public CompileException() : base("Unable to compile script!") { }
-    }
+    public string Name { get; set; }
+    public object? Value { get; set; }
+}
 
-    public class ScriptDocument : INotifyPropertyChanged
-    {
-        private string _result;
-
-        private string _text = "";
-        private bool? compiled;
-        private object? obj;
+public class CompileException : Exception
+{
+    public CompileException() : base("Unable to compile script!") { }
+}
 
-        private Type? type;
+public class ScriptDocument : INotifyPropertyChanged
+{
+    private string _result;
 
-        static ScriptDocument()
-        {
-            DefaultAssemblies = new FluentList<Assembly>()
-                .Add(typeof(object).Assembly)
-                .Add(typeof(Regex).Assembly)
-                .Add(typeof(List<>).Assembly)
-                .Add(typeof(Enumerable).Assembly)
-                .Add(typeof(Bitmap).Assembly)
-                .Add(typeof(Expression).Assembly);
-        }
+    private string _text = "";
+    private bool? compiled;
+    private object? obj;
 
+    private Type? type;
 
-        public ScriptDocument(string text)
-        {
-            if (Host == null)
-                Initialize();
-            Text = text;
-            Properties = new List<ScriptProperty>();
-        }
+    static ScriptDocument()
+    {
+        DefaultAssemblies = new FluentList<Assembly>()
+            .Add(typeof(object).Assembly)
+            .Add(typeof(Regex).Assembly)
+            .Add(typeof(List<>).Assembly)
+            .Add(typeof(Enumerable).Assembly)
+            .Add(typeof(Bitmap).Assembly)
+            .Add(typeof(Expression).Assembly);
+    }
 
-        public static RoslynHost Host { get; private set; }
 
-        public static FluentList<Assembly> DefaultAssemblies { get; }
+    public ScriptDocument(string text)
+    {
+        if (Host == null)
+            Initialize();
+        Text = text;
+        Properties = new List<ScriptProperty>();
+    }
 
-        public Script<object> Script { get; private set; }
+    public static RoslynHost Host { get; private set; }
 
-        public string Text
-        {
-            get => _text;
-            set => SetProperty(ref _text, value);
-        }
+    public static FluentList<Assembly> DefaultAssemblies { get; }
 
-        public DocumentId Id { get; set; }
+    public Script<object> Script { get; private set; }
 
-        public string Result
-        {
-            get => _result;
-            private set => SetProperty(ref _result, value);
-        }
+    public string Text
+    {
+        get => _text;
+        set => SetProperty(ref _text, value);
+    }
 
-        private static MethodInfo HasSubmissionResult { get; } =
-            typeof(Compilation).GetMethod(nameof(HasSubmissionResult), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
-                ?? throw new NullReferenceException();
+    public DocumentId Id { get; set; }
 
-        private static PrintOptions PrintOptions { get; } = new() { MemberDisplayFormat = MemberDisplayFormat.SeparateLines };
+    public string Result
+    {
+        get => _result;
+        private set => SetProperty(ref _result, value);
+    }
 
-        public List<ScriptProperty> Properties { get; }
+    private static MethodInfo HasSubmissionResult { get; } =
+        typeof(Compilation).GetMethod(nameof(HasSubmissionResult), BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
+            ?? throw new NullReferenceException();
 
-        public event PropertyChangedEventHandler? PropertyChanged;
+    private static PrintOptions PrintOptions { get; } = new() { MemberDisplayFormat = MemberDisplayFormat.SeparateLines };
 
-        private static IEnumerable<MetadataReference> CompilationReferences;
+    public List<ScriptProperty> Properties { get; }
 
-        public static void Initialize()
-        {
-            var typelist = CoreUtils.TypeList(
-                AppDomain.CurrentDomain.GetAssemblies(),
-                x =>
-                    x.GetTypeInfo().IsClass
-                    && !x.GetTypeInfo().IsGenericType
-                    && x.GetTypeInfo().IsSubclassOf(typeof(BaseObject))).ToList();
-            for (var i = typelist.Count - 1; i > -1; i--) // var type in typelist)
-            {
-                var type = typelist[i];
-                var module = type.Assembly.Modules.FirstOrDefault();
-                if (module != null && !module.FullyQualifiedName.Equals("<Unknown>"))
-                    DefaultAssemblies.Add(type.Assembly);
-                else
-                    typelist.RemoveAt(i);
-            }
+    public event PropertyChangedEventHandler? PropertyChanged;
 
-            var references = Directory.GetFiles(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "refs"), "*.dll")
-                .Select(x => MetadataReference.CreateFromFile(x)).ToArray();
-
-            var files = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll").Where(
-                x => !Path.GetFileName(x).ToLower().StartsWith("gsdll")
-                     && !Path.GetFileName(x).ToLower().StartsWith("pdfium")
-                     && !Path.GetFileName(x).ToLower().StartsWith("ikvm-native")
-                     && !Path.GetFileName(x).ToLower().StartsWith("sqlite.interop")
-                     && !Path.GetFileName(x).ToLower().StartsWith("microsoft.codeanalysis")
-            );
-
-            var hostReferences = RoslynHostReferences.NamespaceDefault.With(
-                typeNamespaceImports: typelist
-                //, assemblyReferences: DefaultAssemblies
-                , assemblyPathReferences: files,
-                references: references
-            );
-            CompilationReferences = RoslynHostReferences.NamespaceDefault.With(
-                typeNamespaceImports: typelist
-                , assemblyReferences: DefaultAssemblies
-                , assemblyPathReferences: files
-            ).GetReferences();
-
-            Host = new RoslynHost(
-                DefaultAssemblies.ToArray(),
-                hostReferences
-            );
-        }
+    private static IEnumerable<MetadataReference> CompilationReferences;
 
-        public bool Compile()
+    public static void Initialize()
+    {
+        var typelist = CoreUtils.TypeList(
+            AppDomain.CurrentDomain.GetAssemblies(),
+            x =>
+                x.GetTypeInfo().IsClass
+                && !x.GetTypeInfo().IsGenericType
+                && x.GetTypeInfo().IsSubclassOf(typeof(BaseObject))).ToList();
+        for (var i = typelist.Count - 1; i > -1; i--) // var type in typelist)
         {
-            Result = null;
-            compiled = null;
+            var type = typelist[i];
+            var module = type.Assembly.Modules.FirstOrDefault();
+            if (module != null && !module.FullyQualifiedName.Equals("<Unknown>"))
+                DefaultAssemblies.Add(type.Assembly);
+            else
+                typelist.RemoveAt(i);
+        }
 
-            Script = CSharpScript.Create(Text, ScriptOptions.Default
-                .AddReferences(CompilationReferences)
-                .AddImports(Host.DefaultImports));
+        var references = Directory.GetFiles(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "refs"), "*.dll")
+            .Select(x => MetadataReference.CreateFromFile(x)).ToArray();
+
+        var files = Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll").Where(
+            x => !Path.GetFileName(x).ToLower().StartsWith("gsdll")
+                 && !Path.GetFileName(x).ToLower().StartsWith("pdfium")
+                 && !Path.GetFileName(x).ToLower().StartsWith("ikvm-native")
+                 && !Path.GetFileName(x).ToLower().StartsWith("sqlite.interop")
+                 && !Path.GetFileName(x).ToLower().StartsWith("microsoft.codeanalysis")
+        );
+
+        var hostReferences = RoslynHostReferences.NamespaceDefault.With(
+            typeNamespaceImports: typelist
+            //, assemblyReferences: DefaultAssemblies
+            , assemblyPathReferences: files,
+            references: references
+        );
+        CompilationReferences = RoslynHostReferences.NamespaceDefault.With(
+            typeNamespaceImports: typelist
+            , assemblyReferences: DefaultAssemblies
+            , assemblyPathReferences: files
+        ).GetReferences();
+
+        Host = new RoslynHost(
+            DefaultAssemblies.ToArray(),
+            hostReferences
+        );
+    }
 
-            var compilation = Script.GetCompilation();
-            var hasResult = (bool)HasSubmissionResult.Invoke(compilation, null);
-            var diagnostics = Script.Compile();
-            if (diagnostics.Any(t => t.Severity == DiagnosticSeverity.Error))
-            {
-                var result = new List<string>();
-                var errors = diagnostics.Select(FormatObject).Where(x => x.StartsWith("CSDiagnostic("));
-                foreach (var error in errors)
-                    result.Add(
-                        error.Split(new[] { Environment.NewLine }, StringSplitOptions.None).First().Replace("CSDiagnostic(", "").Replace(") {", ""));
-                Result = string.Join(Environment.NewLine, result);
-                return false;
-            }
+    public bool Compile()
+    {
+        Result = null;
+        compiled = null;
 
-            return true;
-        }
+        Script = CSharpScript.Create(Text, ScriptOptions.Default
+            .AddReferences(CompilationReferences)
+            .AddImports(Host.DefaultImports));
 
-        public void SetValue(string name, object value)
+        var compilation = Script.GetCompilation();
+        var hasResult = (bool)HasSubmissionResult.Invoke(compilation, null);
+        var diagnostics = Script.Compile();
+        if (diagnostics.Any(t => t.Severity == DiagnosticSeverity.Error))
         {
-            var prop = Properties.FirstOrDefault(x => x.Name.Equals(name));
-            if (prop == null)
-                Properties.Add(new ScriptProperty(name, value));
-            else
-                prop.Value = value;
+            var result = new List<string>();
+            var errors = diagnostics.Select(FormatObject).Where(x => x.StartsWith("CSDiagnostic("));
+            foreach (var error in errors)
+                result.Add(
+                    error.Split(new[] { Environment.NewLine }, StringSplitOptions.None).First().Replace("CSDiagnostic(", "").Replace(") {", ""));
+            Result = string.Join(Environment.NewLine, result);
+            return false;
         }
 
-        public object? GetValue(string name, object? defaultvalue = null)
-        {
-            var prop = Properties.FirstOrDefault(x => x.Name.Equals(name));
-            return prop != null ? prop.Value : defaultvalue;
-        }
+        return true;
+    }
 
-        private Type? GetClassType(string className = "Module")
+    public void SetValue(string name, object value)
+    {
+        var prop = Properties.FirstOrDefault(x => x.Name.Equals(name));
+        if (prop == null)
+            Properties.Add(new ScriptProperty(name, value));
+        else
+            prop.Value = value;
+    }
+
+    public object? GetValue(string name, object? defaultvalue = null)
+    {
+        var prop = Properties.FirstOrDefault(x => x.Name.Equals(name));
+        return prop != null ? prop.Value : defaultvalue;
+    }
+
+    private Type? GetClassType(string className = "Module")
+    {
+        if (!compiled.HasValue)
         {
-            if (!compiled.HasValue)
+            compiled = false;
+            var stream = new MemoryStream();
+            var emitResult = Script.GetCompilation().Emit(stream);
+            if (emitResult.Success)
             {
-                compiled = false;
-                var stream = new MemoryStream();
-                var emitResult = Script.GetCompilation().Emit(stream);
-                if (emitResult.Success)
+                var asm = Assembly.Load(stream.ToArray());
+                type = asm.GetTypes().Where(x => x.Name.Equals(className)).FirstOrDefault();
+                if (type != null)
                 {
-                    var asm = Assembly.Load(stream.ToArray());
-                    type = asm.GetTypes().Where(x => x.Name.Equals(className)).FirstOrDefault();
-                    if (type != null)
-                    {
-                        obj = Activator.CreateInstance(type);
-                        compiled = true;
-                    }
+                    obj = Activator.CreateInstance(type);
+                    compiled = true;
                 }
             }
-            return type;
         }
+        return type;
+    }
 
-        public object? GetObject(string className = "Module")
+    public object? GetObject(string className = "Module")
+    {
+        GetClassType(className);
+        return obj;
+    }
+
+    public MethodInfo? GetMethod(string className = "Module", string methodName = "Execute")
+    {
+        var type = GetClassType(className);
+        if (compiled == true && type != null)
         {
-            GetClassType(className);
-            return obj;
+            return type.GetMethod(methodName);
         }
+        else
+        {
+            return null;
+        }
+    }
 
-        public MethodInfo? GetMethod(string className = "Module", string methodName = "Execute")
+    public bool Execute(string classname = "Module", string methodname = "Execute", object[]? parameters = null, bool defaultResult = false)
+    {
+        var result = defaultResult;
+
+        var type = GetClassType(classname);
+        var obj = GetObject(classname);
+        var method = GetMethod(classname, methodname);
+
+        if (compiled == true && type != null && method != null)
         {
-            var type = GetClassType(className);
-            if (compiled == true && type != null)
+            foreach (var property in Properties)
+            {
+                var prop = type.GetProperty(property.Name);
+                prop?.SetValue(obj, property.Value);
+            }
+
+            if (method.ReturnType == typeof(bool))
             {
-                return type.GetMethod(methodName);
+                result = (bool)(method.Invoke(obj, parameters ?? Array.Empty<object>()) ?? false);
             }
             else
             {
-                return null;
+                method.Invoke(obj, parameters ?? Array.Empty<object>());
+                result = true;
             }
-        }
-
-        public bool Execute(string classname = "Module", string methodname = "Execute", object[]? parameters = null, bool defaultResult = false)
-        {
-            var result = defaultResult;
-
-            var type = GetClassType(classname);
-            var obj = GetObject(classname);
-            var method = GetMethod(classname, methodname);
 
-            if (compiled == true && type != null && method != null)
+            if (result)
             {
                 foreach (var property in Properties)
                 {
                     var prop = type.GetProperty(property.Name);
-                    prop?.SetValue(obj, property.Value);
-                }
-
-                if (method.ReturnType == typeof(bool))
-                {
-                    result = (bool)(method.Invoke(obj, parameters ?? Array.Empty<object>()) ?? false);
-                }
-                else
-                {
-                    method.Invoke(obj, parameters ?? Array.Empty<object>());
-                    result = true;
-                }
-
-                if (result)
-                {
-                    foreach (var property in Properties)
-                    {
-                        var prop = type.GetProperty(property.Name);
-                        if (prop != null)
-                            property.Value = prop.GetValue(obj);
-                    }
+                    if (prop != null)
+                        property.Value = prop.GetValue(obj);
                 }
             }
-
-            return result;
         }
 
-        private static string FormatException(Exception ex)
-        {
-            return CSharpObjectFormatter.Instance.FormatException(ex);
-        }
+        return result;
+    }
 
-        private static string FormatObject(object o)
+    private static string FormatException(Exception ex)
+    {
+        return CSharpObjectFormatter.Instance.FormatException(ex);
+    }
+
+    private static string FormatObject(object o)
+    {
+        return CSharpObjectFormatter.Instance.FormatObject(o, PrintOptions);
+    }
+
+    protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
+    {
+        if (!EqualityComparer<T>.Default.Equals(field, value))
         {
-            return CSharpObjectFormatter.Instance.FormatObject(o, PrintOptions);
+            field = value;
+            // ReSharper disable once ExplicitCallerInfoArgument
+            OnPropertyChanged(propertyName);
+            return true;
         }
 
-        protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
-        {
-            if (!EqualityComparer<T>.Default.Equals(field, value))
-            {
-                field = value;
-                // ReSharper disable once ExplicitCallerInfoArgument
-                OnPropertyChanged(propertyName);
-                return true;
-            }
+        return false;
+    }
 
-            return false;
-        }
+    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+    {
+        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+    }
 
-        protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+    public static bool RunCustomModule(DataModel model, Dictionary<string, object[]> selected, string code)
+    {
+        var script = new ScriptDocument(code);
+        if (!script.Compile())
         {
-            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+            throw new CompileException();
         }
 
-        public static bool RunCustomModule(DataModel model, Dictionary<string, object[]> selected, string code)
-        {
-            var script = new ScriptDocument(code);
-            if (!script.Compile())
-            {
-                throw new CompileException();
-            }
-
-            script.SetValue("Data", selected);
-            script.SetValue("Model", model);
+        script.SetValue("Data", selected);
+        script.SetValue("Model", model);
 
-            script.Execute(methodname: "BeforeLoad");
-            var tableNames = model.DefaultTableNames.ToList();
-            script.Execute(methodname: "CheckTables", parameters: new[] { tableNames });
+        script.Execute(methodname: "BeforeLoad");
+        var tableNames = model.DefaultTableNames.ToList();
+        script.Execute(methodname: "CheckTables", parameters: new[] { tableNames });
 
-            model.LoadModel(tableNames);
+        model.LoadModel(tableNames);
 
-            return script.Execute();
-        }
+        return script.Execute();
     }
 }

+ 34 - 0
inabox.wpf/Converters/FuncConverter.cs

@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Data;
+
+namespace InABox.Wpf;
+
+public class FuncConverter<TIn, TOut>(Func<TIn, TOut> convert, Func<TOut, TIn>? convertBack) : IValueConverter
+{
+    public Func<TIn, TOut> ConvertFunc { get; set; } = convert;
+
+    public Func<TOut, TIn>? ConvertBackFunc { get; set; } = convertBack;
+
+    public object? Convert(object? value, Type targetType, object parameter, CultureInfo culture)
+    {
+        if(value is TIn tIn)
+        {
+            return ConvertFunc(tIn);
+        }
+        return null;
+    }
+
+    public object? ConvertBack(object? value, Type targetType, object parameter, CultureInfo culture)
+    {
+        if(value is TOut tOut && ConvertBackFunc is not null)
+        {
+            return ConvertBackFunc.Invoke(tOut);
+        }
+        return null;
+    }
+}

+ 4 - 0
inabox.wpf/DynamicGrid/Columns/DynamicActionColumn.cs

@@ -4,6 +4,7 @@ using System.Windows.Controls;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using InABox.Core;
+using Syncfusion.UI.Xaml.Grid;
 using Image = System.Windows.Controls.Image;
 using Label = System.Windows.Controls.Label;
 
@@ -19,6 +20,8 @@ namespace InABox.DynamicGrid
         public ActionDelegate? Action { get; set; }
 
         public ActionColumnToolTip? ToolTip { get; set; }
+
+        public Func<GridSummaryColumn?>? GetSummary = null;
         
         public string[]? SelectedFilters { get; set; }
         public string[]? Filters { get; set; } = null;
@@ -65,5 +68,6 @@ namespace InABox.DynamicGrid
 
         public abstract object? Data(CoreRow? row);
 
+        public GridSummaryColumn? Summary() => GetSummary?.Invoke();
     }
 }

+ 2 - 2
inabox.wpf/DynamicGrid/Columns/DynamicTemplateColumn.cs

@@ -6,9 +6,9 @@ namespace InABox.DynamicGrid;
 
 public class DynamicTemplateColumn : DynamicActionColumn
 {
-    public Func<FrameworkElement> Template { get; protected set; }
+    public Func<CoreRow, FrameworkElement?> Template { get; protected set; }
     
-    public DynamicTemplateColumn(Func<FrameworkElement> template)
+    public DynamicTemplateColumn(Func<CoreRow, FrameworkElement?> template)
     {
         Template = template;
         VerticalHeader = false;

+ 1 - 1
inabox.wpf/DynamicGrid/DynamicDocumentGrid.cs

@@ -98,7 +98,7 @@ namespace InABox.DynamicGrid
             }
         }
 
-        private FrameworkElement DocumentTemplate()
+        private FrameworkElement DocumentTemplate(CoreRow row)
         {
             return SimpleTemplate
                 ? CreateSimpleTemplate()

+ 50 - 4
inabox.wpf/DynamicGrid/DynamicGrid.cs

@@ -134,7 +134,7 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
 
     public delegate void BeforeSelectionEvent(CancelEventArgs cancel);
     public event BeforeSelectionEvent? OnBeforeSelection;
-
+    
     protected virtual void Changed()
     {
         
@@ -145,7 +145,7 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
         Changed();
         OnChanged?.Invoke(this, EventArgs.Empty);
     }
-        
+
 
     public event EditorValueChangedHandler? OnEditorValueChanged;
 
@@ -184,6 +184,7 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
         };
 
         ActionColumns = new DynamicActionColumns();
+        ColumnGroupings = new DynamicGridColumnGroupings();
         
         RowStyleSelector = GetRowStyleSelector();
         RowStyleSelector.GetStyle += (row, style) => GetRowStyle(row, style);
@@ -841,6 +842,36 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
 
     #region Column Handling
 
+    #region Column Grouping
+
+    public DynamicGridColumnGroupings ColumnGroupings { get; set; }
+
+    /// <summary>
+    /// Create a new column header group, and return it for editing.
+    /// </summary>
+    /// <returns></returns>
+    public DynamicGridColumnGrouping AddColumnGrouping()
+    {
+        var group = new DynamicGridColumnGrouping();
+        ColumnGroupings.Add(group);
+        return group;
+    }
+
+    /// <summary>
+    /// Gets the current column header group, and if there is none, create a new one.
+    /// </summary>
+    /// <returns></returns>
+    public DynamicGridColumnGrouping GetColumnGrouping()
+    {
+        if(ColumnGroupings.Count == 0)
+        {
+            return AddColumnGrouping();
+        }
+        return ColumnGroupings[^1];
+    }
+
+    #endregion
+
     protected virtual DynamicGridColumns LoadColumns()
     {
         var result = new DynamicGridColumns();
@@ -877,7 +908,6 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
         return columns;
     }
 
-
     private bool SwapRows(int row1, int row2)
     {
 
@@ -906,6 +936,19 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
     {
         DoCustomiseColumnsEvent(this,columns);
     }
+
+    /// <summary>
+    /// Handle to configure column groups.
+    /// </summary>
+    /// <remarks>
+    /// This is called after <see cref="LoadColumns"/>, so by the time this is called, both <see cref="VisibleColumns"/>
+    /// and <see cref="ActionColumns"/> will be loaded, which means one can reference these in the column groups.
+    /// <br/>
+    /// <b>Note:</b> <see cref="ColumnGroupings"/> is cleared before this function is called.
+    /// </remarks>
+    protected virtual void ConfigureColumnGroups()
+    {
+    }
     
     private void ReloadColumns()
     {
@@ -915,7 +958,10 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
 
         ConfigureColumns(VisibleColumns /*, true */);
 
-        UIComponent.RefreshColumns(VisibleColumns, ActionColumns);
+        ColumnGroupings.Clear();
+        ConfigureColumnGroups();
+
+        UIComponent.RefreshColumns(VisibleColumns, ActionColumns, ColumnGroupings);
     }
 
     #endregion

+ 23 - 0
inabox.wpf/DynamicGrid/DynamicGridColumns.cs

@@ -64,4 +64,27 @@ public class DynamicGridColumns<T> : DynamicGridColumns
     {
         return Add<T, TProperty>(member, width, caption, format, alignment);
     }
+}
+
+public class DynamicGridColumnGroup(string header, DynamicColumnBase start, DynamicColumnBase end)
+{
+    public string Header { get; set; } = header;
+
+    public DynamicColumnBase StartColumn { get; set; } = start;
+
+    public DynamicColumnBase EndColumn { get; set; } = end;
+}
+
+public class DynamicGridColumnGrouping
+{
+    public List<DynamicGridColumnGroup> Groups = new();
+
+    public void AddGroup(string header, DynamicColumnBase start, DynamicColumnBase end)
+    {
+        Groups.Add(new(header, start, end));
+    }
+}
+
+public class DynamicGridColumnGroupings : List<DynamicGridColumnGrouping>
+{
 }

+ 1 - 1
inabox.wpf/DynamicGrid/DynamicGridUtils.cs

@@ -545,7 +545,7 @@ public static class DynamicGridUtils
     }
 
     /// <summary>
-    /// Edit (using <see cref="DynamicItemsListGrid{T}"/>) a <see cref="BaseObject"/>s. Use for objects not saved in the database.
+    /// Edit (using <see cref="DynamicItemsListGrid{T}"/>) a <see cref="BaseObject"/>. Use for objects not saved in the database.
     /// </summary>
     /// <typeparam name="T"></typeparam>
     /// <param name="items"></param>

+ 225 - 226
inabox.wpf/DynamicGrid/DynamicManyToManyCrossTab.cs

@@ -10,297 +10,296 @@ using System.Threading.Tasks;
 using System.Windows.Controls;
 using System.Windows.Media.Imaging;
 
-namespace InABox.DynamicGrid
+namespace InABox.DynamicGrid;
+
+public abstract class DynamicManyToManyCrossTab<TManyToMany, TRow, TColumn> : DynamicGrid<TRow>
+    where TManyToMany : Entity, IRemotable, IPersistent, new()
+    where TRow : Entity, IRemotable, IPersistent, new()
+    where TColumn : Entity, IRemotable, IPersistent, new()
 {
-    public abstract class DynamicManyToManyCrossTab<TManyToMany, TRow, TColumn> : DynamicGrid<TRow>
-        where TManyToMany : Entity, IRemotable, IPersistent, new()
-        where TRow : Entity, IRemotable, IPersistent, new()
-        where TColumn : Entity, IRemotable, IPersistent, new()
+    private static readonly BitmapImage tick = Wpf.Resources.tick.AsBitmapImage();
+
+    /// <summary>
+    /// Property on <typeparamref name="TManyToMany"/> which is an <see cref="EntityLink{T}"/> for <typeparamref name="TRow"/>.
+    /// </summary>
+    private readonly PropertyInfo rowProperty;
+    /// <summary>
+    /// Property on <typeparamref name="TManyToMany"/> which is an <see cref="EntityLink{T}"/> for <typeparamref name="TColumn"/>.
+    /// </summary>
+    private readonly PropertyInfo columnProperty;
+
+    private CoreTable? ColumnData;
+    /// <summary>
+    /// {ColumnID : { RowID }}
+    /// </summary>
+    private Dictionary<Guid, Dictionary<Guid, TManyToMany>>? ManyToManyData;
+
+    public DynamicManyToManyCrossTab()
     {
-        private static readonly BitmapImage tick = Wpf.Resources.tick.AsBitmapImage();
-
-        /// <summary>
-        /// Property on <typeparamref name="TManyToMany"/> which is an <see cref="EntityLink{T}"/> for <typeparamref name="TRow"/>.
-        /// </summary>
-        private readonly PropertyInfo rowProperty;
-        /// <summary>
-        /// Property on <typeparamref name="TManyToMany"/> which is an <see cref="EntityLink{T}"/> for <typeparamref name="TColumn"/>.
-        /// </summary>
-        private readonly PropertyInfo columnProperty;
-
-        private CoreTable? ColumnData;
-        /// <summary>
-        /// {ColumnID : { RowID }}
-        /// </summary>
-        private Dictionary<Guid, Dictionary<Guid, TManyToMany>>? ManyToManyData;
-
-        public DynamicManyToManyCrossTab()
-        {
-            rowProperty = CoreUtils.GetManyToManyThisProperty(typeof(TManyToMany), typeof(TRow));
-            columnProperty = CoreUtils.GetManyToManyThisProperty(typeof(TManyToMany), typeof(TColumn));
-
-            HeaderHeight = 125;
-        }
+        rowProperty = CoreUtils.GetManyToManyThisProperty(typeof(TManyToMany), typeof(TRow));
+        columnProperty = CoreUtils.GetManyToManyThisProperty(typeof(TManyToMany), typeof(TColumn));
 
-        protected override void Init()
-        {
-        }
+        HeaderHeight = 125;
+    }
 
-        protected override void DoReconfigure(FluentList<DynamicGridOption> options)
-        {
-            options.Clear();
-        }
+    protected override void Init()
+    {
+    }
 
-        /// <summary>
-        /// Load the required columns for <typeparamref name="TRow"/>.
-        /// </summary>
-        protected abstract DynamicGridColumns LoadRowColumns();
+    protected override void DoReconfigure(FluentList<DynamicGridOption> options)
+    {
+        options.Clear();
+    }
 
-        protected abstract Columns<TColumn>? LoadColumnColumns();
+    /// <summary>
+    /// Load the required columns for <typeparamref name="TRow"/>.
+    /// </summary>
+    protected abstract DynamicGridColumns LoadRowColumns();
 
-        protected abstract SortOrder<TColumn>? LoadColumnSort();
+    protected abstract Columns<TColumn>? LoadColumnColumns();
 
-        protected abstract string FormatColumnHeader(CoreRow row);
+    protected abstract SortOrder<TColumn>? LoadColumnSort();
 
-        protected virtual Filter<TRow>? RowFilter() => null;
+    protected abstract string FormatColumnHeader(CoreRow row);
 
-        protected virtual Filter<TColumn>? ColumnFilter() => null;
+    protected virtual Filter<TRow>? RowFilter() => null;
 
-        protected override DynamicGridColumns LoadColumns()
-        {
-            var columns = LoadRowColumns();
+    protected virtual Filter<TColumn>? ColumnFilter() => null;
 
-            var client = Client.Create(typeof(TColumn));
+    protected override DynamicGridColumns LoadColumns()
+    {
+        var columns = LoadRowColumns();
 
-            var columnColumns = new Columns<TColumn>(x => x.ID);
-            if(LoadColumnColumns() is Columns<TColumn> extra)
-            {
-                foreach(var col in extra.GetColumns())
-                {
-                    columnColumns.Add(col);
-                }
-            }
+        var client = Client.Create(typeof(TColumn));
 
-            ColumnData = Client.Query(ColumnFilter(), columnColumns, LoadColumnSort());
-            ActionColumns.Clear();
-            foreach(var columnRow in ColumnData.Rows)
+        var columnColumns = new Columns<TColumn>(x => x.ID);
+        if(LoadColumnColumns() is Columns<TColumn> extra)
+        {
+            foreach(var col in extra.GetColumns())
             {
-                var colID = columnRow.Get<TColumn, Guid>(x => x.ID);
-                ActionColumns.Add(new DynamicImageColumn(
-                    (row) =>
-                    {
-                        if (row is null || ManyToManyData is null) return null;
-                        if(ManyToManyData.TryGetValue(colID, out var rowSet))
-                        {
-                            var rowID = row.Get<TRow, Guid>(x => x.ID);
-                            if (rowSet.ContainsKey(rowID))
-                            {
-                                return tick;
-                            }
-                        }
-                        return null;
-                    },
-                    (row) =>
-                    {
-                        if (row is null) return false;
-                        var rowID = row.Get<TRow, Guid>(x => x.ID);
-                        return CellClick(colID, rowID);
-                    }
-                )
-                {
-                    HeaderText = FormatColumnHeader(columnRow),
-                    ContextMenu = (rows) =>
-                    {
-                        var row = rows?.FirstOrDefault();
-                        if (row is null) return null;
-                        var rowID = row.Get<TRow, Guid>(x => x.ID);
-                        return CellMenu(colID, rowID);
-                    }
-                });
+                columnColumns.Add(col);
             }
-
-            return columns;
         }
 
-        private ContextMenu? CellMenu(Guid colID, Guid rowID)
+        ColumnData = Client.Query(ColumnFilter(), columnColumns, LoadColumnSort());
+        ActionColumns.Clear();
+        foreach(var columnRow in ColumnData.Rows)
         {
-            if (ManyToManyData is null) return null;
-
-            var menu = new ContextMenu();
-
-            if (ManyToManyData.TryGetValue(colID, out var rowSet)
-                && rowSet.TryGetValue(rowID, out var obj))
-            {
-                if (Security.CanEdit<TManyToMany>() && CanEditCell(obj))
+            var colID = columnRow.Get<TColumn, Guid>(x => x.ID);
+            ActionColumns.Add(new DynamicImageColumn(
+                (row) =>
                 {
-                    menu.AddItem("Edit Item", Wpf.Resources.pencil, obj, (obj) =>
+                    if (row is null || ManyToManyData is null) return null;
+                    if(ManyToManyData.TryGetValue(colID, out var rowSet))
                     {
-                        if (EditCell(obj))
+                        var rowID = row.Get<TRow, Guid>(x => x.ID);
+                        if (rowSet.ContainsKey(rowID))
                         {
-                            Refresh(false, true);
+                            return tick;
                         }
-                    });
-                }
-                if (Security.CanDelete<TManyToMany>())
+                    }
+                    return null;
+                },
+                (row) =>
                 {
-                    menu.AddItem("Delete Item", Wpf.Resources.delete, obj, DeleteCell);
+                    if (row is null) return false;
+                    var rowID = row.Get<TRow, Guid>(x => x.ID);
+                    return CellClick(colID, rowID);
                 }
-            }
-            else
+            )
             {
-                if (Security.CanEdit<TManyToMany>())
+                HeaderText = FormatColumnHeader(columnRow),
+                ContextMenu = (rows) =>
                 {
-                    menu.AddItem("Create Item", Wpf.Resources.add, (colID, rowID), CreateCell);
+                    var row = rows?.FirstOrDefault();
+                    if (row is null) return null;
+                    var rowID = row.Get<TRow, Guid>(x => x.ID);
+                    return CellMenu(colID, rowID);
                 }
-            }
+            });
+        }
+
+        return columns;
+    }
+
+    private ContextMenu? CellMenu(Guid colID, Guid rowID)
+    {
+        if (ManyToManyData is null) return null;
+
+        var menu = new ContextMenu();
 
-            if(menu.Items.Count > 0)
+        if (ManyToManyData.TryGetValue(colID, out var rowSet)
+            && rowSet.TryGetValue(rowID, out var obj))
+        {
+            if (Security.CanEdit<TManyToMany>() && CanEditCell(obj))
             {
-                return menu;
+                menu.AddItem("Edit Item", Wpf.Resources.pencil, obj, (obj) =>
+                {
+                    if (EditCell(obj))
+                    {
+                        Refresh(false, true);
+                    }
+                });
             }
-            else
+            if (Security.CanDelete<TManyToMany>())
             {
-                return null;
+                menu.AddItem("Delete Item", Wpf.Resources.delete, obj, DeleteCell);
             }
         }
-
-        private void CreateCell((Guid colID, Guid rowID) obj)
+        else
         {
-            var manyToMany = CreateManyToMany(obj.rowID, obj.colID);
-            if (SaveManyToMany(manyToMany))
+            if (Security.CanEdit<TManyToMany>())
             {
-                Refresh(false, true);
+                menu.AddItem("Create Item", Wpf.Resources.add, (colID, rowID), CreateCell);
             }
         }
 
-        private void DeleteCell(TManyToMany obj)
+        if(menu.Items.Count > 0)
         {
-            if (DeleteManyToMany(obj))
-            {
-                Refresh(false, true);
-            }
+            return menu;
+        }
+        else
+        {
+            return null;
+        }
+    }
+
+    private void CreateCell((Guid colID, Guid rowID) obj)
+    {
+        var manyToMany = CreateManyToMany(obj.rowID, obj.colID);
+        if (SaveManyToMany(manyToMany))
+        {
+            Refresh(false, true);
         }
+    }
 
-        protected virtual bool CanEditCell(TManyToMany obj) => false;
-
-        /// <summary>
-        /// Code to edit a <typeparamref name="TManyToMany"/> cell; note that this <b>must not</b> allow for changing either
-        /// the <typeparamref name="TColumn"/> or the <typeparamref name="TRow"/>, otherwise we would easily get duplicate <typeparamref name="TManyToMany"/>s.
-        /// </summary>
-        /// <remarks>
-        /// This method should also save the object.
-        /// </remarks>
-        /// <param name="obj"></param>
-        protected virtual bool EditCell(TManyToMany obj)
+    private void DeleteCell(TManyToMany obj)
+    {
+        if (DeleteManyToMany(obj))
         {
-            return false;
+            Refresh(false, true);
         }
+    }
 
-        private bool CellClick(Guid columnID, Guid rowID)
+    protected virtual bool CanEditCell(TManyToMany obj) => false;
+
+    /// <summary>
+    /// Code to edit a <typeparamref name="TManyToMany"/> cell; note that this <b>must not</b> allow for changing either
+    /// the <typeparamref name="TColumn"/> or the <typeparamref name="TRow"/>, otherwise we would easily get duplicate <typeparamref name="TManyToMany"/>s.
+    /// </summary>
+    /// <remarks>
+    /// This method should also save the object.
+    /// </remarks>
+    /// <param name="obj"></param>
+    protected virtual bool EditCell(TManyToMany obj)
+    {
+        return false;
+    }
+
+    private bool CellClick(Guid columnID, Guid rowID)
+    {
+        if (ManyToManyData is null) return false;
+        if (ManyToManyData.TryGetValue(columnID, out var rowSet)
+            && rowSet.TryGetValue(rowID, out var obj))
         {
-            if (ManyToManyData is null) return false;
-            if (ManyToManyData.TryGetValue(columnID, out var rowSet)
-                && rowSet.TryGetValue(rowID, out var obj))
+            if (Security.CanDelete<TManyToMany>())
             {
-                if (Security.CanDelete<TManyToMany>())
-                {
-                    return DeleteManyToMany(obj);
-                }
-                else
-                {
-                    return false;
-                }
+                return DeleteManyToMany(obj);
             }
             else
             {
-                if (Security.CanEdit<TManyToMany>())
-                {
-                    obj = CreateManyToMany(rowID, columnID);
-                    return SaveManyToMany(obj);
-                }
-                else
-                {
-                    return false;
-                }
+                return false;
             }
         }
-
-        protected virtual TManyToMany CreateManyToMany(Guid rowID, Guid columnID)
+        else
         {
-            var item = new TManyToMany();
-            (rowProperty.GetValue(item) as IEntityLink)!.ID = rowID;
-            (columnProperty.GetValue(item) as IEntityLink)!.ID = columnID;
-            return item;
+            if (Security.CanEdit<TManyToMany>())
+            {
+                obj = CreateManyToMany(rowID, columnID);
+                return SaveManyToMany(obj);
+            }
+            else
+            {
+                return false;
+            }
         }
+    }
 
-        protected virtual bool SaveManyToMany(TManyToMany obj)
-        {
-            Client.Save(obj, "Edited by user");
-            return true;
-        }
+    protected virtual TManyToMany CreateManyToMany(Guid rowID, Guid columnID)
+    {
+        var item = new TManyToMany();
+        (rowProperty.GetValue(item) as IEntityLink)!.ID = rowID;
+        (columnProperty.GetValue(item) as IEntityLink)!.ID = columnID;
+        return item;
+    }
 
-        protected virtual bool DeleteManyToMany(TManyToMany obj)
-        {
-            Client.Delete(obj, "Deleted by user");
-            return true;
-        }
+    protected virtual bool SaveManyToMany(TManyToMany obj)
+    {
+        Client.Save(obj, "Edited by user");
+        return true;
+    }
 
-        protected override void Reload(Filters<TRow> criteria, Columns<TRow> columns, ref SortOrder<TRow>? sort, Action<CoreTable?, Exception?> action)
-        {
-            var filter = criteria.Add(RowFilter()).Combine();
+    protected virtual bool DeleteManyToMany(TManyToMany obj)
+    {
+        Client.Delete(obj, "Deleted by user");
+        return true;
+    }
 
-            var manyToManyColumns = new Columns<TManyToMany>(x => x.ID);
-            manyToManyColumns.Add(rowProperty.Name + ".ID").Add(columnProperty.Name + ".ID");
+    protected override void Reload(Filters<TRow> criteria, Columns<TRow> columns, ref SortOrder<TRow>? sort, Action<CoreTable?, Exception?> action)
+    {
+        var filter = criteria.Add(RowFilter()).Combine();
+
+        var manyToManyColumns = new Columns<TManyToMany>(x => x.ID);
+        manyToManyColumns.Add(rowProperty.Name + ".ID").Add(columnProperty.Name + ".ID");
 
-            Client.QueryMultiple(
-                (results, e) =>
+        Client.QueryMultiple(
+            (results, e) =>
+            {
+                if(e is not null)
+                {
+                    action(null, e);
+                }
+                else
                 {
-                    if(e is not null)
+                    var manyToManyTable = results!.Get<TManyToMany>();
+
+                    ManyToManyData = new Dictionary<Guid, Dictionary<Guid, TManyToMany>>();
+                    foreach(var row in manyToManyTable.Rows)
                     {
-                        action(null, e);
+                        var obj = row.ToObject<TManyToMany>();
+                        var rowID = (rowProperty.GetValue(obj) as IEntityLink)!.ID;
+                        var colID = (columnProperty.GetValue(obj) as IEntityLink)!.ID;
+                        var rowSet = ManyToManyData.GetValueOrAdd(colID);
+                        rowSet[rowID] = obj;
                     }
-                    else
-                    {
-                        var manyToManyTable = results!.Get<TManyToMany>();
 
-                        ManyToManyData = new Dictionary<Guid, Dictionary<Guid, TManyToMany>>();
-                        foreach(var row in manyToManyTable.Rows)
-                        {
-                            var obj = row.ToObject<TManyToMany>();
-                            var rowID = (rowProperty.GetValue(obj) as IEntityLink)!.ID;
-                            var colID = (columnProperty.GetValue(obj) as IEntityLink)!.ID;
-                            var rowSet = ManyToManyData.GetValueOrAdd(colID);
-                            rowSet[rowID] = obj;
-                        }
-
-                        action(results!.Get<TRow>(), null);
-                    }
-                },
-                new KeyedQueryDef<TRow>(filter, columns, sort),
-                new KeyedQueryDef<TManyToMany>(
-                    new Filter<TManyToMany>(rowProperty.Name + ".ID").InQuery(filter, x => x.ID)
-                        .And(columnProperty.Name + ".ID").InQuery(ColumnFilter(), x => x.ID),
-                    manyToManyColumns));
-        }
+                    action(results!.Get<TRow>(), null);
+                }
+            },
+            new KeyedQueryDef<TRow>(filter, columns, sort),
+            new KeyedQueryDef<TManyToMany>(
+                new Filter<TManyToMany>(rowProperty.Name + ".ID").InQuery(filter, x => x.ID)
+                    .And(columnProperty.Name + ".ID").InQuery(ColumnFilter(), x => x.ID),
+                manyToManyColumns));
+    }
 
-        public override void SaveItem(TRow item)
-        {
-            // Never should get called.
-        }
+    public override void SaveItem(TRow item)
+    {
+        // Never should get called.
+    }
 
-        public override void DeleteItems(params CoreRow[] rows)
-        {
-            // Never should get called.
-        }
+    public override void DeleteItems(params CoreRow[] rows)
+    {
+        // Never should get called.
+    }
 
-        public override TRow LoadItem(CoreRow row)
-        {
-            var id = row.Get<TRow, Guid>(x => x.ID);
-            return Client.Query(
-                new Filter<TRow>(x => x.ID).IsEqualTo(id),
-                DynamicGridUtils.LoadEditorColumns(DataColumns()))
-                .ToObjects<TRow>()
-                .FirstOrDefault() ?? throw new Exception($"No {typeof(TRow).Name} with ID {id}");
-        }
+    public override TRow LoadItem(CoreRow row)
+    {
+        var id = row.Get<TRow, Guid>(x => x.ID);
+        return Client.Query(
+            new Filter<TRow>(x => x.ID).IsEqualTo(id),
+            DynamicGridUtils.LoadEditorColumns(DataColumns()))
+            .ToObjects<TRow>()
+            .FirstOrDefault() ?? throw new Exception($"No {typeof(TRow).Name} with ID {id}");
     }
 }

+ 7 - 1
inabox.wpf/DynamicGrid/DynamicSelectorGrid.cs

@@ -17,6 +17,8 @@ public class DynamicSelectorGrid<T> : DynamicDataGrid<T>
 
     public HashSet<Guid> SelectedIDs { get; set; } = new();
 
+    private HashSet<Guid> LastVisible = new();
+
     public delegate void SelectionChangedEvent(HashSet<Guid> selected);
     public event SelectionChangedEvent? SelectionChanged;
 
@@ -61,7 +63,11 @@ public class DynamicSelectorGrid<T> : DynamicDataGrid<T>
     private void DoSelectionChanged()
     {
         var ids = SelectedIDs.Intersect(FilteredGuids).ToHashSet();
-        SelectionChanged?.Invoke(ids);
+        if(ids.Count != LastVisible.Count || ids.Any(x => !LastVisible.Contains(x)))
+        {
+            LastVisible = ids;
+            SelectionChanged?.Invoke(LastVisible);
+        }
     }
 
     private bool Selected_Click(CoreRow? row)

+ 35 - 32
inabox.wpf/DynamicGrid/Editors/BaseDynamicEditorControl.cs

@@ -105,40 +105,43 @@ namespace InABox.DynamicGrid
         {
             var buttons = new List<Button>();
             bDisableEditor = false;
-            if (EditorDefinition != null && EditorDefinition is IButtonEditor)
+            if (EditorDefinition != null && EditorDefinition is IButtonEditor ce && ce.Buttons is not null)
             {
-                var ce = (IButtonEditor)EditorDefinition;
-                if (ce.Buttons != null)
-                    foreach (var ceb in ce.Buttons.Reverse())
+                foreach (var ceb in ce.Buttons.Reverse())
+                {
+                    if (ceb.DisableEditor)
+                        bDisableEditor = true;
+
+                    var button = new Button
+                    {
+                        HorizontalAlignment = HorizontalAlignment.Left,
+                        VerticalAlignment = VerticalAlignment.Stretch,
+                        VerticalContentAlignment = VerticalAlignment.Center,
+                        HorizontalContentAlignment = HorizontalAlignment.Center,
+                        Content = ceb.Caption,
+                        Width = ceb.Width,
+                        Margin = new Thickness(5, 0, 0, 0),
+                        Padding = new Thickness(5, 0, 5, 0),
+                        Tag = ceb,
+                        IsEnabled = ceb.IsEnabled,
+                        Visibility = ceb.IsVisible ? Visibility.Visible : Visibility.Collapsed
+                    };
+                    button.Click += (o, e) =>
+                    {
+                        var b = (Button)o;
+                        var eb = b.Tag as EditorButton;
+                        eb.Click(this);
+                    };
+                    ceb.OnVisible += (visible) =>
+                    {
+                        button.Visibility = visible ? Visibility.Visible : Visibility.Collapsed;
+                    };
+                    ceb.OnEnabled += (enabled) =>
                     {
-                        if (ceb.DisableEditor)
-                            bDisableEditor = true;
-
-                        var button = new Button
-                        {
-                            HorizontalAlignment = HorizontalAlignment.Left,
-                            VerticalAlignment = VerticalAlignment.Stretch,
-                            VerticalContentAlignment = VerticalAlignment.Center,
-                            HorizontalContentAlignment = HorizontalAlignment.Center,
-                            Content = ceb.Caption,
-                            Width = ceb.Width,
-                            Margin = new Thickness(5, 0, 0, 0),
-                            Padding = new Thickness(5, 0, 5, 0),
-                            Tag = ceb,
-                            IsEnabled = ceb.IsEnabled
-                        };
-                        button.Click += (o, e) =>
-                        {
-                            var b = (Button)o;
-                            var eb = b.Tag as EditorButton;
-                            eb.Click(this);
-                        };
-                        ceb.OnEnabled += (enabled) =>
-                        {
-                            button.IsEnabled = enabled;
-                        };
-                        buttons.Add(button);
-                    }
+                        button.IsEnabled = enabled;
+                    };
+                    buttons.Add(button);
+                }
             }
             return buttons;
         }

+ 248 - 28
inabox.wpf/DynamicGrid/UIComponent/DynamicGridGridUIComponent.cs

@@ -3,7 +3,9 @@ using InABox.Core;
 using InABox.WPF;
 using org.omg.PortableInterceptor;
 using Syncfusion.Data;
+using Syncfusion.Data.Extensions;
 using Syncfusion.UI.Xaml.Grid;
+using Syncfusion.UI.Xaml.Grid.Cells;
 using Syncfusion.UI.Xaml.Grid.Helpers;
 using Syncfusion.UI.Xaml.ScrollAxis;
 using Syncfusion.Windows.Shared;
@@ -105,7 +107,6 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
 
         DataGrid.CurrentCellBeginEdit += DataGrid_CurrentCellBeginEdit;
         DataGrid.CurrentCellEndEdit += DataGrid_CurrentCellEndEdit;
-        DataGrid.CurrentCellValueChanged += DataGrid_CurrentCellValueChanged;
         DataGrid.CurrentCellDropDownSelectionChanged += DataGrid_CurrentCellDropDownSelectionChanged;
         DataGrid.PreviewKeyUp += DataGrid_PreviewKeyUp;
 
@@ -114,8 +115,8 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         DataGrid.Background = new SolidColorBrush(Colors.DimGray);
         DataGrid.AutoGenerateColumns = false;
         DataGrid.ColumnSizer = GridLengthUnitType.AutoLastColumnFill;
-        DataGrid.SelectionForegroundBrush = DynamicGridUtils.SelectionForeground;
-        DataGrid.RowSelectionBrush = DynamicGridUtils.SelectionBackground;
+        DataGrid.SelectionForegroundBrush = GetCellSelectionForegroundBrush() ?? DynamicGridUtils.SelectionForeground;
+        DataGrid.RowSelectionBrush =  GetCellSelectionBackgroundBrush() ?? DynamicGridUtils.SelectionBackground;
         
         DataGrid.AllowDraggingRows = false;
         DataGrid.Drop += DataGrid_Drop;
@@ -165,6 +166,63 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         //DataGrid.HeaderStyle = headstyle;
 
         DataGrid.SizeChanged += DataGrid_SizeChanged;
+
+        //DataGrid.CellRenderers.Remove("Numeric");
+        //DataGrid.CellRenderers.Add("Numeric", new CustomNumericCellRenderer(this));
+        //DataGrid.CellRenderers.Remove("TextBox");
+        //DataGrid.CellRenderers.Add("TextBox", new CustomTextCellRenderer(this));
+    }
+
+    //private class CustomTextCellRenderer : GridCellTextBoxRenderer
+    //{
+    //    private DynamicGridGridUIComponent<T> Component;
+
+    //    public CustomTextCellRenderer(DynamicGridGridUIComponent<T> component)
+    //    {
+    //        Component = component;
+    //    }
+
+    //    public override void OnInitializeEditElement(DataColumnBase dataColumn, TextBox uiElement, object dataContext)
+    //    {
+    //        base.OnInitializeEditElement(dataColumn, uiElement, dataContext);
+    //        uiElement.TextChanged += UiElement_TextChanged;
+    //    }
+
+    //    private void UiElement_TextChanged(object sender, TextChangedEventArgs e)
+    //    {
+    //        throw new NotImplementedException();
+    //    }
+
+    //    private void UiElement_ValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+    //    {
+    //        Component.ChangeValue(e.OldValue, e.NewValue);
+    //    }
+    //}
+
+    //private class CustomNumericCellRenderer : GridCellNumericRenderer
+    //{
+    //    private DynamicGridGridUIComponent<T> Component;
+
+    //    public CustomNumericCellRenderer(DynamicGridGridUIComponent<T> component)
+    //    {
+    //        Component = component;
+    //    }
+
+    //    public override void OnInitializeEditElement(DataColumnBase dataColumn, DoubleTextBox uiElement, object dataContext)
+    //    {
+    //        base.OnInitializeEditElement(dataColumn, uiElement, dataContext);
+    //        uiElement.ValueChanged += UiElement_ValueChanged;
+    //    }
+
+    //    private void UiElement_ValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+    //    {
+    //        Component.ChangeValue(e.OldValue, e.NewValue);
+    //    }
+    //}
+
+    private void ChangeValue(object oldValue, object newValue)
+    {
+
     }
 
     private void ColumnsMenu_ContextMenuOpening(object sender, RoutedEventArgs e)
@@ -570,6 +628,8 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         DataGrid.ScrollInView(new RowColumnIndex(rowIdx, 0));
     }
 
+    protected virtual Brush? GetCellSelectionForegroundBrush() => DynamicGridUtils.SelectionForeground;
+    protected virtual Brush? GetCellSelectionBackgroundBrush() => DynamicGridUtils.SelectionBackground;
     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;
@@ -578,6 +638,70 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
 
     #region Columns
 
+    private class StackedHeaderRenderer : GridStackedHeaderCellRenderer
+    {
+        private Style Style;
+
+        public StackedHeaderRenderer()
+        {
+            var headstyle = new Style(typeof(GridStackedHeaderCellControl));
+            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.BorderThicknessProperty, new Thickness(0.0, 0.0, 0, 0)));
+            headstyle.Setters.Add(new Setter(Control.MarginProperty, new Thickness(0, 0, 1, 1)));
+
+            Style = headstyle;
+        }
+
+        public override void OnInitializeEditElement(DataColumnBase dataColumn, GridStackedHeaderCellControl uiElement, object dataContext)
+        {
+            uiElement.Style = Style;
+            base.OnInitializeEditElement(dataColumn, uiElement, dataContext);
+        }
+    }
+
+    private void LoadStackedHeaders(DynamicGridColumnGroupings groupings)
+    {
+        DataGrid.StackedHeaderRows.Clear();
+        foreach(var grouping in groupings)
+        {
+            var row = new StackedHeaderRow();
+            var i = 0;
+            foreach(var group in grouping.Groups)
+            {
+                var start = Math.Max(i, ColumnList.IndexOf(group.StartColumn));
+                var end = Math.Max(start, ColumnList.IndexOf(group.EndColumn));
+
+                if(end < start)
+                {
+                    i = end + 1;
+                    continue;
+                }
+
+                var cols = Enumerable.Range(start, end - start + 1).Select(i => DataGrid.Columns[i]).ToArray();
+
+                var stackedColumn = new StackedColumn
+                {
+                    HeaderText = group.Header,
+                    ChildColumns = string.Join(',', cols.Select(x => x.MappingName))
+                };
+
+                row.StackedColumns.Add(stackedColumn);
+
+                i = end + 1;
+            }
+
+            DataGrid.StackedHeaderRows.Add(row);
+        }
+
+        if(groupings.Count > 0)
+        {
+            DataGrid.CellRenderers.Remove("StackedHeader");
+            DataGrid.CellRenderers.Add("StackedHeader", new StackedHeaderRenderer());
+        }
+    }
+
     private readonly List<DynamicColumnBase> ColumnList = new();
 
     private List<DynamicActionColumn> ActionColumns = new();
@@ -602,6 +726,8 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         });
     }
 
+    private ObservableCollection<ISummaryColumn> Summaries = new();
+
     private void LoadActionColumns(DynamicActionColumnPosition position)
     {
         for (var i = 0; i < ActionColumns.Count; i++)
@@ -613,6 +739,14 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                 var sColName = string.Format("ActionColumn{0}", i);
                 gridRowResizingOptions.ExcludeColumns.Add(sColName);
 
+                var summary = column.Summary();
+                if (summary != null)
+                {
+                    summary.Name = sColName;
+                    summary.MappingName = sColName;
+                    Summaries.Add(summary);
+                }
+
                 if (column is DynamicImageColumn imgcol)
                 {
                     var newcol = new GridImageColumn();
@@ -644,7 +778,7 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                     {
                         //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, 0.75, 0.75)));
+                        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));
@@ -710,7 +844,7 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                     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.BorderThicknessProperty, new Thickness(0.0, 0.0, 0, 0)));
-                    headstyle.Setters.Add(new Setter(Control.MarginProperty, new Thickness(0, 0, 0.75, 0.75)));
+                    headstyle.Setters.Add(new Setter(Control.MarginProperty, new Thickness(0, 0, 1, 1)));
                     if (txtCol.VerticalHeader)
                     {
                         headstyle.Setters.Add(new Setter(Control.HorizontalContentAlignmentProperty, HorizontalAlignment.Left));
@@ -761,9 +895,10 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                 else if (column is DynamicTemplateColumn tmplCol)
                 {
                     var newcol = new GridTemplateColumn();
-                    newcol.CellTemplateSelector = new TemplateColumnSelector() { DataTemplate = tmplCol.Template };
+                    newcol.CellTemplateSelector = new TemplateColumnSelector(this, tmplCol.Template);
                     newcol.AllowEditing = false;
                     newcol.UpdateTrigger = UpdateSourceTrigger.PropertyChanged;
+                    newcol.MappingName = sColName;
                     
                     newcol.Width = tmplCol.Width;
                     newcol.ColumnSizer = GridLengthUnitType.None;
@@ -783,10 +918,31 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                     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)));
+                    headstyle.Setters.Add(new Setter(Control.MarginProperty, new Thickness(0, 0, 1, 1)));
+                    headstyle.Setters.Add(new Setter(Control.BorderThicknessProperty, new Thickness(0.0)));
                     newcol.HeaderStyle = headstyle;
 
+                    var cellstyle = new Style();
+                    cellstyle.Setters.Add(new Setter(Control.BackgroundProperty,
+                        new Binding()
+                        {
+                            Path = new PropertyPath("."), Converter = CellBackgroundConverter,
+                            ConverterParameter = column
+                        }));
+                    cellstyle.Setters.Add(new Setter(Control.ForegroundProperty,
+                        new Binding()
+                            { Converter = CellForegroundConverter, ConverterParameter = column }));
+                    cellstyle.Setters.Add(new Setter(Control.FontSizeProperty,
+                        new Binding()
+                            { Converter = CellFontSizeConverter, ConverterParameter = column }));
+                    cellstyle.Setters.Add(new Setter(Control.FontStyleProperty,
+                        new Binding()
+                            { Converter = CellFontStyleConverter, ConverterParameter = column }));
+                    cellstyle.Setters.Add(new Setter(Control.FontWeightProperty,
+                        new Binding()
+                            { Converter = CellFontWeightConverter, ConverterParameter = column }));
+                    newcol.CellStyle = cellstyle;
+
                     DataGrid.Columns.Add(newcol);
                     ColumnList.Add(column);
                 }
@@ -794,12 +950,41 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         }
     }
 
-    public class TemplateColumnSelector : DataTemplateSelector
+    public class TemplateColumnSelector(DynamicGridGridUIComponent<T> parent, Func<CoreRow, FrameworkElement?> dataTemplate) : DataTemplateSelector
     {
-        public Func<FrameworkElement> DataTemplate { get; init; }
-        
-        public override DataTemplate SelectTemplate(object item, DependencyObject container) 
-            => TemplateGenerator.CreateDataTemplate(DataTemplate);
+        public Func<CoreRow, FrameworkElement?> DataTemplate { get; init; } = dataTemplate;
+
+        public DynamicGridGridUIComponent<T> Parent { get; init; } = parent;
+
+        public override DataTemplate? SelectTemplate(object item, DependencyObject container)
+        {
+            if (item is not DataRowView) return null;
+
+            CoreRow? row;
+            if(item is DataRowView view && Parent.DataGridItems is DataTable table)
+            {
+                var rowIdx = table.Rows.IndexOf(view.Row);
+                if (rowIdx < 0)
+                {
+                    row = null;
+                }
+                else
+                {
+                    row = Parent.Parent.Data.Rows[rowIdx];
+                }
+            }
+            else
+            {
+                row = null;
+            }
+
+            if (row is null) return null;
+
+            return TemplateGenerator.CreateDataTemplate(() =>
+            {
+                return DataTemplate(row);
+            });
+        }
     }
 
     private void ApplyFilterStyle(GridColumn column, bool filtering, bool isactioncolumn)
@@ -831,11 +1016,8 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         column.FilterRowCellStyle = filterstyle;
     }
 
-
     private void LoadDataColumns(DynamicGridColumns columns)
     {
-        var Summaries = new ObservableCollection<ISummaryColumn>();
-
         foreach (var column in columns)
         {
             if (this.CreateEditorColumn(column, out var newcol, out var prop))
@@ -931,6 +1113,10 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
             }
         }
 
+    }
+
+    private void LoadSummaries()
+    {
         if (Summaries.Any())
         {
             DataGrid.CellRenderers.Remove("TableSummary");
@@ -945,7 +1131,7 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         }
     }
 
-    public void RefreshColumns(DynamicGridColumns columns, DynamicActionColumns actionColumns)
+    public void RefreshColumns(DynamicGridColumns columns, DynamicActionColumns actionColumns, DynamicGridColumnGroupings groupings)
     {
         // Yo, please don't remove this.
         // The issue was when we were dynamically adding ActionColumns, and if we had to remove and then re-add them, we were getting massive performance hits
@@ -964,9 +1150,12 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
 
         ActionColumns = actionColumns.ToList();
 
+        Summaries.Clear();
         LoadActionColumns(DynamicActionColumnPosition.Start);
         LoadDataColumns(columns);
         LoadActionColumns(DynamicActionColumnPosition.End);
+        LoadSummaries();
+        LoadStackedHeaders(groupings);
 
         DataGrid.Columns.Resume();
         DataGrid.RefreshColumns();
@@ -993,10 +1182,12 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
 
     #region Data
 
+    private bool _invalidating = false;
+
     public void BeforeRefresh()
     {
-        DataGrid.SelectionForegroundBrush = DynamicGridUtils.SelectionForeground;
-        DataGrid.RowSelectionBrush = DynamicGridUtils.SelectionBackground;
+        DataGrid.SelectionForegroundBrush = GetCellSelectionForegroundBrush();
+        DataGrid.RowSelectionBrush = GetCellSelectionBackgroundBrush();
     }
 
     public void RefreshData(CoreTable data)
@@ -1029,6 +1220,8 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
             result.Rows.Add(newrow);
         }
 
+        result.ColumnChanged += Result_ColumnChanged;
+
         //int rowIndex = DataGrid.SelectionController.CurrentCellManager.CurrentRowColumnIndex.RowIndex;
         //int columnIndex = DataGrid.SelectionController.CurrentCellManager.CurrentRowColumnIndex.ColumnIndex;
         //int scrollRowIndex = DataGrid.GetVisualContainer().ScrollRows.LastBodyVisibleLineIndex;
@@ -1047,6 +1240,8 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
             return;
         }
 
+        _invalidating = true;
+
         var rowdata = new List<object?>(row.Values);
         foreach (var ac in ActionColumns)
             rowdata.Add(ac.Data(row));
@@ -1054,6 +1249,7 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         var datarow = DataGridItems.Rows[row.Index];
         for (var i = 0; i < rowdata.Count; i++)
             datarow[i] = rowdata[i] ?? DBNull.Value;
+        _invalidating = false;
         //datarow.ItemArray = rowdata.ToArray(); 
     }
 
@@ -1224,22 +1420,46 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         bChanged = false;
     }
 
-    private void DataGrid_CurrentCellValueChanged(object? sender, CurrentCellValueChangedEventArgs e)
+    private void Result_ColumnChanged(object sender, DataColumnChangeEventArgs e)
     {
-        var row = GetRowFromIndex(e.RowColumnIndex.RowIndex);
-        // Are we sure that this function is ever useful? It seems that since the data in the grid hasn't been updated by this point, this function is essentially useless (the data is updated in EndEdit). Probably need to check the GridCheckBoxColumn
+        if (_invalidating) return;
+        if (sender is not DataTable table) return;
+
+        var rowIdx = table.Rows.IndexOf(e.Row);
+        if (rowIdx < 0)
+            return;
+
+        var row = Parent.Data.Rows[rowIdx];
 
-        if (row is null) return;
+        var colIdx = table.Columns.IndexOf(e.Column);
+        if (colIdx < 0 || colIdx >= Parent.Data.Columns.Count)
+            return;
 
-        if (e.Column is GridCheckBoxColumn)
+        var data = Parent.Data;
+
+        var dataCol = Parent.Data.Columns[colIdx];
+        var col = ColumnList.OfType<DynamicGridColumn>()
+            .FirstOrDefault(x => x.ColumnName.Equals(dataCol.ColumnName));
+        
+        if (col is null)
+            return;
+
+        if (col is DynamicGridCheckBoxColumn<T>)
         {
             EnsureEditingObject(row);
-        }
+            if(_editingObject is not null)
+            {
+                var value = e.Row[e.Column!];
+                if (value is DBNull)
+                    value = CoreUtils.GetDefault(dataCol.DataType);
+
+                _invalidating = true;
+                UpdateData(dataCol.ColumnName, new Dictionary<CoreColumn, object?>() { { dataCol, value } });
+                _invalidating = false;
+            }
 
-        if (_editingObject is not null)
-            UpdateData(_editingObject.Row.Index, e.RowColumnIndex.ColumnIndex);
-        if (e.Column is GridCheckBoxColumn)
             _editingObject = null;
+        }
         if (_editingObject is not null)
             bChanged = true;
     }

+ 90 - 10
inabox.wpf/DynamicGrid/UIComponent/DynamicGridTreeUIComponent.cs

@@ -21,6 +21,7 @@ using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using Syncfusion.UI.Xaml.TreeGrid.Filtering;
+using Syncfusion.UI.Xaml.TreeGrid.Cells;
 
 namespace InABox.DynamicGrid;
 
@@ -596,6 +597,29 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
 
     #region Columns
 
+    private class StackedHeaderRenderer : TreeGridStackedHeaderCellRenderer
+    {
+        private Style Style;
+
+        public StackedHeaderRenderer()
+        {
+            var headstyle = new Style(typeof(TreeGridStackedHeaderCell));
+            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.BorderThicknessProperty, new Thickness(0.0, 0.0, 0, 0)));
+            headstyle.Setters.Add(new Setter(Control.MarginProperty, new Thickness(0, 0, 1, 1)));
+
+            Style = headstyle;
+        }
+
+        public override void OnInitializeEditElement(TreeDataColumnBase dataColumn, TreeGridStackedHeaderCell uiElement, object dataContext)
+        {
+            uiElement.Style = Style;
+            base.OnInitializeEditElement(dataColumn, uiElement, dataContext);
+        }
+    }
+
     private readonly List<DynamicColumnBase> ColumnList = new();
 
     private List<DynamicActionColumn> ActionColumns = new();
@@ -628,12 +652,25 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         }
     }
 
-    public class TemplateColumnSelector : DataTemplateSelector
+    public class TemplateColumnSelector(DynamicGridTreeUIComponent<T> parent, Func<CoreRow, FrameworkElement?> dataTemplate) : DataTemplateSelector
     {
-        public Func<FrameworkElement> DataTemplate { get; init; }
-        
-        public override DataTemplate SelectTemplate(object item, DependencyObject container) 
-            => TemplateGenerator.CreateDataTemplate(DataTemplate);
+        public Func<CoreRow, FrameworkElement?> DataTemplate { get; init; } = dataTemplate;
+
+        public DynamicGridTreeUIComponent<T> Parent { get; init; } = parent;
+
+        public override DataTemplate? SelectTemplate(object item, DependencyObject container)
+        {
+            if (item is not CoreTreeNode node) return null;
+
+            var row = Parent.MapRow(node.Row);
+
+            if (row is null) return null;
+
+            return TemplateGenerator.CreateDataTemplate(() =>
+            {
+                return DataTemplate(row);
+            });
+        }
     }
 
     private void LoadActionColumns(DynamicActionColumnPosition position)
@@ -649,7 +686,7 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                 {
                     var newcol = new TreeGridTemplateColumn();
 
-                    newcol.CellTemplateSelector = new TemplateColumnSelector() { DataTemplate = () =>
+                    newcol.CellTemplateSelector = new TemplateColumnSelector(this, row =>
                     {
                         var image = new Image
                         {
@@ -658,7 +695,7 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                         };
                         image.SetBinding(Image.SourceProperty, new Binding(sColName));
                         return image;
-                    } };
+                    });
 
                     newcol.AllowEditing = false;
                     newcol.UpdateTrigger = UpdateSourceTrigger.PropertyChanged;
@@ -666,6 +703,7 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                     newcol.Padding = new Thickness(4);
                     newcol.ColumnSizer = TreeColumnSizer.None;
                     newcol.HeaderText = column.HeaderText;
+                    newcol.MappingName = sColName;
 
                     ApplyFilterStyle(newcol, false, true);
 
@@ -681,7 +719,7 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                     {
                         //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, 0.75, 0.75)));
+                        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));
@@ -759,7 +797,7 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                 else if (column is DynamicTemplateColumn tmplCol)
                 {
                     var newcol = new TreeGridTemplateColumn();
-                    newcol.CellTemplateSelector = new TemplateColumnSelector() { DataTemplate = tmplCol.Template };
+                    newcol.CellTemplateSelector = new TemplateColumnSelector(this, tmplCol.Template);
                     newcol.AllowEditing = false;
                     newcol.UpdateTrigger = UpdateSourceTrigger.PropertyChanged;
                     
@@ -770,6 +808,7 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                     newcol.AllowSorting = false;
                     newcol.ShowToolTip = false;
                     newcol.ShowHeaderToolTip = false;
+                    newcol.MappingName = sColName;
                     
                     ApplyFilterStyle(newcol, false, true);
 
@@ -872,7 +911,47 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         }
     }
 
-    public void RefreshColumns(DynamicGridColumns columns, DynamicActionColumns actionColumns)
+    private void LoadStackedHeaders(DynamicGridColumnGroupings groupings)
+    {
+        _tree.StackedHeaderRows.Clear();
+        foreach(var grouping in groupings)
+        {
+            var row = new StackedHeaderRow();
+            var i = 0;
+            foreach(var group in grouping.Groups)
+            {
+                var start = Math.Max(i, ColumnList.IndexOf(group.StartColumn));
+                var end = Math.Max(start, ColumnList.IndexOf(group.EndColumn));
+
+                if(end < start)
+                {
+                    i = end + 1;
+                    continue;
+                }
+
+                var cols = Enumerable.Range(start, end - start + 1).Select(i => _tree.Columns[i]).ToArray();
+
+                var stackedColumn = new StackedColumn
+                {
+                    HeaderText = group.Header,
+                    ChildColumns = string.Join(',', cols.Select(x => x.MappingName))
+                };
+
+                row.StackedColumns.Add(stackedColumn);
+
+                i = end + 1;
+            }
+
+            _tree.StackedHeaderRows.Add(row);
+        }
+        if(groupings.Count > 0)
+        {
+            _tree.CellRenderers.Remove("StackedHeader");
+            _tree.CellRenderers.Add("StackedHeader", new StackedHeaderRenderer());
+        }
+    }
+
+    public void RefreshColumns(DynamicGridColumns columns, DynamicActionColumns actionColumns, DynamicGridColumnGroupings groupings)
     {
         _tree.ItemsSource = null;
         _tree.Columns.Suspend();
@@ -893,6 +972,7 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         LoadActionColumns(DynamicActionColumnPosition.Start);
         LoadDataColumns(columns);
         LoadActionColumns(DynamicActionColumnPosition.End);
+        LoadStackedHeaders(groupings);
 
         _tree.Columns.Resume();
         _tree.RefreshColumns();

+ 2 - 2
inabox.wpf/DynamicGrid/UIComponent/IDynamicGridGridUIComponent.cs

@@ -172,9 +172,9 @@ internal static class DynamicGridGridUIComponentExtension
     private static bool FilterByPredicate(CoreRow row, string column, FilterPredicate predicate)
     {
         var value = row[column];
-        var vStr = value?.ToString() ?? "";
+        var vStr = value?.ToString()?.ToLower() ?? "";
         var pValue = predicate.FilterValue;
-        var pStr = pValue?.ToString() ?? "";
+        var pStr = pValue?.ToString()?.ToLower() ?? "";
         return predicate.FilterType switch
         {
             FilterType.Contains => vStr.Contains(pStr),

+ 1 - 1
inabox.wpf/DynamicGrid/UIComponent/IDynamicGridUIComponent.cs

@@ -72,7 +72,7 @@ public interface IDynamicGridUIComponent<T>
     List<Tuple<string, Func<CoreRow, bool>>> GetFilterPredicates();
 
     void BeforeRefresh();
-    void RefreshColumns(DynamicGridColumns columns, DynamicActionColumns actionColumns);
+    void RefreshColumns(DynamicGridColumns columns, DynamicActionColumns actionColumns, DynamicGridColumnGroupings groupings);
     void RefreshData(CoreTable data);
     void InvalidateRow(CoreRow row);