Kenric Nugteren 5 месяцев назад
Родитель
Сommit
1bd0af62bb

+ 1 - 1
InABox.Core/CoreTable/CoreTable.cs

@@ -29,7 +29,7 @@ namespace InABox.Core
         public int Offset { get; set; } = 0;
 
         public IList<CoreColumn> Columns { get => columns; }
-        public IList<CoreRow> Rows
+        public List<CoreRow> Rows
         {
             get
             {

+ 1 - 1
InABox.Core/CoreTable/ICoreTable.cs

@@ -9,7 +9,7 @@ namespace InABox.Core
     public interface ICoreTable
     {
         IList<CoreColumn> Columns { get; }
-        IList<CoreRow> Rows { get; }
+        List<CoreRow> Rows { get; }
         Dictionary<string, IList<Action<object, object>?>> Setters { get; }
         string TableName { get; }
 

+ 15 - 82
inabox.wpf/DynamicGrid/DynamicGridColumn/DynamicColumnGrid.cs

@@ -1,29 +1,24 @@
 using System;
 using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Threading;
 using InABox.Core;
 
 namespace InABox.DynamicGrid;
 
+
 public class DynamicColumnGrid : DynamicGrid<DynamicGridColumn>
 {
-    public event GetAvailableColumnsEvent? OnProcessColumns;
-
-    public DynamicColumnGrid()
+    public DynamicColumnGrid(IDynamicGridColumnSchema schema)
     {
         Columns = new DynamicGridColumns();
+        Schema = schema;
     }
 
     protected override void Init()
     {
         base.Init();
-
-        var column = MasterColumns.FirstOrDefault(x => string.Equals(x.ColumnName, nameof(DynamicGridColumn.ColumnName)));
-        if(column is not null && column.Editor is DynamicColumnNameEditor edit)
-        {
-            edit.ColumnNames = () => ProcessColumns().Select(x => x.ColumnName).ToArray();
-        }
     }
 
     protected override void DoReconfigure(DynamicGridOptions options)
@@ -39,21 +34,24 @@ public class DynamicColumnGrid : DynamicGrid<DynamicGridColumn>
         options.ReorderRows = true;
     }
 
-    private Type _type;
-    public Type Type
+    private IDynamicGridColumnSchema _schema;
+    public IDynamicGridColumnSchema Schema
     {
-        get => _type;
+        get => _schema;
+        [MemberNotNull(nameof(_schema))]
         set
         {
-            _type = value;
+            _schema = value;
+
             var column = MasterColumns.FirstOrDefault(x => string.Equals(x.ColumnName, nameof(DynamicGridColumn.ColumnName)));
             if(column is not null && column.Editor is DynamicColumnNameEditor edit)
             {
-                edit.Type = value;
+                edit.Schema = value;
             }
         }
     }
 
+
     public DynamicGridColumns Columns { get; }
 
     public bool DirectEdit { get; set; }
@@ -89,9 +87,9 @@ public class DynamicColumnGrid : DynamicGrid<DynamicGridColumn>
 
     protected override void DoAdd(bool openEditorOnDirectEdit = false)
     {
-        if(DynamicGridColumnNameSelectorGrid.SelectColumnName(Type, ProcessColumns().Select(x => x.ColumnName).ToArray(), out var column))
+        if(DynamicGridColumnNameSelectorGrid.SelectColumnName(Schema, out var column))
         {
-            var item = DynamicGridColumn.FromCoreGridColumn(DefaultColumns.GetColumn(Type, column));
+            var item = Schema.GetColumn(column);
             SaveItem(item);
             DoChanged();
             Refresh(false, true);
@@ -111,7 +109,7 @@ public class DynamicColumnGrid : DynamicGrid<DynamicGridColumn>
         var changes = base.EditorValueChanged(editor, items, name, value);
         if(name == nameof(DynamicGridColumn.ColumnName) && value is string columnName)
         {
-            var newCol = DynamicGridColumn.FromCoreGridColumn(DefaultColumns.GetColumn(Type, columnName));
+            var newCol = Schema.GetColumn(columnName);
 
             foreach(var item in items)
             {
@@ -128,61 +126,6 @@ public class DynamicColumnGrid : DynamicGrid<DynamicGridColumn>
         return changes;
     }
 
-    private IEnumerable<DynamicGridColumn> ProcessColumns()
-    {
-        var result = new List<DynamicGridColumn>();
-        var cols = new DynamicGridColumns();
-        cols.ExtractColumns(Type);
-        foreach (var col in cols)
-        {
-            if (col.Editor == null)
-                continue;
-            if (col.Editor is NullEditor)
-                continue;
-            if (col.Editor.Visible != Visible.Hidden)
-            {
-                result.Add(col);
-                continue;
-            }
-
-            if (!DirectEdit)
-                continue;
-
-            if (col.Editor.Editable.IsEditable() && col.ColumnName.Split('.').Length <= 2)
-                result.Add(col);
-        }
-        result.Sort((a, b) => a.ColumnName.CompareTo(b.ColumnName));
-
-        var args = new GetAvailableColumnsEventArgs(result);
-        OnProcessColumns?.Invoke(args);
-
-        return args.Columns;
-    }
-
-    protected override void DefineLookups(ILookupEditorControl sender, DynamicGridColumn[] items, bool async = true)
-    {
-        if (Type != null && sender.ColumnName.Equals("ColumnName"))
-        {
-            var results = new CoreTable();
-            results.Columns.Add(new CoreColumn { ColumnName = sender.ColumnName, DataType = typeof(string) });
-            results.Columns.Add(new CoreColumn { ColumnName = "Display", DataType = typeof(string) });
-
-            var cols = ProcessColumns();
-            foreach (var col in cols)
-            {
-                var row = results.NewRow();
-                row[sender.ColumnName] = col.ColumnName;
-                row["Display"] = col.ColumnName;
-                results.Rows.Add(row);
-            }
-
-            sender.LoadLookups(results);
-        }
-        else
-        {
-            base.DefineLookups(sender, items, async);
-        }
-    }
 
     #region Save / Load
 
@@ -207,16 +150,6 @@ public class DynamicColumnGrid : DynamicGrid<DynamicGridColumn>
 
     public override void SaveItem(DynamicGridColumn item)
     {
-        try
-        {
-            var prop = DatabaseSchema.Property(Type, item.ColumnName);
-            item.Editor = prop.Editor;
-        }
-        catch (Exception e)
-        {
-            Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace));
-        }
-
         if (!Columns.Contains(item))
             Columns.Add(item);
     }

+ 6 - 8
inabox.wpf/DynamicGrid/DynamicGridColumn/DynamicColumnNameEditorControl.cs

@@ -16,12 +16,10 @@ public class DynamicColumnNameEditorControl : DynamicEditorControl<string, Dynam
     private TextBox TextBox = null!;
     private Button Edit = null!;
 
-    string Value = "";
-    string[] ColumnNames;
+    private string _value = "";
 
     public override void Configure()
     {
-        ColumnNames = EditorDefinition.GetColumnNames();
     }
 
     protected override FrameworkElement CreateEditor()
@@ -65,10 +63,10 @@ public class DynamicColumnNameEditorControl : DynamicEditorControl<string, Dynam
 
     private void EditButton_Click(object sender, RoutedEventArgs e)
     {
-        if(DynamicGridColumnNameSelectorGrid.SelectColumnName(EditorDefinition.Type, ColumnNames, out var value))
+        if(EditorDefinition.Schema is not null && DynamicGridColumnNameSelectorGrid.SelectColumnName(EditorDefinition.Schema, out var value))
         {
-            Value = value;
-            TextBox.Text = Value;
+            _value = value;
+            TextBox.Text = _value;
             CheckChanged();
         }
     }
@@ -95,12 +93,12 @@ public class DynamicColumnNameEditorControl : DynamicEditorControl<string, Dynam
 
     protected override string RetrieveValue()
     {
-        return Value;
+        return _value;
     }
 
     protected override void UpdateValue(string value)
     {
-        Value = value;
+        _value = value;
         TextBox.Text = value;
     }
 }

+ 75 - 8
inabox.wpf/DynamicGrid/DynamicGridColumn/DynamicGridColumn.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Windows;
 using InABox.Core;
 
@@ -18,20 +19,15 @@ public class ColumnNameGenerator : LookupGenerator<object>
 
 public class DynamicColumnNameEditor : BaseEditor
 {
-    public Func<string[]>? ColumnNames;
-
-    public Type Type { get; set; }
+    public IDynamicGridColumnSchema? Schema { get; set; }
 
     protected override BaseEditor DoClone()
     {
         return new DynamicColumnNameEditor()
         {
-            ColumnNames = ColumnNames,
-            Type = Type
+            Schema = Schema
         };
     }
-
-    public string[] GetColumnNames() => ColumnNames?.Invoke() ?? [];
 }
 
 public class DynamicGridColumn : DynamicColumnBase
@@ -113,4 +109,75 @@ public class DynamicGridColumn : DynamicColumnBase
             Editor = editor.CloneEditor()
         };
     }
-}
+}
+
+#region Column Schema
+
+public interface IDynamicGridColumnSchema
+{
+    IEnumerable<string> ColumnNames { get; }
+
+    DynamicGridColumn GetColumn(string column);
+
+    string? GetComment(string column);
+
+    bool IsVisible(string column);
+}
+public class DynamicGridObjectColumnSchema(Type t, IEnumerable<string>? columnNames = null) : IDynamicGridColumnSchema
+{
+    public event GetAvailableColumnsEvent? OnProcessColumns;
+
+    public bool DirectEdit { get; set; }
+
+    private string[]? _columnNames = columnNames?.ToArray();
+
+    public IEnumerable<string> ColumnNames => _columnNames ?? ProcessColumns().Select(x => x.ColumnName);
+
+    private IEnumerable<DynamicGridColumn> ProcessColumns()
+    {
+        var result = new List<DynamicGridColumn>();
+        var cols = new DynamicGridColumns();
+        cols.ExtractColumns(t);
+        foreach (var col in cols)
+        {
+            if (col.Editor == null)
+                continue;
+            if (col.Editor is NullEditor)
+                continue;
+            if (col.Editor.Visible != Visible.Hidden)
+            {
+                result.Add(col);
+                continue;
+            }
+
+            if (!DirectEdit)
+                continue;
+
+            if (col.Editor.Editable.IsEditable() && col.ColumnName.Split('.').Length <= 2)
+                result.Add(col);
+        }
+        result.Sort((a, b) => a.ColumnName.CompareTo(b.ColumnName));
+
+        var args = new GetAvailableColumnsEventArgs(result);
+        OnProcessColumns?.Invoke(args);
+
+        return args.Columns;
+    }
+
+    public DynamicGridColumn GetColumn(string column)
+    {
+        return DynamicGridColumn.FromCoreGridColumn(DefaultColumns.GetColumn(t, column));
+    }
+
+    public string? GetComment(string column)
+    {
+        return DatabaseSchema.Property(t, column)?.Comment;
+    }
+
+    public bool IsVisible(string column)
+    {
+        return (DatabaseSchema.Property(t, column)?.Editor.Visible ?? Visible.Optional) != Visible.Hidden;
+    }
+}
+
+#endregion

+ 11 - 10
inabox.wpf/DynamicGrid/DynamicGridColumn/DynamicGridColumnNameSelectorWindow.cs

@@ -47,14 +47,14 @@ public class DynamicGridColumnNameSelectorGrid : DynamicItemsListGrid<DynamicGri
 
     private List<DynamicGridColumnNameSelectorItem> _items;
 
-    public string SearchText { get; set; }
+    public string SearchText { get; set; } = "";
 
-    public DynamicGridColumnNameSelectorGrid(Type type, IEnumerable<string> columnNames)
+    public DynamicGridColumnNameSelectorGrid(IDynamicGridColumnSchema schema)
     {
         var itemMap = new Dictionary<string, DynamicGridColumnNameSelectorItem>();
         var items = new List<DynamicGridColumnNameSelectorItem>();
         var parentCols = new Dictionary<string, List<DynamicGridColumnNameSelectorItem>>();
-        foreach (var column in columnNames)
+        foreach (var column in schema.ColumnNames)
         {
             var item = new DynamicGridColumnNameSelectorItem();
 
@@ -73,26 +73,23 @@ public class DynamicGridColumnNameSelectorGrid : DynamicItemsListGrid<DynamicGri
                 parentCols.GetValueOrAdd(parent).Add(item);
             }
 
-            var prop = DatabaseSchema.Property(type, column);
             item.ColumnName = column;
             item.ParentColumn = parent;
             item.Display = props[^1];
             item.IsParent = false;
-            item.Comment = prop?.Comment ?? "";
-            item.IsVisible = (prop?.Editor.Visible ?? Visible.Optional) != Visible.Hidden;
+            item.Comment = schema.GetComment(column) ?? "";
+            item.IsVisible = schema.IsVisible(column);
             items.Add(item);
         }
 
         foreach (var (col, children) in parentCols)
         {
-            var prop = DatabaseSchema.Property(type, col);
-
             var lastColIdx = col.LastIndexOf('.');
             var item = new DynamicGridColumnNameSelectorItem
             {
                 ColumnName = col,
                 IsParent = true,
-                Comment = prop?.Comment ?? "",
+                Comment = schema.GetComment(col) ?? "",
                 IsVisible = children.Any(x => x.IsVisible)
             };
             if (lastColIdx == -1)
@@ -246,7 +243,11 @@ public class DynamicGridColumnNameSelectorGrid : DynamicItemsListGrid<DynamicGri
 
     public static bool SelectColumnName(Type type, IEnumerable<string> columnNames, out string value, bool showVisibilityButton = false)
     {
-        var grid = new DynamicGridColumnNameSelectorGrid(type, columnNames)
+        return SelectColumnName(new DynamicGridObjectColumnSchema(type, columnNames), out value, showVisibilityButton: showVisibilityButton);
+    }
+    public static bool SelectColumnName(IDynamicGridColumnSchema schema, out string value, bool showVisibilityButton = false)
+    {
+        var grid = new DynamicGridColumnNameSelectorGrid(schema)
         {
             OnlyVisible = showVisibilityButton
         };

+ 10 - 14
inabox.wpf/DynamicGrid/DynamicGridColumn/DynamicGridColumnsEditor.xaml.cs

@@ -14,21 +14,20 @@ public partial class DynamicGridColumnsEditor : ThemableWindow
 {
     private readonly DynamicColumnGrid ColumnGrid;
 
-    public event GetAvailableColumnsEvent? GetAvailableColumns
-    {
-        add => ColumnGrid.OnProcessColumns += value;
-        remove => ColumnGrid.OnProcessColumns -= value;
-    }
-
-    public DynamicGridColumnsEditor(Type type)
+    public DynamicGridColumnsEditor(IDynamicGridColumnSchema schema, Type? type)
     {
         InitializeComponent();
 
-        Type = type;
+        if(type is null)
+        {
+            Title = "Select Columns";
+        }
+        else
+        {
+            Title = $"Select Columns for {CoreUtils.Neatify(type.Name)}";
+        }
 
-        Title = $"Select Columns for {CoreUtils.Neatify(type.Name)}";
-
-        ColumnGrid = new DynamicColumnGrid { Type = type };
+        ColumnGrid = new DynamicColumnGrid(schema);
         ColumnGrid.SetValue(Grid.ColumnSpanProperty, 3);
         ColumnGrid.Margin = new Thickness(5F, 5F, 5F, 5F);
         grid.Children.Add(ColumnGrid);
@@ -38,8 +37,6 @@ public partial class DynamicGridColumnsEditor : ThemableWindow
         //ColumnGrid.OnEditItem += Columns_OnEditItem;
     }
 
-    public Type Type { get; set; }
-
     public DynamicGridColumns Columns { get; }
 
     public bool DirectEdit
@@ -77,7 +74,6 @@ public partial class DynamicGridColumnsEditor : ThemableWindow
 
     private void Window_Loaded(object sender, RoutedEventArgs e)
     {
-        ColumnGrid.Type = Type;
         ColumnGrid.Refresh(true, true);
     }
 }

+ 3 - 1
inabox.wpf/DynamicGrid/DynamicGridColumn/DynamicGridColumnsEditorControl.cs

@@ -132,7 +132,9 @@ public class DynamicGridColumnsEditorControl : DynamicEditorControl<DynamicGridC
     {
         if (ColumnsType is null || Columns is null) return;
 
-        var editor = new DynamicGridColumnsEditor(ColumnsType) { WindowStartupLocation = WindowStartupLocation.CenterScreen };
+        var schema = new DynamicGridObjectColumnSchema(ColumnsType);
+
+        var editor = new DynamicGridColumnsEditor(schema, ColumnsType) { WindowStartupLocation = WindowStartupLocation.CenterScreen };
 
         editor.Columns.AddRange(Columns);
 

+ 23 - 10
inabox.wpf/DynamicGrid/DynamicGridStyle.cs

@@ -137,7 +137,7 @@ public abstract class DynamicGridRowStyleSelector : StyleSelector, IBaseDynamicG
 
     private static DynamicGridStyle defaultstyle;
 
-    protected abstract string SectionName { get; }
+    protected abstract string? SectionName { get; }
 
     public CoreTable Data { get; set; }
     
@@ -200,16 +200,19 @@ public abstract class DynamicGridRowStyleSelector : StyleSelector, IBaseDynamicG
 
         defaultstyle = CreateStyle();
 
-        var stylescript = ClientFactory.ClientType == null
-            ? null
-            : new Client<Script>()
-                .Load(new Filter<Script>(x => x.Section).IsEqualTo(SectionName).And(x => x.ScriptType).IsEqualTo(ScriptType.RowStyle))
-                .FirstOrDefault();
-        if (stylescript != null)
+        if(SectionName is not null)
         {
-            helper = new ScriptDocument(stylescript.Code);
-            if (!helper.Compile())
-                helper = null;
+            var stylescript = ClientFactory.ClientType == null
+                ? null
+                : new Client<Script>()
+                    .Load(new Filter<Script>(x => x.Section).IsEqualTo(SectionName).And(x => x.ScriptType).IsEqualTo(ScriptType.RowStyle))
+                    .FirstOrDefault();
+            if (stylescript != null)
+            {
+                helper = new ScriptDocument(stylescript.Code);
+                if (!helper.Compile())
+                    helper = null;
+            }
         }
 
         initialized = true;
@@ -289,6 +292,16 @@ public class DynamicGridRowStyleSelector<TEntity, TStyle> : DynamicGridRowStyleS
     }
 }
 
+public class SimpleDynamicGridRowStyleSelector<TStyle> : DynamicGridRowStyleSelector where TStyle : DynamicGridStyle, new()
+{
+    protected override string? SectionName => null;
+
+    protected override DynamicGridStyle CreateStyle()
+    {
+        return new TStyle();
+    }
+}
+
 public class DynamicGridCellStyleParameters
 {
     public DynamicColumnBase Column { get; }

+ 1705 - 0
inabox.wpf/DynamicGrid/Grids/BaseDynamicGrid.cs

@@ -0,0 +1,1705 @@
+using InABox.Core;
+using InABox.Wpf;
+using InABox.WPF;
+using Syncfusion.Data;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Animation;
+using System.Windows.Media.Imaging;
+using Color = System.Drawing.Color;
+
+namespace InABox.DynamicGrid;
+
+public abstract class BaseDynamicGrid : ContentControl, IDynamicGridUIComponentParent
+{
+    public static readonly DependencyProperty UseWaitCursorProperty =
+        DependencyProperty.Register(nameof(UseWaitCursor), typeof(bool), typeof(DynamicGrid<>));
+
+    public bool UseWaitCursor
+    {
+        get => (bool)GetValue(UseWaitCursorProperty);
+        set => SetValue(UseWaitCursorProperty, value);
+    }
+
+    protected enum ClipAction
+    {
+        Cut,
+        Copy
+    }
+
+    private IDynamicGridUIComponent UIComponent;
+
+    private UIElement? _header;
+    private readonly Button Add;
+
+    public bool bRefreshing;
+    bool IDynamicGridUIComponentParent.IsRefreshing => bRefreshing;
+
+    private readonly Label ClipboardSpacer;
+    private readonly Button Copy;
+
+    private readonly Label Count;
+
+    private readonly Border Disabler;
+
+    private readonly DynamicActionColumn? drag;
+
+    private readonly Button Delete;
+    private readonly DockPanel Docker;
+    private readonly Button Edit;
+    private readonly Label EditSpacer;
+    private readonly Button? ExportButton;
+    private readonly Label ExportSpacer;
+    private readonly Button? DuplicateBtn;
+    private readonly Button SwitchViewBtn;
+
+    private readonly Button? Help;
+    private readonly Button? ImportButton;
+
+    private readonly Grid Layout;
+    private readonly Label Loading;
+
+    private readonly DoubleAnimation LoadingFader = new(1d, 0.2d, new Duration(TimeSpan.FromSeconds(2))) { AutoReverse = true };
+
+    private readonly Button Print;
+    private readonly Label PrintSpacer;
+
+    private readonly StackPanel LeftButtonStack;
+    private readonly StackPanel RightButtonStack;
+
+    protected DynamicGridRowStyleSelector RowStyleSelector;
+
+    protected virtual bool CanDuplicate { get; } = false;
+
+    #region Events
+
+    private event IDynamicGrid.ReconfigureEvent? _onReconfigure;
+    public event IDynamicGrid.ReconfigureEvent? OnReconfigure
+    {
+        add
+        {
+            _onReconfigure += value;
+            Reconfigure();
+        }
+        remove
+        {
+            _onReconfigure -= value;
+            Reconfigure();
+        }
+    }
+
+    public OnGetDynamicGridRowStyle? OnGetRowStyle { get; set; }
+
+    public event OnPrintData? OnPrintData;
+    
+    public event BeforeRefreshEventHandler? BeforeRefresh;
+    public event AfterRefreshEventHandler? AfterRefresh;
+
+    /// <summary>
+    /// Called when an item is selected in the grid. It is not called if <see cref="IsReady"/> is not <see langword="true"/>.
+    /// </summary>
+    /// <remarks>
+    /// It is unnecessary to use this if within a grid. Instead, override <see cref="SelectItems(CoreRow[]?)"/>.
+    /// </remarks>
+    public event SelectItemHandler? OnSelectItem;
+
+    public event OnCellDoubleClick? OnCellDoubleClick;
+
+    public event EventHandler? OnChanged;
+
+    public delegate void BeforeSelectionEvent(CancelEventArgs cancel);
+    public event BeforeSelectionEvent? OnBeforeSelection;
+    
+    protected virtual void Changed()
+    {
+    }
+
+    public virtual void DoChanged()  
+    {
+        Changed();
+        OnChanged?.Invoke(this, EventArgs.Empty);
+    }
+
+    public event OnFilterRecord? OnFilterRecord;
+
+    public event OnDoubleClick? OnDoubleClick;
+
+    #endregion
+
+    protected DynamicGridSettings Settings { get; set; }
+
+    public BaseDynamicGrid() : base()
+    {
+        UseWaitCursor = true;
+
+        Options = new DynamicGridOptions();
+        Options.OnChanged += () =>
+        {
+            _hasLoadedOptions = true;
+            OptionsChanged();
+        };
+
+        ActionColumns = new DynamicActionColumns();
+        ColumnGroupings = new DynamicGridColumnGroupings();
+        
+        RowStyleSelector = GetRowStyleSelector();
+        RowStyleSelector.GetStyle += (row, style) => GetRowStyle(row, style);
+        
+        IsReady = false;
+
+        Data = new CoreTable();
+
+        drag = new DynamicImageColumn(InABox.Wpf.Resources.drag.AsBitmapImage()) { Position = DynamicActionColumnPosition.Start };
+
+        VisibleColumns = new DynamicGridColumns();
+
+        PreInit();
+
+        UIComponent = CreateUIComponent();
+        
+        Loading = new Label();
+        Loading.Content = "Loading...";
+        Loading.Foreground = new SolidColorBrush(Colors.White);
+        Loading.VerticalContentAlignment = VerticalAlignment.Center;
+        Loading.HorizontalContentAlignment = HorizontalAlignment.Center;
+        Loading.Visibility = Visibility.Collapsed;
+        Loading.SetValue(Panel.ZIndexProperty, 999);
+        Loading.SetValue(Grid.RowProperty, 1);
+        Loading.FontSize = 14.0F;
+        LoadingFader.Completed += (sender, args) =>
+        {
+            if (Loading.Visibility == Visibility.Visible)
+            {
+                //Logger.Send(LogType.Information, this.GetType().EntityName().Split(".").Last(), "Loading Fader Restarting");
+                Loading.BeginAnimation(Label.OpacityProperty, LoadingFader);
+            }
+        };
+
+        if(this is IHelpDynamicGrid helpGrid)
+        {
+            Help = CreateButton(Wpf.Resources.help.AsBitmapImage(Color.White));
+            Help.Margin = new Thickness(0, 2, 2, 0);
+            Help.SetValue(DockPanel.DockProperty, Dock.Right);
+            Help.Click += (o, e) => ShowHelp(helpGrid.HelpSlug());
+        }
+
+        Add = CreateButton(Wpf.Resources.add.AsBitmapImage(Color.White));
+        Add.Margin = new Thickness(0, 2, 2, 0);
+        Add.Click += Add_Click;
+
+        Edit = CreateButton(Wpf.Resources.pencil.AsBitmapImage(Color.White));
+        Edit.Margin = new Thickness(0, 2, 2, 0);
+        Edit.Click += Edit_Click;
+
+        SwitchViewBtn = CreateButton(Wpf.Resources.alter.AsBitmapImage());
+        SwitchViewBtn.Margin = new Thickness(0, 2, 2, 0);
+        SwitchViewBtn.Click += SwitchView_Click;
+
+        EditSpacer = new Label { Width = 5 };
+
+        Print = CreateButton(Wpf.Resources.print.AsBitmapImage(Color.White));
+        Print.Margin = new Thickness(0, 2, 2, 0);
+        Print.Click += (o, e) => DoPrint(o);
+
+        PrintSpacer = new Label { Width = 5 };
+
+        Copy = CreateButton(Wpf.Resources.duplicate.AsBitmapImage(Color.White), tooltip: "Duplicate Rows");
+        Copy.Margin = new Thickness(0, 2, 2, 0);
+        Copy.Click += Copy_Click;
+
+        ClipboardSpacer = new Label { Width = 5 };
+
+        if(this is IExportDynamicGrid)
+        {
+            ExportButton = CreateButton(Wpf.Resources.doc_xls.AsBitmapImage(Color.White), "Export");
+            ExportButton.Margin = new Thickness(0, 2, 2, 0);
+            ExportButton.Click += ExportButtonClick;
+        }
+
+        if(this is IImportDynamicGrid)
+        {
+            ImportButton = CreateButton(Wpf.Resources.doc_xls.AsBitmapImage(Color.White), "Import");
+            ImportButton.Margin = new Thickness(0, 2, 2, 0);
+            ImportButton.Click += ImportButton_Click;
+        }
+
+        ExportSpacer = new Label { Width = 5 };
+        
+
+        LeftButtonStack = new StackPanel();
+        LeftButtonStack.Orientation = Orientation.Horizontal;
+        LeftButtonStack.SetValue(DockPanel.DockProperty, Dock.Left);
+
+        if(Help is not null)
+        {
+            LeftButtonStack.Children.Add(Help);
+        }
+        LeftButtonStack.Children.Add(Add);
+        LeftButtonStack.Children.Add(Edit);
+        LeftButtonStack.Children.Add(SwitchViewBtn);
+        //Stack.Children.Add(MultiEdit);
+        LeftButtonStack.Children.Add(EditSpacer);
+
+        LeftButtonStack.Children.Add(Print);
+        LeftButtonStack.Children.Add(PrintSpacer);
+
+        LeftButtonStack.Children.Add(Copy);
+        LeftButtonStack.Children.Add(ClipboardSpacer);
+
+        if(ExportButton is not null)
+        {
+            LeftButtonStack.Children.Add(ExportButton);
+        }
+        if(ImportButton is not null)
+        {
+            LeftButtonStack.Children.Add(ImportButton);
+        }
+        if(ExportButton is not null || ImportButton is not null)
+        {
+            LeftButtonStack.Children.Add(ExportSpacer);
+        }
+
+        RightButtonStack = new StackPanel();
+        RightButtonStack.Orientation = Orientation.Horizontal;
+        RightButtonStack.SetValue(DockPanel.DockProperty, Dock.Right);
+
+        Delete = CreateButton(Wpf.Resources.delete.AsBitmapImage(Color.White));
+        Delete.Margin = new Thickness(2, 2, 0, 0);
+        Delete.SetValue(DockPanel.DockProperty, Dock.Right);
+        Delete.Click += Delete_Click;
+
+        if(this is IDuplicateDynamicGrid)
+        {
+            DuplicateBtn = AddButton("Duplicate", Wpf.Resources.paste.AsBitmapImage(Color.White), DuplicateButton_Click);
+        }
+
+        Count = new Label();
+        Count.Height = 30;
+        Count.Margin = new Thickness(0, 2, 0, 0);
+        Count.VerticalContentAlignment = VerticalAlignment.Center;
+        Count.HorizontalContentAlignment = HorizontalAlignment.Center;
+        Count.SetValue(DockPanel.DockProperty, Dock.Left);
+
+        Docker = new DockPanel();
+
+        Docker.SetValue(Grid.RowProperty, 2);
+        Docker.SetValue(Grid.ColumnProperty, 0);
+        Docker.Children.Add(LeftButtonStack);
+        Docker.Children.Add(Delete);
+        Docker.Children.Add(RightButtonStack);
+        Docker.Children.Add(Count);
+
+
+        Layout = new Grid();
+        Layout.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
+        Layout.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) });
+        Layout.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });
+        Layout.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) });
+
+        var control = UIComponent.Control;
+        control.SetValue(Grid.RowProperty, 1);
+
+        Layout.Children.Add(control);
+        Layout.Children.Add(Loading);
+        Layout.Children.Add(Docker);
+        
+        Disabler = new Border()
+        {
+            BorderBrush = new SolidColorBrush(Colors.Transparent),
+            Background = new SolidColorBrush(Colors.DimGray) { Opacity = 0.2 },
+            Visibility = Visibility.Collapsed,
+        };
+        Disabler.SetValue(Canvas.ZIndexProperty, 99);
+        Disabler.SetValue(Grid.RowSpanProperty, 3);
+
+        Layout.Children.Add(Disabler);
+        
+        //Scroll.ApplyTemplate();
+
+        Content = Layout;
+
+        IsEnabledChanged += (sender, args) =>
+        {
+            Disabler.Visibility = Equals(args.NewValue, true)
+                ? Visibility.Collapsed
+                : Visibility.Visible;
+        };
+
+        Settings = LoadSettings();
+        
+        Init();
+        Reconfigure();
+    }
+
+    protected virtual void PreInit()
+    {
+    }
+
+    #region IDynamicGridUIComponentParent
+
+    protected virtual IDynamicGridUIComponent CreateUIComponent()
+    {
+        return new DynamicGridGridUIComponent()
+        {
+            Parent = this
+        };
+    }
+
+    protected IDynamicGridUIComponent GetUIComponent() => UIComponent;
+
+    bool IDynamicGridUIComponentParent.CanFilter()
+    {
+        return !Options.ReorderRows || !Options.EditRows;
+    }
+
+    bool IDynamicGridUIComponentParent.CanSort()
+    {
+        return !Options.ReorderRows || !Options.EditRows;
+    }
+
+    DynamicGridRowStyleSelector IDynamicGridUIComponentParent.RowStyleSelector => RowStyleSelector;
+
+    void IDynamicGridUIComponentParent.BeforeSelection(CancelEventArgs cancel)
+    {
+        BeforeSelection(cancel);
+    }
+
+    void IDynamicGridUIComponentParent.SelectItems(CoreRow[] rows)
+    {
+        SelectItems(rows);
+    }
+
+    void IDynamicGridUIComponentParent.HandleKey(KeyEventArgs e)
+    {
+        if (Options.ReorderRows)
+        {
+            if (e.Key == Key.X && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
+            {
+                CutToClipBuffer();
+            }
+            else if (e.Key == Key.C && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
+            {
+                CopyToClipBuffer();
+            }
+            else if (e.Key == Key.V && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control)
+            {
+                PasteFromClipBuffer();
+            }
+            else if (e.Key == Key.Escape)
+            {
+                ResetClipBuffer();
+                InvalidateGrid();
+            }
+        }
+    }
+
+    void IDynamicGridUIComponentParent.DoubleClickCell(CoreRow? row, DynamicColumnBase? column)
+    {
+        var args = new DynamicGridCellClickEventArgs(row, column);
+        if (OnCellDoubleClick is not null)
+        {
+            
+            OnCellDoubleClick?.Invoke(this, args);
+            if (args.Handled)
+                return;
+        }
+
+        if (row is not null)
+            DoDoubleClick(this, args);
+    }
+
+    void IDynamicGridUIComponentParent.ExecuteActionColumn(DynamicActionColumn column, CoreRow[]? rows)
+    {
+        var bRefresh = false;
+        if(rows is null)
+        {
+            bRefresh = column.Action?.Invoke(null) ?? false;
+        }
+        else
+        {
+            foreach (var row in rows)
+                if (column.Action?.Invoke(row) == true)
+                    bRefresh = true;
+        }
+        if (bRefresh)
+            Dispatcher.BeginInvoke(() => { Refresh(true, true); });
+    }
+
+    void IDynamicGridUIComponentParent.OpenColumnMenu(DynamicColumnBase column)
+    {
+        if(column is DynamicMenuColumn menuColumn)
+        {
+            menuColumn.Action?.Invoke(SelectedRows.FirstOrDefault());
+        }
+        else if(column is DynamicActionColumn actionColumn)
+        {
+            var menu = actionColumn?.ContextMenu?.Invoke(SelectedRows);
+            if (menu != null && menu.Items.Count > 0)
+            {
+                menu.IsOpen = true;
+            }
+        }
+    }
+
+    void IDynamicGridUIComponentParent.UpdateRecordCount(int count)
+    {
+        Count.Content = FormatRecordCount(count);
+    }
+
+    protected virtual string FormatRecordCount(int count) => $"{count} Records";
+    
+
+    void IDynamicGridUIComponentParent.LoadColumnsMenu(ContextMenu menu)
+    {
+        menu.AddItem("Select Columns", null, SelectColumnsClick);
+        LoadColumnsMenu(menu);
+    }
+
+
+    void IDynamicGridUIComponentParent.DragOver(object sender, DragEventArgs e)
+    {
+        HandleDragOver(sender, e);
+    }
+
+    void IDynamicGridUIComponentParent.Drop(object sender, DragEventArgs e)
+    {
+        if (!Options.DragTarget)
+            return;
+
+        if(DynamicGridUtils.TryGetDropData(e, out var entityType, out var table))
+        {
+            OnDragEnd(entityType, table, e);
+        }
+        else
+        {
+            HandleDragDrop(sender, e);
+        }
+    }
+
+    void IDynamicGridUIComponentParent.DragStart(object? sender, CoreRow[] rows)
+    {
+        Logger.Send(LogType.Information, "", "RowDragDropController_DragStart");
+        
+        if (!Options.DragSource)
+            return;
+        
+        OnRowsDragStart(rows);
+    }
+    
+    public void UIFilterChanged(object sender) => DoFilterChanged();
+    
+
+    //void IDynamicGridUIComponentParent<T>.UIFilterChanged(object sender) => DoFilterChanged();
+
+    protected virtual void DoFilterChanged()
+    {
+        
+    }
+
+    private Dictionary<DynamicColumnBase, IDynamicGridColumnFilter?> ColumnFilters { get; set; } = new();
+
+    IDynamicGridColumnFilter? IBaseDynamicGrid.GetColumnFilter(DynamicColumnBase column) => GetColumnFilter(column);
+
+    protected IDynamicGridColumnFilter? GetColumnFilter(DynamicColumnBase column)
+    {
+        if(!ColumnFilters.TryGetValue(column, out var filter))
+        {
+            filter = GenerateColumnFilter(column);
+            ColumnFilters.Add(column, filter);
+        }
+        return filter;
+    }
+
+    protected virtual IDynamicGridColumnFilter? GenerateColumnFilter(DynamicColumnBase column)
+    {
+        if(column is DynamicGridColumn gc)
+        {
+            if(gc.Editor is DateTimeEditor || gc.Editor is DateEditor)
+            {
+                return new DateTreeDynamicGridColumnFilter(this, column);
+            }
+            else
+            {
+                return new StandardDynamicGridColumnFilter(this, column);
+            }
+        }
+        else if(column is DynamicActionColumn ac)
+        {
+            if(ac.GetFilter is not null)
+            {
+                return ac.GetFilter();
+            }
+            else if(ac is DynamicTextColumn textColumn)
+            {
+                return new StandardDynamicGridColumnFilter(this, textColumn);
+            }
+            else
+            {
+                return null;
+            }
+        }
+        else
+        {
+            return null;
+        }
+    }
+
+    #endregion
+
+    protected virtual DynamicGridRowStyleSelector GetRowStyleSelector()
+    {
+        return new SimpleDynamicGridRowStyleSelector<DynamicGridRowStyle>();
+    }
+
+    protected virtual DynamicGridStyle GetRowStyle(CoreRow row, DynamicGridStyle style)
+    {
+        DynamicGridStyle? result = null;
+
+        if (ClipBuffer != null)
+            if (ClipBuffer.Item2.Contains(row))
+            {
+                var bgbrush = style.Background as SolidColorBrush;
+                var bgcolor = bgbrush != null ? bgbrush.Color : Colors.Transparent;
+
+                result = new DynamicGridRowStyle(style);
+
+                result.Background = ClipBuffer.Item1 == ClipAction.Cut
+                    ? new SolidColorBrush(bgcolor.MixColors(0.5, Colors.Orchid))
+                    : new SolidColorBrush(bgcolor.MixColors(0.5, Colors.LightGreen));
+
+                result.Foreground = new SolidColorBrush(Colors.Gray);
+                result.FontStyle = FontStyles.Italic;
+            }
+        result ??= OnGetRowStyle != null ? OnGetRowStyle(row, style) : style;
+
+        return result;
+    }
+
+    protected virtual void BeforeSelection(CancelEventArgs cancel)
+    {
+        OnBeforeSelection?.Invoke(cancel);
+    }
+
+    public bool IsReady { get; protected set; }
+
+    public UIElement? Header
+    {
+        get => _header;
+        set
+        {
+            if (_header is not null && Layout.Children.Contains(_header))
+                Layout.Children.Remove(_header);
+            _header = value;
+            if (_header is not null)
+            {
+                _header.SetValue(Grid.RowProperty, 0);
+                _header.SetValue(Grid.ColumnProperty, 0);
+                _header.SetValue(Grid.ColumnSpanProperty, 2);
+                Layout.Children.Add(_header);
+            }
+        }
+    }
+
+    /// <summary>
+    /// Represents the unfiltered data in the grid. This is <see langword="null"/> until <see cref="Refresh(bool, bool)"/> is called.
+    /// </summary>
+    /// <remarks>
+    /// This differs from <see cref="Data"/> in that <see cref="Data"/> has been filtered by <see cref="FilterRecord(CoreRow)"/>,
+    /// whereas <see cref="MasterData"/> contains every record loaded from the database.
+    /// </remarks>
+    public CoreTable? MasterData { get; set; }
+
+    public DynamicGridColumns VisibleColumns { get; protected set; }
+    public DynamicActionColumns ActionColumns { get; protected set; }
+    private List<DynamicColumnBase> ColumnList = new();
+    IList<DynamicColumnBase> IBaseDynamicGrid.ColumnList => ColumnList;
+
+    public CoreTable Data { get; set; }
+
+    public double RowHeight
+    {
+        get => UIComponent.RowHeight;
+        set => UIComponent.RowHeight = value;
+    }
+
+    public double HeaderHeight
+    {
+        get => UIComponent.HeaderRowHeight;
+        set => UIComponent.HeaderRowHeight = value;
+    }
+
+    #region Options
+
+    /// <summary>
+    /// Initialise things like custom buttons; called once during construction.
+    /// </summary>
+    protected abstract void Init();
+
+    protected abstract void DoReconfigure(DynamicGridOptions options);
+
+    private bool _hasLoadedOptions = false;
+
+    protected virtual void OptionsChanged()
+    {
+        var reloadColumns = false;
+
+        if(Help is not null)
+        {
+            Help.Visibility = Options.ShowHelp ? Visibility.Visible : Visibility.Collapsed;
+        }
+
+        Add.Visibility = Options.AddRows ? Visibility.Visible : Visibility.Collapsed;
+        Edit.Visibility = Options.EditRows ? Visibility.Visible : Visibility.Collapsed;
+
+        EditSpacer.Visibility = Options.AddRows || Options.EditRows
+            ? Visibility.Visible
+            : Visibility.Collapsed;
+
+        Print.Visibility = Options.Print ? Visibility.Visible : Visibility.Collapsed;
+        PrintSpacer.Visibility = Options.Print ? Visibility.Visible : Visibility.Collapsed;
+
+        Copy.Visibility = Options.ReorderRows ? Visibility.Visible : Visibility.Collapsed;
+        ClipboardSpacer.Visibility = Options.ReorderRows ? Visibility.Visible : Visibility.Collapsed;
+
+        if(ExportButton is not null)
+        {
+            ExportButton.Visibility = Options.ExportData ? Visibility.Visible : Visibility.Collapsed;
+        }
+        if(ImportButton is not null)
+        {
+            ImportButton.Visibility = Options.ImportData ? Visibility.Visible : Visibility.Collapsed;
+        }
+        ExportSpacer.Visibility = Options.ExportData || Options.ImportData
+            ? Visibility.Visible
+            : Visibility.Collapsed;
+
+        SwitchViewBtn.Visibility = Options.DirectEdit 
+            ? Options.HideDirectEditButton
+                ? Visibility.Collapsed 
+                : Visibility.Visible 
+            : Visibility.Collapsed;
+
+        Count.Visibility = Options.RecordCount ? Visibility.Visible : Visibility.Collapsed;
+
+        Delete.Visibility = Options.DeleteRows ? Visibility.Visible : Visibility.Collapsed;
+
+        if (drag is not null)
+        {
+            var hasSequence = drag.Position == DynamicActionColumnPosition.Start;
+            if (Options.ReorderRows)
+            {
+                if (!ActionColumns.Contains(drag))
+                {
+                    ActionColumns.Insert(0, drag);
+                }
+            }
+            else
+            {
+                ActionColumns.Remove(drag);
+            }
+            if(hasSequence != Options.ReorderRows)
+            {
+                drag.Position = Options.ReorderRows ? DynamicActionColumnPosition.Start : DynamicActionColumnPosition.Hidden;
+                reloadColumns = true;
+            }
+        }
+
+        if (DuplicateBtn != null)
+            DuplicateBtn.Visibility = Visibility.Collapsed;
+
+        if (UIComponent.OptionsChanged())
+        {
+            reloadColumns = true;
+        }
+
+        if(reloadColumns && IsReady)
+        {
+            Refresh(true, false);
+        }
+    }
+
+    public bool IsDirectEditMode()
+    {
+        return Options.DirectEdit
+            && (Settings.ViewMode == DynamicGridSettings.DynamicGridViewMode.DirectEdit
+                || Settings.ViewMode == DynamicGridSettings.DynamicGridViewMode.Default);
+    }
+
+    private void SwitchView_Click(object sender, RoutedEventArgs e)
+    {
+        Settings.ViewMode = Settings.ViewMode switch
+        {
+            DynamicGridSettings.DynamicGridViewMode.Default => DynamicGridSettings.DynamicGridViewMode.Normal,
+            DynamicGridSettings.DynamicGridViewMode.Normal => DynamicGridSettings.DynamicGridViewMode.DirectEdit,
+            DynamicGridSettings.DynamicGridViewMode.DirectEdit or _ => DynamicGridSettings.DynamicGridViewMode.Normal
+        };
+        SaveSettings(Settings);
+        Reconfigure();
+    }
+    public DynamicGridOptions Options { get; }
+    
+    protected void OnReconfigureEvent(DynamicGridOptions options)
+    {
+        _onReconfigure?.Invoke(options);
+    }
+
+    /// <summary>
+    /// Configure custom buttons and options.
+    /// </summary>
+    public void Reconfigure(DynamicGridOptions options)
+    {
+        options.BeginUpdate().Clear();
+        DoReconfigure(options);
+        OnReconfigureEvent(options);
+        options.EndUpdate();
+        if (!_hasLoadedOptions)
+        {
+            _hasLoadedOptions = true;
+            OptionsChanged();
+        }
+    }
+    
+    public void Reconfigure()
+    {
+        Reconfigure(Options);
+    }
+    
+    public void Reconfigure(IDynamicGrid.ReconfigureEvent onReconfigure)
+    {
+        OnReconfigure += onReconfigure;
+        Reconfigure();
+    }
+
+    #endregion
+
+    protected virtual DynamicGridSettings LoadSettings()
+    {
+        return new DynamicGridSettings();
+    }
+    protected virtual void SaveSettings(DynamicGridSettings settings)
+    {
+    }
+
+    protected virtual void LoadColumnsMenu(ContextMenu menu)
+    {
+    }
+
+    protected void UpdateCell(int row, string colname, object? value)
+    {
+        var coreRow = Data.Rows[row];
+        coreRow[colname] = value;
+        UIComponent.UpdateCell(coreRow, colname, value);
+    }
+
+    protected void UpdateCell(CoreRow row, DynamicColumnBase column)
+    {
+        UIComponent.UpdateCell(row, column);
+    }
+
+    #region Row Selections
+
+    protected CoreRow[] GetVisibleRows()
+    {
+        return UIComponent.GetVisibleRows();
+    }
+
+
+    public CoreRow[] SelectedRows
+    {
+        get => UIComponent.SelectedRows;
+        set => UIComponent.SelectedRows = value;
+    }
+
+    /// <summary>
+    /// Call the <see cref="OnSelectItem"/> event, and do any updating which needs to occur when items are selected.
+    /// </summary>
+    /// <param name="rows"></param>
+    protected virtual void SelectItems(CoreRow[]? rows)
+    {
+        if (IsReady)
+            OnSelectItem?.Invoke(this, new DynamicGridSelectionEventArgs(rows));
+
+        if(DuplicateBtn is not null)
+        {
+            DuplicateBtn.Visibility = CanDuplicate && rows != null && rows.Length >= 1 ? Visibility.Visible : Visibility.Collapsed;
+        }
+    }
+
+    protected virtual void DoDoubleClick(object sender, DynamicGridCellClickEventArgs args)
+    {
+        if (IsDirectEditMode())
+            return;
+
+        //SelectItems(SelectedRows);
+        var e = new HandledEventArgs(false);
+        OnDoubleClick?.Invoke(sender, e);
+        if (e.Handled)
+            return;
+        if (Options.EditRows)
+            DoEdit();
+    }
+
+    #endregion
+
+    #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()
+    {
+        return GenerateColumns();
+    }
+
+    /// <summary>
+    /// Provide a set of columns which is the default for this grid.
+    /// </summary>
+    public abstract DynamicGridColumns GenerateColumns();
+
+    protected abstract void SaveColumns(DynamicGridColumns columns);
+
+    public int DesiredWidth()
+    {
+        return UIComponent.DesiredWidth();
+    }
+
+    /// <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()
+    {
+    }
+
+    protected virtual void ConfigureColumns(DynamicGridColumns columns)
+    {
+    }
+
+    public class ColumnsLoadedEventArgs : EventArgs
+    {
+        public List<DynamicColumnBase> Columns { get; private set; }
+        public DynamicGridColumnGroupings ColumnGroupings { get; private set; }
+
+        public IEnumerable<DynamicActionColumn> ActionColumns => Columns.OfType<DynamicActionColumn>();
+        public IEnumerable<DynamicGridColumn> DataColumns => Columns.OfType<DynamicGridColumn>();
+
+        public ColumnsLoadedEventArgs(List<DynamicColumnBase> columns, DynamicGridColumnGroupings columnGroupings)
+        {
+            Columns = columns;
+            ColumnGroupings = columnGroupings;
+        }
+
+        public DynamicGridColumn Add<T>(
+            Expression<Func<T, object?>> member,
+            int? width = null,
+            string? caption = null,
+            string? format = null,
+            Alignment? alignment = null)
+        {
+            var col = DynamicGridColumns.CreateColumn(member, width: width, caption: caption, format: format, alignment: alignment);
+            Columns.Add(col);
+            return col;
+        }
+    }
+    
+    public delegate void ColumnsLoadedEvent(BaseDynamicGrid sender, ColumnsLoadedEventArgs args);
+
+    public event ColumnsLoadedEvent? ColumnsLoaded;
+
+    protected virtual void OnColumnsLoaded(List<DynamicColumnBase> columns, DynamicGridColumnGroupings groupings)
+    {
+        ColumnsLoaded?.Invoke(this, new ColumnsLoadedEventArgs(columns, groupings));
+    }
+    
+    private void ReloadColumns()
+    {
+        ColumnFilters.Clear();
+
+        VisibleColumns = LoadColumns();
+        ConfigureColumns(VisibleColumns);
+        
+        ColumnGroupings.Clear();
+        ConfigureColumnGroups();
+
+        ColumnList = new List<DynamicColumnBase>();
+        ColumnList.AddRange(ActionColumns.Where(x => x.Position == DynamicActionColumnPosition.Start));
+        ColumnList.AddRange(VisibleColumns);
+        ColumnList.AddRange(ActionColumns.Where(x => x.Position == DynamicActionColumnPosition.End));
+
+        OnColumnsLoaded(ColumnList, ColumnGroupings);
+        
+        UIComponent.RefreshColumns(ColumnList, ColumnGroupings);
+    }
+
+    #endregion
+
+    #region Refresh / Reload
+
+    protected bool IsPaging { get; set; } = false;
+    
+    protected virtual bool FilterRecord(CoreRow row)
+    {
+        if (OnFilterRecord is not null)
+            return OnFilterRecord(row);
+        return true;
+    }
+
+    private class RowRange(int rowIdx, int size)
+    {
+        public int RowIdx { get; set; } = rowIdx;
+
+        public int Size { get; set; } = size;
+    }
+
+    protected abstract void ReloadData(CancellationToken token, Action<CoreTable?, Exception?> action);
+
+    private CancellationTokenSource? RefreshCancellationToken;
+    public virtual void Refresh(bool reloadcolumns, bool reloaddata)
+    {
+        if (bRefreshing)
+            return;
+
+        if (!DoBeforeRefresh())
+            return;
+
+        UIComponent.BeforeRefresh();
+
+        using var cursor = UseWaitCursor ? new WaitCursor() : null;
+
+        Loading.Visibility = Visibility.Visible;
+        Loading.BeginAnimation(Label.OpacityProperty, LoadingFader);
+
+        bRefreshing = true;
+
+        if (reloadcolumns)
+        {
+            ReloadColumns();
+        }
+
+        if (reloaddata)
+        {
+            RefreshCancellationToken?.Cancel();
+
+            var tokenSource = new CancellationTokenSource();
+            RefreshCancellationToken = tokenSource;
+            var token = tokenSource.Token;
+
+            ReloadData(token, (table, exception) =>
+            {
+                if (token.IsCancellationRequested) return; // Don't bother even checking exceptions if task was cancelled.
+
+                if (exception != null)
+                {
+                    Dispatcher.Invoke(() =>
+                    {
+                        MessageWindow.ShowError("Sorry! We couldn't load the data.", exception);
+                    });
+                }
+                else if (table is not null)
+                {
+                    if (table.Offset == 0 || MasterData is null)
+                    {
+                        MasterData = table;
+                        Dispatcher.Invoke(() =>
+                        {
+                            try
+                            {
+                                ProcessData(null);
+                            }
+                            catch (Exception)
+                            {
+
+                            }
+                            DoAfterRefresh();
+                            bRefreshing = false;
+                            IsReady = true;
+                        });
+                    }
+                    else
+                    {
+                        int idx = MasterData.Rows.Count;
+                        MasterData.AddPage(table);
+                        Dispatcher.Invoke(() =>
+                        {
+                            try
+                            {
+                                ProcessData(new(idx, table.Rows.Count));
+                            }
+                            catch (Exception)
+                            {
+
+                            }
+                        });
+                    }
+                }
+            });
+        }
+        else
+        {
+            ProcessData(null);
+            DoAfterRefresh();
+            bRefreshing = false;
+            IsReady = true;
+        }
+    }
+
+    public void Shutdown()
+    {
+        RefreshCancellationToken?.Cancel();
+    }
+    
+    protected void NotifyBeforeRefresh(BeforeRefreshEventArgs args) => BeforeRefresh?.Invoke(this, args);
+    
+    protected void NotifyAfterRefresh(AfterRefreshEventArgs args) => AfterRefresh?.Invoke(this, args);
+
+    protected bool OnBeforeRefresh()
+    {
+        return true;
+    }
+
+    private bool DoBeforeRefresh()
+    {
+        var result = OnBeforeRefresh();
+        if (result)
+        {
+            var args = new BeforeRefreshEventArgs() { Cancel = false };
+            NotifyBeforeRefresh(args);
+            result = args.Cancel == false;
+        }
+
+        return result;
+    }
+
+    protected virtual void OnAfterRefresh()
+    {
+    }
+
+    protected void DoAfterRefresh()
+    {
+        OnAfterRefresh();
+        NotifyAfterRefresh(new AfterRefreshEventArgs());
+    }
+
+    /// <summary>
+    /// Process the data from <see cref="MasterData"/> according to <paramref name="range"/>.
+    /// </summary>
+    /// <remarks>
+    /// Set <paramref name="range"/> to <see langword="null"/> if this is the first page of data to be loaded. This will thus update the grid accordingly,
+    /// clearing all current rows, resetting columns, selection, etc. If the <paramref name="range"/> is provided, this will add to the grid the rows
+    /// according to the range from <see cref="MasterData"/>.
+    /// </remarks>
+    private void ProcessData(RowRange? range)
+    {
+        if(range is null)
+        {
+            Data.Columns.Clear();
+            Data.Setters.Clear();
+            if (MasterData != null)
+                foreach (var column in MasterData.Columns)
+                    Data.Columns.Add(column);
+        }
+
+        LoadData(range);
+    }
+
+    protected readonly Dictionary<CoreRow, CoreRow> _recordmap = new();
+
+    public void UpdateRow<TRow, TType>(CoreRow row, Expression<Func<TRow, TType>> column, TType value, bool refresh = true)
+    {
+        row.Set(column, value);
+        _recordmap[row].Set(column, value);
+        if (refresh)
+            InvalidateRow(row);
+    }
+
+    public void UpdateRow<TType>(CoreRow row, string column, TType value, bool refresh = true)
+    {
+        row.Set(column, value);
+        _recordmap[row].Set(column, value);
+        if (refresh)
+            InvalidateRow(row);
+    }
+
+    void IDynamicGridUIComponentParent.UpdateData(CoreRow row, string changedColumn, Dictionary<CoreColumn, object?> updates)
+    {
+        var result = new Dictionary<string, object?>();
+
+        foreach (var (col, value) in updates)
+        {
+            UpdateRow(row, col.ColumnName, value, refresh: false);
+        }
+    }
+
+    public void AddRow(CoreRow row)
+    {
+        if (MasterData is null) return;
+
+        var masterrow = MasterData.NewRow();
+        MasterData.FillRow(masterrow, row);
+        Refresh(false, false);
+    }
+
+    public void DeleteRow(CoreRow row)
+    {
+        if (MasterData is null) return;
+
+        var masterrow = _recordmap[row];
+        MasterData.Rows.Remove(masterrow);
+        Refresh(false, false);
+    }
+
+    /// <summary>
+    /// Filter all given rows into <paramref name="into"/>, given that they match <paramref name="filter"/> and <see cref="FilterRecord(CoreRow)"/>.
+    /// If <paramref name="recordMap"/> is given, also updates the map from <paramref name="from"/> to <paramref name="into"/>.
+    /// </summary>
+    protected IList<CoreRow> FilterRows(
+        IEnumerable<CoreRow> from,
+        CoreTable into,
+        Dictionary<CoreRow, CoreRow>? recordMap = null,
+        Func<CoreRow, bool>? filter = null)
+    {
+        var newRows = new List<CoreRow>();
+        foreach (var row in from)
+            if (FilterRecord(row) && filter?.Invoke(row) != false)
+            {
+                var newrow = into.NewRow();
+                for (var i = 0; i < into.Columns.Count; i++)
+                {
+                    var value = i < row.Values.Count ? row.Values[i] : null;
+                    if (into.Columns[i].DataType.IsNumeric())
+                        value = into.Columns[i].DataType.IsDefault(value) ? null : value;
+                    newrow.Values.Add(value);
+                }
+
+                newRows.Add(newrow);
+                into.Rows.Add(newrow);
+                recordMap?.TryAdd(newrow, row);
+            }
+        return newRows;
+    }
+    
+    private void LoadData(RowRange? range)
+    {
+        if (MasterData is null)
+            return;
+        if(range is null)
+        {
+            ResetClipBuffer();
+            Data.Rows.Clear();
+            _recordmap.Clear();
+
+            FilterRows(MasterData.Rows, Data, _recordmap);
+
+            InvalidateGrid();
+            SelectedRows = Array.Empty<CoreRow>();
+        }
+        else
+        {
+            var _newRows = FilterRows(Enumerable.Range(range.RowIdx, range.Size).Select(i => MasterData.Rows[i]), Data, _recordmap);
+            UIComponent.AddPage(_newRows);
+        }
+    }
+
+    public void InvalidateRow(CoreRow row)
+    {
+        UIComponent.InvalidateRow(row);
+    }
+
+    protected void InvalidateGrid()
+    {
+        if (RowStyleSelector != null)
+            RowStyleSelector.Data = Data;
+
+        UIComponent.RefreshData(Data);
+
+        Loading.BeginAnimation(Label.OpacityProperty, null);
+        Loading.Visibility = Visibility.Collapsed;
+    }
+
+    public void AddVisualFilter(string column, string value, FilterType filtertype = FilterType.Contains)
+    {
+        UIComponent.AddVisualFilter(column, value, filtertype);
+    }
+
+    protected List<Tuple<string, Func<CoreRow, bool>>> GetFilterPredicates()
+    {
+        return UIComponent.GetFilterPredicates();
+    }
+
+    public object? GetData(CoreRow row, DynamicColumnBase column)
+    {
+        if(column is DynamicActionColumn ac)
+        {
+            return ac.Data(row);
+        }
+        else if(column is DynamicGridColumn gc)
+        {
+            return row[gc.ColumnName];
+        }
+        else
+        {
+            return null;
+        }
+    }
+
+    #endregion
+
+    #region Item Manipulation
+
+    #region Abstract/Virtual Functions
+
+    /// <summary>
+    /// Create a new row.
+    /// </summary>
+    protected abstract void NewRow();
+
+    /// <summary>
+    /// Edit <paramref name="rows"/> or create a new row and edit it. This should update the rows, and if it creates a new row,
+    /// it should be added to the grid.
+    /// </summary>
+    protected abstract bool EditRows(CoreRow[]? rows);
+
+    public abstract void DeleteRows(params CoreRow[] rows);
+
+    protected virtual bool CanDeleteRows(params CoreRow[] rows)
+    {
+        return true;
+    }
+
+    private bool DuplicateButton_Click(Button button, CoreRow[] rows)
+    {
+        return DoDuplicate(rows);
+    }
+
+    private bool DoDuplicate(CoreRow[] rows)
+    {
+        return this is IDuplicateDynamicGrid grid && grid.DoDuplicate(rows);
+    }
+
+    #endregion
+
+    #region Load/Save/Delete
+
+    protected virtual void DoDelete()
+    {
+        var rows = SelectedRows.ToArray();
+
+        if (rows.Any())
+            if (CanDeleteRows(rows))
+                if (MessageBox.Show("Are you sure you wish to delete the selected records?", "Confirm Delete", MessageBoxButton.YesNo) ==
+                    MessageBoxResult.Yes)
+                {
+                    DeleteRows(rows);
+                    SelectedRows = Array.Empty<CoreRow>();
+                    Refresh(false, true);
+                    DoChanged();
+                    SelectItems(null);
+                }
+    }
+
+    private void Delete_Click(object sender, RoutedEventArgs e)
+    {
+        DoDelete();
+    }
+
+    #endregion
+
+    #region Edit
+
+    protected virtual void DoEdit()
+    {
+        if (SelectedRows.Length == 0)
+            return;
+
+        if (AddEditClick(SelectedRows))
+        {
+            SelectItems(SelectedRows);
+        }
+    }
+
+    private void Edit_Click(object sender, RoutedEventArgs e)
+    {
+        DoEdit();
+    }
+
+    protected virtual void DoAdd(bool openEditorOnDirectEdit = false)
+    {
+        if (IsDirectEditMode() && !openEditorOnDirectEdit)
+        {
+            NewRow();
+        }
+        else if (AddEditClick(null))
+        {
+            Refresh(false, true);
+        }
+    }
+
+    private void Add_Click(object sender, RoutedEventArgs e)
+    {
+        if (CanCreateRows())
+            DoAdd();
+    }
+
+    BaseEditor IDynamicGridUIComponentParent.CustomiseEditor(DynamicGridColumn column, BaseEditor editor)
+    {
+        return editor.CloneEditor();
+    }
+
+    protected virtual bool CanCreateRows()
+    {
+        return true;
+    }
+
+    private bool AddEditClick(CoreRow[]? rows)
+    {
+        if (!IsEnabled || bRefreshing)
+            return false;
+
+        if (rows == null || rows.Length == 0)
+        {
+            if (!CanCreateRows())
+                return false;
+
+            return EditRows(null);
+        }
+        else
+        {
+            return EditRows(rows);
+        }
+    }
+
+    #endregion
+
+    protected virtual void DoPrint(object sender)
+    {
+        OnPrintData?.Invoke(sender);
+    }
+
+    protected virtual void ShowHelp(string slug)
+    {
+        Process.Start(new ProcessStartInfo("https://prsdigital.com.au/wiki/index.php/" + slug) { UseShellExecute = true });
+    }
+
+    void IDynamicGridUIComponentParent.MoveRows(InABox.Core.CoreRow[] rows, int index) => MoveRows(rows, index);
+
+    #region ClipBuffer
+
+    private Tuple<ClipAction, CoreRow[]>? ClipBuffer;
+
+    protected void ResetClipBuffer()
+    {
+        ClipBuffer = null;
+    }
+
+    protected void SetClipBuffer(ClipAction action, CoreRow[] rows)
+    {
+        ClipBuffer = new Tuple<ClipAction, CoreRow[]>(action, rows);
+    }
+
+    private void CutToClipBuffer()
+    {
+        SetClipBuffer(ClipAction.Cut, SelectedRows);
+        InvalidateGrid();
+    }
+
+    private void CopyToClipBuffer()
+    {
+        SetClipBuffer(ClipAction.Copy, SelectedRows);
+        InvalidateGrid();
+    }
+
+    private void PasteFromClipBuffer()
+    {
+        if (ClipBuffer == null)
+            return;
+        var row = SelectedRows.FirstOrDefault();
+        MoveRows(ClipBuffer.Item2, row is not null ? (int)row.Index + 1 : Data.Rows.Count, isCopy: ClipBuffer.Item1 == ClipAction.Copy);
+    }
+
+    /// <summary>
+    /// Reorder the given rows, to place them at <paramref name="index"/>; that is, the first row of <paramref name="rows"/> will
+    /// be at <paramref name="index"/> after this method executes. If <paramref name="isCopy"/> is <see langword="true"/>, the items will copied, rather
+    /// than moved.
+    /// </summary>
+    /// <remarks>
+    /// To move the rows to the end, <paramref name="index"/> should be equal to the number of rows in <see cref="Data"/>.
+    /// </remarks>
+    protected abstract void MoveRows(CoreRow[] rows, int index, bool isCopy = false);
+
+    private void Copy_Click(object sender, RoutedEventArgs e)
+    {
+        var rows = SelectedRows;
+        if (rows.Length == 0) return;
+        MoveRows(rows, rows[^1].Index + 1, isCopy: true);
+    }
+
+    #endregion
+
+    #region Import / Export
+
+    private void DoImport()
+    {
+        if(this is IImportDynamicGrid grid)
+        {
+            grid.DoImport();
+        }
+    }
+
+    private void ImportButton_Click(object sender, RoutedEventArgs e)
+    {
+        DoImport();
+    }
+
+    public void Import() => DoImport();
+
+    private void DoExport()
+    {
+        if(this is IExportDynamicGrid grid)
+        {
+            grid.DoExport();
+        }
+    }
+
+    private void ExportButtonClick(object sender, RoutedEventArgs e)
+    {
+        DoExport();
+    }
+
+    #endregion
+
+    public void ScrollIntoView(CoreRow row)
+    {
+        UIComponent.ScrollIntoView(row);
+    }
+
+    #endregion
+
+    #region Custom Buttons
+
+    private Button CreateButton(BitmapImage? image = null, string? text = null, string? tooltip = null)
+    {
+        var button = new Button();
+        button.SetValue(BorderBrushProperty, new SolidColorBrush(Colors.Gray));
+        button.SetValue(BorderThicknessProperty, new Thickness(0.75));
+        button.Height = 30;
+        UpdateButton(button, image, text, tooltip);
+        return button;
+    }
+
+    public void UpdateButton(Button button, BitmapImage? image, string? text, string? tooltip = null)
+    {
+        var stackPnl = new StackPanel();
+        stackPnl.Orientation = Orientation.Horizontal;
+        //stackPnl.Margin = new Thickness(2);
+
+        if (image != null)
+        {
+            var img = new Image();
+            img.Source = image;
+            img.Margin = new Thickness(2);
+            img.ToolTip = tooltip;
+            stackPnl.Children.Add(img);
+        }
+
+        if (!string.IsNullOrEmpty(text))
+        {
+            button.MaxWidth = double.MaxValue;
+            var lbl = new Label();
+            lbl.Content = text;
+            lbl.VerticalAlignment = VerticalAlignment.Stretch;
+            lbl.VerticalContentAlignment = VerticalAlignment.Center;
+            lbl.Margin = new Thickness(2, 0, 5, 0);
+            lbl.ToolTip = ToolTip;
+            stackPnl.Children.Add(lbl);
+        }
+        else
+            button.MaxWidth = 30;
+
+        button.Content = stackPnl;
+        button.ToolTip = tooltip;
+    }
+
+    private bool bFirstButtonAdded = true;
+
+    private bool AnyButtonsVisible()
+    {
+        if (Add.Visibility != Visibility.Collapsed)
+            return true;
+        if (Edit.Visibility != Visibility.Collapsed)
+            return true;
+        /*if (MultiEdit.Visibility != Visibility.Collapsed)
+            return true;*/
+        if (ExportButton is not null && ExportButton.Visibility != Visibility.Collapsed)
+            return true;
+        return false;
+    }
+
+
+    public Button AddButton(string? caption, BitmapImage? image, string? tooltip, DynamicGridButtonClickEvent action, DynamicGridButtonPosition position = DynamicGridButtonPosition.Left)
+    {
+        var button = CreateButton(image, caption, tooltip);
+        button.Margin = position == DynamicGridButtonPosition.Right
+            ? new Thickness(2, 2, 0, 0)
+            : bFirstButtonAdded && AnyButtonsVisible()
+                ? new Thickness(0, 2, 0, 0)
+                : new Thickness(0, 2, 2, 0);
+        button.Padding = !String.IsNullOrWhiteSpace(caption) ? new Thickness(5, 1, 5, 1) : new Thickness(1);
+        button.Tag = action;
+        button.Click += Button_Click;
+        if (position == DynamicGridButtonPosition.Right)
+            RightButtonStack.Children.Add(button);
+        else
+            LeftButtonStack.Children.Add(button);
+        bFirstButtonAdded = false;
+        return button;
+    }
+
+    public Button AddButton(string? caption, BitmapImage? image, DynamicGridButtonClickEvent action, DynamicGridButtonPosition position = DynamicGridButtonPosition.Left)
+    {
+        var result = AddButton(caption, image, null, action, position);
+        return result;
+    }
+
+    private void Button_Click(object sender, RoutedEventArgs e)
+    {
+        var button = (Button)sender;
+        var action = (DynamicGridButtonClickEvent)button.Tag;
+
+        //CoreRow row = (CurrentRow > -1) && (CurrentRow < Data.Rows.Count) ? Data.Rows[this.CurrentRow] : null;
+        if (action.Invoke(button, SelectedRows))
+            Refresh(false, true);
+    }
+
+    #endregion
+
+    #region Header Actions
+
+    protected abstract bool SelectColumns([NotNullWhen(true)] out DynamicGridColumns? columns);
+
+    private void SelectColumnsClick()
+    {
+        if (SelectColumns(out var columns))
+        {
+            VisibleColumns.Clear();
+            VisibleColumns.AddRange(columns);
+            SaveColumns(columns);
+            Refresh(true, true);
+        }
+    }
+
+    #endregion
+
+    #region Drag + Drop
+
+    /// <summary>
+    /// Handle a number of rows from a different <see cref="DynamicGrid{T}"/> being dragged into this one.
+    /// </summary>
+    /// <param name="entity">The type of entity that that the rows of <paramref name="table"/> represent.</param>
+    /// <param name="table">The data being dragged.</param>
+    /// <param name="e"></param>
+    protected virtual void OnDragEnd(Type entity, CoreTable table, DragEventArgs e)
+    {
+        Logger.Send(LogType.Information,"","OnDragEnd");
+    }
+
+    /// <summary>
+    /// Handle all types of items being dragged onto this grid that aren't handled by <see cref="OnDragEnd(Type, CoreTable, DragEventArgs)"/>,
+    /// i.e., data which is not a <see cref="CoreTable"/> from another <see cref="DynamicGrid{T}"/>
+    /// </summary>
+    /// <remarks>
+    /// Can be used to handle files, for example.
+    /// </remarks>
+    /// <param name="sender"></param>
+    /// <param name="e"></param>
+    protected virtual void HandleDragDrop(object sender, DragEventArgs e)
+    {
+    }
+
+    protected virtual void HandleDragOver(object sender, DragEventArgs e)
+    {
+    }
+
+    protected virtual DragDropEffects OnRowsDragStart(CoreRow[] rows)
+    {
+        return DragDropEffects.None;
+    }
+
+    #endregion
+
+}
+
+/// <summary>
+/// Shows that this <see cref="BaseDynamicGrid"/> can be used to import data.
+/// </summary>
+public interface IImportDynamicGrid
+{
+    void DoImport();
+}
+
+/// <summary>
+/// Shows that this <see cref="BaseDynamicGrid"/> can be used to export data.
+/// </summary>
+public interface IExportDynamicGrid
+{
+    void DoExport();
+}
+
+/// <summary>
+/// Shows that this <see cref="BaseDynamicGrid"/> can be used to duplicate data.
+/// </summary>
+public interface IDuplicateDynamicGrid
+{
+    bool DoDuplicate(CoreRow[] rows);
+}
+
+/// <summary>
+/// Shows that this <see cref="BaseDynamicGrid"/> can show a help menu.
+/// </summary>
+public interface IHelpDynamicGrid
+{
+    string HelpSlug();
+}

+ 243 - 0
inabox.wpf/DynamicGrid/Grids/CoreTableGrid.cs

@@ -0,0 +1,243 @@
+using InABox.Core;
+using NPOI.HSSF.Record;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace InABox.DynamicGrid;
+
+public class CoreTableColumnSchema(CoreTable table) : IDynamicGridColumnSchema
+{
+    private DynamicGridColumn[] _columns = table.Columns.Select(DynamicGridColumn.FromCoreColumn).NotNull().ToArray();
+
+    public IEnumerable<string> ColumnNames => _columns.Select(x => x.ColumnName);
+
+    public DynamicGridColumn GetColumn(string column)
+    {
+        return _columns.First(x => x.ColumnName == column);
+    }
+
+    public string? GetComment(string column)
+    {
+        return null;
+    }
+
+    public bool IsVisible(string column)
+    {
+        return true;
+    }
+}
+
+public class CoreTableGrid : BaseDynamicGrid
+{
+    public CoreTable Table { get; set; } = new();
+
+    public IDynamicGridColumnSchema? ColumnSchema { get; set; }
+
+    #region Config
+
+    protected override void Init()
+    {
+    }
+
+    protected override void DoReconfigure(DynamicGridOptions options)
+    {
+        // We can add rows by default, but not edit them.
+        options.EditRows = false;
+    }
+
+    #endregion
+
+    #region Row Manipulation
+
+    protected override void NewRow()
+    {
+        // This makes use of the fact that MasterData will be equivalent to Table (see 'ReloadData()').
+
+        var row = Table.NewRow(true);
+        Table.Rows.Add(row);
+
+        var dataRow = Data.NewRow();
+        dataRow.LoadValues(row.Values);
+        Data.Rows.Add(dataRow);
+
+        _recordmap[dataRow] = row;
+
+        InvalidateGrid();
+        SelectedRows = [dataRow];
+        DoChanged();
+    }
+
+    protected override bool EditRows(CoreRow[]? rows)
+    {
+        if(rows is null)
+        {
+            var row = Table.NewRow(true);
+            Table.Rows.Add(row);
+
+            var dataRow = Data.NewRow();
+            dataRow.LoadValues(row.Values);
+            Data.Rows.Add(dataRow);
+
+            _recordmap[dataRow] = row;
+
+            InvalidateGrid();
+            SelectedRows = [dataRow];
+            DoChanged();
+
+            return true;
+        }
+        else
+        {
+            // It's not obvious how to edit a row, without this being overriden by a subclass. Hence, we have turned off editing,
+            // and the case that 'rows' is null is just basically adding a new row.
+            return false;
+        }
+    }
+
+    public override void DeleteRows(params CoreRow[] rows)
+    {
+        foreach(var row in rows)
+        {
+            // This relies on MasterData being equivalent to Table.
+            if(_recordmap.Remove(row, out var tableRow))
+            {
+                Table.Rows.Remove(tableRow);
+            }
+        }
+    }
+
+    protected override void MoveRows(CoreRow[] rows, int index, bool isCopy = false)
+    {
+        CoreRow? targetRow;
+        if(index < Data.Rows.Count)
+        {
+            var targetDataRow = Data.Rows[index];
+            targetRow = _recordmap.GetValueOrDefault(targetDataRow);
+        }
+        else
+        {
+            var lastDataRow = Data.Rows.LastOrDefault();
+            if(lastDataRow is null)
+            {
+                targetRow = null;
+            }
+            else if(!_recordmap.TryGetValue(lastDataRow, out var lastTargetRow))
+            {
+                targetRow = null;
+            }
+            else if(lastTargetRow == Table.Rows.LastOrDefault())
+            {
+                targetRow = null;
+            }
+            else
+            {
+                targetRow = Table.Rows[lastTargetRow.Index + 1];
+            }
+        }
+ 
+        if (isCopy)
+        {
+            rows = rows.ToArray(x =>
+            {
+                var row = Table.NewRow();
+                row.LoadValues(x.Values);
+                return row;
+            });
+        }
+        else
+        {
+            foreach(var row in rows)
+            {
+                if(_recordmap.TryGetValue(row, out var tableRow))
+                {
+                    Table.Rows.Remove(tableRow);
+                }
+            }
+        }
+        if(targetRow is not null)
+        {
+            var idx = Table.Rows.IndexOf(targetRow);
+            Table.Rows.InsertRange(idx, rows);
+        }
+        else
+        {
+            Table.Rows.AddRange(rows);
+        }
+
+        Refresh(false, true);
+        SelectedRows = rows.Select(row =>
+        {
+            return _recordmap.FirstOrDefault(x => x.Value == row).Key;
+        }).NotNull().ToArray();
+    }
+
+    #endregion
+
+    #region Columns
+
+    private IDynamicGridColumnSchema GetColumnSchema()
+    {
+        return ColumnSchema ?? new CoreTableColumnSchema(Table);
+    }
+
+    protected override bool SelectColumns([NotNullWhen(true)] out DynamicGridColumns? columns)
+    {
+        var editor = new DynamicGridColumnsEditor(GetColumnSchema(), null)
+        {
+            WindowStartupLocation = WindowStartupLocation.CenterScreen
+        };
+        editor.DirectEdit = IsDirectEditMode();
+
+        editor.Columns.AddRange(VisibleColumns);
+
+        if (editor.ShowDialog().Equals(true))
+        {
+            columns = editor.Columns;
+            return true;
+        }
+        else
+        {
+            columns = null;
+            return false;
+        }
+    }
+
+    private DynamicGridColumns? _columns;
+
+    protected override DynamicGridColumns LoadColumns()
+    {
+        return _columns ?? base.LoadColumns();
+    }
+
+    public override DynamicGridColumns GenerateColumns()
+    {
+        var schema = GetColumnSchema();
+        var columns = new DynamicGridColumns();
+        columns.AddRange(schema.ColumnNames.Select(schema.GetColumn));
+        return columns;
+    }
+
+    protected override void SaveColumns(DynamicGridColumns columns)
+    {
+        _columns = columns;
+    }
+
+    #endregion
+
+    #region Data
+
+    protected override void ReloadData(CancellationToken token, Action<CoreTable?, Exception?> action)
+    {
+        // I think this is all we need to do. If we were to do paging or something like that, this would fail,
+        // since we would be adding the table to itself; but for now this will suffice.
+        action(Table, null);
+    }
+
+    #endregion
+}

Разница между файлами не показана из-за своего большого размера
+ 2 - 1580
inabox.wpf/DynamicGrid/Grids/DynamicGrid.cs


+ 210 - 182
inabox.wpf/DynamicGrid/UIComponent/DynamicGridGridUIComponent.cs

@@ -190,6 +190,10 @@ public class DynamicGridGridUIComponent : IDynamicGridUIComponent, IDynamicGridG
         DataGrid.MouseRightButtonUp += DataGrid_MouseRightButtonUp;
         DataGrid.KeyUp += DataGrid_KeyUp;
         DataGrid.PreviewGotKeyboardFocus += DataGrid_PreviewGotKeyboardFocus;
+
+        DataGrid.CurrentCellBeginEdit += DataGrid_CurrentCellBeginEdit;
+        DataGrid.CurrentCellEndEdit += DataGrid_CurrentCellEndEdit;
+        DataGrid.PreviewKeyUp += DataGrid_PreviewKeyUp;
         
         DataGrid.SetValue(ScrollViewer.VerticalScrollBarVisibilityProperty, ScrollBarVisibility.Visible);
         
@@ -203,6 +207,24 @@ public class DynamicGridGridUIComponent : IDynamicGridUIComponent, IDynamicGridG
         DataGrid.CellToolTipOpening += DataGrid_CellToolTipOpening;
 
         DataGrid.SizeChanged += DataGrid_SizeChanged;
+
+        DataGrid.SelectionController = new GridSelectionControllerExt(DataGrid, this);
+    }
+
+    public class GridSelectionControllerExt(SfDataGrid datagrid, DynamicGridGridUIComponent grid) : GridSelectionController(datagrid)
+    {
+        public override bool HandleKeyDown(KeyEventArgs args)
+        {
+            if (args.Key == Key.Escape)
+            {
+                grid.CancelEdit();
+                return false;
+            }
+            else
+            {
+                return base.HandleKeyDown(args);
+            }
+        }
     }
 
     #region Selection
@@ -1497,6 +1519,37 @@ public class DynamicGridGridUIComponent : IDynamicGridUIComponent, IDynamicGridG
 
     #region Direct Edit
 
+    protected bool bChanged;
+
+    protected class DirectEditingObject
+    {
+        public object? Object { get; set; }
+
+        public CoreRow Row { get; set; }
+
+        public DataRow? DataRow { get; set; }
+
+        public DirectEditingObject(object? obj, CoreRow row, DataRow? dataRow)
+        {
+            Object = obj;
+            Row = row;
+            DataRow = dataRow;
+        }
+    }
+
+    protected DirectEditingObject? _editingObject;
+
+    protected DirectEditingObject EnsureEditingObject(CoreRow row)
+    {
+        _editingObject ??= new(GetEditingObject(row), row, DataGridItems?.Rows[row.Index]);
+        return _editingObject;
+    }
+
+    protected virtual object? GetEditingObject(CoreRow row)
+    {
+        return null;
+    }
+
     private DataRow? GetDataRow(CoreRow row)
     {
         return DataGridItems?.Rows[row.Index];
@@ -1559,10 +1612,139 @@ public class DynamicGridGridUIComponent : IDynamicGridUIComponent, IDynamicGridG
     {
     }
 
+    protected virtual void BeginEdit(CoreRow row, CurrentCellBeginEditEventArgs e)
+    {
+        EnsureEditingObject(row);
+    }
+
+    private void DataGrid_CurrentCellBeginEdit(object? sender, CurrentCellBeginEditEventArgs e)
+    {
+        var row = GetRowFromIndex(e.RowColumnIndex.RowIndex);
+        if (row is null)
+            return;
+
+        BeginEdit(row, e);
+
+        bChanged = false;
+    }
+
+    protected virtual void UpdateData(string column, Dictionary<CoreColumn, object?> updates)
+    {
+        if (_editingObject is null) return;
+
+        var coreRow = _editingObject.Row;
+
+        Parent.UpdateData(coreRow, column, updates);
+    }
+
+    private void UpdateData(int rowIndex, int columnIndex)
+    {
+        var table = DataGridItems;
+        if (table is null)
+            return;
+        
+        if (GetColumn(columnIndex) is DynamicGridColumn gridcol)
+        {
+            var datacol = Parent.Data.Columns.FirstOrDefault(x => x.ColumnName.Equals(gridcol.ColumnName));
+            if (datacol != null)
+            {
+                var datacolindex = Parent.Data.Columns.IndexOf(datacol);
+
+                var value = table.Rows[rowIndex][datacolindex];
+                if (value is DBNull)
+                    value = CoreUtils.GetDefault(datacol.DataType);
+
+                UpdateData(datacol.ColumnName, new Dictionary<CoreColumn, object?>() { { datacol, value } });
+            }
+        }
+    }
+
     protected virtual void ColumnChanged(CoreColumn dataCol, CoreRow row, object? value)
     {
+        var col = ColumnList.OfType<DynamicGridColumn>()
+            .FirstOrDefault(x => x.ColumnName.Equals(dataCol.ColumnName));
+        
+        if (col is null)
+            return;
+
+        if (col.Editor is CheckBoxEditor)
+        {
+            EnsureEditingObject(row);
+            if(_editingObject is not null)
+            {
+                if (value is DBNull)
+                    value = CoreUtils.GetDefault(dataCol.DataType);
+
+                _invalidating = true;
+                UpdateData(dataCol.ColumnName, new Dictionary<CoreColumn, object?>() { { dataCol, value } });
+                _invalidating = false;
+            }
+
+            _editingObject = null;
+        }
+        if (_editingObject is not null)
+            bChanged = true;
+    }
+
+    private void DataGrid_CurrentCellEndEdit(object? sender, CurrentCellEndEditEventArgs e)
+    {
+        if (_editingObject is not null && bChanged)
+        {
+            UpdateData(_editingObject.Row.Index, e.RowColumnIndex.ColumnIndex);
+        }
+        if (bChanged)
+            Parent.DoChanged();
+        bChanged = false;
+        _editingObject = null;
+
+        // Commented out on 19/02/2024 by Kenric. I don't see this being necessary, though I could be wrong. Nevertheless, it was causing a bug when
+        // editing the filter row. It seems that this causes Syncfusion to commit the filter predicates internally, which means that after leaving a 
+        // filter row cell, the filter remained even once it was cleared, meaning a refresh was necessary to get the data back.
+        // I've tested on Bills to see if editing works with this empty, and it seems so.
+        //DataGridItems?.AcceptChanges();
     }
 
+    private void DataGrid_PreviewKeyUp(object sender, KeyEventArgs e)
+    {
+        if (e.Key == Key.OemPeriod)
+        {
+            if (e.OriginalSource is TimeSpanEdit editor && editor.SelectionStart < 2)
+            {
+                editor.SelectionStart = 3;
+            }
+        }
+        else if (e.Key == Key.Tab)
+        {
+            if (Parent.IsDirectEditMode())
+            {
+                DataGrid.SelectionController.CurrentCellManager.EndEdit();
+                DataGrid.MoveFocus(new TraversalRequest(FocusNavigationDirection.Right));
+                DataGrid.SelectionController.CurrentCellManager.BeginEdit();
+                e.Handled = true;
+            }
+        }
+        else if(e.Key == Key.Escape)
+        {
+            if (Parent.IsDirectEditMode())
+            {
+                bChanged = false;
+            }
+        }
+    }
+
+    protected virtual void CancelEdit()
+    {
+        var obj = _editingObject;
+        bChanged = false;
+        _editingObject = null;
+        DataGrid.SelectionController.CurrentCellManager.EndEdit(false);
+        if(obj is not null && obj.DataRow is not null)
+        {
+            UpdateRow(obj.Row, obj.DataRow);
+        }
+    }
+
+
     private void Result_ColumnChanged(object sender, DataColumnChangeEventArgs e)
     {
         if (_invalidating) return;
@@ -1673,28 +1855,7 @@ public class DynamicGridGridUIComponent<T> : DynamicGridGridUIComponent, IDynami
 
     public DynamicGridGridUIComponent()
     {
-        DataGrid.CurrentCellBeginEdit += DataGrid_CurrentCellBeginEdit;
-        DataGrid.CurrentCellEndEdit += DataGrid_CurrentCellEndEdit;
         DataGrid.CurrentCellDropDownSelectionChanged += DataGrid_CurrentCellDropDownSelectionChanged;
-        DataGrid.PreviewKeyUp += DataGrid_PreviewKeyUp;
-
-        DataGrid.SelectionController = new GridSelectionControllerExt(DataGrid, this);
-    }
-
-    public class GridSelectionControllerExt(SfDataGrid datagrid, DynamicGridGridUIComponent<T> grid) : GridSelectionController(datagrid)
-    {
-        public override bool HandleKeyDown(KeyEventArgs args)
-        {
-            if (args.Key == Key.Escape)
-            {
-                grid.CancelEdit();
-                return false;
-            }
-            else
-            {
-                return base.HandleKeyDown(args);
-            }
-        }
     }
 
     protected override bool CreateEditorColumn(DynamicGridColumn column, [NotNullWhen(true)] out IDynamicGridEditorColumn? newColumn)
@@ -1703,7 +1864,7 @@ public class DynamicGridGridUIComponent<T> : DynamicGridGridUIComponent, IDynami
         {
             if(newColumn is IDynamicGridEditorColumn<T> typed)
             {
-                typed.GetEntity = () => _editingObject?.Object;
+                typed.GetEntity = () => _editingObject?.Object as T;
                 typed.EntityChanged += DoEntityChanged;
             }
             foreach (var extra in newColumn.ExtraColumns)
@@ -1718,30 +1879,9 @@ public class DynamicGridGridUIComponent<T> : DynamicGridGridUIComponent, IDynami
 
     #region Direct Edit
 
-    private bool bChanged;
-
-    private class DirectEditingObject
-    {
-        public T Object { get; set; }
-
-        public CoreRow Row { get; set; }
-
-        public DataRow? DataRow { get; set; }
-
-        public DirectEditingObject(T obj, CoreRow row, DataRow? dataRow)
-        {
-            Object = obj;
-            Row = row;
-            DataRow = dataRow;
-        }
-    }
-
-    private DirectEditingObject? _editingObject;
-
-    private DirectEditingObject EnsureEditingObject(CoreRow row)
+    protected override object? GetEditingObject(CoreRow row)
     {
-        _editingObject ??= new(Parent.LoadItem(row), row, DataGridItems?.Rows[row.Index]);
-        return _editingObject;
+        return Parent.LoadItem(row);
     }
 
     private DataRow? GetDataRow(CoreRow row)
@@ -1753,113 +1893,18 @@ public class DynamicGridGridUIComponent<T> : DynamicGridGridUIComponent, IDynami
     {
         base.DoEntityChanged(column, args);
 
-        if (_editingObject is null) return;
+        if (_editingObject?.Object is not T obj) return;
 
-        Parent.EntityChanged(_editingObject.Object, _editingObject.Row, args.ColumnName, args.Changes);
+        Parent.EntityChanged(obj, _editingObject.Row, args.ColumnName, args.Changes);
     }
 
-    private void UpdateData(string column, Dictionary<CoreColumn, object?> updates)
+    protected override void UpdateData(string column, Dictionary<CoreColumn, object?> updates)
     {
-        if (_editingObject is null)
-            return;
+        if (_editingObject?.Object is not T obj) return;
 
         var coreRow = _editingObject.Row;
 
-        Parent.UpdateData(_editingObject.Object, coreRow, column, updates);
-    }
-
-    private void UpdateData(int rowIndex, int columnIndex)
-    {
-        var table = DataGridItems;
-        if (table is null)
-            return;
-        
-        if (GetColumn(columnIndex) is DynamicGridColumn gridcol)
-        {
-            var datacol = Parent.Data.Columns.FirstOrDefault(x => x.ColumnName.Equals(gridcol.ColumnName));
-            if (datacol != null)
-            {
-                var datacolindex = Parent.Data.Columns.IndexOf(datacol);
-
-                var value = table.Rows[rowIndex][datacolindex];
-                if (value is DBNull)
-                    value = CoreUtils.GetDefault(datacol.DataType);
-
-                UpdateData(datacol.ColumnName, new Dictionary<CoreColumn, object?>() { { datacol, value } });
-            }
-        }
-    }
-
-    private void DataGrid_CurrentCellBeginEdit(object? sender, CurrentCellBeginEditEventArgs e)
-    {
-        var table = DataGridItems;
-        var row = GetRowFromIndex(e.RowColumnIndex.RowIndex);
-        if (table is null || row is null)
-            return;
-
-        EnsureEditingObject(row);
-
-        var column = DataGrid.Columns[e.RowColumnIndex.ColumnIndex] as GridComboBoxColumn;
-        if (column != null && column.ItemsSource == null)
-        {
-            var colname = column.MappingName;
-            var colno = table.Columns.IndexOf(colname);
-            var property = Parent.Data.Columns[colno].ColumnName;
-
-            var col = ColumnList.OfType<DynamicGridColumn>()
-                .FirstOrDefault(x => x.ColumnName.Equals(property));
-
-            if (col?.Editor is ILookupEditor lookupEditor)
-            {
-                if (!Lookups.ContainsKey(property))
-                    Lookups[property] = lookupEditor.Values(property);
-                var combo = column;
-                combo.ItemsSource = Lookups[property].ToDictionary(Lookups[property].Columns[0].ColumnName, "Display");
-                combo.SelectedValuePath = "Key";
-                combo.DisplayMemberPath = "Value";
-            }
-        }
-
-        bChanged = false;
-    }
-
-    protected override void ColumnChanged(CoreColumn dataCol, CoreRow row, object? value)
-    {
-        var col = ColumnList.OfType<DynamicGridColumn>()
-            .FirstOrDefault(x => x.ColumnName.Equals(dataCol.ColumnName));
-        
-        if (col is null)
-            return;
-
-        if (col.Editor is CheckBoxEditor)
-        {
-            EnsureEditingObject(row);
-            if(_editingObject is not null)
-            {
-                if (value is DBNull)
-                    value = CoreUtils.GetDefault(dataCol.DataType);
-
-                _invalidating = true;
-                UpdateData(dataCol.ColumnName, new Dictionary<CoreColumn, object?>() { { dataCol, value } });
-                _invalidating = false;
-            }
-
-            _editingObject = null;
-        }
-        if (_editingObject is not null)
-            bChanged = true;
-    }
-
-    private void CancelEdit()
-    {
-        var obj = _editingObject;
-        bChanged = false;
-        _editingObject = null;
-        DataGrid.SelectionController.CurrentCellManager.EndEdit(false);
-        if(obj is not null && obj.DataRow is not null)
-        {
-            base.UpdateRow(obj.Row, obj.DataRow);
-        }
+        Parent.UpdateData(obj, coreRow, column, updates);
     }
 
     private void DataGrid_CurrentCellDropDownSelectionChanged(object? sender,
@@ -1916,48 +1961,31 @@ public class DynamicGridGridUIComponent<T> : DynamicGridGridUIComponent, IDynami
         }
     }
 
-    private void DataGrid_CurrentCellEndEdit(object? sender, CurrentCellEndEditEventArgs e)
+    protected override void BeginEdit(CoreRow row, CurrentCellBeginEditEventArgs e)
     {
-        if (_editingObject is not null && bChanged)
-        {
-            UpdateData(_editingObject.Row.Index, e.RowColumnIndex.ColumnIndex);
-        }
-        if (bChanged)
-            Parent.DoChanged();
-        bChanged = false;
-        _editingObject = null;
+        base.BeginEdit(row, e);
 
-        // Commented out on 19/02/2024 by Kenric. I don't see this being necessary, though I could be wrong. Nevertheless, it was causing a bug when
-        // editing the filter row. It seems that this causes Syncfusion to commit the filter predicates internally, which means that after leaving a 
-        // filter row cell, the filter remained even once it was cleared, meaning a refresh was necessary to get the data back.
-        // I've tested on Bills to see if editing works with this empty, and it seems so.
-        //DataGridItems?.AcceptChanges();
-    }
+        var table = DataGridItems;
+        if (table is null) return;
 
-    private void DataGrid_PreviewKeyUp(object sender, KeyEventArgs e)
-    {
-        if (e.Key == Key.OemPeriod)
-        {
-            if (e.OriginalSource is TimeSpanEdit editor && editor.SelectionStart < 2)
-            {
-                editor.SelectionStart = 3;
-            }
-        }
-        else if (e.Key == Key.Tab)
-        {
-            if (Parent.IsDirectEditMode())
-            {
-                DataGrid.SelectionController.CurrentCellManager.EndEdit();
-                DataGrid.MoveFocus(new TraversalRequest(FocusNavigationDirection.Right));
-                DataGrid.SelectionController.CurrentCellManager.BeginEdit();
-                e.Handled = true;
-            }
-        }
-        else if(e.Key == Key.Escape)
+        var column = DataGrid.Columns[e.RowColumnIndex.ColumnIndex] as GridComboBoxColumn;
+        if (column != null && column.ItemsSource == null)
         {
-            if (Parent.IsDirectEditMode())
+            var colname = column.MappingName;
+            var colno = table.Columns.IndexOf(colname);
+            var property = Parent.Data.Columns[colno].ColumnName;
+
+            var col = ColumnList.OfType<DynamicGridColumn>()
+                .FirstOrDefault(x => x.ColumnName.Equals(property));
+
+            if (col?.Editor is ILookupEditor lookupEditor)
             {
-                bChanged = false;
+                if (!Lookups.ContainsKey(property))
+                    Lookups[property] = lookupEditor.Values(property);
+                var combo = column;
+                combo.ItemsSource = Lookups[property].ToDictionary(Lookups[property].Columns[0].ColumnName, "Display");
+                combo.SelectedValuePath = "Key";
+                combo.DisplayMemberPath = "Value";
             }
         }
     }

+ 2 - 0
inabox.wpf/DynamicGrid/UIComponent/IDynamicGridUIComponent.cs

@@ -52,6 +52,8 @@ public interface IDynamicGridUIComponentParent : IBaseDynamicGrid
     void MoveRows(CoreRow[] rows, int index);
     
     void UIFilterChanged(object sender);
+
+    void UpdateData(CoreRow row, string changedColumn, Dictionary<CoreColumn, object?> updates);
 }
 
 public interface IDynamicGridUIComponentParent<T> : IDynamicGrid<T>, IDynamicGridUIComponentParent

Некоторые файлы не были показаны из-за большого количества измененных файлов