فهرست منبع

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

frankvandenbos 9 ماه پیش
والد
کامیت
dfde816b9b

+ 194 - 141
inabox.wpf/DynamicGrid/DynamicEditorForm/DynamicEditorForm.xaml.cs

@@ -9,195 +9,248 @@ using InABox.WPF;
 using Syncfusion.Windows.Shared;
 using Syncfusion.Windows.Tools.Controls;
 
+namespace InABox.DynamicGrid;
 
-namespace InABox.DynamicGrid
+public class DynamicEditorFormModel
 {
-    
-    public class DynamicEditorFormModel
+    /// <summary>
+    ///     Constructor of the UtilityViewModel class.
+    /// </summary>
+    public DynamicEditorFormModel()
     {
-        /// <summary>
-        ///     Constructor of the UtilityViewModel class.
-        /// </summary>
-        public DynamicEditorFormModel()
-        {
-            var utilities = new ObservableCollection<UtilityItem>();
-            utilities.Add(new UtilityItem
-                { Name = "Help", Icon = Resources.help.AsBitmapImage(), Text = "", Mode = SizeMode.Normal, Command = HelpCommand });
-            Utilities = utilities;
-        }
-
-        /// <summary>
-        ///     Collection containing the complete details of the items to be bound in the title bar.
-        /// </summary>
-        public ObservableCollection<UtilityItem> Utilities { get; }
+        var utilities = new ObservableCollection<UtilityItem>();
+        utilities.Add(new UtilityItem
+            { Name = "Help", Icon = Resources.help.AsBitmapImage(), Text = "", Mode = SizeMode.Normal, Command = HelpCommand });
+        Utilities = utilities;
+    }
 
-        /// <summary>
-        ///     Commmand for the Help button.
-        /// </summary>
-        public DelegateCommand HelpCommand => new(HelpCommandAction);
+    /// <summary>
+    ///     Collection containing the complete details of the items to be bound in the title bar.
+    /// </summary>
+    public ObservableCollection<UtilityItem> Utilities { get; }
 
-        public static string Slug { get; set; }
+    /// <summary>
+    ///     Commmand for the Help button.
+    /// </summary>
+    public DelegateCommand HelpCommand => new(HelpCommandAction);
 
-        /// <summary>
-        ///     Action that is performed when clicking the help button.
-        /// </summary>
-        private void HelpCommandAction(object param)
-        {
-            Process.Start("https://prsdigital.com.au/wiki/index.php/" + Slug);
-        }
-    }
+    public static string Slug { get; set; }
 
     /// <summary>
-    ///     Interaction logic for DynamicEditor.xaml
+    ///     Action that is performed when clicking the help button.
     /// </summary>
-    public partial class DynamicEditorForm : ThemableChromelessWindow, IDynamicEditorForm
+    private void HelpCommandAction(object param)
     {
-        #region IDynamicEditorForm
+        Process.Start("https://prsdigital.com.au/wiki/index.php/" + Slug);
+    }
+}
 
-        public bool ReadOnly { get => Form.ReadOnly; set => Form.ReadOnly = value; }
-        public BaseObject[] Items { get => Form.Items; set => Form.Items = value; }
-        public DynamicEditorPages? Pages { get => Form.Pages; }
+/// <summary>
+///     Interaction logic for DynamicEditor.xaml
+/// </summary>
+public partial class DynamicEditorForm : ThemableChromelessWindow, IDynamicEditorForm, ISubPanel
+{
+    #region IDynamicEditorForm
 
-        public event OnBeforeLoad? OnBeforeLoad;
-        public void BeforeLoad() => OnBeforeLoad?.Invoke(this);
+    public bool ReadOnly { get => Form.ReadOnly; set => Form.ReadOnly = value; }
+    public BaseObject[] Items { get => Form.Items; set => Form.Items = value; }
+    public DynamicEditorPages? Pages { get => Form.Pages; }
 
-        public event OnAfterLoad? OnAfterLoad;
-        public void AfterLoad() => OnAfterLoad?.Invoke(this);
+    public event OnBeforeLoad? OnBeforeLoad;
+    public void BeforeLoad() => OnBeforeLoad?.Invoke(this);
 
-        public event OnValidateData? OnValidateData { add => Form.OnValidateData += value; remove => Form.OnValidateData -= value; }
+    public event OnAfterLoad? OnAfterLoad;
+    public void AfterLoad() => OnAfterLoad?.Invoke(this);
 
-        public OnCustomiseColumns? OnCustomiseColumns
-        {
-            get => Form.OnCustomiseColumns; 
-            set => Form.OnCustomiseColumns = value;
-        }
-        public OnDefineLookupFilter? OnDefineFilter { get => Form.OnDefineFilter; set { Form.OnDefineFilter = value; } }
+    public event OnValidateData? OnValidateData { add => Form.OnValidateData += value; remove => Form.OnValidateData -= value; }
 
-        public OnDefineLookup? OnDefineLookups { get => Form.OnDefineLookups; set { Form.OnDefineLookups = value; } }
+    public OnCustomiseColumns? OnCustomiseColumns
+    {
+        get => Form.OnCustomiseColumns; 
+        set => Form.OnCustomiseColumns = value;
+    }
+    public OnDefineLookupFilter? OnDefineFilter { get => Form.OnDefineFilter; set { Form.OnDefineFilter = value; } }
 
-        public DefineEditorEventHandler? OnDefineEditor { get => Form.OnDefineEditor; set { Form.OnDefineEditor = value; } }
-        public event OnFormCustomiseEditor? OnFormCustomiseEditor;
-        public OnReconfigureEditors? OnReconfigureEditors { get => Form.OnReconfigureEditors; set { Form.OnReconfigureEditors = value; } }
-        
-        public event OnAfterEditorValueChanged? OnAfterEditorValueChanged { add => Form.OnAfterEditorValueChanged += value; remove => Form.OnAfterEditorValueChanged -= value; }
-        public event EditorValueChangedHandler? OnEditorValueChanged { add => Form.OnEditorValueChanged += value; remove => Form.OnEditorValueChanged -= value; }
+    public OnDefineLookup? OnDefineLookups { get => Form.OnDefineLookups; set { Form.OnDefineLookups = value; } }
 
+    public DefineEditorEventHandler? OnDefineEditor { get => Form.OnDefineEditor; set { Form.OnDefineEditor = value; } }
+    public event OnFormCustomiseEditor? OnFormCustomiseEditor;
 
-        public event OnSelectPage? OnSelectPage { add => Form.OnSelectPage += value; remove => Form.OnSelectPage -= value; }
+    public OnReconfigureEditors? OnReconfigureEditors { get => Form.OnReconfigureEditors; set { Form.OnReconfigureEditors = value; } }
+    
+    public event OnAfterEditorValueChanged? OnAfterEditorValueChanged { add => Form.OnAfterEditorValueChanged += value; remove => Form.OnAfterEditorValueChanged -= value; }
+    public event EditorValueChangedHandler? OnEditorValueChanged { add => Form.OnEditorValueChanged += value; remove => Form.OnEditorValueChanged -= value; }
 
-        public DynamicGridSaveEvent? OnSaveItem { get => Form.OnSaveItem; set { Form.OnSaveItem = value; } }
 
+    public event OnSelectPage? OnSelectPage { add => Form.OnSelectPage += value; remove => Form.OnSelectPage -= value; }
 
-        public IDynamicEditorControl FindEditor(string columnName) => Form.FindEditor(columnName);
-        public object? GetEditorValue(string columnName) => Form.GetEditorValue(columnName);
-        public void SetEditorValue(string columnName, object? value) => Form.SetEditorValue(columnName, value);
+    public DynamicGridSaveEvent? OnSaveItem { get => Form.OnSaveItem; set { Form.OnSaveItem = value; } }
 
-        public void UnloadEditorPages(bool saved) => Form.UnloadEditorPages(saved);
 
-        #endregion
+    public IDynamicEditorControl FindEditor(string columnName) => Form.FindEditor(columnName);
+    public object? GetEditorValue(string columnName) => Form.GetEditorValue(columnName);
+    public void SetEditorValue(string columnName, object? value) => Form.SetEditorValue(columnName, value);
 
-        public DynamicEditorForm()
-        {
-            InitializeComponent();
-            
-            //this.Loaded += new RoutedEventHandler(ConfigureSystemMenu);
-
-            Form.OnEditorCreated += Editor_OnEditorCreated;
-            Form.OnOK += Form_OnOK;
-            Form.OnCancel += Form_OnCancel;
-            Form.OnFormCustomiseEditor += (sender, items, column, editor) => OnFormCustomiseEditor?.Invoke(sender, items, column, editor);
-        }
+    public void UnloadEditorPages(bool saved) => Form.UnloadEditorPages(saved);
 
-        public DynamicEditorForm(Type type, DynamicEditorPages? pages = null, DynamicEditorButtons? buttons = null,
-            Func<Type, CoreTable>? pageDataHandler = null, bool preloadPages = false): this()
-        {
-            Setup(type, pages, buttons, pageDataHandler, preloadPages);
-        }
+    #endregion
 
-        public void Setup(Type type, DynamicEditorPages? pages = null, DynamicEditorButtons? buttons = null,
-            Func<Type, CoreTable?>? pageDataHandler = null, bool preloadPages = false)
-        {
-            Form.Setup(type, pages, buttons, pageDataHandler, preloadPages);
-        }
-        public void SetLayoutType<T>() where T : DynamicEditorGridLayout => Form.SetLayoutType<T>();
+    public bool? Result { get; set; }
+
+    public DynamicEditorForm()
+    {
+        InitializeComponent();
+        
+        //this.Loaded += new RoutedEventHandler(ConfigureSystemMenu);
+
+        Form.OnEditorCreated += Editor_OnEditorCreated;
+        Form.OnOK += Form_OnOK;
+        Form.OnCancel += Form_OnCancel;
+        Form.OnFormCustomiseEditor += (sender, items, column, editor) => OnFormCustomiseEditor?.Invoke(sender, items, column, editor);
+    }
+
+    public DynamicEditorForm(Type type, DynamicEditorPages? pages = null, DynamicEditorButtons? buttons = null,
+        Func<Type, CoreTable>? pageDataHandler = null, bool preloadPages = false): this()
+    {
+        Setup(type, pages, buttons, pageDataHandler, preloadPages);
+    }
 
-        private void Form_OnCancel()
+    public void Setup(Type type, DynamicEditorPages? pages = null, DynamicEditorButtons? buttons = null,
+        Func<Type, CoreTable?>? pageDataHandler = null, bool preloadPages = false)
+    {
+        Form.Setup(type, pages, buttons, pageDataHandler, preloadPages);
+    }
+    public void SetLayoutType<T>() where T : DynamicEditorGridLayout => Form.SetLayoutType<T>();
+
+    // Providing new implementation to avoid using DIalogResult, which breaks if non-modal.
+    public new bool? ShowDialog()
+    {
+        base.ShowDialog();
+        return Result;
+    }
+
+    private void Form_OnCancel()
+    {
+        if (bChanged)
         {
-            if (bChanged)
+            var result = MessageWindow.ShowYesNoCancel("Save Changes?", "Confirm");
+            switch (result)
             {
-                var result = MessageWindow.ShowYesNoCancel("Save Changes?", "Confirm");
-                switch (result)
-                {
-                    case MessageWindowResult.Yes:
-                        DialogResult = true;
-                        break;
-                    case MessageWindowResult.No:
-                        DialogResult = false;
-                        break;
-                }
+                case MessageWindowResult.Yes:
+                    Result = true;
+                    Close();
+                    break;
+                case MessageWindowResult.No:
+                    Result = false;
+                    Close();
+                    break;
             }
-            else
-                DialogResult = false;
         }
-
-        private void Form_OnOK()
+        else
         {
-            DialogResult = true;
+            Result = false;
+            Close();
         }
+    }
 
-        private void Editor_OnEditorCreated(object sender, double height, double width)
-        {
-        }
+    private void Form_OnOK()
+    {
+        Result = true;
+        Close();
+    }
 
-        private void Window_Closing(object sender, CancelEventArgs e)
+    private void Editor_OnEditorCreated(object sender, double height, double width)
+    {
+    }
+
+    private void Window_Closing(object sender, CancelEventArgs e)
+    {
+        if (bChanged && Result == null)
         {
-            if (bChanged && DialogResult == null)
+            var result = MessageWindow.ShowYesNoCancel("Save Changes?", "Confirm");
+            switch (result)
             {
-                var result = MessageWindow.ShowYesNoCancel("Save Changes?", "Confirm");
-                switch (result)
-                {
-                    case MessageWindowResult.Yes:
-                        DialogResult = true;
-                        break;
-                    case MessageWindowResult.No:
-                        DialogResult = false;
-                        break;
-                    case MessageWindowResult.Cancel:
-                        e.Cancel = true;
-                        return;
-                }
+                case MessageWindowResult.Yes:
+                    Result = true;
+                    break;
+                case MessageWindowResult.No:
+                    Result = false;
+                    break;
+                case MessageWindowResult.Cancel:
+                    e.Cancel = true;
+                    return;
             }
-            if (DialogResult == true)
-                Form.SaveItem(e);
         }
+        if (Result == true)
+            Form.SaveItem(e);
+        SubPanelClosed?.Invoke(this);
+    }
 
-        private bool bChanged = false;
-        private void Form_OnOnChanged(object? sender, EventArgs e)
-        {
-            bChanged = true;
-        }
+    private bool bChanged = false;
+    private void Form_OnOnChanged(object? sender, EventArgs e)
+    {
+        bChanged = true;
+    }
 
-        private void ThemableChromelessWindow_Loaded(object sender, RoutedEventArgs e)
-        {
-            var screen = WpfScreen.GetScreenFrom(new Point(Left, Top));
+    private void ThemableChromelessWindow_Loaded(object sender, RoutedEventArgs e)
+    {
+        var screen = WpfScreen.GetScreenFrom(new Point(Left, Top));
+
+        double spareheight = 90;
+        double sparewidth = 25;
 
-            double spareheight = 90;
-            double sparewidth = 25;
+        var desiredheight = Form.ContentHeight + spareheight;
+        var desiredwidth = Form.ContentWidth + sparewidth;
 
-            var desiredheight = Form.ContentHeight + spareheight;
-            var desiredwidth = Form.ContentWidth + sparewidth;
+        var maxheight = screen.WorkingArea.Height - 0;
+        Height = desiredheight > maxheight ? maxheight : desiredheight;
 
-            var maxheight = screen.WorkingArea.Height - 0;
-            Height = desiredheight > maxheight ? maxheight : desiredheight;
+        var maxwidth = screen.WorkingArea.Width - 0;
+        Width = desiredwidth > maxwidth ? maxwidth : desiredwidth;
 
-            var maxwidth = screen.WorkingArea.Width - 0;
-            Width = desiredwidth > maxwidth ? maxwidth : desiredwidth;
+        Left = screen.DeviceBounds.Left + (screen.DeviceBounds.Width - Width) / 2.0F;
+        Top = screen.DeviceBounds.Top + (screen.DeviceBounds.Height - Height) / 2.0F;
+
+        var scaption = Form.Items[0].GetType().GetCaption();
+        Title = "Edit " + scaption.SplitCamelCase();
+    }
 
-            Left = screen.DeviceBounds.Left + (screen.DeviceBounds.Width - Width) / 2.0F;
-            Top = screen.DeviceBounds.Top + (screen.DeviceBounds.Height - Height) / 2.0F;
+    #region ISubPanel
 
-            var scaption = Form.Items[0].GetType().GetCaption();
-            Title = "Edit " + scaption.SplitCamelCase();
+    public event ISubPanel.ClosedEvent? SubPanelClosed;
+
+    void ISubPanel.Shutdown(CancelEventArgs? cancel)
+    {
+        if (bChanged)
+        {
+            this.Focus();
+            var window = cancel is null
+                ? MessageWindow.NewYesNo($"You have unsaved changes: do you wish to save this {CoreUtils.Neatify(Form.Items[0].GetType().GetCaption())}?", "Save changes?")
+                : MessageWindow.NewYesNoCancel($"You have unsaved changes: do you wish to save this {CoreUtils.Neatify(Form.Items[0].GetType().GetCaption())}?", "Save changes?");
+            var result = window.Display().Result;
+            switch (result)
+            {
+                case MessageWindowResult.Yes:
+                    Result = true;
+                    Close();
+                    break;
+                case MessageWindowResult.No:
+                    Result = false;
+                    Close();
+                    break;
+                case MessageWindowResult.Cancel:
+                    if(cancel is not null)
+                    {
+                        cancel.Cancel = true;
+                    }
+                    return;
+            }
+        }
+        else
+        {
+            Close();
         }
     }
+
+    #endregion
 }

+ 4 - 0
inabox.wpf/DynamicGrid/DynamicEditorForm/EmbeddedDynamicEditorForm.xaml.cs

@@ -53,6 +53,10 @@ namespace InABox.DynamicGrid
                 _items = value;
                 DynamicEditorFormModel.Slug = Items?.FirstOrDefault()?.GetType().EntityName().Split('.').Last() ?? "";
                 Editor.Load(Pages);
+
+                bChanged = _items.Any(x => x is not Entity e || e.ID == Guid.Empty);
+                UpdateButtonEnabled();
+
                 foreach (var page in Pages)
                     page.OnChanged += (sender, args) => DoChanged();
             }

+ 144 - 22
inabox.wpf/DynamicGrid/DynamicGrid.cs

@@ -106,7 +106,20 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
 
     #region Events
 
-    public event IDynamicGrid.ReconfigureEvent? OnReconfigure;
+    private event IDynamicGrid.ReconfigureEvent? _onReconfigure;
+    public event IDynamicGrid.ReconfigureEvent? OnReconfigure
+    {
+        add
+        {
+            _onReconfigure += value;
+            Reconfigure();
+        }
+        remove
+        {
+            _onReconfigure -= value;
+            Reconfigure();
+        }
+    }
 
     public OnGetDynamicGridRowStyle? OnGetRowStyle { get; set; }
     
@@ -747,7 +760,7 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
 
     protected void OnReconfigureEvent(DynamicGridOptions options)
     {
-        OnReconfigure?.Invoke(options);
+        _onReconfigure?.Invoke(options);
     }
 
     /// <summary>
@@ -1071,7 +1084,9 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
         bRefreshing = true;
 
         if (reloadcolumns)
+        {
             ReloadColumns();
+        }
 
         if (reloaddata)
         {
@@ -1622,6 +1637,11 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
         AfterLoad(editor, items);
     }
 
+    BaseEditor IDynamicGridUIComponentParent<T>.CustomiseEditor(DynamicGridColumn column, BaseEditor editor)
+    {
+        return editor.CloneEditor();
+    }
+
     private void DoCustomiseEditor(IDynamicEditorForm sender, object[] items, DynamicGridColumn column, BaseEditor editor)
     {
         CustomiseEditor((T[])items, column, editor);
@@ -1642,7 +1662,64 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
         OnBeforeSave?.Invoke(editor, items);
     }
 
+    /// <summary>
+    /// Edit the <paramref name="items"/> with a non-modal editor window, attaching them to the <paramref name="host"/> provided.
+    /// </summary>
+    /// <param name="items">List of objects to edit.</param>
+    /// <param name="callback">A callback for when the items are finished being edited.</param>
+    public virtual void EditItemsNonModal(ISubPanelHost host, T[] items, Action<T[], bool> callback, Func<Type, CoreTable?>? PageDataHandler = null, bool PreloadPages = false)
+    {
+        if (!DynamicGridUtils.TryEdit(items, out var editLock))
+        {
+            if(editLock.Panel is Window window)
+            {
+                Task.Delay(100).ContinueWith(task =>
+                {
+                    window.WindowState = WindowState.Normal;
+                    window.Activate();
+                }, TaskScheduler.FromCurrentSynchronizationContext());
+            }
+            else
+            {
+                MessageWindow.ShowMessage("One or more items are already being edited in an open window. You cannot edit the same entity multiple times simultaneously.", "Simultaneous edit.");
+            }
+            return;
+        }
+
+        DynamicEditorForm editor;
+        using (var cursor = new WaitCursor())
+        {
+            editor = new DynamicEditorForm();
+            editor.SetValue(Panel.ZIndexProperty, 999);
+            editor.Form.DisableOKIfUnchanged = true;
+
+            InitialiseEditorForm(editor, items, PageDataHandler, PreloadPages);
+            OnEditorLoaded?.Invoke(editor, items);
+        }
+        editLock.Panel = editor;
+
+        host.AddSubPanel(editor);
+        editor.SubPanelClosed += o =>
+        {
+            try
+            {
+                DynamicGridUtils.FinishEdit(items);
+                callback(items, editor.Result == true);
+            }
+            catch(Exception e)
+            {
+                MessageWindow.ShowError("Error occurred while closing editor.", e);
+            }
+        };
+
+        editor.Show();
+    }
 
+    /// <summary>
+    /// Edit the <paramref name="items"/> with a modal editor window.
+    /// </summary>
+    /// <param name="items">List of objects to edit.</param>
+    /// <returns><see langword="true"/> if the items were saved.</returns>
     public virtual bool EditItems(T[] items, Func<Type, CoreTable?>? PageDataHandler = null, bool PreloadPages = false)
     {
         DynamicEditorForm editor;
@@ -1824,19 +1901,38 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
             //foreach (String key in VisualFilters.Keys)
             //    CoreUtils.SetPropertyValue(item, key, VisualFilters[key]);
 
-            if (EditItems([item]))
+            if(Options.NonModalEditorHost is not null)
             {
-                //_CurrentRow = Data.Rows.Count;
-                var row = Data.NewRow();
-                ObjectToRow(item, row);
-                Data.Rows.Add(row);
-                InvalidateGrid();
-                SelectedRows = [row];
-                DoChanged();
-                return true;
+                EditItemsNonModal(Options.NonModalEditorHost, [item], (items, result) =>
+                {
+                    if (result)
+                    {
+                        var row = Data.NewRow();
+                        ObjectToRow(item, row);
+                        Data.Rows.Add(row);
+                        InvalidateGrid();
+                        SelectedRows = [row];
+                        DoChanged();
+                    }
+                });
+                return false;
             }
+            else
+            {
+                if (EditItems([item]))
+                {
+                    //_CurrentRow = Data.Rows.Count;
+                    var row = Data.NewRow();
+                    ObjectToRow(item, row);
+                    Data.Rows.Add(row);
+                    InvalidateGrid();
+                    SelectedRows = [row];
+                    DoChanged();
+                    return true;
+                }
 
-            return false;
+                return false;
+            }
         }
 
         var items = Array.Empty<T>();
@@ -1852,22 +1948,48 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
         if (items.Length != 0)
         {
             var snaps = items.ToArray(x => x.TakeSnapshot());
-            if (EditItems(items))
+
+            if(Options.NonModalEditorHost is not null)
             {
-                var sel = SelectedRows;
-                UpdateRows(rows, items, invalidateRows: false);
-                InvalidateGrid();
-                SelectedRows = sel;
-                DoChanged();
-                return true;
+                EditItemsNonModal(Options.NonModalEditorHost, items, (items, result) =>
+                {
+                    if (result)
+                    {
+                        var sel = SelectedRows;
+                        UpdateRows(rows, items, invalidateRows: false);
+                        InvalidateGrid();
+                        SelectedRows = sel;
+                        DoChanged();
+                    }
+                    else
+                    {
+                        foreach(var snap in snaps)
+                        {
+                            snap.ResetObject();
+                        }
+                    }
+                });
+                return false;
             }
             else
             {
-                foreach(var snap in snaps)
+                if (EditItems(items))
                 {
-                    snap.ResetObject();
+                    var sel = SelectedRows;
+                    UpdateRows(rows, items, invalidateRows: false);
+                    InvalidateGrid();
+                    SelectedRows = sel;
+                    DoChanged();
+                    return true;
                 }
-            } 
+                else
+                {
+                    foreach(var snap in snaps)
+                    {
+                        snap.ResetObject();
+                    }
+                } 
+            }
             return false;
         }
 

+ 2 - 21
inabox.wpf/DynamicGrid/DynamicGridColumn.cs

@@ -45,28 +45,9 @@ namespace InABox.DynamicGrid
 
         public BaseEditor Editor { get; set; }
 
-        public VerticalAlignment VerticalAlignment()
-        {
-            if (Alignment.Equals(Alignment.NotSet))
-                return System.Windows.VerticalAlignment.Center;
-            if (Alignment.Equals(Alignment.TopLeft) || Alignment.Equals(Alignment.TopCenter) || Alignment.Equals(Alignment.TopRight))
-                return System.Windows.VerticalAlignment.Top;
-            if (Alignment.Equals(Alignment.MiddleLeft) || Alignment.Equals(Alignment.MiddleCenter) || Alignment.Equals(Alignment.MiddleRight))
-                return System.Windows.VerticalAlignment.Center;
-            return System.Windows.VerticalAlignment.Bottom;
-        }
+        public VerticalAlignment VerticalAlignment() => Alignment.VerticalAlignment();
 
-        public HorizontalAlignment HorizontalAlignment(Type datatype)
-        {
-            if (Alignment.Equals(Alignment.NotSet))
-                return datatype.IsNumeric() ? System.Windows.HorizontalAlignment.Right :
-                    datatype == typeof(bool) ? System.Windows.HorizontalAlignment.Center : System.Windows.HorizontalAlignment.Left;
-            if (Alignment.Equals(Alignment.TopLeft) || Alignment.Equals(Alignment.MiddleLeft) || Alignment.Equals(Alignment.BottomLeft))
-                return System.Windows.HorizontalAlignment.Left;
-            if (Alignment.Equals(Alignment.TopCenter) || Alignment.Equals(Alignment.MiddleCenter) || Alignment.Equals(Alignment.BottomCenter))
-                return System.Windows.HorizontalAlignment.Center;
-            return System.Windows.HorizontalAlignment.Right;
-        }
+        public HorizontalAlignment HorizontalAlignment(Type datatype) => Alignment.HorizontalAlignment(datatype);
 
         public override string ToString()
         {

+ 16 - 0
inabox.wpf/DynamicGrid/DynamicGridCommon.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.ComponentModel;
 using System.Windows.Controls;
 using InABox.Core;
+using InABox.Wpf;
 
 namespace InABox.DynamicGrid;
 
@@ -77,6 +78,7 @@ public class DynamicGridOptions
         HideDatabaseFilters = false;
         HideDirectEditButton = false;
 		PageSize = 0;
+		NonModalEditorHost = null;
         return EndUpdate();
     }
 
@@ -319,6 +321,20 @@ public class DynamicGridOptions
 		}
 	}
 
+	private ISubPanelHost? _nonModalEditorHost;
+	public ISubPanelHost? NonModalEditorHost
+	{
+		get => _nonModalEditorHost;
+		set
+		{
+			if(_nonModalEditorHost != value)
+			{
+				_nonModalEditorHost = value;
+				Changed();
+			}
+		}
+	}
+
 	private int _pageSize = 0;
 	/// <summary>
 	/// The page size for loading data in pages; set to 0 for no paging functionality.

+ 104 - 0
inabox.wpf/DynamicGrid/DynamicGridUtils.cs

@@ -493,6 +493,71 @@ public static class DynamicGridUtils
 
     #endregion
 
+    #region Non-modal Editing
+
+    public class DynamicGridEntityEditLock(IEnumerable<Guid> ids)
+    {
+        public HashSet<Guid> ObjectIDs { get; set; } = ids.ToHashSet();
+
+        public ISubPanel? Panel { get; set; }
+    }
+
+    private static Dictionary<Guid, DynamicGridEntityEditLock> CurrentEditLocks = new();
+
+    /// <summary>
+    /// Attempt to begin editing <paramref name="objs"/>, failing if those entities are already being edited elsewhere.
+    /// </summary>
+    /// <remarks>
+    /// If returns <see langword="true"/>, then <paramref name="editLock"/> will contain a new edit lock which contains the
+    /// new entities. Otherwise, <paramref name="editLock"/> will be the lock on the entities that are already being edited.
+    /// </remarks>
+    public static bool TryEdit(BaseObject[] objs, out DynamicGridEntityEditLock editLock)
+    {
+        lock (CurrentEditLocks)
+        {
+            var ids = new List<Guid>();
+            foreach(var obj in objs)
+            {
+                if(obj is Entity entity && entity.ID != Guid.Empty)
+                {
+                    if (CurrentEditLocks.TryGetValue(entity.ID, out editLock))
+                    {
+                        return false;
+                    }
+                    else
+                    {
+                        ids.Add(entity.ID);
+                    }
+                }
+            }
+            editLock = new(ids);
+            foreach(var id in ids)
+            {
+                CurrentEditLocks.Add(id, editLock);
+            }
+            return true;
+        }
+    }
+
+    public static void FinishEdit(BaseObject[] objs)
+    {
+        lock (CurrentEditLocks)
+        {
+            foreach(var obj in objs)
+            {
+                if(obj is Entity entity && entity.ID != Guid.Empty)
+                {
+                    if(CurrentEditLocks.Remove(entity.ID, out var editLock))
+                    {
+                        editLock.ObjectIDs.Remove(entity.ID);
+                    }
+                }
+            }
+        }
+    }
+
+    #endregion
+
     #region Editing BaseObject
 
     /// <summary>
@@ -733,4 +798,43 @@ public static class DynamicGridUtils
             menu.Items.Add(manageForms);
         }, TaskScheduler.FromCurrentSynchronizationContext());
     }
+
+    #region Alignment
+
+    public static VerticalAlignment VerticalAlignment(this Alignment alignment)
+    {
+        if (alignment.Equals(Alignment.NotSet))
+            return System.Windows.VerticalAlignment.Center;
+        if (alignment.Equals(Alignment.TopLeft) || alignment.Equals(Alignment.TopCenter) || alignment.Equals(Alignment.TopRight))
+            return System.Windows.VerticalAlignment.Top;
+        if (alignment.Equals(Alignment.MiddleLeft) || alignment.Equals(Alignment.MiddleCenter) || alignment.Equals(Alignment.MiddleRight))
+            return System.Windows.VerticalAlignment.Center;
+        return System.Windows.VerticalAlignment.Bottom;
+    }
+
+    public static HorizontalAlignment HorizontalAlignment(this Alignment alignment, Type datatype)
+    {
+        if (alignment.Equals(Alignment.NotSet))
+            return datatype.IsNumeric() ? System.Windows.HorizontalAlignment.Right :
+                datatype == typeof(bool) ? System.Windows.HorizontalAlignment.Center : System.Windows.HorizontalAlignment.Left;
+        if (alignment.Equals(Alignment.TopLeft) || alignment.Equals(Alignment.MiddleLeft) || alignment.Equals(Alignment.BottomLeft))
+            return System.Windows.HorizontalAlignment.Left;
+        if (alignment.Equals(Alignment.TopCenter) || alignment.Equals(Alignment.MiddleCenter) || alignment.Equals(Alignment.BottomCenter))
+            return System.Windows.HorizontalAlignment.Center;
+        return System.Windows.HorizontalAlignment.Right;
+    }
+
+    public static TextAlignment TextAlignment(this Alignment alignment, Type datatype)
+    {
+        if (alignment.Equals(Alignment.NotSet))
+            return datatype.IsNumeric() ? System.Windows.TextAlignment.Right :
+                datatype == typeof(bool) ? System.Windows.TextAlignment.Center : System.Windows.TextAlignment.Left;
+        if (alignment.Equals(Alignment.TopLeft) || alignment.Equals(Alignment.MiddleLeft) || alignment.Equals(Alignment.BottomLeft))
+            return System.Windows.TextAlignment.Left;
+        if (alignment.Equals(Alignment.TopCenter) || alignment.Equals(Alignment.MiddleCenter) || alignment.Equals(Alignment.BottomCenter))
+            return System.Windows.TextAlignment.Center;
+        return System.Windows.TextAlignment.Right;
+    }
+
+    #endregion
 }

+ 94 - 95
inabox.wpf/DynamicGrid/Editors/CodeEditor/CodeEditorControl.cs

@@ -6,122 +6,121 @@ using System.Windows.Input;
 using System.Windows.Media;
 using InABox.Core;
 
-namespace InABox.DynamicGrid
+namespace InABox.DynamicGrid;
+
+public class CodeEditorControl : DynamicEditorControl<string, BaseCodeEditor>
 {
-    public class CodeEditorControl : DynamicEditorControl<string, BaseCodeEditor>
+    
+    static CodeEditorControl()
     {
-        
-        static CodeEditorControl()
-        {
-            //DynamicEditorControlFactory.Register<CodeEditorControl, BaseCodeEditor, CodeEditor>();
-            //DynamicEditorControlFactory.Register<CodeEditorControl, BaseCodeEditor, UniqueCodeEditor>();
-        }
-        
-        private TextBox Editor;
+        //DynamicEditorControlFactory.Register<CodeEditorControl, BaseCodeEditor, CodeEditor>();
+        //DynamicEditorControlFactory.Register<CodeEditorControl, BaseCodeEditor, UniqueCodeEditor>();
+    }
+    
+    private TextBox Editor;
 
-        public override void Configure()
-        {
-        }
+    public override void Configure()
+    {
+    }
 
-        protected override FrameworkElement CreateEditor()
+    protected override FrameworkElement CreateEditor()
+    {
+        var dock = new DockPanel
         {
-            var dock = new DockPanel
-            {
-                HorizontalAlignment = HorizontalAlignment.Stretch,
-                VerticalAlignment = VerticalAlignment.Stretch
-            };
-
-            var buttons = CreateButtons(out var DisableEditor);
-            foreach (var button in buttons)
-            {
-                button.SetValue(DockPanel.DockProperty, Dock.Right);
-                dock.Children.Add(button);
-                dock.Width += button.Width + 5;
-            }
-
-            Editor = new TextBox
-            {
-                CharacterCasing = CharacterCasing.Upper,
-                VerticalContentAlignment = VerticalAlignment.Center
-            };
-            Editor.SetValue(DockPanel.DockProperty, Dock.Left);
-            if (DisableEditor)
-            {
-                Editor.Background = new SolidColorBrush(Colors.Silver);
-                Editor.IsEnabled = false;
-            }
-
-            Editor.TextChanged += (o, e) => { CheckChanged(); };
-
-            Editor.PreviewTextInput += Editor_PreviewTextInput;
-            DataObject.AddPastingHandler(Editor, Editor_TextPaste);
-
-            dock.Children.Add(Editor);
-
-            return dock;
-        }
+            HorizontalAlignment = HorizontalAlignment.Stretch,
+            VerticalAlignment = VerticalAlignment.Stretch
+        };
 
-        private void Editor_TextPaste(object sender, DataObjectPastingEventArgs e)
+        var buttons = CreateButtons(out var DisableEditor);
+        foreach (var button in buttons)
         {
-            if (e.DataObject.GetDataPresent(typeof(string)))
-            {
-                var allowable = (EditorDefinition as BaseCodeEditor)?.ValidChars;
-                var text = (string)e.DataObject.GetData(typeof(string));
-                if (!IsTextAllowed(text, allowable))
-                    e.CancelCommand();
-            }
-            else
-            {
-                e.CancelCommand();
-            }
+            button.SetValue(DockPanel.DockProperty, Dock.Right);
+            dock.Children.Add(button);
+            dock.Width += button.Width + 5;
         }
 
-        private bool IsTextAllowed(string text, string allowable)
+        Editor = new TextBox
+        {
+            CharacterCasing = CharacterCasing.Upper,
+            VerticalContentAlignment = VerticalAlignment.Center
+        };
+        Editor.SetValue(DockPanel.DockProperty, Dock.Left);
+        if (DisableEditor)
         {
-            if (string.IsNullOrWhiteSpace(allowable))
-                return true;
-            return Array.TrueForAll(text.ToCharArray(), delegate(char c) { return allowable.Contains(c); });
+            Editor.Background = new SolidColorBrush(Colors.Silver);
+            Editor.IsEnabled = false;
         }
 
-        private void Editor_PreviewTextInput(object sender, TextCompositionEventArgs e)
+        Editor.TextChanged += (o, e) => { CheckChanged(); };
+
+        Editor.PreviewTextInput += Editor_PreviewTextInput;
+        DataObject.AddPastingHandler(Editor, Editor_TextPaste);
+
+        dock.Children.Add(Editor);
+
+        return dock;
+    }
+
+    private void Editor_TextPaste(object sender, DataObjectPastingEventArgs e)
+    {
+        if (e.DataObject.GetDataPresent(typeof(string)))
         {
             var allowable = (EditorDefinition as BaseCodeEditor)?.ValidChars;
-            e.Handled = !IsTextAllowed(e.Text, allowable);
+            var text = (string)e.DataObject.GetData(typeof(string));
+            if (!IsTextAllowed(text, allowable))
+                e.CancelCommand();
         }
-
-        public override int DesiredHeight()
+        else
         {
-            return 25;
+            e.CancelCommand();
         }
+    }
 
-        public override int DesiredWidth()
-        {
-            var result = 150;
-            var btnEditor = EditorDefinition as IButtonEditor;
-            if (btnEditor?.Buttons != null)
-                foreach (var button in ((IButtonEditor)EditorDefinition).Buttons)
-                    result += button.Width + 5;
-            return result;
-        }
+    private bool IsTextAllowed(string text, string allowable)
+    {
+        if (string.IsNullOrWhiteSpace(allowable))
+            return true;
+        return Array.TrueForAll(text.ToCharArray(), delegate(char c) { return allowable.Contains(c); });
+    }
 
-        protected override string RetrieveValue()
-        {
-            return Editor.Text;
-        }
+    private void Editor_PreviewTextInput(object sender, TextCompositionEventArgs e)
+    {
+        var allowable = (EditorDefinition as BaseCodeEditor)?.ValidChars;
+        e.Handled = !IsTextAllowed(e.Text, allowable);
+    }
 
-        protected override void UpdateValue(string value)
-        {
-            Editor.Text = value;
-        }
+    public override int DesiredHeight()
+    {
+        return 25;
+    }
 
-        public override void SetFocus()
-        {
-            Editor.Focus();
-        }
+    public override int DesiredWidth()
+    {
+        var result = 150;
+        var btnEditor = EditorDefinition as IButtonEditor;
+        if (btnEditor?.Buttons != null)
+            foreach (var button in ((IButtonEditor)EditorDefinition).Buttons)
+                result += button.Width + 5;
+        return result;
+    }
 
-        public override void SetColor(Color color)
-        {
-            Editor.Background = new SolidColorBrush(color);
-        }
+    protected override string RetrieveValue()
+    {
+        return Editor.Text;
+    }
+
+    protected override void UpdateValue(string value)
+    {
+        Editor.Text = value;
+    }
+
+    public override void SetFocus()
+    {
+        Editor.Focus();
+    }
+
+    public override void SetColor(Color color)
+    {
+        Editor.Background = new SolidColorBrush(color);
     }
 }

+ 1 - 1
inabox.wpf/DynamicGrid/Editors/DurationEditor/DurationEditorControl.cs

@@ -105,7 +105,7 @@ namespace InABox.DynamicGrid
             Editor.ValueChanged += (o, e) =>
             {
                 IsChanged = true;
-                //CheckChanged();
+                CheckChanged();
             };
 
             Editor.LostFocus += (o, e) =>

+ 1 - 1
inabox.wpf/DynamicGrid/Editors/MemoEditor/MemoEditorControl.cs

@@ -36,7 +36,7 @@ namespace InABox.DynamicGrid
             Editor.TextChanged += (o, e) =>
             {
                 IsChanged = true;
-                //CheckChanged();
+                CheckChanged();
             };
             Editor.LostFocus += (o, e) =>
             {

+ 4 - 2
inabox.wpf/DynamicGrid/Editors/TextBoxEditor/TextBoxEditorControl.cs

@@ -55,9 +55,11 @@ public class TextBoxEditorControl : DynamicEditorControl<string, TextBoxEditor>
 
         Editor.TextChanged += (o, e) =>
         {
-            if(Loaded)
+            if (Loaded)
+            {
                 IsChanged = true;
-            //CheckChanged();
+                CheckChanged();
+            }
         };
         Editor.LostFocus += (o, e) =>
         {

+ 1 - 1
inabox.wpf/DynamicGrid/Editors/TimeOfDayEditor/TimeOfDayEditorControl.cs

@@ -133,7 +133,7 @@ namespace InABox.DynamicGrid
             Editor.DateTimeChanged += (o, e) =>
             {
                 IsChanged = true;
-                //CheckChanged();
+                CheckChanged();
             };
 
             Editor.LostFocus += (o, e) =>

+ 19 - 19
inabox.wpf/DynamicGrid/UIComponent/DynamicGridGridUIComponent.cs

@@ -800,6 +800,10 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
             style.Setters.Add(new Setter(Control.HorizontalContentAlignmentProperty,
                 column != null ? gridColumn.HorizontalAlignment(typeof(double)) : HorizontalAlignment.Right));
         }
+        else if(column is DynamicTextColumn textColumn)
+        {
+            style.Setters.Add(new Setter(Control.HorizontalContentAlignmentProperty, textColumn.Alignment.HorizontalAlignment(typeof(string))));
+        }
         else
         {
             style.Setters.Add(new Setter(Control.HorizontalContentAlignmentProperty, HorizontalAlignment.Right));
@@ -969,6 +973,7 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                     newcol.ImageWidth = DataGrid.RowHeight - 8;
                     newcol.ColumnSizer = GridLengthUnitType.None;
                     newcol.HeaderText = column.HeaderText;
+                    newcol.AllowEditing = false;
 
                     ApplyFilterStyle(newcol, true, true);
 
@@ -992,15 +997,7 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                     gridRowResizingOptions.ExcludeColumns.Add(sColName);
                     newcol.TextWrapping = TextWrapping.NoWrap;
 
-                    newcol.TextAlignment = txtCol.Alignment == Alignment.NotSet
-                        ? TextAlignment.Left
-                        : txtCol.Alignment == Alignment.BottomLeft || txtCol.Alignment == Alignment.MiddleLeft ||
-                          txtCol.Alignment == Alignment.TopLeft
-                            ? TextAlignment.Left
-                            : txtCol.Alignment == Alignment.BottomCenter || txtCol.Alignment == Alignment.MiddleCenter ||
-                              txtCol.Alignment == Alignment.TopCenter
-                                ? TextAlignment.Center
-                                : TextAlignment.Right;
+                    newcol.TextAlignment = txtCol.Alignment.TextAlignment(typeof(string));
 
                     newcol.AllowEditing = false;
                     newcol.UpdateTrigger = UpdateSourceTrigger.PropertyChanged;
@@ -1253,7 +1250,8 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                 var cellstyle = new Style();
                 if (Parent.IsDirectEditMode())
                 {
-                    if (prop.Editor is null || !prop.Editor.Editable.IsDirectEditable())
+                    var editor = Parent.CustomiseEditor(column, column.Editor);
+                    if (editor is null || !editor.Editable.IsDirectEditable())
                     {
                         cellstyle.Setters.Add(new Setter(Control.BackgroundProperty,
                             new Binding()
@@ -1310,13 +1308,7 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
 
     public void RefreshColumns(DynamicGridColumns columns, DynamicActionColumns actionColumns, DynamicGridColumnGroupings groupings)
     {
-        // Yo, please don't remove this.
-        // The issue was when we were dynamically adding ActionColumns, and if we had to remove and then re-add them, we were getting massive performance hits
-        // for no reason. I think perhaps the image columns were trying to refer to data that didn't exist anymore when calling DataGrid.Columns.Refresh(),
-        // and thus some mega problems (perhaps even exceptions within Syncfusion) were occurring, and this seems to fix it.
-        // I don't pretend to know why it works; this is probably the strangest problem I've ever come across.
-        //DataGrid.ItemsSource = null;
-
+        DataGrid.ItemsSource = null;
         DataGrid.Columns.Suspend();
 
         ColumnList.Clear();
@@ -1353,6 +1345,14 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
             }
 
         ResizeColumns(DataGrid, DataGrid.ActualWidth - 2, DataGrid.ActualHeight - 2);
+
+        if(groupings.Count > 0)
+        {
+            // THis here is to fix a problem with Syncfusion when we have stackedHeaderRows; the above setting of the ItemsSource to null
+            // was causing the resetting of it to fail when reloading, due to some internal OutOfRange Exception. THe use case was selecting columns on the
+            // Stock Forecast grid.
+            RefreshData(new CoreTable());
+        }
     }
 
     #endregion
@@ -1386,7 +1386,7 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         for (var i = 0; i < ActionColumns.Count; i++)
             result.Columns.Add(string.Format("ActionColumn{0}", i),
                 ActionColumns[i] is DynamicImageColumn
-                    ? typeof(BitmapImage)
+                    ? typeof(ImageSource)
                     : typeof(String)
             );
 
@@ -1402,8 +1402,8 @@ public class DynamicGridGridUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         //int rowIndex = DataGrid.SelectionController.CurrentCellManager.CurrentRowColumnIndex.RowIndex;
         //int columnIndex = DataGrid.SelectionController.CurrentCellManager.CurrentRowColumnIndex.ColumnIndex;
         //int scrollRowIndex = DataGrid.GetVisualContainer().ScrollRows.LastBodyVisibleLineIndex;
-        DataGrid.ItemsSource = result;
         //this.DataGrid.ScrollInView(new Syncfusion.UI.Xaml.ScrollAxis.RowColumnIndex(scrollRowIndex, columnIndex));
+        DataGrid.ItemsSource = result;
         ResizeColumns(DataGrid, DataGrid.ActualWidth - 1, DataGrid.ActualHeight);
 
         UpdateRecordCount();

+ 91 - 77
inabox.wpf/DynamicGrid/UIComponent/DynamicGridTreeUIComponent.cs

@@ -23,6 +23,7 @@ using System.Windows.Media.Imaging;
 using Syncfusion.UI.Xaml.TreeGrid.Filtering;
 using Syncfusion.UI.Xaml.TreeGrid.Cells;
 using System.Windows.Controls.Primitives;
+using NPOI.OpenXmlFormats.Dml;
 
 namespace InABox.DynamicGrid;
 
@@ -271,86 +272,92 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
 
     private void _tree_Loaded(object sender, RoutedEventArgs e)
     {
-        _summaryRow = new Grid();
-        _summaryRow.Visibility = Visibility.Collapsed;
-
-        var scroll = _tree.FindChild<ScrollViewer>("PART_ScrollViewer");
-        if(scroll is not null)
+        Application.Current.Dispatcher.BeginInvoke(() =>
         {
-            var grid = scroll.FindVisualChildren<Grid>(recursive: false).FirstOrDefault();
-            if(grid is not null)
-            {
-                var row1 = grid.RowDefinitions[0];
-                var row2 = grid.RowDefinitions[1];
-                grid.RowDefinitions.Clear();
-                grid.RowDefinitions.Add(row1);
-                var rowDef = grid.AddRow(GridUnitType.Auto);
 
-                grid.RowDefinitions.Add(row2);
+            _summaryRow = new Grid();
+            _summaryRow.Visibility = Nodes is not null && Summaries.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
 
-                foreach(var child in grid.Children.OfType<UIElement>())
+            var scroll = _tree.FindChild<ScrollViewer>("PART_ScrollViewer");
+            if(scroll is not null)
+            {
+                var grid = scroll.FindVisualChildren<Grid>(recursive: false).FirstOrDefault();
+                if(grid is not null)
                 {
-                    var row = Grid.GetRow(child);
-                    if(row >= 1)
-                    {
-                        Grid.SetRow(child, row + 1);
-                    }
-                    else
+                    var row1 = grid.RowDefinitions[0];
+                    var row2 = grid.RowDefinitions[1];
+                    grid.RowDefinitions.Clear();
+                    grid.RowDefinitions.Add(row1);
+                    var rowDef = grid.AddRow(GridUnitType.Auto);
+
+                    grid.RowDefinitions.Add(row2);
+
+                    foreach(var child in grid.Children.OfType<UIElement>())
                     {
-                        var rowSpan = Grid.GetRowSpan(child);
-                        if(row + rowSpan >= 1 && child is ScrollBar)
+                        var row = Grid.GetRow(child);
+                        if(row >= 1)
+                        {
+                            Grid.SetRow(child, row + 1);
+                        }
+                        else
                         {
-                            Grid.SetRowSpan(child, rowSpan + 1);
+                            var rowSpan = Grid.GetRowSpan(child);
+                            if(row + rowSpan >= 1 && child is ScrollBar)
+                            {
+                                Grid.SetRowSpan(child, rowSpan + 1);
+                            }
                         }
                     }
-                }
-                var horizontalScrollbar = grid.Children.OfType<ScrollBar>().First(x => x.Orientation == Orientation.Horizontal);
+                    var horizontalScrollbar = grid.Children.OfType<ScrollBar>().First(x => x.Orientation == Orientation.Horizontal);
 
-                var treeGridPanel = scroll.FindChild<TreeGridPanel>("PART_TreeGridPanel");
+                    var treeGridPanel = scroll.FindChild<TreeGridPanel>("PART_TreeGridPanel");
 
-                var summaryScroll = new ScrollViewer
-                {
-                    VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
-                    HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden
-                };
-                summaryScroll.Content = _summaryRow;
+                    var summaryScroll = new ScrollViewer
+                    {
+                        VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
+                        HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden
+                    };
+                    summaryScroll.Content = _summaryRow;
 
-                scroll.ScrollChanged += (o, e) =>
-                {
-                    summaryScroll.ScrollToHorizontalOffset(scroll.HorizontalOffset);
+                    scroll.ScrollChanged += (o, e) =>
+                    {
+                        summaryScroll.ScrollToHorizontalOffset(scroll.HorizontalOffset);
 
-                    var panel = treeGridPanel;
-                };
+                        var panel = treeGridPanel;
+                    };
 
-                var transform = new TranslateTransform();
-                summaryScroll.RenderTransform = transform;
+                    var transform = new TranslateTransform();
+                    summaryScroll.RenderTransform = transform;
 
-                void UpdateSize(double height)
-                {
-                    var desiredHeight = treeGridPanel.RowHeights.TotalExtent;
-                    if(desiredHeight < height)
+                    void UpdateSize(double height)
                     {
-                        var diff = height - desiredHeight;
-                        transform.Y = -diff - 1;
-                    }
-                    else
-                    {
-                        transform.Y = 0;
+                        var desiredHeight = treeGridPanel.RowHeights.TotalExtent;
+                        if(desiredHeight < height)
+                        {
+                            var diff = height - desiredHeight;
+                            transform.Y = -diff - 1;
+                        }
+                        else
+                        {
+                            transform.Y = 0;
+                        }
                     }
-                }
 
-                treeGridPanel.SizeChanged += (o, e) =>
-                {
-                    UpdateSize(e.NewSize.Height);
-                };
-                _tree.FilterChanged += (o, e) =>
-                {
-                    UpdateSize(treeGridPanel.ActualHeight);
-                };
+                    treeGridPanel.SizeChanged += (o, e) =>
+                    {
+                        UpdateSize(e.NewSize.Height);
+                    };
+                    _tree.FilterChanged += (o, e) =>
+                    {
+                        UpdateSize(treeGridPanel.ActualHeight);
+                    };
 
-                grid.AddChild(summaryScroll, 1, 0);
+                    grid.AddChild(summaryScroll, 1, 0);
+                }
             }
-        }
+
+            RebuildSummaryRow();
+        });
     }
 
     private class TreeGridSelectionControllerExt(SfTreeGrid treeGrid, DynamicGridTreeUIComponent<T> grid) : TreeGridRowSelectionController(treeGrid)
@@ -716,7 +723,7 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                     var image = imgCol.Image?.Invoke(null);
                     if (image != null)
                     {
-                        var template = new ControlTemplate(typeof(GridHeaderCellControl));
+                        var template = new ControlTemplate(typeof(TreeGridHeaderCell));
                         var border = new FrameworkElementFactory(typeof(Border));
                         border.SetValue(Border.BackgroundProperty, new SolidColorBrush(Colors.Gainsboro));
                         border.SetValue(Border.PaddingProperty, new Thickness(4));
@@ -750,6 +757,10 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
             style.Setters.Add(new Setter(Control.HorizontalContentAlignmentProperty,
                 column != null ? gridColumn.HorizontalAlignment(typeof(double)) : HorizontalAlignment.Right));
         }
+        else if(column is DynamicTextColumn textColumn)
+        {
+            style.Setters.Add(new Setter(Control.HorizontalContentAlignmentProperty, textColumn.Alignment.HorizontalAlignment(typeof(string))));
+        }
         else
         {
             style.Setters.Add(new Setter(Control.HorizontalContentAlignmentProperty, HorizontalAlignment.Right));
@@ -1014,7 +1025,7 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
 
                     var headstyle = GetHeaderCellStyle(column);
                     headstyle.Setters.Add(new EventSetter(Control.MouseLeftButtonUpEvent, new MouseButtonEventHandler(HeaderCell_LeftMouseButtonEvent)));
-                    newcol.HeaderStyle = GetHeaderCellStyle(column);
+                    newcol.HeaderStyle = headstyle;
 
                     _tree.Columns.Add(newcol);
                     ColumnList.Add(column);
@@ -1024,15 +1035,7 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                     var newcol = new TreeGridTextColumn();
                     newcol.TextWrapping = TextWrapping.NoWrap;
 
-                    newcol.TextAlignment = txtCol.Alignment == Alignment.NotSet
-                        ? TextAlignment.Left
-                        : txtCol.Alignment == Alignment.BottomLeft || txtCol.Alignment == Alignment.MiddleLeft ||
-                          txtCol.Alignment == Alignment.TopLeft
-                            ? TextAlignment.Left
-                            : txtCol.Alignment == Alignment.BottomCenter || txtCol.Alignment == Alignment.MiddleCenter ||
-                              txtCol.Alignment == Alignment.TopCenter
-                                ? TextAlignment.Center
-                                : TextAlignment.Right;
+                    newcol.TextAlignment = txtCol.Alignment.TextAlignment(typeof(string));
 
                     newcol.AllowEditing = false;
                     newcol.UpdateTrigger = UpdateSourceTrigger.PropertyChanged;
@@ -1106,7 +1109,8 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
                 var cellstyle = new Style();
                 if (Parent.IsDirectEditMode())
                 {
-                    if (prop.Editor is null || !prop.Editor.Editable.IsDirectEditable())
+                    var editor = Parent.CustomiseEditor(column, column.Editor);
+                    if (editor is null || !editor.Editable.IsDirectEditable())
                     {
                         cellstyle.Setters.Add(new Setter(Control.BackgroundProperty,
                             new SolidColorBrush(Colors.WhiteSmoke)));
@@ -1305,6 +1309,8 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
 
     private void RebuildSummaryRow()
     {
+        if (_summaryRow is null) return;
+
         _summaryRow.RowDefinitions.Clear();
         _summaryRow.ColumnDefinitions.Clear();
         _summaryRow.Children.Clear();
@@ -1342,15 +1348,18 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
 
     private object? CalculateSummaryData(IDynamicGridSummary summary, DynamicColumnBase column)
     {
+        var nodes = _tree.View is not null
+            ? _tree.View.Nodes.Select(x => x.Item as CoreTreeNode).NotNull()
+            : Nodes.Nodes;
         if(summary is DynamicGridCountSummary count)
         {
-            return string.Format("{0:N0}", _tree.View.Nodes.Count);
+            return string.Format("{0:N0}", nodes.Count());
         }
         else if(summary is DynamicGridSumSummary sum)
         {
             if(column is DynamicGridColumn gridColumn)
             {
-                var data = _tree.View.Nodes.Select(x => MapRow((x.Item as CoreTreeNode)?.Row)).NotNull()
+                var data = nodes.Select(x => MapRow(x.Row)).NotNull()
                     .Select(x => x[gridColumn.ColumnName]);
 
                 object? result;
@@ -1378,7 +1387,7 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         }
         else if(summary is DynamicGridCustomSummary custom)
         {
-            var data = _tree.View.Nodes.Select(x => MapRow((x.Item as CoreTreeNode)?.Row)).NotNull();
+            var data = nodes.Select(x => MapRow(x.Row)).NotNull();
             var result = custom.Aggregate(data);
             if(result is not null)
             {
@@ -1464,7 +1473,10 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         nodes.ColumnChanged += Nodes_ColumnChanged;
         Nodes = nodes;
         _tree.ItemsSource = nodes.Nodes;
-        _summaryRow.Visibility = Visibility.Visible;
+        if(_summaryRow is not null && Summaries.Count > 0)
+        {
+            _summaryRow.Visibility = Visibility.Visible;
+        }
 
         CalculateSummaries();
 
@@ -1539,6 +1551,8 @@ public class DynamicGridTreeUIComponent<T> : IDynamicGridUIComponent<T>, IDynami
         var coreTreeNode = Nodes.Find(_innerRow);
         coreTreeNode?.InvalidateData();
 
+        CalculateSummaries();
+
         _invalidating = false;
     }
 

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

@@ -29,6 +29,8 @@ public interface IDynamicGridUIComponentParent<T> : IDynamicGrid<T>
 
     void LoadColumnsMenu(ContextMenu menu);
 
+    BaseEditor CustomiseEditor(DynamicGridColumn column, BaseEditor editor);
+
     void DoubleClickCell(CoreRow? row, DynamicColumnBase? column);
     void ExecuteActionColumn(DynamicActionColumn column, CoreRow[]? rows);
     void OpenColumnMenu(DynamicColumnBase column);

+ 87 - 0
inabox.wpf/Panel/IPanel.cs

@@ -255,4 +255,91 @@ public static class IPanelHostExtensions
     {
         host.CreateSetupActionIf(caption, image, onExecute, Security.CanView<T>(), menu);
     }
+}
+
+/// <summary>
+/// Implement this interface to cause a class to act as a host of <see cref="ISubPanel"/>. Then, you can use the extension functions
+/// <see cref="ISubPanelHostExtensions.ShutdownSubPanels(ISubPanelHost, CancelEventArgs?)"/> and
+/// <see cref="ISubPanelHostExtensions.AddSubPanel(ISubPanelHost, ISubPanel)"/> to interact with it.
+/// </summary>
+/// <remarks>
+/// If you mark an <see cref="IPanel{T}"/> as an <see cref="ISubPanelHost"/>, then the shutdown method is called automatically by <see cref="IPanelHost"/>.
+/// </remarks>
+public interface ISubPanelHost
+{
+    List<ISubPanel> SubPanels { get; }
+
+    static ISubPanelHost Global = new GlobalSubPanelHost();
+
+    private class GlobalSubPanelHost : ISubPanelHost
+    {
+        public List<ISubPanel> SubPanels { get; set; } = new();
+    }
+}
+
+public static class ISubPanelHostExtensions
+{
+    public static void ShutdownSubPanels(this ISubPanelHost host, CancelEventArgs? cancel)
+    {
+        ISubPanel[] panels;
+        lock (host.SubPanels)
+        {
+            panels = host.SubPanels.ToArray();
+            host.SubPanels.Clear();
+
+            var isCancelled = false;
+
+            foreach(var panel in panels)
+            {
+                if (isCancelled)
+                {
+                    host.SubPanels.Add(panel);
+                }
+                else
+                {
+                    panel.Shutdown(cancel);
+                    if (cancel?.Cancel == true)
+                    {
+                        isCancelled = true;
+                        host.SubPanels.Add(panel);
+                    }
+                }
+            }
+        }
+    }
+
+    public static void AddSubPanel(this ISubPanelHost host, ISubPanel panel)
+    {
+        host.SubPanels.Add(panel);
+        panel.SubPanelClosed += p =>
+        {
+            lock (host.SubPanels)
+            {
+                host.SubPanels.Remove(p);
+            }
+        };
+    }
+}
+
+/// <summary>
+/// An <see cref="ISubPanel"/> is a non-modal window, which is tied to a parent 
+/// </summary>
+public interface ISubPanel
+{
+    public delegate void ClosedEvent(ISubPanel panel);
+
+    /// <summary>
+    /// Event to be called when a sub-panel closes itself; in this case, <see cref="Shutdown(CancelEventArgs?)"/> will not be called. This allows
+    /// the host to get rid of the sub-panel, instead of keeping it forever.
+    /// </summary>
+    /// <remarks>
+    /// You may call this after <see cref="Shutdown(CancelEventArgs?)"/> has been called.
+    /// </remarks>
+    public event ClosedEvent? SubPanelClosed;
+
+    /// <summary>
+    /// Shutdown the panel.
+    /// </summary>
+    /// <param name="cancel">If the operation can be cancelled, this is not <see langword="null"/></param>
+    void Shutdown(CancelEventArgs? cancel);
 }