Explorar o código

wpf: Made Digital Form grids into DataGrids so that they can be separated into a DynamicSplitPanel

Kenric Nugteren hai 1 mes
pai
achega
b68b0699ae

+ 1 - 1
InABox.Core/DigitalForms/Forms/DigitalFormDocument.cs

@@ -1,6 +1,6 @@
 namespace InABox.Core
 {
-    public class DigitalFormDocument : EntityDocument<DigitalFormLink>, ILicense<DigitalFormsLicense>, IManyToMany<DigitalForm, Document>
+    public class DigitalFormDocument : EntityDocument<DigitalFormLink>, ILicense<DigitalFormsLicense>
     {
     }
 }

+ 5 - 2
InABox.Core/DigitalForms/Forms/DigitalFormLayout.cs

@@ -4,8 +4,11 @@ using System.Linq.Expressions;
 namespace InABox.Core
 {
     [Caption("Layouts")]
-    public class DigitalFormLayout : Entity, IRemotable, IPersistent, IOneToMany<DigitalForm>,
-        ILicense<DigitalFormsLicense>
+    [EntitySecurity(
+        CanEdit = typeof(CanEdit<DigitalForm>),
+        CanDelete = typeof(CanEdit<DigitalForm>),
+        CanView = typeof(CanView<DigitalForm>))]
+    public class DigitalFormLayout : Entity, IRemotable, IPersistent, ILicense<DigitalFormsLicense>
     {
         [NullEditor]
         [Obsolete("Replaced by Form")]

+ 5 - 1
InABox.Core/DigitalForms/Forms/DigitalFormVariable.cs

@@ -23,7 +23,11 @@ namespace InABox.Core
     }
 
     [Caption("Variables")]
-    public class DigitalFormVariable : Entity, IRemotable, IPersistent, IStringAutoIncrement<DigitalFormVariable>, IOneToMany<DigitalForm>,
+    [EntitySecurity(
+        CanEdit = typeof(CanEdit<DigitalForm>),
+        CanDelete = typeof(CanEdit<DigitalForm>),
+        CanView = typeof(CanView<DigitalForm>))]
+    public class DigitalFormVariable : Entity, IRemotable, IPersistent, IStringAutoIncrement<DigitalFormVariable>,
         ISequenceable, ILicense<DigitalFormsLicense>
     {
         private string _parameters = "";

+ 3 - 1
inabox.wpf/DigitalForms/Designer/DynamicFormDesignWindow.xaml.cs

@@ -36,7 +36,9 @@ public partial class DynamicFormDesignWindow : Window, IDynamicFormWindow
         {
             _readOnly = value;
             SwitchView.Visibility = value ? Visibility.Collapsed : Visibility.Visible;
-            if (!_readOnly)
+            OK.Visibility = value ? Visibility.Collapsed : Visibility.Visible;
+            Cancel.Visibility = value ? Visibility.Collapsed : Visibility.Visible;
+            if (_readOnly)
             {
                 Preview.Mode = FormMode.Preview;
             }

+ 25 - 14
inabox.wpf/DigitalForms/DigitalFormGrid.cs

@@ -369,22 +369,33 @@ namespace InABox.DynamicGrid
             base.Reload(criteria, columns, ref sort, token, action);
         }
 
-        public override DynamicEditorPages LoadEditorPages(DigitalForm item)
-        {
-            var pages = base.LoadEditorPages(item);
-
-            pages.Add(new DigitalFormReportGrid());
+        private List<DigitalFormVariable>? _variables;
 
-            return pages;
+        protected override void BeforeLoad(IDynamicEditorForm form, DigitalForm[] items)
+        {
+            var id = items[0].ID;
+            if (id != Guid.Empty)
+            {
+                var columns = Columns.None<DigitalFormVariable>()
+                    .Add(x => x.Hidden)
+                    .Add(x => x.Code)
+                    .Add(x => x.VariableType)
+                    .Add(x => x.Parameters)
+                    .Add(x => x.Required)
+                    .Add(x => x.Secure)
+                    .Add(x => x.Retain);
+                _variables = Client.Query(
+                    Filter<DigitalFormVariable>.Where(x => x.Form.ID).IsEqualTo(items[0].ID),
+                    columns)
+                    .ToList<DigitalFormVariable>();
+            }
+            else
+            {
+                _variables = [];
+            }
+            base.BeforeLoad(form, items);
         }
 
-        private DynamicVariableGrid? GetVariableGrid(IDynamicEditorForm sender)
-            => sender.Pages?.FirstOrDefault(x => x is DynamicVariableGrid)
-                as DynamicVariableGrid;
-
-        private List<DigitalFormVariable> GetVariables(IDynamicEditorForm sender)
-            => GetVariableGrid(sender)?.Items.ToList() ?? new List<DigitalFormVariable>();
-
         // Using the event because it also has the editor form 'sender'.
         private void DigitalFormGrid_OnCustomiseEditor(IDynamicEditorForm sender, DigitalForm[]? items, DynamicGridColumn column, BaseEditor editor)
         {
@@ -393,7 +404,7 @@ namespace InABox.DynamicGrid
                 exp.OnGetVariables += () =>
                 {
                     var variables = new List<string>();
-                    foreach (var variable in GetVariables(sender))
+                    foreach (var variable in _variables ?? [])
                     {
                         foreach (var col in variable.GetVariableColumns())
                             variables.Add($"Data.{col.ColumnName}");

+ 111 - 178
inabox.wpf/DigitalForms/DigitalFormReportGrid.cs

@@ -15,221 +15,163 @@ using FastReport.Utils;
 using Border = System.Windows.Controls.Border;
 using Size = System.Windows.Size;
 using netDxf.Objects;
+using System.Threading;
 
-namespace InABox.DynamicGrid
-{
-    public class DigitalFormReportGrid : DynamicItemsListGrid<ReportTemplate>, IDynamicEditorPage
-    {
-        public DynamicEditorGrid EditorGrid { get; set; }
-
-        public PageType PageType => PageType.Other;
+namespace InABox.DynamicGrid;
 
-        public bool Ready { get; set; }
+public class DigitalFormReportGrid : DynamicDataGrid<ReportTemplate>
+{
+    public DigitalForm? Form { get; set; }
 
-        public bool Visible => true;
+    public DynamicVariableGrid? VariableGrid { get; set; }
 
-        private DigitalForm Form { get; set; }
+    public DynamicFormLayoutGrid? LayoutGrid { get; set; }
 
-        private List<ReportTemplate> OriginalItems { get; set; } = new();
+    protected override void Init()
+    {
+        base.Init();
 
-        private bool _readOnly;
-        public bool ReadOnly
+        foreach(var column in Columns.All<ReportTemplate>())
         {
-            get => _readOnly;
-            set
-            {
-                if (_readOnly != value)
-                {
-                    _readOnly = value;
-                    Reconfigure();
-                }
-            }
+            HiddenColumns.Add(column);
         }
 
-        protected override void Init()
+        if (Security.CanEdit<ReportTemplate>())
         {
-            base.Init();
-
-            if (Security.CanEdit<ReportTemplate>())
-            {
-                ActionColumns.Add(new DynamicImageColumn(ScriptImage, ScriptClick));
-                ActionColumns.Add(new DynamicImageColumn(Wpf.Resources.pencil.AsBitmapImage(), DesignClick));
-            }
+            ActionColumns.Add(new DynamicImageColumn(ScriptImage, ScriptClick));
+            ActionColumns.Add(new DynamicImageColumn(Wpf.Resources.pencil.AsBitmapImage(), DesignClick));
         }
+    }
 
-        protected override void DoReconfigure(DynamicGridOptions options)
-        {
-            base.DoReconfigure(options);
+    protected override void DoReconfigure(DynamicGridOptions options)
+    {
+        base.DoReconfigure(options);
 
-            options.ShowHelp = true;
+        options.ShowHelp = true;
+    }
 
-            if (Security.CanEdit<ReportTemplate>() && !ReadOnly)
-            {
-                options.AddRows = true;
-                options.EditRows = true;
-            }
-            if (Security.CanDelete<ReportTemplate>() && !ReadOnly)
-                options.DeleteRows = true;
+    protected override void Reload(Filters<ReportTemplate> criteria, Columns<ReportTemplate> columns, ref SortOrder<ReportTemplate>? sort, CancellationToken token, Action<CoreTable?, Exception?> action)
+    {
+        if(Form is null)
+        {
+            criteria.Add(Filter.None<ReportTemplate>());
         }
-
-        public void Load(object item, Func<Type, CoreTable?>? PageDataHandler)
+        else
         {
-            Form = (DigitalForm)item;
+            criteria.Add(Filter<ReportTemplate>.Where(x => x.Section).IsEqualTo(Form.ID.ToString()));
+        }
+        base.Reload(criteria, columns, ref sort, token, action);
+    }
 
-            CoreTable? data = null;
-            if (PageDataHandler != null)
-                data = PageDataHandler?.Invoke(typeof(ReportTemplate));
+    private IList<DigitalFormVariable> GetVariables()
+        => VariableGrid?.Variables ?? [];
 
-            if(data is null)
-            {
-                if (Form.ID == Guid.Empty)
-                {
-                    data = new CoreTable();
-                    data.LoadColumns(typeof(ReportTemplate));
-                }
-                else
-                {
-                    data = new Client<ReportTemplate>()
-                        .Query(Filter<ReportTemplate>.Where(x => x.Section).IsEqualTo(Form.ID.ToString()));
-                }
-            }
-            OriginalItems = data.ToList<ReportTemplate>();
+    private IList<DigitalFormLayout> GetLayouts()
+        => LayoutGrid?.Layouts ?? [];
 
-            Items = OriginalItems.ToList();
-            Refresh(true, true);
 
-            Ready = true;
-        }
+    private BitmapImage? ScriptImage(CoreRow? arg)
+    {
+        return arg == null ? Wpf.Resources.edit.AsBitmapImage() :
+            arg.Get<ReportTemplate, bool>(x => x.IsRDL) ? null : Wpf.Resources.edit.AsBitmapImage();
+    }
 
-        public void Cancel()
+    private bool ScriptClick(CoreRow? arg)
+    {
+        if (Form is null) return false;
+
+        if (arg != null && Options.EditRows)
         {
-            foreach(var item in OriginalItems)
+            if (DigitalFormUtils.GetDataModel(Form.AppliesTo, GetVariables()) is not DataModel model)
             {
-                item.CancelChanges();
+                Logger.Send(LogType.Error, "", "Invalid entity type for data model.");
+                return false;
             }
-            Items = OriginalItems.ToList();
-            Refresh(false, true);
-        }
 
+            var template = LoadItem(arg);
+            var script = template.Script;
+            if (string.IsNullOrWhiteSpace(script))
+                script = string.Format(ReportTemplate.DefaultScriptTemplate, model.GetType().Name.Split('.').Last());
+            var editor = new ScriptEditorWindow(script);
+            if (editor.ShowDialog() == true)
+            {
+                template.Script = editor.Script;
 
-        private DynamicVariableGrid? GetVariableGrid()
-            => EditorGrid.Pages?.FirstOrDefault(x => x is DynamicVariableGrid)
-                as DynamicVariableGrid;
-        private List<DigitalFormVariable> GetVariables()
-            => GetVariableGrid()?.Items.ToList() ?? new List<DigitalFormVariable>();
+                SaveItem(template);
+                return true;
+            }
+        }
 
-        private DynamicFormLayoutGrid? GetLayoutGrid()
-            => EditorGrid.Pages?.FirstOrDefault(x => x is DynamicFormLayoutGrid)
-                as DynamicFormLayoutGrid;
-        private List<DigitalFormLayout> GetLayouts()
-            => GetLayoutGrid()?.Items.ToList() ?? new List<DigitalFormLayout>();
+        return false;
+    }
 
- 
-        private BitmapImage? ScriptImage(CoreRow? arg)
-        {
-            return arg == null ? Wpf.Resources.edit.AsBitmapImage() :
-                arg.Get<ReportTemplate, bool>(x => x.IsRDL) ? null : Wpf.Resources.edit.AsBitmapImage();
-        }
+    private bool DesignClick(CoreRow? arg)
+    {
+        if (Form is null) return false;
 
-        private bool ScriptClick(CoreRow? arg)
-        {
-            if (arg != null && !ReadOnly)
-            {
-                if (DigitalFormUtils.GetDataModel(Form.AppliesTo,GetVariables()) is not DataModel model)
-                {
-                    Logger.Send(LogType.Error, "", "Invalid entity type for data model.");
-                    return false;
-                }
-
-                var template = LoadItem(arg);
-                var script = template.Script;
-                if (string.IsNullOrWhiteSpace(script))
-                    script = string.Format(ReportTemplate.DefaultScriptTemplate, model.GetType().Name.Split('.').Last());
-                var editor = new ScriptEditorWindow(script);
-                if (editor.ShowDialog() == true)
-                {
-                    template.Script = editor.Script;
-
-                    SaveItem(template);
-                    return true;
-                }
-            }
+        if (arg is null || !Options.EditRows) return false;
 
+        if (DigitalFormUtils.GetDataModel(Form.AppliesTo, GetVariables()) is not DataModel model)
+        {
+            Logger.Send(LogType.Error, "", "Invalid entity type for data model.");
             return false;
         }
 
-        private bool DesignClick(CoreRow? arg)
-        {
-            if (arg is null && !ReadOnly) return false;
-            if (DigitalFormUtils.GetDataModel(Form.AppliesTo,GetVariables()) is not DataModel model)
-            {
-                Logger.Send(LogType.Error, "", "Invalid entity type for data model.");
-                return false;
-            }
+        var template = LoadItem(arg);
+        ReportUtils.DesignReport(template, model, true, saveTemplate: (template) => SaveItem(template));
+        return false;
+    }
 
-            var template = LoadItem(arg);
-            ReportUtils.DesignReport(template, model, true, saveTemplate: (template) => SaveItem(template));
-            return false;
-        }
+    protected override bool CanCreateItems()
+    {
+        return base.CanCreateItems() && Form is not null;
+    }
 
-        protected override void DoAdd(bool openEditorOnDirectEdit = false)
+    protected override void DoAdd(bool openEditorOnDirectEdit = false)
+    {
+        if (Form is null) return;
+
+        var layouts = GetLayouts();
+        if(layouts.Count == 0)
         {
-            var layouts = GetLayouts();
-            if(layouts.Count == 0)
-            {
-                base.DoAdd(openEditorOnDirectEdit);
-            }
-            else
-            {
-                var menu = new ContextMenu();
-                foreach (var layout in layouts)
-                {
-                    menu.AddItem($"{layout.Description.NotWhiteSpaceOr(layout.Type.ToString())}", null, layout, AddLayout);
-                }
-                menu.AddSeparatorIfNeeded();
-                menu.AddItem("Create blank report", null, () => base.DoAdd());
-                menu.IsOpen = true;
-            }
+            base.DoAdd(openEditorOnDirectEdit);
         }
-        
-        private void AddLayout(DigitalFormLayout layout)
+        else
         {
-            var model = DigitalFormUtils.GetDataModel(Form.AppliesTo, GetVariables());
-            
-            var item = CreateItem();
-            item.DataModel = model.Name;
-            item.Name = layout.Description.NotWhiteSpaceOr(layout.Type.ToString());
-            item.RDL = DigitalFormUtils.GenerateReport(layout, model)?.SaveToString();
-
-            if (EditItems(new[] { item }))
+            var menu = new ContextMenu();
+            foreach (var layout in layouts)
             {
-                SaveItem(item);
-                Refresh(false, true);
+                menu.AddItem($"{layout.Description.NotWhiteSpaceOr(layout.Type.ToString())}", null, layout, AddLayout);
             }
+            menu.AddSeparatorIfNeeded();
+            menu.AddItem("Create blank report", null, () => base.DoAdd());
+            menu.IsOpen = true;
         }
+    }
+    
+    private void AddLayout(DigitalFormLayout layout)
+    {
+        if (Form is null) return;
 
-        public void BeforeSave(object item)
-        {
-        }
+        var model = DigitalFormUtils.GetDataModel(Form.AppliesTo, GetVariables());
+        
+        var item = CreateItem();
+        item.DataModel = model.Name;
+        item.Name = layout.Description.NotWhiteSpaceOr(layout.Type.ToString());
+        item.RDL = DigitalFormUtils.GenerateReport(layout, model)?.SaveToString();
 
-        public void AfterSave(object item)
+        if (EditItems(new[] { item }))
         {
-            // First remove any deleted files
-            foreach (var original in OriginalItems)
-                if (!Items.Contains(original))
-                    new Client<ReportTemplate>().Delete(original, typeof(ReportTemplate).Name + " Deleted by User");
-
-            foreach (var template in Items)
-            {
-                template.Section = Form.ID.ToString();
-            }
-
-            new Client<ReportTemplate>().Save(Items.Where(x => x.IsChanged()), "Updated by User");
+            SaveItem(item);
+            Refresh(false, true);
         }
+    }
 
-        public override ReportTemplate CreateItem()
+    public override ReportTemplate CreateItem()
+    {
+        var item = base.CreateItem();
+        if(Form is not null)
         {
-            var item = base.CreateItem();
             var model = DigitalFormUtils.GetDataModel(Form.AppliesTo, GetVariables());
             item.DataModel = model.Name;
             
@@ -237,16 +179,7 @@ namespace InABox.DynamicGrid
             {
                 item.Section = Form.ID.ToString();
             }
-            return item;
         }
-
-        public string Caption() => "Reports";
-
-        public Size MinimumSize()
-        {
-            return new Size(400, 400);
-        }
-
-        public int Order { get; set; } = int.MinValue;
+        return item;
     }
 }

+ 100 - 43
inabox.wpf/DigitalForms/DynamicFormLayoutGrid.cs

@@ -4,9 +4,11 @@ using System.Drawing;
 using System.IO;
 using System.Linq;
 using System.Text.RegularExpressions;
+using System.Threading;
 using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Media.Imaging;
+using InABox.Clients;
 using InABox.Core;
 using InABox.DynamicGrid;
 using InABox.Scripting;
@@ -19,11 +21,25 @@ using UnderlineType = InABox.Core.UnderlineType;
 
 namespace InABox.DynamicGrid;
 
-public abstract class DynamicFormLayoutGrid : DynamicOneToManyGrid<DigitalForm, DigitalFormLayout>
+public class DynamicFormLayoutGrid : DynamicDataGrid<DigitalFormLayout>
 {
     private readonly BitmapImage design = Wpf.Resources.design.AsBitmapImage();
     private readonly BitmapImage tick = Wpf.Resources.tick.AsBitmapImage();
 
+    public DigitalForm? Form { get; set; }
+
+    public DynamicVariableGrid? VariableGrid { get; set; }
+
+    private DigitalFormLayout[]? _layouts;
+    public DigitalFormLayout[] Layouts
+    {
+        get
+        {
+            _layouts ??= Data.ToArray<DigitalFormLayout>();
+            return _layouts;
+        }
+    }
+
     protected override void Init()
     {
         base.Init();
@@ -36,10 +52,13 @@ public abstract class DynamicFormLayoutGrid : DynamicOneToManyGrid<DigitalForm,
         HiddenColumns.Add(x => x.Form.Code);
         HiddenColumns.Add(x => x.Form.Description);
 
-        if (!ReadOnly)
+        if (Security.CanEdit<DigitalFormLayout>())
         {
             ActionColumns.Add(new DynamicTickColumn<DigitalFormLayout, bool>(x => x.Active, tick, tick, null,
-                action: ActiveClick));
+                action: ActiveClick)
+            {
+                ToolTip = ActiveToolTip
+            });
         }
         ActionColumns.Add(new DynamicImageColumn(DesignImage, DesignClick));
 
@@ -47,22 +66,50 @@ public abstract class DynamicFormLayoutGrid : DynamicOneToManyGrid<DigitalForm,
         AddEditButton("Duplicate", null, Duplicate_Click);
     }
 
+    private FrameworkElement? ActiveToolTip(DynamicActionColumn column, CoreRow? row)
+    {
+        if(row is null)
+        {
+            return column.TextToolTip("Active");
+        }
+        var active = row.Get<DigitalFormLayout, bool>(x => x.Active);
+        var type = row.Get<DigitalFormLayout, DFLayoutType>(x => x.Type);
+        if (active)
+        {
+            return column.TextToolTip($"This is the active layout for {type}.");
+        }
+        else
+        {
+            return column.TextToolTip($"This layout is inactive.");
+        }
+    }
+
     private bool ActiveClick(CoreRow? row)
     {
         if (row is null) return false;
 
-        var item = LoadItem(row);
-        if (!item.Active)
+        var item = row.ToObject<DigitalFormLayout>();
+        var allItems = Data.ToArray<DigitalFormLayout>();
+        var toSave = new List<DigitalFormLayout>();
+        
+        item.Active = !item.Active;
+        toSave.Add(item);
+
+        if (item.Active)
         {
-            foreach(var otherItem in Items)
+            foreach(var otherItem in allItems)
             {
+                if (otherItem.ID == item.ID) continue;
+
                 if(otherItem.Type == item.Type)
                 {
                     otherItem.Active = false;
+                    toSave.Add(otherItem);
                 }
             }
         }
-        item.Active = !item.Active;
+
+        Client.Save(toSave, "Changed Active flag");
 
         return true;
     }
@@ -121,13 +168,12 @@ public abstract class DynamicFormLayoutGrid : DynamicOneToManyGrid<DigitalForm,
                     }
                     if(newVariables.Count > 0)
                     {
-                        var variables = GetVariableGrid();
-                        if (variables is not null)
+                        if (VariableGrid is not null)
                         {
                             var save = new List<DigitalFormVariable>();
                             foreach(var newVariable in newVariables)
                             {
-                                var variable = variables.GetVariable(newVariable.Code);
+                                var variable = VariableGrid.GetVariable(newVariable.Code);
                                 if(variable is not null)
                                 {
                                     if(variable.FieldType() != newVariable.FieldType())
@@ -141,8 +187,8 @@ public abstract class DynamicFormLayoutGrid : DynamicOneToManyGrid<DigitalForm,
                                 }
                             }
 
-                            variables.SaveItems(save.ToArray());
-                            variables.Refresh(false, true);
+                            VariableGrid.SaveItems(save.ToArray());
+                            VariableGrid.Refresh(false, true);
                         }
                     }
 
@@ -221,29 +267,28 @@ public abstract class DynamicFormLayoutGrid : DynamicOneToManyGrid<DigitalForm,
         }
     }
 
-    private DynamicVariableGrid? GetVariableGrid()
-        => EditorGrid.Pages?.FirstOrDefault(x => x is DynamicVariableGrid)
-            as DynamicVariableGrid;
-
-    private List<DigitalFormVariable> GetVariables()
-        => GetVariableGrid()?.Items.ToList() ?? new List<DigitalFormVariable>();
+    private IList<DigitalFormVariable> GetVariables()
+        => VariableGrid?.Variables ?? [];
 
     private void Design(DigitalFormLayout layout)
     {
-        var variables = GetVariables();
+        if (Form is null) return;
+
+        var variables = GetVariables().ToList();
 
         var newVariables = new List<DigitalFormVariable>();
 
         var form = new DynamicFormDesignWindow
         {
             Type = layout.Type,
-            ReadOnly = ReadOnly
+            ReadOnly = !Security.CanEdit<DigitalFormLayout>()
         };
         form.OnCreateVariable += (fieldType) =>
         {
-            if (DynamicVariableUtils.CreateAndEdit(Item, GetVariables(), fieldType, out var variable))
+            if (DynamicVariableUtils.CreateAndEdit(Form, variables, fieldType, out var variable))
             {
                 newVariables.Add(variable);
+                variables.Add(variable);
                 return variable;
             }
             return null;
@@ -266,12 +311,12 @@ public abstract class DynamicFormLayoutGrid : DynamicOneToManyGrid<DigitalForm,
             layout.Layout = form.SaveLayout();
             SaveItem(layout);
 
-            var grid = GetVariableGrid();
-            if (grid is not null)
+            if (VariableGrid is not null)
             {
-                grid.SaveItems(newVariables.ToArray());
-                grid.Refresh(false, true);
+                VariableGrid.SaveItems(newVariables);
+                VariableGrid.Refresh(false, true);
             }
+            Refresh(false, true);
         }
     }
 
@@ -280,32 +325,44 @@ public abstract class DynamicFormLayoutGrid : DynamicOneToManyGrid<DigitalForm,
         if (row == null)
             return false;
 
-        Design(LoadItem(row));
+        Design(row.ToObject<DigitalFormLayout>());
 
         return false;
     }
 
+    protected override bool CanCreateItems()
+    {
+        return base.CanCreateItems() && Form is not null;
+    }
 
-    //public override void SaveItem(DigitalFormLayout item)
-    //{
-    //    bool bActive = item.Active;
-    //    foreach (var other in Items.Where(x=>(x != item) && (x.Type == item.Type)))
-    //    {
-    //        if (item.Active)
-    //        {
-    //            if (other.Active)
-    //                other.Active = false;
-    //        }
-    //        else
-    //            bActive = bActive || other.Active;
-    //    }
-    //    if (!bActive)
-    //        item.Active = true;
-    //    base.SaveItem(item);
-    //}
+    public override DigitalFormLayout CreateItem()
+    {
+        var item = base.CreateItem();
+        item.Form.ID = Form?.ID ?? Guid.Empty;
+        return item;
+    }
 
     protected override void DoDoubleClick(object sender, DynamicGridCellClickEventArgs args)
     {
         DesignClick(SelectedRows.FirstOrDefault());
     }
+
+    protected override void OnAfterRefresh()
+    {
+        base.OnAfterRefresh();
+        _layouts = null;
+    }
+
+    protected override void Reload(Filters<DigitalFormLayout> criteria, Columns<DigitalFormLayout> columns, ref SortOrder<DigitalFormLayout>? sort, CancellationToken token, Action<CoreTable?, Exception?> action)
+    {
+        if(Form is null)
+        {
+            criteria.Add(Filter.None<DigitalFormLayout>());
+        }
+        else
+        {
+            criteria.Add(Filter<DigitalFormLayout>.Where(x => x.Form.ID).IsEqualTo(Form.ID));
+        }
+        base.Reload(criteria, columns, ref sort, token, action);
+    }
 }

+ 361 - 312
inabox.wpf/DigitalForms/DynamicVariableGrid.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using System.Reflection;
+using System.Threading;
 using System.Windows;
 using System.Windows.Controls;
 using InABox.Core;
@@ -13,430 +14,478 @@ using NPOI.OpenXmlFormats;
 using NPOI.SS.Formula.Functions;
 using NPOI.Util.Collections;
 
-namespace InABox.DynamicGrid
+namespace InABox.DynamicGrid;
+
+public class DynamicVariableGrid : DynamicDataGrid<DigitalFormVariable>
 {
-    internal class DynamicVariableGrid : DynamicOneToManyGrid<DigitalForm, DigitalFormVariable>
-    {
-        private bool ShowHidden = false;
-        private Button ShowHiddenButton;
-        private Button HideButton;
+    private bool ShowHidden = false;
+    private Button ShowHiddenButton;
+    private Button HideButton;
+
+    public DigitalForm? Form { get; set; }
 
-        protected override void Init()
+    private DigitalFormVariable[]? _variables;
+    public DigitalFormVariable[] Variables
+    {
+        get
         {
-            base.Init();
+            _variables ??= Data.ToArray<DigitalFormVariable>();
+            return _variables;
+        }
+    }
 
-            ShowHiddenButton = AddButton("Show Hidden", null, ToggleHidden_Click);
-            HideButton = AddEditButton("Hide Variable", null, Hide_Click);
-            HideButton.IsEnabled = false;
+    protected override void Init()
+    {
+        base.Init();
+
+        ShowHiddenButton = AddButton("Show Hidden", null, ToggleHidden_Click);
+        HideButton = AddEditButton("Hide Variable", null, Hide_Click);
+        HideButton.IsEnabled = false;
+
+        HiddenColumns.Add(x => x.Hidden);
+        HiddenColumns.Add(x => x.Code);
+        HiddenColumns.Add(x => x.VariableType);
+        HiddenColumns.Add(x => x.Parameters);
+        HiddenColumns.Add(x => x.Required);
+        HiddenColumns.Add(x => x.Secure);
+        HiddenColumns.Add(x => x.Retain);
+    }
 
-            HiddenColumns.Add(x => x.Hidden);
-            HiddenColumns.Add(x => x.Code);
-            HiddenColumns.Add(x => x.VariableType);
-            HiddenColumns.Add(x => x.Parameters);
-            HiddenColumns.Add(x => x.Required);
-            HiddenColumns.Add(x => x.Secure);
-            HiddenColumns.Add(x => x.Retain);
-        }
+    protected override void DoReconfigure(DynamicGridOptions options)
+    {
+        base.DoReconfigure(options);
 
-        protected override void DoReconfigure(DynamicGridOptions options)
-        {
-            base.DoReconfigure(options);
+        options.MultiSelect = true;
+    }
+
+    public DigitalFormVariable? GetVariable(string code)
+    {
+        return Data.Rows
+            .Where(x => x.Get<DigitalFormVariable, string>(x => x.Code) == code)
+            .FirstOrDefault()
+            ?.ToObject<DigitalFormVariable>();
+    }
+
+    private static bool ShouldHide(CoreRow[] rows)
+    {
+        return rows.Any(x => !x.Get<DigitalFormVariable, bool>(x => x.Hidden));
+    }
 
-            options.MultiSelect = true;
+    private bool Hide_Click(Button btn, CoreRow[] rows)
+    {
+        if(rows.Length == 0)
+        {
+            MessageBox.Show("No rows selected");
+            return false;
         }
 
-        public DigitalFormVariable? GetVariable(string code)
+        var hide = ShouldHide(rows);
+        var items = rows.ToArray<DigitalFormVariable>();
+        foreach (var item in items)
+        {
+            item.Hidden = hide;
+        }
+        if(items.Length > 0)
         {
-            return Items.Where(x => x.Code == code).FirstOrDefault();
+            SaveItems(items);
+            return true;
         }
+        return false;
+    }
+
+    protected override void SelectItems(CoreRow[]? rows)
+    {
+        base.SelectItems(rows);
 
-        private static bool ShouldHide(CoreRow[] rows)
+        if(rows is null)
+        {
+            HideButton.IsEnabled = false;
+        }
+        else
         {
-            return rows.Any(x => !x.Get<DigitalFormVariable, bool>(x => x.Hidden));
+            HideButton.IsEnabled = true;
+            HideButton.Content = (ShouldHide(rows) ? "Hide Variable" : "Un-hide Variable") + (rows.Length > 1 ? "s" : "");
         }
+    }
+
+    private bool ToggleHidden_Click(Button btn, CoreRow[] rows)
+    {
+        ShowHidden = !ShowHidden;
+        ShowHiddenButton.Content = ShowHidden ? "Hide Hidden" : "Show Hidden";
+        return true;
+    }
+
+    private void CreateMenu(ContextMenu parent, string header, Type type)
+    {
+        if (Form is null) return;
 
-        private bool Hide_Click(Button btn, CoreRow[] rows)
+        parent.AddItem(header, null, type, (itemtype) =>
         {
-            if(rows.Length == 0)
+            if(DynamicVariableUtils.CreateAndEdit(Form, Data.ToArray<DigitalFormVariable>(), itemtype, out var variable))
             {
-                MessageBox.Show("No rows selected");
-                return false;
+                SaveItem(variable);
+                Refresh(false, true);
             }
+        });
+    }
 
-            var hide = ShouldHide(rows);
-            var items = LoadItems(rows);
-            foreach (var item in items)
-            {
-                item.Hidden = hide;
-            }
-            if(items.Length > 0)
-            {
-                SaveItems(items);
-                return true;
-            }
-            return false;
-        }
+    protected override void DoAdd(bool openEditorOnDirectEdit = false)
+    {
+        var menu = new ContextMenu();
 
-        protected override void SelectItems(CoreRow[]? rows)
+        foreach(var fieldType in DFUtils.GetFieldTypes())
         {
-            base.SelectItems(rows);
-
-            if(rows is null)
-            {
-                HideButton.IsEnabled = false;
-            }
-            else
+            var caption = fieldType.GetCaption();
+            if (string.IsNullOrWhiteSpace(caption))
             {
-                HideButton.IsEnabled = true;
-                HideButton.Content = (ShouldHide(rows) ? "Hide Variable" : "Un-hide Variable") + (rows.Length > 1 ? "s" : "");
+                caption = CoreUtils.Neatify(fieldType.Name);
             }
+            CreateMenu(menu, caption, fieldType);
         }
 
-        private bool ToggleHidden_Click(Button btn, CoreRow[] rows)
-        {
-            ShowHidden = !ShowHidden;
-            ShowHiddenButton.Content = ShowHidden ? "Hide Hidden" : "Show Hidden";
-            return true;
-        }
+        menu.IsOpen = true;
+    }
+
+    protected override bool CanCreateItems()
+    {
+        return base.CanCreateItems() && Form is not null;
+    }
+
+    public override DigitalFormVariable CreateItem()
+    {
+        var item = base.CreateItem();
+        item.Form.ID = Form?.ID ?? Guid.Empty;
+        return item;
+    }
 
-        private void CreateMenu(ContextMenu parent, string header, Type type)
+    protected override void DoEdit()
+    {
+        if (!SelectedRows.Any() || Form is null)
+            return;
+        var variable = SelectedRows.First().ToObject<DigitalFormVariable>();
+        var properties = variable.CreateProperties();
+        if (DynamicVariableUtils.EditProperties(Form, Variables, properties.GetType(), properties, !Security.CanEdit<DigitalFormVariable>()))
         {
-            parent.AddItem(header, null, type, (itemtype) =>
-            {
-                if(DynamicVariableUtils.CreateAndEdit(Item, Items, itemtype, out var variable))
-                {
-                    SaveItem(variable);
-                    Refresh(false, true);
-                }
-            });
+            variable.SaveProperties(properties);
+            SaveItem(variable);
+            Refresh(false, true);
         }
+    }
 
-        protected override void DoAdd(bool openEditorOnDirectEdit = false)
-        {
-            var menu = new ContextMenu();
+    protected override void DoDelete()
+    {
+        var rows = SelectedRows.ToArray();
 
-            foreach(var fieldType in DFUtils.GetFieldTypes())
-            {
-                var caption = fieldType.GetCaption();
-                if (string.IsNullOrWhiteSpace(caption))
+        if (rows.Length != 0)
+            if (CanDeleteItems(rows))
+                if (MessageWindow.ShowYesNo(
+                    "Are you sure you want to delete this variable? This will all cause data associated with this variable to be lost.\n(If you want to just hide the variable, set it to 'Hidden' instead.)",
+                    "Confirm Deletion"))
                 {
-                    caption = CoreUtils.Neatify(fieldType.Name);
+                    DeleteItems(rows);
+                    SelectedRows = Array.Empty<CoreRow>();
+                    DoChanged();
+                    Refresh(false, true);
+                    SelectItems(null);
                 }
-                CreateMenu(menu, caption, fieldType);
-            }
-
-            menu.IsOpen = true;
-        }
+    }
 
-        /*public override DigitalFormVariable LoadItem(CoreRow row)
-        {
-            return Items.FirstOrDefault(r => r.ID.Equals(row.Get<DigitalFormVariable, Guid>(c => c.ID)));
-        }*/
+    protected override bool FilterRecord(CoreRow row)
+    {
+        return ShowHidden || !row.Get<DigitalFormVariable, bool>(x => x.Hidden);
+    }
 
-        protected override void DoEdit()
-        {
-            if (!SelectedRows.Any())
-                return;
-            var variable = LoadItem(SelectedRows.First());
-            var properties = variable.CreateProperties();
-            if (DynamicVariableUtils.EditProperties(Item, Items, properties.GetType(), properties, ReadOnly))
-            {
-                variable.SaveProperties(properties);
-                SaveItem(variable);
-                Refresh(false, true);
-            }
-        }
+    protected override void OnAfterRefresh()
+    {
+        base.OnAfterRefresh();
+        _variables = null;
+    }
 
-        protected override void DoDelete()
+    protected override void Reload(Filters<DigitalFormVariable> criteria, Columns<DigitalFormVariable> columns, ref SortOrder<DigitalFormVariable>? sort, CancellationToken token, Action<CoreTable?, Exception?> action)
+    {
+        if(Form is null)
         {
-            var rows = SelectedRows.ToArray();
-
-            if (rows.Any())
-                if (CanDeleteItems(rows))
-                    if (MessageBox.Show("Are you sure you want to delete this variable? This will all cause data associated with this variable to be lost.\n(If you want to just hide the variable, set it to 'Hidden' instead.)", "Confirm Deletion", MessageBoxButton.YesNo) == MessageBoxResult.Yes)
-                    {
-                        DeleteItems(rows);
-                        SelectedRows = Array.Empty<CoreRow>();
-                        DoChanged();
-                        Refresh(false, true);
-                        SelectItems(null);
-                    }
+            criteria.Add(Filter.None<DigitalFormVariable>());
         }
-
-        protected override bool FilterRecord(CoreRow row)
+        else
         {
-            return ShowHidden || !row.Get<DigitalFormVariable, bool>(x => x.Hidden);
+            criteria.Add(Filter<DigitalFormVariable>.Where(x => x.Form.ID).IsEqualTo(Form.ID));
         }
+        base.Reload(criteria, columns, ref sort, token, action);
     }
+}
 
-    public class DFLookupFilterNode : ComboBox, ICustomValueNode
+public class DFLookupFilterNode : ComboBox, ICustomValueNode
+{
+    public DFLookupFilterNode(Type entityType, CustomFilterValue? selectedValue)
     {
-        public DFLookupFilterNode(Type entityType, CustomFilterValue? selectedValue)
+        var properties = CoreUtils.PropertyList(entityType, x => x.GetCustomAttribute<DoNotSerialize>() == null, true).Keys.ToList();
+        properties.Sort();
+        Items.Add("");
+        foreach (var property in properties)
         {
-            var properties = CoreUtils.PropertyList(entityType, x => x.GetCustomAttribute<DoNotSerialize>() == null, true).Keys.ToList();
-            properties.Sort();
-            Items.Add("");
-            foreach (var property in properties)
-            {
-                Items.Add(property);
-            }
+            Items.Add(property);
+        }
 
-            Value = selectedValue;
+        Value = selectedValue;
 
-            SelectionChanged += DFLookupFilterNode_SelectionChanged;
+        SelectionChanged += DFLookupFilterNode_SelectionChanged;
 
-            VerticalAlignment = System.Windows.VerticalAlignment.Stretch;
-            VerticalContentAlignment = System.Windows.VerticalAlignment.Center;
-            MinWidth = 50;
+        VerticalAlignment = System.Windows.VerticalAlignment.Stretch;
+        VerticalContentAlignment = System.Windows.VerticalAlignment.Center;
+        MinWidth = 50;
+    }
+
+    private void DFLookupFilterNode_SelectionChanged(object sender, SelectionChangedEventArgs e)
+    {
+        var text = SelectedItem as string;
+        if (text.IsNullOrWhiteSpace())
+        {
+            _value = null;
+        }
+        else
+        {
+            _value = new CustomFilterValue(System.Text.Encoding.UTF8.GetBytes(text));
         }
+        ValueChanged?.Invoke(this, Value);
+    }
 
-        private void DFLookupFilterNode_SelectionChanged(object sender, SelectionChangedEventArgs e)
+    private CustomFilterValue? _value;
+    public CustomFilterValue? Value
+    {
+        get => _value;
+        set
         {
-            var text = SelectedItem as string;
-            if (text.IsNullOrWhiteSpace())
+            if(value is not null)
             {
-                _value = null;
+                var text = System.Text.Encoding.UTF8.GetString(value.Data);
+                SelectedItem = text;
+                _value = value;
             }
             else
             {
-                _value = new CustomFilterValue(System.Text.Encoding.UTF8.GetBytes(text));
-            }
-            ValueChanged?.Invoke(this, Value);
-        }
-
-        private CustomFilterValue? _value;
-        public CustomFilterValue? Value
-        {
-            get => _value;
-            set
-            {
-                if(value is not null)
-                {
-                    var text = System.Text.Encoding.UTF8.GetString(value.Data);
-                    SelectedItem = text;
-                    _value = value;
-                }
-                else
-                {
-                    SelectedItem = null;
-                    _value = null;
-                }
+                SelectedItem = null;
+                _value = null;
             }
         }
+    }
 
-        public FrameworkElement FrameworkElement => this;
+    public FrameworkElement FrameworkElement => this;
 
-        public event ICustomValueNode.ValueChangedHandler? ValueChanged;
-    }
+    public event ICustomValueNode.ValueChangedHandler? ValueChanged;
+}
 
-    public static class DynamicVariableUtils
+public static class DynamicVariableUtils
+{
+    public static bool CreateAndEdit(
+        DigitalForm form, IList<DigitalFormVariable> variables,
+        Type fieldType,
+        [NotNullWhen(true)] out DigitalFormVariable? variable)
     {
-        public static bool CreateAndEdit(
-            DigitalForm form, IList<DigitalFormVariable> variables,
-            Type fieldType,
-            [NotNullWhen(true)] out DigitalFormVariable? variable)
+        var fieldBaseType = fieldType.GetSuperclassDefinition(typeof(DFLayoutField<>));
+        if (fieldBaseType != null)
         {
-            var fieldBaseType = fieldType.GetSuperclassDefinition(typeof(DFLayoutField<>));
-            if (fieldBaseType != null)
+            var propertiesType = fieldBaseType.GetGenericArguments()[0];
+            var properties = (Activator.CreateInstance(propertiesType) as DFLayoutFieldProperties)!;
+            if (DynamicVariableUtils.EditProperties(form, variables, propertiesType, properties, readOnly: false))
             {
-                var propertiesType = fieldBaseType.GetGenericArguments()[0];
-                var properties = (Activator.CreateInstance(propertiesType) as DFLayoutFieldProperties)!;
-                if (DynamicVariableUtils.EditProperties(form, variables, propertiesType, properties, readOnly: false))
-                {
-                    variable = new DigitalFormVariable();
-                    variable.SaveProperties(fieldType, properties);
-                    return true;
-                }
+                variable = new DigitalFormVariable();
+                variable.Form.CopyFrom(form);
+                variable.SaveProperties(fieldType, properties);
+                return true;
             }
-            variable = null;
-            return false;
         }
+        variable = null;
+        return false;
+    }
 
-        public static bool EditProperties(DigitalForm form, IList<DigitalFormVariable> variables, Type type, DFLayoutFieldProperties item, bool readOnly)
+    public static bool EditProperties(DigitalForm form, IList<DigitalFormVariable> variables, Type type, DFLayoutFieldProperties item, bool readOnly)
+    {
+        var editor = new DynamicEditorForm(type);
+        editor.ReadOnly = readOnly;
+        if (item is DFLayoutLookupFieldProperties)
         {
-            var editor = new DynamicEditorForm(type);
-            editor.ReadOnly = readOnly;
-            if (item is DFLayoutLookupFieldProperties)
+            var appliesToType = DFUtils.FormEntityType(form);
+            editor.OnReconfigureEditors = grid =>
             {
-                var appliesToType = DFUtils.FormEntityType(form);
-                editor.OnReconfigureEditors = grid =>
+                var filter = grid.FindEditor("Filter");
+                if(filter is FilterEditorControl filterEditor)
                 {
-                    var filter = grid.FindEditor("Filter");
-                    if(filter is FilterEditorControl filterEditor)
-                    {
-                        var config = new FilterEditorConfiguration();
+                    var config = new FilterEditorConfiguration();
 
-                        config.OnCreateCustomValueNode += (type, prop, op, value) =>
-                        {
-                            if (appliesToType is not null)
-                            {
-                                return new DFLookupFilterNode(appliesToType, value);
-                            }
-                            else
-                            {
-                                return null;
-                            }
-                        };
-
-                        filterEditor.Configuration = config;
-                    }
-                };
-                editor.OnFormCustomiseEditor += (sender, items, column, editor) => LookupEditor_OnFormCustomiseEditor(sender, variables, items, column, editor);
-                editor.OnEditorValueChanged += (sender, name, value) =>
-                {
-                    var result = DynamicGridUtils.UpdateEditorValue(new[] { item }, name, value);
-                    if (name == "LookupType")
+                    config.OnCreateCustomValueNode += (type, prop, op, value) =>
                     {
-                        var grid = (sender as EmbeddedDynamicEditorForm)?.Editor!;
-                        var edit = grid.FindEditor("Filter");
-                        if (edit is FilterEditorControl filter)
+                        if (appliesToType is not null)
                         {
-                            filter.FilterType = value is string str ?
-                                CoreUtils.GetEntityOrNull(str) :
-                                null;
+                            return new DFLookupFilterNode(appliesToType, value);
                         }
-
-                        var propertiesEditor = grid.FindEditor(nameof(DFLayoutLookupFieldProperties.AdditionalProperties));
-                        if (propertiesEditor is MultiLookupEditorControl multi && multi.EditorDefinition is MultiLookupEditor combo)
+                        else
                         {
-                            combo.Clear();
-                            multi.Configure();
+                            return null;
                         }
+                    };
+
+                    filterEditor.Configuration = config;
+                }
+            };
+            editor.OnFormCustomiseEditor += (sender, items, column, editor) => LookupEditor_OnFormCustomiseEditor(sender, variables, items, column, editor);
+            editor.OnEditorValueChanged += (sender, name, value) =>
+            {
+                var result = DynamicGridUtils.UpdateEditorValue(new[] { item }, name, value);
+                if (name == "LookupType")
+                {
+                    var grid = (sender as EmbeddedDynamicEditorForm)?.Editor!;
+                    var edit = grid.FindEditor("Filter");
+                    if (edit is FilterEditorControl filter)
+                    {
+                        filter.FilterType = value is string str ?
+                            CoreUtils.GetEntityOrNull(str) :
+                            null;
+                    }
+
+                    var propertiesEditor = grid.FindEditor(nameof(DFLayoutLookupFieldProperties.AdditionalProperties));
+                    if (propertiesEditor is MultiLookupEditorControl multi && multi.EditorDefinition is MultiLookupEditor combo)
+                    {
+                        combo.Clear();
+                        multi.Configure();
                     }
-                    OnEditorValueChanged(sender, name, value);
-                    return new();
-                };
+                }
+                OnEditorValueChanged(sender, name, value);
+                return new();
+            };
+        }
+        else
+        {
+            editor.OnFormCustomiseEditor += (sender, items, column, editor) => Editor_OnFormCustomiseEditor(sender, variables, column, editor);
+            editor.OnEditorValueChanged += (sender, name, value) =>
+            {
+                var result = DynamicGridUtils.UpdateEditorValue(new[] { item }, name, value);
+                OnEditorValueChanged(sender, name, value);
+                return result;
+            };
+        }
+        editor.OnCreateEditorControl += Editor_OnCreateEditorControl;
+
+        editor.OnDefineLookups += o =>
+        {
+            var def = (o.EditorDefinition as ILookupEditor)!;
+            var colname = o.ColumnName;
+            // Nope, there is nothing dodgy about this at all
+            // I am not breaking any rules by passing in the QA Form instance, rather than the Field instance 
+            // so that I can get access to the "AppliesTo" property, and thus the list of properties that can be updated
+            // Nothing to see here, I promise!
+            CoreTable? values;
+            if (o.ColumnName == "Property")
+            {
+                values = def.Values(colname, new[] { form });
             }
             else
             {
-                editor.OnFormCustomiseEditor += (sender, items, column, editor) => Editor_OnFormCustomiseEditor(sender, variables, column, editor);
-                editor.OnEditorValueChanged += (sender, name, value) =>
-                {
-                    var result = DynamicGridUtils.UpdateEditorValue(new[] { item }, name, value);
-                    OnEditorValueChanged(sender, name, value);
-                    return result;
-                };
+                values = def.Values(colname, new[] { item });
             }
-            editor.OnCreateEditorControl += Editor_OnCreateEditorControl;
 
-            editor.OnDefineLookups += o =>
+            o.LoadLookups(values);
+        };
+
+        var thisVariable = variables.Where(x => x.Code == item.Code).ToList();
+
+        editor.OnValidateData += (sender, items) =>
+        {
+            var errors = new List<string>();
+            foreach(var item in items.Cast<DFLayoutFieldProperties>())
             {
-                var def = (o.EditorDefinition as ILookupEditor)!;
-                var colname = o.ColumnName;
-                // Nope, there is nothing dodgy about this at all
-                // I am not breaking any rules by passing in the QA Form instance, rather than the Field instance 
-                // so that I can get access to the "AppliesTo" property, and thus the list of properties that can be updated
-                // Nothing to see here, I promise!
-                CoreTable? values;
-                if (o.ColumnName == "Property")
+                // Check Codes
+                if (string.IsNullOrWhiteSpace(item.Code))
                 {
-                    values = def.Values(colname, new[] { form });
+                    errors.Add("[Code] may not be blank!");
                 }
                 else
                 {
-                    values = def.Values(colname, new[] { item });
-                }
-
-                o.LoadLookups(values);
-            };
-
-            var thisVariable = variables.Where(x => x.Code == item.Code).ToList();
-
-            editor.OnValidateData += (sender, items) =>
-            {
-                var errors = new List<string>();
-                foreach(var item in items.Cast<DFLayoutFieldProperties>())
-                {
-                    // Check Codes
-                    if (string.IsNullOrWhiteSpace(item.Code))
+                    var codeVars = variables.Where(x => x.Code == item.Code).ToList();
+                    if(codeVars.Count > 1)
                     {
-                        errors.Add("[Code] may not be blank!");
+                        errors.Add($"Duplicate code [{item.Code}]");
                     }
-                    else
+                    else if(codeVars.Count == 1)
                     {
-                        var codeVars = variables.Where(x => x.Code == item.Code).ToList();
-                        if(codeVars.Count > 1)
-                        {
-                            errors.Add($"Duplicate code [{item.Code}]");
-                        }
-                        else if(codeVars.Count == 1)
+                        if (!thisVariable.Contains(codeVars.First()))
                         {
-                            if (!thisVariable.Contains(codeVars.First()))
-                            {
-                                errors.Add($"There is already a variable with code [{item.Code}]");
-                            }
+                            errors.Add($"There is already a variable with code [{item.Code}]");
                         }
                     }
+                }
 
-                    // Check Read-Only property
-                    if(item.ReadOnlyProperty && item.Property.IsNullOrWhiteSpace() && item.Expression.IsNullOrWhiteSpace())
-                    {
-                        errors.Add("A field cannot be read-only if [Property] or [Expression] have not been set.");
-                    }
+                // Check Read-Only property
+                if(item.ReadOnlyProperty && item.Property.IsNullOrWhiteSpace() && item.Expression.IsNullOrWhiteSpace())
+                {
+                    errors.Add("A field cannot be read-only if [Property] or [Expression] have not been set.");
                 }
-                return errors;
-            };
-            editor.Items = new BaseObject[] { item };
-            return editor.ShowDialog() == true;
-        }
+            }
+            return errors;
+        };
+        editor.Items = new BaseObject[] { item };
+        return editor.ShowDialog() == true;
+    }
 
-        private static void Editor_OnCreateEditorControl(string column, BaseEditor editor, IDynamicEditorControl control)
+    private static void Editor_OnCreateEditorControl(string column, BaseEditor editor, IDynamicEditorControl control)
+    {
+        var properties = (control.Host.GetItems()[0] as DFLayoutFieldProperties)!;
+        if(column == nameof(DFLayoutFieldProperties.ReadOnlyProperty))
         {
-            var properties = (control.Host.GetItems()[0] as DFLayoutFieldProperties)!;
-            if(column == nameof(DFLayoutFieldProperties.ReadOnlyProperty))
+            if (properties.Property.IsNullOrWhiteSpace())
             {
-                if (properties.Property.IsNullOrWhiteSpace())
-                {
-                    control.IsEnabled = false;
-                }
+                control.IsEnabled = false;
             }
         }
+    }
 
-        private static void Editor_OnFormCustomiseEditor(IDynamicEditorForm sender, IList<DigitalFormVariable> vars, DynamicGridColumn column, BaseEditor editor)
+    private static void Editor_OnFormCustomiseEditor(IDynamicEditorForm sender, IList<DigitalFormVariable> vars, DynamicGridColumn column, BaseEditor editor)
+    {
+        var properties = (sender.Items[0] as DFLayoutFieldProperties)!;
+        if ((column.ColumnName == "Expression" || column.ColumnName == "ColourExpression") && editor is ExpressionEditor exp)
         {
-            var properties = (sender.Items[0] as DFLayoutFieldProperties)!;
-            if ((column.ColumnName == "Expression" || column.ColumnName == "ColourExpression") && editor is ExpressionEditor exp)
+            var variables = new List<string>();
+            foreach (var variable in vars)
             {
-                var variables = new List<string>();
-                foreach (var variable in vars)
+                //variables.Add(variable.Code);
+                foreach (var col in variable.GetVariableColumns())
                 {
-                    //variables.Add(variable.Code);
-                    foreach (var col in variable.GetVariableColumns())
-                    {
-                        variables.Add(col.ColumnName);
-                    }
+                    variables.Add(col.ColumnName);
                 }
+            }
+            if(column.ColumnName == "Expression")
+            {
                 variables.Remove(properties.Code);
-                variables.Sort();
-                exp.VariableNames = variables;
             }
+            variables.Sort();
+            exp.VariableNames = variables;
         }
+    }
 
-        private static void LookupEditor_OnFormCustomiseEditor(IDynamicEditorForm sender, IList<DigitalFormVariable> vars, object[] items, DynamicGridColumn column, BaseEditor editor)
+    private static void LookupEditor_OnFormCustomiseEditor(IDynamicEditorForm sender, IList<DigitalFormVariable> vars, object[] items, DynamicGridColumn column, BaseEditor editor)
+    {
+        if (column.ColumnName == "Filter" && editor is FilterEditor fe)
         {
-            if (column.ColumnName == "Filter" && editor is FilterEditor fe)
-            {
-                var properties = (items[0] as DFLayoutLookupFieldProperties)!;
-                var lookupType = properties.LookupType;
-                var entityType = CoreUtils.GetEntityOrNull(lookupType);
-                fe.Type = entityType;
-            }
-            Editor_OnFormCustomiseEditor(sender, vars, column, editor);
+            var properties = (items[0] as DFLayoutLookupFieldProperties)!;
+            var lookupType = properties.LookupType;
+            var entityType = CoreUtils.GetEntityOrNull(lookupType);
+            fe.Type = entityType;
         }
+        Editor_OnFormCustomiseEditor(sender, vars, column, editor);
+    }
 
-        private static void OnEditorValueChanged(IDynamicEditorForm sender, string name, object value)
+    private static void OnEditorValueChanged(IDynamicEditorForm sender, string name, object value)
+    {
+        if(name == nameof(DFLayoutFieldProperties.Property))
         {
-            if(name == nameof(DFLayoutFieldProperties.Property))
+            var properties = (sender.Items[0] as DFLayoutFieldProperties)!;
+            var grid = (sender as EmbeddedDynamicEditorForm)?.Editor!;
+            var edit = grid.FindEditor(nameof(DFLayoutFieldProperties.ReadOnlyProperty));
+            if (edit is not null)
             {
-                var properties = (sender.Items[0] as DFLayoutFieldProperties)!;
-                var grid = (sender as EmbeddedDynamicEditorForm)?.Editor!;
-                var edit = grid.FindEditor(nameof(DFLayoutFieldProperties.ReadOnlyProperty));
-                if (edit is not null)
-                {
-                    edit.IsEnabled = !properties.Property.IsNullOrWhiteSpace() && !grid.ReadOnly && edit.EditorDefinition.Editable.IsEditable();
-                }
+                edit.IsEnabled = !properties.Property.IsNullOrWhiteSpace() && !grid.ReadOnly && edit.EditorDefinition.Editable.IsEditable();
             }
         }
-
     }
+
 }

+ 1 - 2
inabox.wpf/DynamicGrid/Grids/BaseDynamicGrid.cs

@@ -817,7 +817,6 @@ public abstract class BaseDynamicGrid : ContentControl, IDynamicGridUIComponentP
     public void Reconfigure(IDynamicGrid.ReconfigureEvent onReconfigure)
     {
         OnReconfigure += onReconfigure;
-        Reconfigure();
     }
 
     #endregion
@@ -1290,7 +1289,7 @@ public abstract class BaseDynamicGrid : ContentControl, IDynamicGridUIComponentP
         UIComponent.InvalidateRow(row);
     }
 
-    protected void InvalidateGrid()
+    public void InvalidateGrid()
     {
         if (RowStyleSelector != null)
             RowStyleSelector.Data = Data;

+ 256 - 87
inabox.wpf/DynamicGrid/Grids/DynamicDocumentGrid.cs

@@ -16,6 +16,7 @@ using Microsoft.Xaml.Behaviors.Core;
 using Image = System.Windows.Controls.Image;
 using InABox.Wpf;
 using System.Threading;
+using InABox.Clients;
 
 namespace InABox.DynamicGrid;
 
@@ -41,12 +42,11 @@ public class TimeStampToBrushConverter : AbstractConverter<DateTime, System.Wind
     }
 }
 
-public class DynamicDocumentGrid<TDocument, TEntity, TEntityLink> : DynamicManyToManyGrid<TDocument, TEntity>
+public class DynamicDocumentGridImplementation<TDocument, TEntity, TEntityLink>(DynamicGrid<TDocument> grid)
     where TEntity : Entity, IPersistent, IRemotable, new()
     where TDocument : Entity, IEntityDocument<TEntityLink>, IPersistent, IRemotable, new() // Entity, IPersistent, IRemotable, IManyToMany<TEntity, Document>, new()
     where TEntityLink : EntityLink<TEntity>, new()
 {
-    
     public bool ShowSupercededColumn { get; set; }
 
     private bool _simpleTemplate;
@@ -56,42 +56,47 @@ public class DynamicDocumentGrid<TDocument, TEntity, TEntityLink> : DynamicManyT
         set
         {
             _simpleTemplate = value;
-            RowHeight = value
+            grid.RowHeight = value
                 ? 150
                 : 100;
         } 
     }
 
-    private DynamicTemplateColumn _template;
-    
-    public DynamicDocumentGrid()
+    public void Init()
     {
-        MultiSelect = false;
-        HiddenColumns.Add(x => x.DocumentLink.ID);
-        HiddenColumns.Add(x => x.Superceded);
-        HiddenColumns.Add(x => x.DocumentLink.FileName);
-        HiddenColumns.Add(x => x.Thumbnail);
-        HiddenColumns.Add(x => x.Notes);
+        grid.HiddenColumns.Add(x => x.DocumentLink.ID);
+        grid.HiddenColumns.Add(x => x.Superceded);
+        grid.HiddenColumns.Add(x => x.DocumentLink.FileName);
+        grid.HiddenColumns.Add(x => x.Thumbnail);
+        grid.HiddenColumns.Add(x => x.Notes);
         //ActionColumns.Add(new DynamicImageColumn(DocumentImage, ViewDocument) { Position = DynamicActionColumnPosition.Start });
         //ActionColumns.Add(new DynamicImageColumn(DiskImage, SaveDocument) { Position = DynamicActionColumnPosition.Start });
-        _template = new DynamicTemplateColumn(DocumentTemplate)
+        var template = new DynamicTemplateColumn(DocumentTemplate)
         {
             Position = DynamicActionColumnPosition.Start,
             Width = 0,
             HeaderText = "Attached Documents"
         };
-        ActionColumns.Add(_template);
+        grid.ActionColumns.Add(template);
         //supercedecolumn = new DynamicImageColumn(SupercededImage, SupercedeDocument);
         //ActionColumns.Add(supercedecolumn);
-        RowHeight = 100;
+        grid.RowHeight = 100;
+
+        grid.OnReconfigure += Grid_OnReconfigure;
     }
 
-    protected override void DoDoubleClick(object sender, DynamicGridCellClickEventArgs args)
+    private void Grid_OnReconfigure(DynamicGridOptions options)
+    {
+        options.SelectColumns = false;
+        options.DragTarget = true;
+    }
+
+    public void DoDoubleClick()
     {
-        var doc = SelectedRows.FirstOrDefault()?.ToObject<TDocument>();
+        var doc = grid.SelectedRows.FirstOrDefault()?.ToObject<TDocument>();
         if (doc != null)
         {
-            var editor = new DocumentEditor(new IEntityDocument[] { doc });
+            var editor = new DocumentEditor([doc]);
             //editor.PrintAllowed = Security.IsAllowed<CanPrintFactoryFloorDrawings>();
             editor.SaveAllowed = false;
             editor.ShowDialog();
@@ -316,7 +321,7 @@ public class DynamicDocumentGrid<TDocument, TEntity, TEntityLink> : DynamicManyT
 
     private void GetDocuments(Action<Dictionary<string,byte[]>> action)
     {
-        var ids = SelectedRows.Select(r => r.Get<IEntityDocument, Guid>(c => c.DocumentLink.ID)).ToArray();
+        var ids = grid.SelectedRows.Select(r => r.Get<IEntityDocument, Guid>(c => c.DocumentLink.ID)).ToArray();
         var files = Client.Query(
             Filter<Document>.Where(x => x.ID).InList(ids),
             Columns.None<Document>().Add(x => x.FileName).Add(x => x.Data)
@@ -360,7 +365,7 @@ public class DynamicDocumentGrid<TDocument, TEntity, TEntityLink> : DynamicManyT
 
     private void CopyDocuments()
     {
-        if (SelectedRows?.Any() != true)
+        if (grid.SelectedRows?.Any() != true)
             return;
         GetDocuments((files) =>
         {
@@ -377,7 +382,7 @@ public class DynamicDocumentGrid<TDocument, TEntity, TEntityLink> : DynamicManyT
 
     private void SaveDocuments()
     {
-        if (SelectedRows?.Any() != true)
+        if (grid.SelectedRows?.Any() != true)
             return;
         
         using(var fbd = new System.Windows.Forms.FolderBrowserDialog())
@@ -398,7 +403,7 @@ public class DynamicDocumentGrid<TDocument, TEntity, TEntityLink> : DynamicManyT
 
     private void PrintDocuments()
     {
-        if (SelectedRows?.Any() != true)
+        if (grid.SelectedRows?.Any() != true)
             return;
         GetDocuments(files =>
         {
@@ -420,21 +425,9 @@ public class DynamicDocumentGrid<TDocument, TEntity, TEntityLink> : DynamicManyT
 
     }
 
-    protected override DynamicGridColumns LoadColumns()
-    {
-        return new DynamicGridColumns();
-    }
-    
-    protected override void DoReconfigure(DynamicGridOptions options)
-    {
-        base.DoReconfigure(options);
-        options.SelectColumns = false;
-        options.DragTarget = true;
-    }
-
-    public override int Order { get; set; } = int.MaxValue;
+    #region Drag + Drop
 
-    protected override void HandleDragOver(object sender, DragEventArgs e)
+    public bool HandleDragOver(object sender, DragEventArgs e)
     {
         if (e.Data.GetDataPresent(DataFormats.FileDrop) || e.Data.GetDataPresent("FileGroupDescriptor"))
         {
@@ -445,9 +438,10 @@ public class DynamicDocumentGrid<TDocument, TEntity, TEntityLink> : DynamicManyT
             e.Effects = DragDropEffects.None;
         }
         e.Handled = true;
+        return true; // True because it has been handled.
     }
 
-    protected override void HandleDragDrop(object sender, DragEventArgs e)
+    public bool HandleDragDrop(object sender, DragEventArgs e)
     {
         var result = DocumentUtils.HandleFileDrop(e);
         if(result is not null)
@@ -474,9 +468,11 @@ public class DynamicDocumentGrid<TDocument, TEntity, TEntityLink> : DynamicManyT
             }
             AddDocuments(docs);
         }
+
+        return true; // True because handled.
     }
 
-    protected override void OnDragEnd(Type entity, CoreTable table, DragEventArgs e)
+    public bool OnDragEnd(Type entity, CoreTable table, DragEventArgs e)
     {
         if (entity == typeof(Document))
         {
@@ -485,7 +481,7 @@ public class DynamicDocumentGrid<TDocument, TEntity, TEntityLink> : DynamicManyT
             var docIDS = table.Rows.Select(x => x.Get<Document, Guid>(x => x.ID)).ToArray();
 
             var columns = Columns.None<Document>().Add(x => x.ID);
-            foreach (var column in VisibleColumns)
+            foreach (var column in grid.VisibleColumns)
             {
                 if (column.ColumnName.StartsWith("DocumentLink."))
                 {
@@ -498,25 +494,27 @@ public class DynamicDocumentGrid<TDocument, TEntity, TEntityLink> : DynamicManyT
 
             foreach (var doc in docs.ToObjects<Document>())
             {
-                var entityDocument = new TDocument();
-                entityDocument.EntityLink.ID = Item.ID;
-                entityDocument.DocumentLink.ID = doc.ID;
-                entityDocument.DocumentLink.Synchronise(doc);
-                SaveItem(entityDocument);
+                var entityDocument = grid.CreateItem();
+                entityDocument.DocumentLink.CopyFrom(doc);
+                grid.SaveItem(entityDocument);
                 refresh = true;
             }
             if (refresh)
             {
-                DoChanged();
-                Refresh(false, true);
+                grid.DoChanged();
+                grid.Refresh(false, true);
             }
+
+            return true;
         }
         else
         {
-            base.OnDragEnd(entity, table, e);
+            return false;
         }
     }
 
+    #endregion
+
     private void AddDocuments(IList<Document> documents)
     {
         if (documents.Any())
@@ -524,18 +522,16 @@ public class DynamicDocumentGrid<TDocument, TEntity, TEntityLink> : DynamicManyT
             Client.Save(documents, "Initial Upload");
             foreach (var doc in documents)
             {
-                var newitem = CreateItem();
-                var prop = GetOtherLink(newitem);
-                prop.ID = doc.ID;
-                prop.Synchronise(doc);
-                SaveItem(newitem);
+                var newitem = grid.CreateItem();
+                newitem.DocumentLink.CopyFrom(doc);
+                grid.SaveItem(newitem);
             }
-            DoChanged();
-            Refresh(false, true);
+            grid.DoChanged();
+            grid.Refresh(false, true);
         }
     }
 
-    protected override void DoAdd(bool openEditorOnDirectEdit = false)
+    public void DoAdd(bool openEditorOnDirectEdit = false)
     {
         var dlg = new OpenFileDialog();
         dlg.Multiselect = true;
@@ -559,6 +555,210 @@ public class DynamicDocumentGrid<TDocument, TEntity, TEntityLink> : DynamicManyT
         }
     }
 
+    public void AfterReload(CoreTable? t, Exception? e, CancellationToken token)
+    {
+        // Download Hi Res images in the background and replace them when available
+        if (t != null && SimpleTemplate)
+        {
+            var ids = t.ExtractValues<TDocument, Guid>(x => x.DocumentLink.ID).Distinct().ToArray();
+            Client.Query(
+                Filter<Document>.Where(x => x.ID).InList(ids),
+                Columns.None<Document>().Add(x => x.ID).Add(x => x.Data),
+                null,
+                null,
+                (d, _) =>
+                {
+                    if (token.IsCancellationRequested) return;
+
+                    if (d == null)
+                        return;
+                    var docs = d.ToDictionary<Document, Guid, byte[]>(x => x.ID, x => x.Data);
+                    foreach (var row in t.Rows)
+                    {
+                        if (docs.TryGetValue(row.Get<TDocument, Guid>(x => x.DocumentLink.ID),
+                                out byte[]? data) && (data?.Any() == true))
+                        {
+                            if (ImageUtils.IsPdf(data))
+                                data = ImageUtils.PDFToBitmap(data, 0);
+                            row.Set<TDocument, byte[]>(x => x.Thumbnail!, data);
+                        }
+                    }
+                    grid.Dispatcher.BeginInvoke(() => grid.InvalidateGrid());
+                }
+            );
+        }
+    }
+}
+
+public class DynamicDocumentDataGrid<TDocument, TEntity, TEntityLink> : DynamicDataGrid<TDocument>
+    where TEntity : Entity, IPersistent, IRemotable, new()
+    where TDocument : Entity, IEntityDocument<TEntityLink>, IPersistent, IRemotable, new() // Entity, IPersistent, IRemotable, IManyToMany<TEntity, Document>, new()
+    where TEntityLink : EntityLink<TEntity>, new()
+{
+    private DynamicDocumentGridImplementation<TDocument, TEntity, TEntityLink> Implementation;
+
+    public bool ShowSupercededColumn
+    {
+        get => Implementation.ShowSupercededColumn;
+        set => Implementation.ShowSupercededColumn = value;
+    }
+
+    public bool SimpleTemplate
+    {
+        get => Implementation.SimpleTemplate;
+        set => Implementation.SimpleTemplate = value;
+    }
+
+    public TEntity? Item { get; set; }
+    
+    public DynamicDocumentDataGrid()
+    {
+        Implementation = new(this);
+        Implementation.Init();
+    }
+
+    protected override void DoDoubleClick(object sender, DynamicGridCellClickEventArgs args)
+    {
+        Implementation.DoDoubleClick();
+    }
+
+    protected override DynamicGridColumns LoadColumns()
+    {
+        return new DynamicGridColumns();
+    }
+
+    protected override void HandleDragOver(object sender, DragEventArgs e)
+    {
+        if(!Implementation.HandleDragOver(sender, e))
+        {
+            base.HandleDragOver(sender, e);
+        }
+    }
+
+    protected override void HandleDragDrop(object sender, DragEventArgs e)
+    {
+        if(!Implementation.HandleDragDrop(sender, e))
+        {
+            base.HandleDragOver(sender, e);
+        }
+    }
+
+    protected override void OnDragEnd(Type entity, CoreTable table, DragEventArgs e)
+    {
+        if(!Implementation.OnDragEnd(entity, table, e))
+        {
+            base.OnDragEnd(entity, table, e);
+        }
+    }
+
+    protected override void DoAdd(bool openEditorOnDirectEdit = false)
+    {
+        Implementation.DoAdd(openEditorOnDirectEdit);
+    }
+
+    protected override bool CanCreateItems()
+    {
+        return Item is not null;
+    }
+
+    public override TDocument CreateItem()
+    {
+        var item = base.CreateItem();
+        item.EntityLink.ID = Item?.ID ?? Guid.Empty;
+        return item;
+    }
+
+    protected override void Reload(
+        Filters<TDocument> criteria, Columns<TDocument> columns, ref SortOrder<TDocument>? sort, 
+        CancellationToken token, Action<CoreTable?, Exception?> action)
+    {
+        if(Item is null)
+        {
+            criteria.Add(Filter.None<TDocument>());
+        }
+        else
+        {
+            criteria.Add(Filter<TDocument>.Where(x => x.EntityLink.ID).IsEqualTo(Item.ID));
+        }
+        base.Reload(criteria, columns, ref sort, token, (t, e) =>
+        {
+            if (token.IsCancellationRequested) return;
+
+            action(t, e);
+
+            Implementation.AfterReload(t, e, token);
+        });
+    }
+}
+
+public class DynamicDocumentGrid<TDocument, TEntity, TEntityLink> : DynamicManyToManyGrid<TDocument, TEntity>
+    where TEntity : Entity, IPersistent, IRemotable, new()
+    where TDocument : Entity, IEntityDocument<TEntityLink>, IPersistent, IRemotable, new() // Entity, IPersistent, IRemotable, IManyToMany<TEntity, Document>, new()
+    where TEntityLink : EntityLink<TEntity>, new()
+{
+    private DynamicDocumentGridImplementation<TDocument, TEntity, TEntityLink> Implementation;
+
+    public bool ShowSupercededColumn
+    {
+        get => Implementation.ShowSupercededColumn;
+        set => Implementation.ShowSupercededColumn = value;
+    }
+
+    public bool SimpleTemplate
+    {
+        get => Implementation.SimpleTemplate;
+        set => Implementation.SimpleTemplate = value;
+    }
+    
+    public DynamicDocumentGrid()
+    {
+        MultiSelect = true;
+
+        Implementation = new(this);
+        Implementation.Init();
+    }
+
+    protected override void DoDoubleClick(object sender, DynamicGridCellClickEventArgs args)
+    {
+        Implementation.DoDoubleClick();
+    }
+
+    protected override DynamicGridColumns LoadColumns()
+    {
+        return new DynamicGridColumns();
+    }
+
+    public override int Order { get; set; } = int.MaxValue;
+
+    protected override void HandleDragOver(object sender, DragEventArgs e)
+    {
+        if(!Implementation.HandleDragOver(sender, e))
+        {
+            base.HandleDragOver(sender, e);
+        }
+    }
+
+    protected override void HandleDragDrop(object sender, DragEventArgs e)
+    {
+        if(!Implementation.HandleDragDrop(sender, e))
+        {
+            base.HandleDragOver(sender, e);
+        }
+    }
+
+    protected override void OnDragEnd(Type entity, CoreTable table, DragEventArgs e)
+    {
+        if(!Implementation.OnDragEnd(entity, table, e))
+        {
+            base.OnDragEnd(entity, table, e);
+        }
+    }
+
+    protected override void DoAdd(bool openEditorOnDirectEdit = false)
+    {
+        Implementation.DoAdd(openEditorOnDirectEdit);
+    }
+
     protected override void Reload(
         Filters<TDocument> criteria, Columns<TDocument> columns, ref SortOrder<TDocument>? sort, 
         CancellationToken token, Action<CoreTable?, Exception?> action)
@@ -568,39 +768,8 @@ public class DynamicDocumentGrid<TDocument, TEntity, TEntityLink> : DynamicManyT
             if (token.IsCancellationRequested) return;
 
             action(t,e);
-            
-            // Download Hi Res images in the background and replace them when available
-            if (t != null && SimpleTemplate)
-            {
-                var ids = t.ExtractValues<TDocument, Guid>(x => x.DocumentLink.ID).Distinct().ToArray();
-                Client.Query(
-                    Filter<Document>.Where(x => x.ID).InList(ids),
-                    Columns.None<Document>().Add(x => x.ID).Add(x => x.Data),
-                    null,
-                    null,
-                    (d, _) =>
-                    {
-                        if (token.IsCancellationRequested) return;
-
-                        if (d == null)
-                            return;
-                        var docs = d.ToDictionary<Document, Guid, byte[]>(x => x.ID, x => x.Data);
-                        foreach (var row in t.Rows)
-                        {
-                            if (docs.TryGetValue(row.Get<TDocument, Guid>(x => x.DocumentLink.ID),
-                                    out byte[]? data) && (data?.Any() == true))
-                            {
-                                if (ImageUtils.IsPdf(data))
-                                    data = ImageUtils.PDFToBitmap(data, 0);
-                                row.Set<TDocument, byte[]>(x => x.Thumbnail!, data);
-                            }
-                        }
-                        Dispatcher.BeginInvoke(() => base.Refresh(false,false));
-                    }
-                );
-            }
-
 
+            Implementation.AfterReload(t, e, token);
         });
     }
 }

+ 10 - 4
inabox.wpf/WPFUtils.cs

@@ -196,12 +196,14 @@ public static class WPFUtils
         T source,
         Expression<Func<T, TProperty>> expression,
         IValueConverter? converter = null,
+        BindingMode mode = BindingMode.Default,
         string? format = null)
     {
         return new Binding(CoreUtils.GetFullPropertyName(expression, "_"))
         {
             Source = source,
             Converter = converter,
+            Mode = mode,
             StringFormat = format
         };
     }
@@ -228,9 +230,10 @@ public static class WPFUtils
         Expression<Func<T, TProperty>> expression,
         TValue value,
         IValueConverter<TProperty, TValue>? converter,
+        BindingMode mode = BindingMode.Default,
         string? format = null)
     {
-        trigger.Binding = CreateBinding(source, expression, converter, format);
+        trigger.Binding = CreateBinding(source, expression, converter, mode, format);
         trigger.Value = value;
         return trigger;
     }
@@ -254,9 +257,10 @@ public static class WPFUtils
         Expression<Func<T, TProperty>> expression,
         TProperty value,
         IValueConverter? converter = null,
+        BindingMode mode = BindingMode.Default,
         string? format = null)
     {
-        trigger.Binding = CreateBinding(source, expression, converter, format);
+        trigger.Binding = CreateBinding(source, expression, converter, mode, format);
         trigger.Value = value;
         return trigger;
     }
@@ -281,12 +285,13 @@ public static class WPFUtils
         T source,
         Expression<Func<T, TProperty>> expression,
         IValueConverter? converter = null,
+        BindingMode mode = BindingMode.Default,
         string? format = null)
         where TElement : FrameworkElement
     {
         element.SetBinding(
             property,
-            CreateBinding(source, expression, converter, format)
+            CreateBinding(source, expression, converter, mode, format)
         );
         return element;
     }
@@ -329,11 +334,12 @@ public static class WPFUtils
         T source,
         Expression<Func<T, TProperty>> expression,
         IValueConverter? converter = null,
+        BindingMode mode = BindingMode.Default,
         string? format = null)
     {
         element.SetBinding(
             property,
-            CreateBinding(source, expression, converter, format)
+            CreateBinding(source, expression, converter, mode, format)
         );
         return element;
     }