Browse Source

Working on data component

Kenric Nugteren 1 year ago
parent
commit
d7e26cb744

+ 0 - 57
inabox.wpf/DigitalForms/Designer/DynamicFormControlGrid.cs

@@ -1,57 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading;
-using InABox.Clients;
-using InABox.Core;
-
-namespace InABox.DynamicGrid
-{
-    public class FormControlGrid<T> : DynamicGrid<T> where T : DFLayoutControl, new()
-    {
-        public FormControlGrid()
-        {
-            Items = new List<T>();
-        }
-
-        protected override void Init()
-        {
-        }
-
-        protected override void DoReconfigure(DynamicGridOptions options)
-        {
-            options.RecordCount = true;
-        }
-
-        public List<T> Items { get; set; }
-
-        public override void DeleteItems(params CoreRow[] rows)
-        {
-            var items = new List<DFLayoutControl>();
-            foreach (var row in rows)
-                items.Add(Items[row.Index]);
-            Items.RemoveAll(x => items.Contains(x));
-        }
-
-        public override T LoadItem(CoreRow row)
-        {
-            return Items[row.Index];
-        }
-
-        protected override void Reload(
-            Filters<T> criteria, Columns<T> columns, ref SortOrder<T>? sort, 
-            CancellationToken token, Action<CoreTable?, Exception?> action)
-        {
-            var table = new CoreTable();
-            table.LoadColumns(typeof(T));
-            table.LoadRows(Items.OrderBy(x => x.Sequence));
-            action?.Invoke(table, null);
-        }
-
-        public override void SaveItem(T item)
-        {
-            if (!Items.Contains(item))
-                Items.Add(item);
-        }
-    }
-}

+ 5 - 10
inabox.wpf/DigitalForms/Designer/DynamicFormDesignGrid.cs

@@ -1451,8 +1451,7 @@ namespace InABox.DynamicGrid
             var element = new TElement();
             SetControlRange(element, range);
 
-            var result = new FormControlGrid<TElement>().EditItems(new[] { element });
-            if (result)
+            if (DynamicGridUtils.EditObject(element))
             {
                 form.Elements.Add(element);
                 Render();
@@ -1484,8 +1483,7 @@ namespace InABox.DynamicGrid
         {
             var label = new DFLayoutLabel();
             SetControlRange(label, range);
-            var result = new FormControlGrid<DFLayoutLabel>().EditItems(new[] { label });
-            if (result)
+            if (DynamicGridUtils.EditObject(label))
             {
                 form.Elements.Add(label);
                 Render();
@@ -1496,8 +1494,7 @@ namespace InABox.DynamicGrid
         {
             var header = new DFLayoutHeader();
             SetControlRange(header, range);
-            var result = new FormControlGrid<DFLayoutHeader>().EditItems(new[] { header });
-            if (result)
+            if (DynamicGridUtils.EditObject(header))
             {
                 form.Elements.Add(header);
                 Render();
@@ -1508,8 +1505,7 @@ namespace InABox.DynamicGrid
         {
             var image = new DFLayoutImage();
             SetControlRange(image, range);
-            var result = new FormControlGrid<DFLayoutImage>().EditItems(new[] { image });
-            if (result)
+            if (DynamicGridUtils.EditObject(image))
             {
                 form.Elements.Add(image);
                 Render();
@@ -1635,8 +1631,7 @@ namespace InABox.DynamicGrid
         {
             if(!elementgrids.TryGetValue(control.GetType(), out var grid))
             {
-                var type = typeof(FormControlGrid<>).MakeGenericType(control.GetType());
-                grid = (Activator.CreateInstance(type) as IDynamicGrid)!;
+                grid = DynamicGridUtils.CreateDynamicGrid(typeof(DynamicGrid<>), control.GetType());
                 elementgrids[control.GetType()] = grid;
             }
 

+ 1 - 1
inabox.wpf/DigitalForms/DigitalFormGrid.cs

@@ -191,7 +191,7 @@ namespace InABox.DynamicGrid
             options.SelectColumns = true;
         }
 
-        protected override void SelectItems(CoreRow[]? rows)
+        public override void SelectItems(CoreRow[]? rows)
         {
             base.SelectItems(rows);
 

+ 27 - 29
inabox.wpf/DigitalForms/DigitalFormReportGrid.cs

@@ -18,7 +18,7 @@ using netDxf.Objects;
 
 namespace InABox.DynamicGrid
 {
-    public class DigitalFormReportGrid : DynamicItemsListGrid<ReportTemplate>, IDynamicEditorPage
+    public class DigitalFormReportGrid : DynamicGrid<ReportTemplate>, IDynamicEditorPage
     {
         public DynamicEditorGrid EditorGrid { get; set; }
 
@@ -28,8 +28,6 @@ namespace InABox.DynamicGrid
 
         private DigitalForm Form { get; set; }
 
-        private List<ReportTemplate> OriginalItems { get; set; } = new();
-
         private bool _readOnly;
         public bool ReadOnly
         {
@@ -44,21 +42,22 @@ namespace InABox.DynamicGrid
             }
         }
 
+        protected new DynamicGridMemoryEntityDataComponent<ReportTemplate> DataComponent;
+
         protected override void Init()
         {
-            base.Init();
-
             if (Security.CanEdit<ReportTemplate>())
             {
                 ActionColumns.Add(new DynamicImageColumn(ScriptImage, ScriptClick));
                 ActionColumns.Add(new DynamicImageColumn(Wpf.Resources.pencil.AsBitmapImage(), DesignClick));
             }
+
+            DataComponent = new DynamicGridMemoryEntityDataComponent<ReportTemplate>(this);
+            base.DataComponent = DataComponent;
         }
 
         protected override void DoReconfigure(DynamicGridOptions options)
         {
-            base.DoReconfigure(options);
-
             options.ShowHelp = true;
 
             if (Security.CanEdit<ReportTemplate>() && !ReadOnly)
@@ -74,23 +73,28 @@ namespace InABox.DynamicGrid
         {
             Form = (DigitalForm)item;
 
-            CoreTable data;
-            if (PageDataHandler != null)
-                data = PageDataHandler?.Invoke(typeof(ReportTemplate));
-            else if (Form.ID == Guid.Empty)
+            Refresh(true, false);
+
+            var data = PageDataHandler?.Invoke(typeof(ReportTemplate));
+
+            if(data is null && Form.ID == Guid.Empty)
             {
                 data = new CoreTable();
                 data.LoadColumns(typeof(ReportTemplate));
             }
+            if(data is not null)
+            {
+                DataComponent.LoadData(data);
+            }
             else
             {
-                data = new Client<ReportTemplate>()
-                    .Query(new Filter<ReportTemplate>(x => x.Section).IsEqualTo(Form.ID.ToString()));
+                DataComponent.LoadData(
+                    new Filter<ReportTemplate>(x => x.Section).IsEqualTo(Form.ID.ToString()),
+                    Columns.All<ReportTemplate>(),
+                    null);
             }
-            OriginalItems = data.Rows.Select(x => x.ToObject<ReportTemplate>()).ToList();
 
-            Items = OriginalItems.ToList();
-            Refresh(true, true);
+            Refresh(false, true);
 
             Ready = true;
         }
@@ -124,7 +128,7 @@ namespace InABox.DynamicGrid
                     return false;
                 }
 
-                var template = LoadItem(arg);
+                var template = DataComponent.LoadItem(arg);
                 var script = template.Script;
                 if (string.IsNullOrWhiteSpace(script))
                     script = string.Format(ReportTemplate.DefaultScriptTemplate, model.GetType().Name.Split('.').Last());
@@ -133,7 +137,7 @@ namespace InABox.DynamicGrid
                 {
                     template.Script = editor.Script;
 
-                    SaveItem(template);
+                    DataComponent.SaveItem(template);
                     return true;
                 }
             }
@@ -150,8 +154,8 @@ namespace InABox.DynamicGrid
                 return false;
             }
 
-            var template = LoadItem(arg);
-            ReportUtils.DesignReport(template, model, true, saveTemplate: (template) => SaveItem(template));
+            var template = DataComponent.LoadItem(arg);
+            ReportUtils.DesignReport(template, model, true, saveTemplate: (template) => DataComponent.SaveItem(template));
             return false;
         }
 
@@ -186,7 +190,7 @@ namespace InABox.DynamicGrid
 
             if (EditItems(new[] { item }))
             {
-                SaveItem(item);
+                DataComponent.SaveItem(item);
                 Refresh(false, true);
             }
         }
@@ -197,17 +201,11 @@ namespace InABox.DynamicGrid
 
         public void AfterSave(object 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)
+            foreach (var template in DataComponent.Items)
             {
                 template.Section = Form.ID.ToString();
             }
-
-            new Client<ReportTemplate>().Save(Items.Where(x => x.IsChanged()), "Updated by User");
+            DataComponent.SaveItems();
         }
 
         public override ReportTemplate CreateItem()

+ 2 - 0
inabox.wpf/DigitalForms/DynamicFormLayoutGrid.cs

@@ -23,6 +23,8 @@ public abstract class DynamicFormLayoutGrid : DynamicOneToManyGrid<DigitalForm,
 {
     private readonly BitmapImage design = Wpf.Resources.design.AsBitmapImage();
 
+    public IEnumerable<DigitalFormLayout> Items => DataComponent.Items;
+
     protected override void Init()
     {
         base.Init();

+ 12 - 10
inabox.wpf/DigitalForms/DynamicVariableGrid.cs

@@ -20,6 +20,8 @@ namespace InABox.DynamicGrid
         private Button ShowHiddenButton;
         private Button HideButton;
 
+        public IEnumerable<DigitalFormVariable> Items => DataComponent.Items;
+
         protected override void Init()
         {
             base.Init();
@@ -46,7 +48,7 @@ namespace InABox.DynamicGrid
 
         public DigitalFormVariable? GetVariable(string code)
         {
-            return Items.Where(x => x.Code == code).FirstOrDefault();
+            return DataComponent.Items.Where(x => x.Code == code).FirstOrDefault();
         }
 
         private static bool ShouldHide(CoreRow[] rows)
@@ -63,20 +65,20 @@ namespace InABox.DynamicGrid
             }
 
             var hide = ShouldHide(rows);
-            var items = LoadItems(rows);
+            var items = DataComponent.LoadItems(rows);
             foreach (var item in items)
             {
                 item.Hidden = hide;
             }
             if(items.Length > 0)
             {
-                SaveItems(items);
+                DataComponent.SaveItems(items);
                 return true;
             }
             return false;
         }
 
-        protected override void SelectItems(CoreRow[]? rows)
+        public override void SelectItems(CoreRow[]? rows)
         {
             base.SelectItems(rows);
 
@@ -102,9 +104,9 @@ namespace InABox.DynamicGrid
         {
             parent.AddItem(header, null, type, (itemtype) =>
             {
-                if(DynamicVariableUtils.CreateAndEdit(Item, Items, itemtype, out var variable))
+                if(DynamicVariableUtils.CreateAndEdit(Item, DataComponent.Items, itemtype, out var variable))
                 {
-                    SaveItem(variable);
+                    DataComponent.SaveItem(variable);
                     Refresh(false, true);
                 }
             });
@@ -136,12 +138,12 @@ namespace InABox.DynamicGrid
         {
             if (!SelectedRows.Any())
                 return;
-            var variable = LoadItem(SelectedRows.First());
+            var variable = DataComponent.LoadItem(SelectedRows.First());
             var properties = variable.CreateProperties();
-            if (DynamicVariableUtils.EditProperties(Item, Items, properties.GetType(), properties))
+            if (DynamicVariableUtils.EditProperties(Item, DataComponent.Items, properties.GetType(), properties))
             {
                 variable.SaveProperties(properties);
-                SaveItem(variable);
+                DataComponent.SaveItem(variable);
                 Refresh(false, true);
             }
         }
@@ -154,7 +156,7 @@ namespace InABox.DynamicGrid
                 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);
+                        DataComponent.DeleteItems(rows);
                         SelectedRows = Array.Empty<CoreRow>();
                         DoChanged();
                         Refresh(false, true);

+ 54 - 0
inabox.wpf/DynamicGrid/DataComponent/DynamicGridClientDataComponent.cs

@@ -228,4 +228,58 @@ public class DynamicGridClientDataComponent<T> : IDynamicGridDataComponent<T>
             Client.Query(criteria.Combine(), columns, sort, (data, e) => action(data is not null ? new(data) : new(e!)));
         }
     }
+
+    public IEnumerable<Tuple<Type?, CoreTable>> LoadExportTables(Filters<T> filter, IEnumerable<Tuple<Type, IColumns>> tableColumns)
+    {
+        var queries = new Dictionary<string, IQueryDef>();
+        var columns = tableColumns.ToList();
+        foreach (var table in columns)
+        {
+            var tableType = table.Item1;
+
+            PropertyInfo? property = null;
+
+            var m2m = CoreUtils.GetManyToMany(tableType, typeof(T));
+
+            IFilter? queryFilter = null;
+            if (m2m != null)
+            {
+                property = CoreUtils.GetManyToManyThisProperty(tableType, typeof(T));
+            }
+            else
+            {
+                var o2m = CoreUtils.GetOneToMany(tableType, typeof(T));
+                if (o2m != null)
+                {
+                    property = CoreUtils.GetOneToManyProperty(tableType, typeof(T));
+                }
+            }
+
+            if (property != null)
+            {
+                var subQuery = new SubQuery<T>();
+                subQuery.Filter = filter.Combine();
+                subQuery.Column = new Column<T>(x => x.ID);
+
+                queryFilter = (Activator.CreateInstance(typeof(Filter<>).MakeGenericType(tableType)) as IFilter)!;
+                queryFilter.Expression = CoreUtils.GetMemberExpression(tableType, property.Name + ".ID");
+                queryFilter.InQuery(subQuery);
+
+                queries[tableType.Name] = new QueryDef(tableType)
+                {
+                    Filter = queryFilter,
+                    Columns = table.Item2
+                };
+            }
+        }
+
+        var results = Client.QueryMultiple(queries);
+
+        return columns.Select(x => new Tuple<Type?, CoreTable>(x.Item1, results[x.Item1.Name]));
+    }
+
+    public CoreTable LoadImportKeys(string[] fields)
+    {
+        return Client.Query(null, Columns.None<T>().Add(fields));
+    }
 }

+ 83 - 0
inabox.wpf/DynamicGrid/DataComponent/DynamicGridItemsListDataComponent.cs

@@ -0,0 +1,83 @@
+using InABox.Clients;
+using InABox.Core;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace InABox.DynamicGrid;
+
+public class DynamicGridItemsListDataComponent<T> : BaseDynamicGridDataComponent<T>
+    where T : BaseObject, new()
+{
+    private List<T> _items = [];
+
+    public List<T> Items
+    { 
+        get => _items; 
+        set => _items = value; 
+    }
+
+    public DynamicGridItemsListDataComponent(DynamicGrid<T> grid): base(grid)
+    {
+    }
+
+    public override void DeleteItems(params CoreRow[] rows)
+    {
+        foreach (var row in rows.OrderByDescending(x => x.Index))
+        {
+            Items.RemoveAt(Grid.GetMasterRow(row).Index);
+        }
+    }
+
+    public override T LoadItem(CoreRow row)
+    {
+        return Items[Grid.GetMasterRow(row).Index];
+    }
+
+    private IOrderedEnumerable<T> OrderItems(IOrderedEnumerable<T> items, SortOrder<T> sort)
+    {
+        var ordered = items.ThenBy(x => CoreUtils.GetPropertyValue(x, sort.ToString()));
+        foreach (var then in sort.Thens)
+        {
+            ordered = OrderItems(ordered, then);
+        }
+        return ordered;
+    }
+    private IOrderedEnumerable<T> OrderItems(IEnumerable<T> items, SortOrder<T> sort)
+    {
+        var ordered = items.OrderBy(x => CoreUtils.GetPropertyValue(x, sort.ToString()));
+        foreach (var then in sort.Thens)
+        {
+            ordered = OrderItems(ordered, then);
+        }
+        return ordered;
+    }
+
+    public override void Reload(
+        Filters<T> criteria, Columns<T> columns, SortOrder<T>? sort, 
+        CancellationToken token, Action<QueryResult> action)
+    {
+        var result = new CoreTable();
+        result.LoadColumns(columns);
+        if(sort is null)
+        {
+            result.LoadRows(Items);
+        }
+        else
+        {
+            result.LoadRows(OrderItems(Items, sort));
+        }
+        action.Invoke(new(result));
+    }
+
+    public override void SaveItem(T item)
+    {
+        if (!Items.Contains(item))
+        {
+            Items.Add(item);
+        }
+    }
+}

+ 139 - 29
inabox.wpf/DynamicGrid/IDynamicMemoryEntityGrid.cs → inabox.wpf/DynamicGrid/DataComponent/DynamicGridMemoryEntityDataComponent.cs

@@ -5,22 +5,21 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Reflection;
 using System.Text;
+using System.Threading;
 using System.Threading.Tasks;
 
 namespace InABox.DynamicGrid;
 
-/// <summary>
-/// Defines a common interface for dealing with grids like <see cref="DynamicOneToManyGrid{TOne, TMany}"/>
-/// or <see cref="DynamicManyToManyGrid{TManyToMany, TThis}"/>, which display <see cref="Entity"/>s, but do not load them necessarily from the database,
-/// instead keeping them in memory.
-/// <br/>
-/// This interface then allows other functions, like
-/// <see cref="DynamicMemoryEntityGridExtensions.EnsureColumns{T}(InABox.DynamicGrid.IDynamicMemoryEntityGrid{T}, Columns{T})"/>, to work based on <see cref="IDynamicMemoryEntityGrid{T}.LoadedColumns"/> and manage our column handling better.
-/// </summary>
-/// <typeparam name="T"></typeparam>
-public interface IDynamicMemoryEntityGrid<T>
+public class DynamicGridMemoryEntityDataComponent<T> : BaseDynamicGridDataComponent<T>
     where T : Entity, IRemotable, IPersistent, new()
 {
+    /// <summary>
+    /// Keeps a cache of initially loaded objects, so that we can figure out which guys to delete when we save.
+    /// </summary>
+    private T[] MasterList = Array.Empty<T>();
+
+    public List<T> Items { get; private set; }
+
     /// <summary>
     /// A set of columns representing which columns have been loaded from the database.
     /// </summary>
@@ -30,18 +29,131 @@ public interface IDynamicMemoryEntityGrid<T>
     /// It is <see langword="null"/> if no data has been loaded from the database (that is, the data was gotten from
     /// a page data handler instead.)
     /// </remarks>
-    public HashSet<string>? LoadedColumns { get; }
+    public HashSet<string>? LoadedColumns { get; private set; }
 
-    public IEnumerable<T> Items { get; }
-}
+    private static bool IsAutoEntity => typeof(T).HasAttribute<AutoEntity>();
 
-public static class DynamicMemoryEntityGridExtensions
-{
-    public static void EnsureColumns<T>(this IDynamicMemoryEntityGrid<T> grid, Columns<T> columns)
-        where T : Entity, IRemotable, IPersistent, new()
+    public DynamicGridMemoryEntityDataComponent(DynamicGrid<T> grid): base(grid)
+    {
+        Items = new List<T>();
+    }
+
+    #region MemoryEntityGrid
+
+    public void LoadData(T[] items)
+    {
+        MasterList = items;
+        Items = MasterList.ToList();
+
+        LoadedColumns = null;
+    }
+
+    public void LoadData(CoreTable data)
+    {
+        MasterList = data.ToArray<T>();
+        Items = MasterList.ToList();
+
+        LoadedColumns = null;
+    }
+
+    public void LoadData(Filter<T>? filter, Columns<T> columns, SortOrder<T>? sort)
+    {
+        var data = Client.Query(filter, columns, sort);
+        MasterList = data.ToArray<T>();
+        Items = MasterList.ToList();
+
+        LoadedColumns = columns.ColumnNames().ToHashSet();
+    }
+
+    public void SaveItems()
+    {
+        if (IsAutoEntity)
+        {
+            return;
+        }
+        // First remove any deleted files
+        foreach (var map in MasterList)
+            if (!Items.Contains(map))
+                Client.Delete(map, typeof(T).Name + " Deleted by User");
+
+        Client.Save(Items.Where(x => x.IsChanged()), "Updated by User");
+    }
+
+    #endregion
+
+    public override CoreTable LoadImportKeys(string[] fields)
+    {
+        var result = new CoreTable();
+        result.LoadColumns(Columns.None<T>().Add(fields));
+        result.LoadRows(MasterList);
+        return result;
+    }
+
+    public override T LoadItem(CoreRow row)
+    {
+        return Items[Grid.GetMasterRow(row).Index];
+    }
+
+    public override void SaveItem(T item)
+    {
+        if (!Items.Contains(item))
+            Items.Add(item);
+
+        if (item is ISequenceable) Items = Items.AsQueryable().OrderBy(x => (x as ISequenceable)!.Sequence).ToList();
+    }
+    public override void SaveItems(T[] items)
+    {
+        foreach(var item in items)
+        {
+            if (!Items.Contains(item))
+                Items.Add(item);
+        }
+
+        if (typeof(T).HasInterface<ISequenceable>())
+            Items = Items.AsQueryable().OrderBy(x => (x as ISequenceable)!.Sequence).ToList();
+    }
+
+    public override void DeleteItems(params CoreRow[] rows)
+    {
+        var items = rows.Select(LoadItem).ToList();
+        foreach (var item in items)
+        {
+            Items.Remove(item);
+        }
+    }
+
+
+    public override void Reload(
+        Filters<T> criteria, Columns<T> columns, SortOrder<T>? sort,
+        CancellationToken token, Action<QueryResult> action)
+    {
+        var results = new CoreTable();
+        results.LoadColumns(typeof(T));
+
+        EnsureColumns(columns);
+
+        if (sort != null)
+        {
+            var exp = IQueryableExtensions.ToLambda<T>(sort.Expression);
+            var sorted = sort.Direction == SortDirection.Ascending
+                ? Items.AsQueryable().OrderBy(exp)
+                : Items.AsQueryable().OrderByDescending(exp);
+            foreach (var then in sort.Thens)
+            {
+                var thexp = IQueryableExtensions.ToLambda<T>(then.Expression);
+                sorted = sort.Direction == SortDirection.Ascending ? sorted.ThenBy(exp) : sorted.ThenByDescending(exp);
+            }
+            Items = sorted.ToList();
+        }
+        results.LoadRows(Items);
+
+        action.Invoke(new(results));
+    }
+
+    public void EnsureColumns(Columns<T> columns)
     {
-        RequireColumns(grid, columns);
-        LoadForeignProperties(grid, columns);
+        RequireColumns(columns);
+        LoadForeignProperties(columns);
     }
 
     /// <summary>
@@ -50,8 +162,7 @@ public static class DynamicMemoryEntityGridExtensions
     /// linked table with the required columns.
     /// </summary>
     /// <param name="columns"></param>
-    public static void LoadForeignProperties<T>(this IDynamicMemoryEntityGrid<T> grid, Columns<T> columns)
-        where T : Entity, IRemotable, IPersistent, new()
+    public void LoadForeignProperties(Columns<T> columns)
     {
         // Lists of properties that we need, arranged by the entity link property which is their parent.
         // LinkIDProperty : (Type, Properties: [(columnName, property)], Objects)
@@ -82,7 +193,7 @@ public static class DynamicMemoryEntityGridExtensions
                 }
 
                 var any = false;
-                foreach (var item in grid.Items)
+                foreach (var item in Items)
                 {
                     if (!item.LoadedColumns.Contains(column.Property))
                     {
@@ -141,22 +252,21 @@ public static class DynamicMemoryEntityGridExtensions
         }
     }
 
-    public static void RequireColumns<T>(this IDynamicMemoryEntityGrid<T> grid, Columns<T> columns)
-        where T : Entity, IRemotable, IPersistent, new()
+    public void RequireColumns(Columns<T> columns)
     {
-        if (grid.LoadedColumns is null) return;
+        if (LoadedColumns is null) return;
 
         // Figure out which columns we still need.
-        var newColumns = columns.Where(x => !grid.LoadedColumns.Contains(x.Property)).ToColumns(ColumnTypeFlags.None);
+        var newColumns = columns.Where(x => !LoadedColumns.Contains(x.Property)).ToColumns(ColumnTypeFlags.None);
         if (newColumns.Count > 0 && typeof(T).GetCustomAttribute<AutoEntity>() is null)
         {
             var data = Client.Query(
-                new Filter<T>(x => x.ID).InList(grid.Items.Select(x => x.ID).Where(x => x != Guid.Empty).ToArray()),
+                new Filter<T>(x => x.ID).InList(Items.Select(x => x.ID).Where(x => x != Guid.Empty).ToArray()),
                 // We also need to add ID, so we know which item to fill.
                 newColumns.Add(x => x.ID));
             foreach (var row in data.Rows)
             {
-                var item = grid.Items.FirstOrDefault(x => x.ID == row.Get<T, Guid>(y => y.ID));
+                var item = Items.FirstOrDefault(x => x.ID == row.Get<T, Guid>(y => y.ID));
                 if (item is not null)
                 {
                     row.FillObject(item, overrideExisting: false);
@@ -165,7 +275,7 @@ public static class DynamicMemoryEntityGridExtensions
             // Remember that we have now loaded this data.
             foreach (var column in newColumns)
             {
-                grid.LoadedColumns.Add(column.Property);
+                LoadedColumns.Add(column.Property);
             }
         }
     }

+ 58 - 7
inabox.wpf/DynamicGrid/DataComponent/IDynamicGridDataComponent.cs

@@ -12,13 +12,7 @@ namespace InABox.DynamicGrid;
 public interface IDynamicGridDataComponent<T>
     where T : BaseObject, new()
 {
-    DynamicGrid<T> Grid { get; set; }
-
-    /// <summary>
-    /// Do any required updates when the options list is changed.
-    /// </summary>
-    /// <returns>Whether the columns need to be reloaded.</returns>
-    bool OptionsChanged();
+    DynamicGrid<T> Grid { get; }
 
     void Reload(Filters<T> criteria, Columns<T> columns, SortOrder<T>? sort, CancellationToken token, Action<QueryResult> action);
 
@@ -31,4 +25,61 @@ public interface IDynamicGridDataComponent<T>
     void SaveItems(T[] items);
 
     void DeleteItems(CoreRow[] rows);
+
+    /// <summary>
+    /// Loads the child tables for an export, based on the filter of the parent table.
+    /// </summary>
+    /// <param name="filter">Filter for the parent table.</param>
+    /// <param name="tableColumns">A list of the child table types, with columns to load for each</param>
+    /// <returns>A list of tables, in the same order as they came in <paramref name="tableColumns"/></returns>
+    IEnumerable<Tuple<Type?, CoreTable>> LoadExportTables(Filters<T> filter, IEnumerable<Tuple<Type, IColumns>> tableColumns);
+
+    CoreTable LoadImportKeys(string[] fields);
+}
+
+public abstract class BaseDynamicGridDataComponent<T> : IDynamicGridDataComponent<T>
+    where T : BaseObject, new()
+{
+    public DynamicGrid<T> Grid { get; private set; }
+
+    public BaseDynamicGridDataComponent(DynamicGrid<T> grid)
+    {
+        Grid = grid;
+    }
+
+    public abstract void DeleteItems(CoreRow[] rows);
+
+    public abstract T LoadItem(CoreRow row);
+
+    public virtual T[] LoadItems(CoreRow[] rows) => rows.ToArray(LoadItem);
+
+    public abstract void Reload(Filters<T> criteria, Columns<T> columns, SortOrder<T>? sort, CancellationToken token, Action<QueryResult> action);
+
+    public abstract void SaveItem(T item);
+
+    public virtual void SaveItems(T[] items)
+    {
+        foreach(var item in items)
+        {
+            SaveItem(item);
+        }
+    }
+
+    public virtual IEnumerable<Tuple<Type?, CoreTable>> LoadExportTables(Filters<T> filter, IEnumerable<Tuple<Type, IColumns>> tableColumns)
+    {
+        return tableColumns.Select(x =>
+        {
+            var table = new CoreTable();
+            table.LoadColumns(x.Item2);
+            return new Tuple<Type?, CoreTable>(x.Item1, table);
+        });
+    }
+
+    public virtual CoreTable LoadImportKeys(String[] fields)
+    {
+        var result = new CoreTable();
+        result.LoadColumns(Columns.None<T>().Add(fields));
+        return result;
+    }
+
 }

+ 6 - 283
inabox.wpf/DynamicGrid/DynamicDataGrid.cs

@@ -24,8 +24,6 @@ public interface IDynamicDataGrid : IDynamicGrid
     /// the name of <typeparamref name="TEntity"/> is used as a default.
     /// </summary>
     string? ColumnsTag { get; set; }
-
-    IColumns LoadEditorColumns();
 }
 
 public class DynamicDataGrid<TEntity> : DynamicGrid<TEntity>, IDynamicDataGrid where TEntity : Entity, IRemotable, IPersistent, new()
@@ -81,10 +79,10 @@ public class DynamicDataGrid<TEntity> : DynamicGrid<TEntity>, IDynamicDataGrid w
         MergeBtn = AddButton("Merge", Wpf.Resources.merge.AsBitmapImage(Color.White), DoMerge);
     }
 
-    protected override void SelectItems(CoreRow[]? rows)
+    public override void SelectItems(CoreRow[]? rows)
     {
         base.SelectItems(rows);
-        MergeBtn.Visibility = Options.MultiSelect && typeof(T).IsAssignableTo(typeof(IMergeable)) && Security.CanMerge<TEntity>()
+        MergeBtn.Visibility = Options.MultiSelect && typeof(TEntity).IsAssignableTo(typeof(IMergeable)) && Security.CanMerge<TEntity>()
             && rows != null && rows.Length > 1
             ? Visibility.Visible
             : Visibility.Collapsed;
@@ -139,20 +137,6 @@ public class DynamicDataGrid<TEntity> : DynamicGrid<TEntity>, IDynamicDataGrid w
         }
     }
 
-    public override void LoadEditorButtons(TEntity item, DynamicEditorButtons buttons)
-    {
-        base.LoadEditorButtons(item, buttons);
-        if (ClientFactory.IsSupported<AuditTrail>())
-            buttons.Add("Audit Trail", Wpf.Resources.view.AsBitmapImage(), item, AuditTrailClick);
-    }
-
-    private void AuditTrailClick(object sender, object? item)
-    {
-        var entity = (item as TEntity)!;
-        var window = new AuditWindow(entity.ID);
-        window.ShowDialog();
-    }
-
     protected override DynamicGridColumns LoadColumns()
     {
         return ColumnsComponent.LoadColumns();
@@ -199,121 +183,6 @@ public class DynamicDataGrid<TEntity> : DynamicGrid<TEntity>, IDynamicDataGrid w
 
     #region Duplicate
 
-    protected bool Duplicate(
-        CoreRow row,
-        Expression<Func<TEntity, object?>> codefield,
-        Type[] childtypes)
-    {
-        var id = row.Get<TEntity, Guid>(x => x.ID);
-
-        var code = row.Get(codefield) as string;
-
-        var tasks = new List<Task>();
-
-        var itemtask = Task.Run(() =>
-        {
-            var filter = new Filter<TEntity>(x => x.ID).IsEqualTo(id);
-            var result = new Client<TEntity>().Load(filter).FirstOrDefault()
-                ?? throw new Exception("Entity does not exist!");
-            return result;
-        });
-        tasks.Add(itemtask);
-        //itemtask.Wait();
-
-        Task<List<string?>>? codetask = null;
-        if (!string.IsNullOrWhiteSpace(code))
-        {
-            codetask = Task.Run(() =>
-            {
-                var columns = Columns.None<TEntity>().Add(codefield);
-                //columns.Add<String>(codefield);
-                var filter = new Filter<TEntity>(codefield).BeginsWith(code);
-                var table = new Client<TEntity>().Query(filter, columns);
-                var result = table.Rows.Select(x => x.Get(codefield) as string).ToList();
-                return result;
-            });
-            tasks.Add(codetask);
-        }
-        //codetask.Wait();
-
-        var children = new Dictionary<Type, CoreTable>();
-        foreach (var childtype in childtypes)
-        {
-            var childtask = Task.Run(() =>
-            {
-                var prop = childtype.GetProperties().FirstOrDefault(x =>
-                    x.PropertyType.GetInterfaces().Contains(typeof(IEntityLink)) &&
-                    x.PropertyType.GetInheritedGenericTypeArguments().FirstOrDefault() == typeof(TEntity));
-
-                if (prop is not null)
-                {
-                    var filter = Core.Filter.Create(childtype);
-                    filter.Expression = CoreUtils.GetMemberExpression(childtype, prop.Name + ".ID");
-                    filter.Operator = Operator.IsEqualTo;
-                    filter.Value = id;
-                    var sort = LookupFactory.DefineSort(childtype);
-                    var client = ClientFactory.CreateClient(childtype);
-                    var table = client.Query(filter, null, sort);
-                    foreach (var r in table.Rows)
-                    {
-                        r["ID"] = Guid.Empty;
-                        r[prop.Name + ".ID"] = Guid.Empty;
-                    }
-
-                    children[childtype] = table;
-                }
-                else
-                {
-                    Logger.Send(LogType.Error, "", $"DynamicDataGrid<{typeof(TEntity)}>.Duplicate(): No parent property found for child type {childtype}");
-                }
-            });
-            tasks.Add(childtask);
-            //childtask.Wait();
-        }
-
-        //var manytomanys = CoreUtils.TypeList(
-        //    AppDomain.CurrentDomain.GetAssemblies(),
-        //    x => x.GetInterfaces().Any(intf => intf.IsGenericType && intf.GetGenericTypeDefinition() == typeof(IManyToMany<,>) && intf.GenericTypeArguments.Contains(typeof(T)))
-        //);
-
-        //Task<CoreTable> childtask = Task.Run(() =>
-        //{
-        //    var result = new CoreTable();
-        //    result.LoadColumns(typeof(TChild));
-        //    var children = new Client<TChild>().Load(new Filter<TChild>(x => linkfield).IsEqualTo(id));
-        //    foreach (var child in children)
-        //    {
-        //        child.ID = Guid.Empty;
-        //        String linkprop = CoreUtils.GetFullPropertyName<TChild, Guid>(linkfield, ".");
-        //        CoreUtils.SetPropertyValue(child, linkprop, Guid.Empty);
-        //        var newrow = result.NewRow();
-        //        result.LoadRow(newrow, child);
-        //        result.Rows.Add(newrow);
-        //    }
-        //    return result;
-        //});
-        //tasks.Add(childtask);
-
-        Task.WaitAll(tasks.ToArray());
-
-        var item = itemtask.Result;
-        item.ID = Guid.Empty;
-
-        if (codetask != null)
-        {
-            var codes = codetask.Result;
-            var i = 1;
-
-            while (codes.Contains(string.Format("{0} ({1})", code, i)))
-                i++;
-            var codeprop = CoreUtils.GetFullPropertyName(codefield, ".");
-            CoreUtils.SetPropertyValue(item, codeprop, string.Format("{0} ({1})", code, i));
-        }
-
-        var grid = new DynamicDataGrid<TEntity>();
-        return grid.EditItems(new[] { item }, t => children.ContainsKey(t) ? children[t] : null, true);
-    }
-
     protected override IEnumerable<TEntity> LoadDuplicatorItems(CoreRow[] rows)
     {
         return rows.Select(x => x.ToObject<TEntity>());
@@ -321,162 +190,16 @@ public class DynamicDataGrid<TEntity> : DynamicGrid<TEntity>, IDynamicDataGrid w
 
     #endregion
 
-    protected override bool BeforePaste(IEnumerable<TEntity> items, ClipAction action)
-    {
-        if (action == ClipAction.Copy)
-        {
-            foreach (var item in items)
-                item.ID = Guid.Empty;
-            return true;
-        }
-
-        return base.BeforePaste(items, action);
-    }
-
-    protected override IEnumerable<Tuple<Type?, CoreTable>> LoadExportTables(Filters<TEntity> filter, IEnumerable<Tuple<Type, IColumns>> tableColumns)
-    {
-        var queries = new Dictionary<string, IQueryDef>();
-        var columns = tableColumns.ToList();
-        foreach (var table in columns)
-        {
-            var tableType = table.Item1;
-
-            PropertyInfo? property = null;
-
-            var m2m = CoreUtils.GetManyToMany(tableType, typeof(TEntity));
-
-            IFilter? queryFilter = null;
-            if (m2m != null)
-            {
-                property = CoreUtils.GetManyToManyThisProperty(tableType, typeof(TEntity));
-            }
-            else
-            {
-                var o2m = CoreUtils.GetOneToMany(tableType, typeof(TEntity));
-                if (o2m != null)
-                {
-                    property = CoreUtils.GetOneToManyProperty(tableType, typeof(TEntity));
-                }
-            }
-
-            if (property != null)
-            {
-                var subQuery = new SubQuery<TEntity>();
-                subQuery.Filter = filter.Combine();
-                subQuery.Column = new Column<TEntity>(x => x.ID);
-
-                queryFilter = (Activator.CreateInstance(typeof(Filter<>).MakeGenericType(tableType)) as IFilter)!;
-                queryFilter.Expression = CoreUtils.GetMemberExpression(tableType, property.Name + ".ID");
-                queryFilter.InQuery(subQuery);
-
-                queries[tableType.Name] = new QueryDef(tableType)
-                {
-                    Filter = queryFilter,
-                    Columns = table.Item2
-                };
-            }
-        }
-
-        var results = Client.QueryMultiple(queries);
-
-        return columns.Select(x => new Tuple<Type?, CoreTable>(x.Item1, results[x.Item1.Name]));
-    }
-
-    protected override CoreTable LoadImportKeys(String[] fields)
-    {
-        return Client.Query(null, Columns.None<TEntity>().Add(fields));
-    }
-    
-
     #region Merge
 
     private bool DoMerge(Button arg1, CoreRow[] arg2)
     {
         if (arg2 == null || arg2.Length <= 1)
             return false;
-        var targetid = arg2.Last().Get<T, Guid>(x => x.ID);
-        var target = arg2.Last().ToObject<T>().ToString();
-        var otherids = arg2.Select(r => r.Get<T, Guid>(x => x.ID)).Where(x => x != targetid).ToArray();
-        string[] others = arg2.Where(r => otherids.Contains(r.Get<Guid>("ID"))).Select(x => x.ToObject<T>().ToString()!).ToArray();
-        var rows = arg2.Length;
-        if (MessageBox.Show(
-                string.Format(
-                    "This will merge the following items:\n\n- {0}\n\n into:\n\n- {1}\n\nAfter this, the items will be permanently removed.\nAre you sure you wish to do this?",
-                    string.Join("\n- ", others),
-                    target
-                ),
-                "Merge Items Warning",
-                MessageBoxButton.YesNo,
-                MessageBoxImage.Stop) != MessageBoxResult.Yes
-           )
-            return false;
-
-        using (new WaitCursor())
-        {
-            var types = CoreUtils.Entities.Where(
-                x =>
-                    x.IsClass
-                    && !x.IsGenericType
-                    && x.IsSubclassOf(typeof(Entity))
-                    && !x.Equals(typeof(AuditTrail))
-                    && !x.Equals(typeof(T))
-                    && x.GetCustomAttribute<AutoEntity>() == null
-                    && x.HasInterface<IRemotable>()
-                    && x.HasInterface<IPersistent>()
-            ).ToArray();
-
-            foreach (var type in types)
-            {
-                var props = CoreUtils.PropertyList(
-                    type,
-                    x =>
-                        x.PropertyType.GetInterfaces().Contains(typeof(IEntityLink))
-                        && x.PropertyType.GetInheritedGenericTypeArguments().Contains(typeof(T))
-                );
-                foreach (var prop in props)
-                {
-                    var propname = string.Format(prop.Name + ".ID");
-                    var filter = Core.Filter.Create(type);
-                    filter.Expression = CoreUtils.CreateMemberExpression(type, propname);
-                    filter.Operator = Operator.InList;
-                    filter.Value = otherids;
-                    var columns = Columns.None(type)
-                        .Add("ID")
-                        .Add(propname);
-                    var updates = ClientFactory.CreateClient(type).Query(filter, columns).Rows.Select(r => r.ToObject(type)).ToArray();
-                    if (updates.Any())
-                    {
-                        foreach (var update in updates)
-                            CoreUtils.SetPropertyValue(update, propname, targetid);
-                        ClientFactory.CreateClient(type).Save(updates,
-                            string.Format("Merged {0} Records", typeof(T).EntityName().Split('.').Last()));
-                    }
-                }
-            }
-
-            var histories = new Client<AuditTrail>()
-                .Query(
-                    new Filter<AuditTrail>(x => x.EntityID).InList(otherids),
-                    Columns.None<AuditTrail>()
-                        .Add(x => x.ID).Add(x => x.EntityID))
-                .ToArray<AuditTrail>();
-            foreach (var history in histories)
-                history.EntityID = targetid;
-            if (histories.Length != 0)
-                new Client<AuditTrail>().Save(histories, "");
-
-            var deletes = new List<object>();
-            foreach (var otherid in otherids)
-            {
-                var delete = new T();
-                CoreUtils.SetPropertyValue(delete, "ID", otherid);
-                deletes.Add(delete);
-            }
-
-            ClientFactory.CreateClient(typeof(T))
-                .Delete(deletes, string.Format("Merged {0} Records", typeof(T).EntityName().Split('.').Last()));
-            return true;
-        }
+        var target = arg2.Last().ToObject<TEntity>();
+        return DynamicGridUtils.MergeEntities(
+            target,
+            arg2.ToObjects<TEntity>().Where(x => x.ID != target.ID).ToArray());
     }
 
     #endregion

+ 67 - 47
inabox.wpf/DynamicGrid/DynamicGrid.cs

@@ -31,6 +31,7 @@ using String = System.String;
 using VerticalAlignment = System.Windows.VerticalAlignment;
 using VirtualizingCellsControl = Syncfusion.UI.Xaml.Grid.VirtualizingCellsControl;
 using System.Threading;
+using System.Reflection;
 
 namespace InABox.DynamicGrid;
 
@@ -46,7 +47,7 @@ public abstract class DynamicGrid : ContentControl
     }
 }
 
-public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParent<T>, IDynamicGrid<T>
+public class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParent<T>, IDynamicGrid<T>
     where T : BaseObject, new()
 {
 
@@ -57,7 +58,19 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
     }
 
     private IDynamicGridUIComponent<T> UIComponent { get; set; }
-    protected IDynamicGridDataComponent<T> DataComponent { get; set; }
+    private IDynamicGridDataComponent<T>? _dataComponent;
+    protected IDynamicGridDataComponent<T> DataComponent
+    {
+        get
+        {
+            _dataComponent ??= new DynamicGridItemsListDataComponent<T>(this);
+            return _dataComponent;
+        }
+        set
+        {
+            _dataComponent = value;
+        }
+    }
 
     private UIElement? _header;
     private readonly Button Add;
@@ -661,9 +674,13 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
     /// <summary>
     /// Initialise things like custom buttons; called once during construction.
     /// </summary>
-    protected abstract void Init();
+    protected virtual void Init()
+    {
+    }
 
-    protected abstract void DoReconfigure(DynamicGridOptions options);
+    protected virtual void DoReconfigure(DynamicGridOptions options)
+    {
+    }
 
     private bool _hasLoadedOptions = false;
 
@@ -720,7 +737,6 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
             DuplicateBtn.Visibility = Visibility.Collapsed;
 
         reloadColumns = reloadColumns || UIComponent.OptionsChanged();
-        reloadColumns = reloadColumns || DataComponent.OptionsChanged();
 
         if(reloadColumns && IsReady)
         {
@@ -976,12 +992,17 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
 
     #region Refresh / Reload
 
-    public Filter<T>? DefineFilter()
+    public virtual void DefineFilter(Filters<T> filter)
     {
-        if (OnDefineFilter is null)
-            return null;
-        var result = OnDefineFilter.Invoke(typeof(T)) as Filter<T>;
-        return result;
+        filter.Add(OnDefineFilter?.Invoke(typeof(T)) as Filter<T>);
+    }
+
+    public virtual SortOrder<T>? DefineSortOrder()
+    {
+        var sort = LookupFactory.DefineSort<T>();
+        if (sort == null && IsSequenced)
+            sort = new SortOrder<T>("Sequence");
+        return sort;
     }
 
     protected virtual bool FilterRecord(CoreRow row)
@@ -1043,15 +1064,11 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
             _lookupcache.Clear();
 
             var criteria = new Filters<T>();
-            var filter = DefineFilter();
-            if (filter != null)
-                criteria.Add(filter);
+            DefineFilter(criteria);
 
             var columns = DataColumns();
 
-            var sort = LookupFactory.DefineSort<T>();
-            if (sort == null && IsSequenced)
-                sort = new SortOrder<T>("Sequence");
+            var sort = DefineSortOrder();
 
             RefreshCancellationToken?.Cancel();
 
@@ -1143,7 +1160,6 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
 
     protected virtual void OnAfterRefresh()
     {
-        DataComponent.AfterRefresh();
     }
 
     protected void DoAfterRefresh()
@@ -1228,6 +1244,8 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
         Refresh(false, false);
     }
 
+    public CoreRow GetMasterRow(CoreRow row) => _recordmap[row];
+
     public void DeleteRow(CoreRow row)
     {
         if (MasterData is null) return;
@@ -1323,6 +1341,16 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
 
     #region Item Manipulation
 
+    public T LoadItem(CoreRow row) => DataComponent.LoadItem(row);
+
+    public T[] LoadItems(CoreRow[] rows) => DataComponent.LoadItems(rows);
+
+    public void SaveItem(T item) => DataComponent.SaveItem(item);
+
+    public void SaveItems(T[] items) => DataComponent.SaveItems(items);
+
+    public void DeleteItems(CoreRow[] rows) => DataComponent.DeleteItems(rows);
+
     #region Load/Save/Delete
 
     protected virtual bool CanDeleteItems(params CoreRow[] rows)
@@ -1445,9 +1473,20 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
             }
         );
 
+        if (typeof(T).IsSubclassOf(typeof(Entity)) && ClientFactory.IsSupported<AuditTrail>())
+            buttons.Add("Audit Trail", Wpf.Resources.view.AsBitmapImage(), item, AuditTrailClick);
+
         OnLoadEditorButtons?.Invoke(item, buttons);
     }
 
+    private void AuditTrailClick(object sender, object? item)
+    {
+        if (item is not Entity entity) return;
+
+        var window = new AuditWindow(entity.ID);
+        window.ShowDialog();
+    }
+
     protected virtual void BeforeLoad(IDynamicEditorForm form, T[] items)
     {
         form.BeforeLoad();
@@ -1894,7 +1933,6 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
                 newforms.Add(targetform);
             }
 
-            page.Items.Clear();
             page.LoadItems(newforms.ToArray());
         }
     }
@@ -1963,8 +2001,16 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
         }
     }
 
-    protected virtual bool BeforePaste(IEnumerable<T> items, ClipAction action)
+    protected bool BeforePaste(IEnumerable<T> items, ClipAction action)
     {
+        if (typeof(T).IsSubclassOf(typeof(Entity)))
+        {
+            if (action == ClipAction.Copy)
+            {
+                foreach (var item in items.Cast<Entity>())
+                    item.ID = Guid.Empty;
+            }
+        }
         return true;
     }
     private void Cut_Click(object sender, RoutedEventArgs e)
@@ -1991,13 +2037,6 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
 
     #region Import / Export
 
-    protected virtual CoreTable LoadImportKeys(String[] fields)
-    {
-        var result = new CoreTable();
-        result.LoadColumns(Columns.None<T>().Add(fields));
-        return result;
-    }
-
     protected virtual Guid GetImportID()
     {
         return Guid.Empty;
@@ -2024,7 +2063,7 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
         list.OnImportItem += o => { return CustomiseImportItem((T)o); };
         list.OnCustomiseImport += (o, args) => { args.FileName = CustomiseImportFileName(args.FileName); };
         list.OnSave += (sender, entity) => DataComponent.SaveItem(entity as T);
-        list.OnLoad += (sender, type, fields, id) => LoadImportKeys(fields);
+        list.OnLoad += (sender, type, fields, id) => DataComponent.LoadImportKeys(fields);
         list.ShowDialog();
         Refresh(false, true);
     }
@@ -2110,7 +2149,7 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
                     });
 
                     var list = new List<Tuple<Type?, CoreTable>>() { new(typeof(T), newData) };
-                    list.AddRange(LoadExportTables(filters, otherColumns));
+                    list.AddRange(DataComponent.LoadExportTables(filters, otherColumns));
                     DoExportTables(list);
                 },
                 err =>
@@ -2125,25 +2164,6 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
         DoExport();
     }
 
-    /// <summary>
-    /// Loads the child tables for an export, based on the filter of the parent table.
-    /// </summary>
-    /// <remarks>
-    /// If not overriden, defaults to creating empty tables with no records.
-    /// </remarks>
-    /// <param name="filter">Filter for the parent table.</param>
-    /// <param name="tableColumns">A list of the child table types, with columns to load for each</param>
-    /// <returns>A list of tables, in the same order as they came in <paramref name="tableColumns"/></returns>
-    protected virtual IEnumerable<Tuple<Type?, CoreTable>> LoadExportTables(Filters<T> filter, IEnumerable<Tuple<Type, IColumns>> tableColumns)
-    {
-        return tableColumns.Select(x =>
-        {
-            var table = new CoreTable();
-            table.LoadColumns(x.Item2);
-            return new Tuple<Type?, CoreTable>(x.Item1, table);
-        });
-    }
-
     private void DoExportTables(List<Tuple<Type?, CoreTable>> data)
     {
         var filename = CustomiseExportFileName(typeof(T).EntityName().Split('.').Last());

+ 197 - 2
inabox.wpf/DynamicGrid/DynamicGridUtils.cs

@@ -15,6 +15,8 @@ using Syncfusion.Data.Extensions;
 using System.Diagnostics.CodeAnalysis;
 using System.Data;
 using System.Windows.Media;
+using System.Linq.Expressions;
+using InABox.WPF;
 
 namespace InABox.DynamicGrid;
 
@@ -313,6 +315,33 @@ public static class DynamicGridUtils
 
     #region Columns
 
+    public static IColumns LoadEditorColumns(Type T, IEnumerable<IColumn> additional)
+    {
+        var result = Columns.Create(T, ColumnTypeFlags.EditorColumns);
+
+        foreach (var col in additional)
+            result.Add(col.Property);
+
+        var newColumns = new List<IColumn>();
+        foreach (var col in result)
+        {
+            var prop = DatabaseSchema.Property(T, col.Property);
+            if (prop?.Editor is DataLookupEditor dataLookup)
+            {
+                foreach (var lookupColumn in LookupFactory.DefineLookupFilterColumns(T, prop.Name))
+                {
+                    newColumns.Add(lookupColumn);
+                }
+            }
+        }
+        foreach(var col in newColumns)
+        {
+            result.Add(col);
+        }
+
+        return result;
+    }
+
     public static Columns<T> LoadEditorColumns<T>(Columns<T> additional)
     {
         var result = new Columns<T>(ColumnTypeFlags.EditorColumns);
@@ -508,7 +537,7 @@ public static class DynamicGridUtils
     public static bool EditObjects<T>(T[] items, Func<Type, CoreTable?>? pageDataHandler = null, bool preloadPages = false, Action<DynamicGrid<T>>? customiseGrid = null)
         where T : BaseObject, new()
     {
-        var grid = new DynamicItemsListGrid<T>();
+        var grid = new DynamicGrid<T>();
         customiseGrid?.Invoke(grid);
         return grid.EditItems(items, PageDataHandler: pageDataHandler, PreloadPages: preloadPages);
     }
@@ -524,7 +553,7 @@ public static class DynamicGridUtils
     public static bool EditObject<T>(T item, Func<Type, CoreTable?>? pageDataHandler = null, bool preloadPages = false, Action<DynamicGrid<T>>? customiseGrid = null)
         where T : BaseObject, new()
     {
-        var grid = new DynamicItemsListGrid<T>();
+        var grid = new DynamicGrid<T>();
         customiseGrid?.Invoke(grid);
         return grid.EditItems(new T[] { item }, PageDataHandler: pageDataHandler, PreloadPages: preloadPages);
     }
@@ -735,4 +764,170 @@ public static class DynamicGridUtils
             menu.Items.Add(manageForms);
         }, TaskScheduler.FromCurrentSynchronizationContext());
     }
+
+    public static bool DuplicateEntity<TEntity>(
+        CoreRow row,
+        Expression<Func<TEntity, object?>> codefield,
+        Type[] childtypes)
+        where TEntity : Entity, IRemotable, IPersistent, new()
+    {
+        var id = row.Get<TEntity, Guid>(x => x.ID);
+
+        var code = row.Get(codefield) as string;
+
+        var tasks = new List<Task>();
+
+        var itemtask = Task.Run(() =>
+        {
+            var filter = new Filter<TEntity>(x => x.ID).IsEqualTo(id);
+            var result = new Client<TEntity>().Load(filter).FirstOrDefault()
+                ?? throw new Exception("Entity does not exist!");
+            return result;
+        });
+        tasks.Add(itemtask);
+        //itemtask.Wait();
+
+        Task<List<string?>>? codetask = null;
+        if (!string.IsNullOrWhiteSpace(code))
+        {
+            codetask = Task.Run(() =>
+            {
+                var columns = Columns.None<TEntity>().Add(codefield);
+                //columns.Add<String>(codefield);
+                var filter = new Filter<TEntity>(codefield).BeginsWith(code);
+                var table = new Client<TEntity>().Query(filter, columns);
+                var result = table.Rows.Select(x => x.Get(codefield) as string).ToList();
+                return result;
+            });
+            tasks.Add(codetask);
+        }
+        //codetask.Wait();
+
+        var children = new Dictionary<Type, CoreTable>();
+        foreach (var childtype in childtypes)
+        {
+            var childtask = Task.Run(() =>
+            {
+                var prop = childtype.GetProperties().FirstOrDefault(x =>
+                    x.PropertyType.GetInterfaces().Contains(typeof(IEntityLink)) &&
+                    x.PropertyType.GetInheritedGenericTypeArguments().FirstOrDefault() == typeof(TEntity));
+
+                if (prop is not null)
+                {
+                    var filter = Core.Filter.Create(childtype);
+                    filter.Expression = CoreUtils.GetMemberExpression(childtype, prop.Name + ".ID");
+                    filter.Operator = Operator.IsEqualTo;
+                    filter.Value = id;
+                    var sort = LookupFactory.DefineSort(childtype);
+                    var client = ClientFactory.CreateClient(childtype);
+                    var table = client.Query(filter, null, sort);
+                    foreach (var r in table.Rows)
+                    {
+                        r["ID"] = Guid.Empty;
+                        r[prop.Name + ".ID"] = Guid.Empty;
+                    }
+
+                    children[childtype] = table;
+                }
+                else
+                {
+                    Logger.Send(LogType.Error, "", $"DynamicGridUtils.DuplicateEntity<{typeof(TEntity)}>(): No parent property found for child type {childtype}");
+                }
+            });
+            tasks.Add(childtask);
+            //childtask.Wait();
+        }
+
+        Task.WaitAll(tasks.ToArray());
+
+        var item = itemtask.Result;
+        item.ID = Guid.Empty;
+
+        if (codetask != null)
+        {
+            var codes = codetask.Result;
+            var i = 1;
+
+            while (codes.Contains(string.Format("{0} ({1})", code, i)))
+                i++;
+            var codeprop = CoreUtils.GetFullPropertyName(codefield, ".");
+            CoreUtils.SetPropertyValue(item, codeprop, string.Format("{0} ({1})", code, i));
+        }
+
+        var grid = new DynamicDataGrid<TEntity>();
+        return grid.EditItems(new[] { item }, t => children.ContainsKey(t) ? children[t] : null, true);
+    }
+
+    public static bool MergeEntities<T>(T target, T[]? entities)
+        where T : Entity, IRemotable, IPersistent, new()
+    {
+        if (entities == null || entities.Length == 0)
+            return false;
+        if (!MessageWindow.ShowYesNo(
+                string.Format(
+                    "This will merge the following items:\n\n- {0}\n\n into:\n\n- {1}\n\nAfter this, the items will be permanently removed.\nAre you sure you wish to do this?",
+                    string.Join("\n- ", entities.Select(x => x.ToString())),
+                    target
+                ),
+                "Merge Items Warning"))
+            return false;
+
+        using (new WaitCursor())
+        {
+            var types = CoreUtils.Entities.Where(
+                x =>
+                    x.IsClass
+                    && !x.IsGenericType
+                    && x.IsSubclassOf(typeof(Entity))
+                    && !x.Equals(typeof(AuditTrail))
+                    && !x.Equals(typeof(T))
+                    && x.GetCustomAttribute<AutoEntity>() == null
+                    && x.HasInterface<IRemotable>()
+                    && x.HasInterface<IPersistent>()
+            ).ToArray();
+
+            var otherIDs = entities.ToArray(x => x.ID);
+
+            foreach (var type in types)
+            {
+                var props = CoreUtils.PropertyList(
+                    type,
+                    x =>
+                        x.PropertyType.GetInterfaces().Contains(typeof(IEntityLink))
+                        && x.PropertyType.GetInheritedGenericTypeArguments().Contains(typeof(T))
+                );
+                foreach (var prop in props)
+                {
+                    var propname = string.Format(prop.Name + ".ID");
+
+                    var filter = Filter.Create(type, propname).InList(otherIDs);
+                    var columns = Columns.None(type)
+                        .Add("ID")
+                        .Add(propname);
+                    var updates = ClientFactory.CreateClient(type).Query(filter, columns).Rows.Select(r => r.ToObject(type)).ToArray();
+                    if (updates.Any())
+                    {
+                        foreach (var update in updates)
+                            CoreUtils.SetPropertyValue(update, propname, target.ID);
+                        ClientFactory.CreateClient(type).Save(updates,
+                            string.Format("Merged {0} Records", typeof(T).EntityName().Split('.').Last()));
+                    }
+                }
+            }
+
+            var histories = new Client<AuditTrail>()
+                .Query(
+                    new Filter<AuditTrail>(x => x.EntityID).InList(otherIDs),
+                    Columns.None<AuditTrail>()
+                        .Add(x => x.ID).Add(x => x.EntityID))
+                .ToArray<AuditTrail>();
+            foreach (var history in histories)
+                history.EntityID = target.ID;
+            if (histories.Length != 0)
+                new Client<AuditTrail>().Save(histories, "");
+
+            Client.Delete(entities, string.Format("Merged {0} Records", typeof(T).EntityName().Split('.').Last()));
+            return true;
+        }
+    }
 }

+ 0 - 84
inabox.wpf/DynamicGrid/DynamicItemsListGrid.cs

@@ -1,84 +0,0 @@
-using InABox.Core;
-using System;
-using System.Collections;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-using Syncfusion.Windows.Tools.Controls;
-using System.Threading;
-
-namespace InABox.DynamicGrid;
-
-
-public interface IDynamicItemsListGrid : IDynamicGrid
-{
-    /// <summary>
-    /// The items list that forms the source for the rows of this grid
-    /// </summary>
-    /// <remarks>
-    /// <b>Note:</b> This must be a list of type <see cref="List{T}"/>, otherwise the assignment to this property <u>will not</u> work.
-    /// </remarks>
-    IList Items { get; set; }
-}
-
-public class DynamicItemsListGrid<T> : DynamicGrid<T>, IDynamicItemsListGrid
-    where T : BaseObject, new()
-{
-    
-    private List<T> _items = [];
-
-    public List<T> Items
-    { 
-        get => _items; 
-        set => _items = value; 
-    }
-
-    IList IDynamicItemsListGrid.Items
-    {
-        get => _items; 
-        set => _items = value as List<T> ?? new List<T>();
-    }
-
-    protected override void Init()
-    {
-    }
-
-    protected override void DoReconfigure(DynamicGridOptions options)
-    {
-        
-    }
-
-    public override void DeleteItems(params CoreRow[] rows)
-    {
-        foreach (var row in rows.OrderByDescending(x => x.Index))
-        {
-            Items.RemoveAt(_recordmap[row].Index);
-        }
-    }
-
-    public override T LoadItem(CoreRow row)
-    {
-        return Items[_recordmap[row].Index];
-    }
-
-    protected override void Reload(
-        Filters<T> criteria, Columns<T> columns, ref SortOrder<T>? sort, 
-        CancellationToken token, Action<CoreTable?, Exception?> action)
-    {
-        var result = new CoreTable();
-        result.LoadColumns(columns);
-        result.LoadRows(Items);
-        action.Invoke(result, null);
-    }
-
-    public override void SaveItem(T item)
-    {
-        if (!Items.Contains(item))
-        {
-            Items.Add(item);
-        }
-    }
-
-}

+ 61 - 49
inabox.wpf/DynamicGrid/DynamicManyToManyCrossTab.cs

@@ -35,12 +35,17 @@ public abstract class DynamicManyToManyCrossTab<TManyToMany, TRow, TColumn> : Dy
     /// </summary>
     private Dictionary<Guid, Dictionary<Guid, TManyToMany>>? ManyToManyData;
 
+    protected new CrossTabDataComponent DataComponent;
+
     public DynamicManyToManyCrossTab()
     {
         rowProperty = CoreUtils.GetManyToManyThisProperty(typeof(TManyToMany), typeof(TRow));
         columnProperty = CoreUtils.GetManyToManyThisProperty(typeof(TManyToMany), typeof(TColumn));
 
         HeaderHeight = 125;
+
+        DataComponent = new CrossTabDataComponent(this);
+        base.DataComponent = DataComponent;
     }
 
     protected override void Init()
@@ -246,63 +251,70 @@ public abstract class DynamicManyToManyCrossTab<TManyToMany, TRow, TColumn> : Dy
         return true;
     }
 
-    protected override void Reload(
-        Filters<TRow> criteria, Columns<TRow> columns, ref SortOrder<TRow>? sort, 
-        CancellationToken token, Action<CoreTable?, Exception?> action)
+    protected class CrossTabDataComponent : BaseDynamicGridDataComponent<TRow>
     {
-        var filter = criteria.Add(RowFilter()).Combine();
+        private new DynamicManyToManyCrossTab<TManyToMany, TRow, TColumn> Grid;
 
-        var manyToManyColumns = Columns.None<TManyToMany>().Add(x => x.ID);
-        manyToManyColumns.Add(rowProperty.Name + ".ID").Add(columnProperty.Name + ".ID");
+        public CrossTabDataComponent(DynamicManyToManyCrossTab<TManyToMany, TRow, TColumn> grid): base(grid)
+        {
+            Grid = grid;
+        }
 
-        Client.QueryMultiple(
-            (results, e) =>
-            {
-                if(e is not null)
-                {
-                    action(null, e);
-                }
-                else
-                {
-                    var manyToManyTable = results!.Get<TManyToMany>();
+        public override TRow LoadItem(CoreRow row)
+        {
+            var id = row.Get<TRow, Guid>(x => x.ID);
+            return Client.Query(
+                new Filter<TRow>(x => x.ID).IsEqualTo(id),
+                DynamicGridUtils.LoadEditorColumns(Grid.DataColumns()))
+                .ToObjects<TRow>()
+                .FirstOrDefault() ?? throw new Exception($"No {typeof(TRow).Name} with ID {id}");
+        }
+
+        public override void Reload(Filters<TRow> criteria, Columns<TRow> columns, SortOrder<TRow>? sort, CancellationToken token, Action<QueryResult> action)
+        {
+            var filter = criteria.Add(Grid.RowFilter()).Combine();
 
-                    ManyToManyData = new Dictionary<Guid, Dictionary<Guid, TManyToMany>>();
-                    foreach(var row in manyToManyTable.Rows)
+            var manyToManyColumns = Columns.None<TManyToMany>().Add(x => x.ID);
+            manyToManyColumns.Add(Grid.rowProperty.Name + ".ID").Add(Grid.columnProperty.Name + ".ID");
+
+            Client.QueryMultiple(
+                (results, e) =>
+                {
+                    if(e is not null)
                     {
-                        var obj = row.ToObject<TManyToMany>();
-                        var rowID = (rowProperty.GetValue(obj) as IEntityLink)!.ID;
-                        var colID = (columnProperty.GetValue(obj) as IEntityLink)!.ID;
-                        var rowSet = ManyToManyData.GetValueOrAdd(colID);
-                        rowSet[rowID] = obj;
+                        action(new(e));
                     }
+                    else
+                    {
+                        var manyToManyTable = results!.Get<TManyToMany>();
 
-                    action(results!.Get<TRow>(), null);
-                }
-            },
-            new KeyedQueryDef<TRow>(filter, columns, sort),
-            new KeyedQueryDef<TManyToMany>(
-                new Filter<TManyToMany>(rowProperty.Name + ".ID").InQuery(filter, x => x.ID)
-                    .And(columnProperty.Name + ".ID").InQuery(ColumnFilter(), x => x.ID),
-                manyToManyColumns));
-    }
-
-    public override void SaveItem(TRow item)
-    {
-        // Never should get called.
-    }
+                        Grid.ManyToManyData = new Dictionary<Guid, Dictionary<Guid, TManyToMany>>();
+                        foreach(var row in manyToManyTable.Rows)
+                        {
+                            var obj = row.ToObject<TManyToMany>();
+                            var rowID = (Grid.rowProperty.GetValue(obj) as IEntityLink)!.ID;
+                            var colID = (Grid.columnProperty.GetValue(obj) as IEntityLink)!.ID;
+                            var rowSet = Grid.ManyToManyData.GetValueOrAdd(colID);
+                            rowSet[rowID] = obj;
+                        }
 
-    public override void DeleteItems(params CoreRow[] rows)
-    {
-        // Never should get called.
-    }
+                        action(new(results!.Get<TRow>()));
+                    }
+                },
+                new KeyedQueryDef<TRow>(filter, columns, sort),
+                new KeyedQueryDef<TManyToMany>(
+                    new Filter<TManyToMany>(Grid.rowProperty.Name + ".ID").InQuery(filter, x => x.ID)
+                        .And(Grid.columnProperty.Name + ".ID").InQuery(Grid.ColumnFilter(), x => x.ID),
+                    manyToManyColumns));
+        }
 
-    public override TRow LoadItem(CoreRow row)
-    {
-        var id = row.Get<TRow, Guid>(x => x.ID);
-        return Client.Query(
-            new Filter<TRow>(x => x.ID).IsEqualTo(id),
-            DynamicGridUtils.LoadEditorColumns(DataColumns()))
-            .ToObjects<TRow>()
-            .FirstOrDefault() ?? throw new Exception($"No {typeof(TRow).Name} with ID {id}");
+        public override void SaveItem(TRow item)
+        {
+            // Never should get called.
+        }
+        public override void DeleteItems(CoreRow[] rows)
+        {
+            // Never should get called.
+        }
     }
 }

+ 0 - 77
inabox.wpf/DynamicGrid/DynamicManyToManyDataGrid.cs

@@ -1,77 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-using System.Threading;
-using InABox.Clients;
-using InABox.Core;
-
-namespace InABox.DynamicGrid
-{
-    public class DynamicManyToManyDataGrid<TManyToMany, TThis> : DynamicManyToManyGrid<TManyToMany, TThis>
-        where TManyToMany : Entity, IPersistent, IRemotable, new() where TThis : Entity, IRemotable, IPersistent, new()
-    {
-        private readonly PropertyInfo prop;
-
-        public DynamicManyToManyDataGrid()
-        {
-            prop = CoreUtils.PropertyList(typeof(TManyToMany), p => p.PropertyType.IsSubclassOf(typeof(EntityLink<TThis>))).FirstOrDefault()
-                ?? throw new Exception($"No EntityLink property to link entities for DynamicManyToManyGrid<{typeof(TManyToMany)},{typeof(TThis)}>");
-        }
-
-        public Guid ID { get; set; }
-
-        protected override Guid[] CurrentGuids()
-        {
-            var result = new List<Guid>();
-            foreach (var row in Data.Rows)
-            {
-                var guid = row.Get<Guid>(otherproperty.Name + ".ID");
-                result.Add(guid);
-            }
-
-            return result.ToArray();
-        }
-
-        public override TManyToMany CreateItem()
-        {
-            var result = base.CreateItem();
-            var link = prop.GetValue(result) as IEntityLink;
-            if (link is null)
-                throw new Exception($"Property {prop.Name} of {typeof(TManyToMany)} is null");
-            link.ID = ID;
-            return result;
-        }
-
-        protected override void Reload(
-            Filters<TManyToMany> criteria, Columns<TManyToMany> columns, ref SortOrder<TManyToMany>? sort, 
-            CancellationToken token, Action<CoreTable?, Exception?> action)
-        {
-            var expr = CoreUtils.CreateLambdaExpression<TManyToMany>(prop.Name + ".ID");
-            criteria.Add(new Filter<TManyToMany>(expr).IsEqualTo(ID));
-            Client.Query(criteria.Combine(), columns, sort, null, action);
-        }
-
-        public override TManyToMany LoadItem(CoreRow row)
-        {
-            var id = row.Get<TManyToMany, Guid>(x => x.ID);
-            return Client
-                .Query(
-                    new Filter<TManyToMany>(x => x.ID).IsEqualTo(id),
-                    DynamicGridUtils.LoadEditorColumns(DataColumns()))
-                .ToObjects<TManyToMany>().FirstOrDefault() ?? throw new Exception($"{typeof(TManyToMany)} with ID {id} does not exist!");
-        }
-
-        public override void DeleteItems(params CoreRow[] rows)
-        {
-            var items = LoadItems(rows);
-            foreach (var item in items)
-                Client.Delete(item, "");
-        }
-
-        public override void SaveItem(TManyToMany item)
-        {
-            Client.Save(item, "");
-        }
-    }
-}

+ 15 - 133
inabox.wpf/DynamicGrid/DynamicManyToManyGrid.cs

@@ -22,19 +22,13 @@ public interface IDynamicManyToManyGrid<TManyToMany, TThis> : IDynamicEditorPage
 
 public class DynamicManyToManyGrid<TManyToMany, TThis> : DynamicGrid<TManyToMany>,
     IDynamicEditorPage,
-    IDynamicManyToManyGrid<TManyToMany, TThis>,
-    IDynamicMemoryEntityGrid<TManyToMany>
+    IDynamicManyToManyGrid<TManyToMany, TThis>
     where TThis : Entity, new()
     where TManyToMany : Entity, IPersistent, IRemotable, new()
 {
     //private Guid ID = Guid.Empty;
     protected TThis Item;
 
-    /// <summary>
-    /// Keeps a cache of initially loaded objects, so that we can figure out which guys to delete when we save.
-    /// </summary>
-    private TManyToMany[] MasterList = Array.Empty<TManyToMany>();
-
     protected PropertyInfo otherproperty;
     protected IEntityLink GetOtherLink(TManyToMany item) => (otherproperty.GetValue(item) as IEntityLink)!;
 
@@ -43,8 +37,6 @@ public class DynamicManyToManyGrid<TManyToMany, TThis> : DynamicGrid<TManyToMany
     
     protected List<TManyToMany> WorkingList = new();
 
-    IEnumerable<TManyToMany> IDynamicMemoryEntityGrid<TManyToMany>.Items => WorkingList;
-
     public PageType PageType => PageType.Other;
 
     private bool _readOnly;
@@ -61,12 +53,9 @@ public class DynamicManyToManyGrid<TManyToMany, TThis> : DynamicGrid<TManyToMany
         }
     }
 
-    private static bool IsAutoEntity => typeof(TManyToMany).HasAttribute<AutoEntity>();
 
     protected DynamicGridCustomColumnsComponent<TManyToMany> ColumnsComponent;
 
-    public HashSet<string>? LoadedColumns { get; set; }
-
     public DynamicManyToManyGrid()
     {
         MultiSelect = true;
@@ -77,12 +66,13 @@ public class DynamicManyToManyGrid<TManyToMany, TThis> : DynamicGrid<TManyToMany
         HiddenColumns.Add(CoreUtils.CreateLambdaExpression<TManyToMany>(otherproperty.Name + ".ID"));
 
         ColumnsComponent = new DynamicGridCustomColumnsComponent<TManyToMany>(this, GetTag());
-    }
 
-    protected override void Init()
-    {
+        DataComponent = new DynamicGridMemoryEntityDataComponent<TManyToMany>(this);
+        base.DataComponent = DataComponent;
     }
 
+    protected new DynamicGridMemoryEntityDataComponent<TManyToMany> DataComponent;
+
     protected override void DoReconfigure(DynamicGridOptions options)
     {
         options.RecordCount = true;
@@ -131,10 +121,12 @@ public class DynamicManyToManyGrid<TManyToMany, TThis> : DynamicGrid<TManyToMany
     {
         Item = (TThis)item;
 
+        Refresh(true, false);
+
         var data = PageDataHandler?.Invoke(typeof(TManyToMany));
         if (data != null)
         {
-            RefreshData(data);
+            DataComponent.LoadData(data);
         }
         else
         {
@@ -142,7 +134,7 @@ public class DynamicManyToManyGrid<TManyToMany, TThis> : DynamicGrid<TManyToMany
             {
                 data = new CoreTable();
                 data.LoadColumns(typeof(TManyToMany));
-                RefreshData(data);
+                DataComponent.LoadData(data);
             }
             else
             {
@@ -152,24 +144,11 @@ public class DynamicManyToManyGrid<TManyToMany, TThis> : DynamicGrid<TManyToMany
 
                 var columns = DynamicGridUtils.LoadEditorColumns(DataColumns());
 
-                Client.Query(filter, columns, sort, null, (o, e) =>
-                {
-                    if (o != null)
-                    {
-                        LoadedColumns = columns.ColumnNames().ToHashSet();
-
-                        Dispatcher.Invoke(() => RefreshData(o));
-                    }
-                    else if(e != null)
-                    {
-                        Dispatcher.Invoke(() =>
-                        {
-                            MessageWindow.ShowError("An error occurred while loading data.", e);
-                        });
-                    }
-                });
+                DataComponent.LoadData(filter, columns, sort);
             }
         }
+        Refresh(false, true);
+        Ready = true;
     }
 
     public void BeforeSave(object item)
@@ -179,24 +158,13 @@ public class DynamicManyToManyGrid<TManyToMany, TThis> : DynamicGrid<TManyToMany
 
     public void AfterSave(object item)
     {
-        if (IsAutoEntity)
-        {
-            return;
-        }
-        // First remove any deleted files
-        foreach (var map in MasterList)
-            if (!WorkingList.Contains(map))
-                Client.Delete(map, typeof(TManyToMany).Name + " Deleted by User");
-
-        foreach (var map in WorkingList)
+        foreach (var map in DataComponent.Items)
         {
             var prop = GetThisLink(map);
             if (prop.ID != Item.ID)
                 prop.ID = Item.ID;
         }
-
-        if (WorkingList.Any(x => x.IsChanged()))
-            Client.Save(WorkingList.Where(x => x.IsChanged()), "Updated by User");
+        DataComponent.SaveItems();
     }
 
     public Size MinimumSize()
@@ -314,7 +282,7 @@ public class DynamicManyToManyGrid<TManyToMany, TThis> : DynamicGrid<TManyToMany
                         var prop = GetOtherLink(newitem);
                         prop.ID = entity.ID;
                         prop.Synchronise(entity);
-                        SaveItem(newitem);
+                        DataComponent.SaveItem(newitem);
                     }
                 }
 
@@ -340,65 +308,6 @@ public class DynamicManyToManyGrid<TManyToMany, TThis> : DynamicGrid<TManyToMany
         return result;
     }
 
-    public override TManyToMany LoadItem(CoreRow row)
-    {
-        return WorkingList[_recordmap[row].Index];
-    }
-
-    public override void SaveItem(TManyToMany item)
-    {
-        if (!WorkingList.Contains(item))
-            WorkingList.Add(item);
-    }
-
-    public override void DeleteItems(params CoreRow[] rows)
-    {
-        foreach (var row in rows)
-        {
-            var id = row.Get<TManyToMany, Guid>(c => c.ID);
-            var item = WorkingList.FirstOrDefault(x => x.ID.Equals(id));
-            if (item != null)
-                WorkingList.Remove(item);
-        }
-    }
-
-    private void RefreshData(CoreTable data)
-    {
-        MasterList = data.ToArray<TManyToMany>();
-        WorkingList = MasterList.ToList();
-        Refresh(true, true);
-        Ready = true;
-    }
-
-    protected override void Reload(
-        Filters<TManyToMany> criteria, Columns<TManyToMany> columns, ref SortOrder<TManyToMany>? sort, 
-        CancellationToken token, Action<CoreTable?, Exception?> action)
-    {
-        var results = new CoreTable();
-        results.LoadColumns(typeof(TManyToMany));
-
-        this.EnsureColumns(columns);
-
-        if (sort != null)
-        {
-            var exp = IQueryableExtensions.ToLambda<TManyToMany>(sort.Expression);
-            var sorted = sort.Direction == SortDirection.Ascending
-                ? WorkingList.AsQueryable().OrderBy(exp)
-                : WorkingList.AsQueryable().OrderByDescending(exp);
-            foreach (var then in sort.Thens)
-            {
-                var thexp = IQueryableExtensions.ToLambda<TManyToMany>(then.Expression);
-                sorted = sort.Direction == SortDirection.Ascending ? sorted.ThenBy(exp) : sorted.ThenByDescending(exp);
-            }
-
-            WorkingList = sorted.ToList();
-        }
-        results.LoadRows(WorkingList);
-
-        //results.LoadRows(WorkingList);
-        action.Invoke(results, null);
-    }
-
     protected override BaseEditor? GetEditor(object item, DynamicGridColumn column)
     {
         var type = CoreUtils.GetProperty(typeof(TManyToMany), column.ColumnName).DeclaringType;
@@ -407,35 +316,8 @@ public class DynamicManyToManyGrid<TManyToMany, TThis> : DynamicGrid<TManyToMany
         return base.GetEditor(item, column);
     }
 
-    public override void LoadEditorButtons(TManyToMany item, DynamicEditorButtons buttons)
-    {
-        base.LoadEditorButtons(item, buttons);
-        if (ClientFactory.IsSupported<AuditTrail>())
-            buttons.Add("Audit Trail", Wpf.Resources.view.AsBitmapImage(), item, AuditTrailClick);
-    }
-
-    private void AuditTrailClick(object sender, object? item)
-    {
-        if (item is not TManyToMany entity) return;
-
-        var window = new AuditWindow(entity.ID);
-        window.ShowDialog();
-    }
-
     public override DynamicEditorPages LoadEditorPages(TManyToMany item)
     {
         return item.ID != Guid.Empty ? base.LoadEditorPages(item) : new DynamicEditorPages();
     }
-
-    protected override bool BeforePaste(IEnumerable<TManyToMany> items, ClipAction action)
-    {
-        if (action == ClipAction.Copy)
-        {
-            foreach (var item in items)
-            {
-                item.ID = Guid.Empty;
-            }
-        }
-        return base.BeforePaste(items, action);
-    }
 }

+ 15 - 133
inabox.wpf/DynamicGrid/DynamicOneToManyGrid.cs

@@ -19,29 +19,21 @@ namespace InABox.DynamicGrid;
 
 public interface IDynamicOneToManyGrid<TOne, TMany> : IDynamicEditorPage
 {
-    List<TMany> Items { get; }
     void LoadItems(TMany[] items);
 }
 
 public class DynamicOneToManyGrid<TOne, TMany> : DynamicGrid<TMany>,
     IDynamicEditorPage,
-    IDynamicOneToManyGrid<TOne, TMany>,
-    IDynamicMemoryEntityGrid<TMany>
+    IDynamicOneToManyGrid<TOne, TMany>
     where TOne : Entity, new() where TMany : Entity, IPersistent, IRemotable, new()
 {
-    private TMany[] MasterList = Array.Empty<TMany>();
     private readonly PropertyInfo property;
 
-    public HashSet<string>? LoadedColumns { get; set; }
-
-    IEnumerable<TMany> IDynamicMemoryEntityGrid<TMany>.Items => Items;
-
     protected DynamicGridCustomColumnsComponent<TMany> ColumnsComponent;
 
     public DynamicOneToManyGrid()
     {
         Ready = false;
-        Items = new List<TMany>();
         Criteria = new Filters<TMany>();
 
         property = CoreUtils.GetOneToManyProperty(typeof(TMany), typeof(TOne));
@@ -51,8 +43,13 @@ public class DynamicOneToManyGrid<TOne, TMany> : DynamicGrid<TMany>,
             HiddenColumns.Add(col);
 
         ColumnsComponent = new DynamicGridCustomColumnsComponent<TMany>(this, GetTag());
+
+        DataComponent = new DynamicGridMemoryEntityDataComponent<TMany>(this);
+        base.DataComponent = DataComponent;
     }
 
+    protected new DynamicGridMemoryEntityDataComponent<TMany> DataComponent;
+
     protected override void Init()
     {
     }
@@ -83,12 +80,9 @@ public class DynamicOneToManyGrid<TOne, TMany> : DynamicGrid<TMany>,
 
     public TOne Item { get; protected set; }
 
-    public List<TMany> Items { get; private set; }
-
     public void LoadItems(TMany[] items)
     {
-        Items.Clear();
-        Items.AddRange(items);
+        DataComponent.LoadData(items);
         Refresh(false, true);
     }
 
@@ -129,12 +123,16 @@ public class DynamicOneToManyGrid<TOne, TMany> : DynamicGrid<TMany>,
 
         var data = PageDataHandler?.Invoke(typeof(TMany));
 
-        if (data == null)
+        if(data is not null)
+        {
+            DataComponent.LoadData(data);
+        }
         {
             if (Item.ID == Guid.Empty)
             {
                 data = new CoreTable();
                 data.LoadColumns(typeof(TMany));
+                DataComponent.LoadData(data);
             }
             else
             {
@@ -146,15 +144,10 @@ public class DynamicOneToManyGrid<TOne, TMany> : DynamicGrid<TMany>,
 
                 var columns = DynamicGridUtils.LoadEditorColumns(DataColumns());
 
-                data = Client.Query(criteria.Combine(), columns, sort);
-
-                LoadedColumns = columns.ColumnNames().ToHashSet();
+                DataComponent.LoadData(criteria.Combine(), columns, sort);
             }
         }
 
-        MasterList = data.Rows.Select(x => x.ToObject<TMany>()).ToArray();
-
-        Items = MasterList.ToList();
         Refresh(false, true);
         Ready = true;
     }
@@ -166,23 +159,14 @@ public class DynamicOneToManyGrid<TOne, TMany> : DynamicGrid<TMany>,
 
     public virtual void AfterSave(object item)
     {
-        if (IsAutoEntity)
-        {
-            return;
-        }
-        // First remove any deleted files
-        foreach (var map in MasterList)
-            if (!Items.Contains(map))
-                OnDeleteItem(map);
-
-        foreach (var map in Items)
+        foreach (var map in DataComponent.Items)
         {
             var prop = (property.GetValue(map) as IEntityLink)!;
             prop.ID = Item.ID;
             prop.Synchronise(Item);
         }
 
-        new Client<TMany>().Save(Items.Where(x => x.IsChanged()), "Updated by User");
+        DataComponent.SaveItems();
     }
 
     public Size MinimumSize()
@@ -208,23 +192,6 @@ public class DynamicOneToManyGrid<TOne, TMany> : DynamicGrid<TMany>,
 
     #region DynamicGrid
 
-    protected virtual void OnDeleteItem(TMany item)
-    {
-        if (IsAutoEntity)
-        {
-            return;
-        }
-        Client.Delete(item, typeof(TMany).Name + " Deleted by User");
-    }
-
-
-    protected override CoreTable LoadImportKeys(string[] fields)
-    {
-        var result = base.LoadImportKeys(fields);
-        result.LoadRows(MasterList);
-        return result;
-    }
-
     protected override bool CustomiseImportItem(TMany item)
     {
         var result = base.CustomiseImportItem(item);
@@ -283,64 +250,6 @@ public class DynamicOneToManyGrid<TOne, TMany> : DynamicGrid<TMany>,
         return result;
     }
 
-    public override TMany LoadItem(CoreRow row)
-    {
-        return Items[_recordmap[row].Index];
-    }
-
-    public override TMany[] LoadItems(CoreRow[] rows)
-    {
-        var result = new List<TMany>();
-        foreach (var row in rows)
-            result.Add(LoadItem(row));
-        return result.ToArray();
-    }
-
-    public override void SaveItem(TMany item)
-    {
-        if (!Items.Contains(item))
-            Items.Add(item);
-
-        if (item is ISequenceable) Items = Items.AsQueryable().OrderBy(x => (x as ISequenceable)!.Sequence).ToList();
-    }
-
-    public override void DeleteItems(params CoreRow[] rows)
-    {
-        var items = rows.Select(LoadItem).ToList();
-        foreach (var item in items)
-        {
-            Items.Remove(item);
-        }
-    }
-
-
-    protected override void Reload(
-        Filters<TMany> criteria, Columns<TMany> columns, ref SortOrder<TMany>? sort,
-        CancellationToken token, Action<CoreTable?, Exception?> action)
-    {
-        var results = new CoreTable();
-        results.LoadColumns(typeof(TMany));
-
-        this.EnsureColumns(columns);
-
-        if (sort != null)
-        {
-            var exp = IQueryableExtensions.ToLambda<TMany>(sort.Expression);
-            var sorted = sort.Direction == SortDirection.Ascending
-                ? Items.AsQueryable().OrderBy(exp)
-                : Items.AsQueryable().OrderByDescending(exp);
-            foreach (var then in sort.Thens)
-            {
-                var thexp = IQueryableExtensions.ToLambda<TMany>(then.Expression);
-                sorted = sort.Direction == SortDirection.Ascending ? sorted.ThenBy(exp) : sorted.ThenByDescending(exp);
-            }
-            Items = sorted.ToList();
-        }
-        results.LoadRows(Items);
-
-        action.Invoke(results, null);
-    }
-
     protected override BaseEditor? GetEditor(object item, DynamicGridColumn column)
     {
         var type = CoreUtils.GetProperty(typeof(TMany), column.ColumnName).DeclaringType;
@@ -349,38 +258,11 @@ public class DynamicOneToManyGrid<TOne, TMany> : DynamicGrid<TMany>,
         return base.GetEditor(item, column);
     }
 
-    public override void LoadEditorButtons(TMany item, DynamicEditorButtons buttons)
-    {
-        base.LoadEditorButtons(item, buttons);
-        if (ClientFactory.IsSupported<AuditTrail>())
-            buttons.Add("Audit Trail", Wpf.Resources.view.AsBitmapImage(), item, AuditTrailClick);
-    }
-
-    private void AuditTrailClick(object sender, object? item)
-    {
-        if (item is not TMany entity) return;
-
-        var window = new AuditWindow(entity.ID);
-        window.ShowDialog();
-    }
-
     public override DynamicEditorPages LoadEditorPages(TMany item)
     {
         return item.ID != Guid.Empty ? base.LoadEditorPages(item) : new DynamicEditorPages();
     }
 
-    protected override bool BeforePaste(IEnumerable<TMany> items, ClipAction action)
-    {
-        if (action == ClipAction.Copy)
-        {
-            foreach (var item in items)
-            {
-                item.ID = Guid.Empty;
-            }
-        }
-        return base.BeforePaste(items, action);
-    }
-
     #endregion
 
 }

+ 58 - 115
inabox.wpf/DynamicGrid/Editors/JsonEditor/JsonEditorControl.cs

@@ -8,138 +8,81 @@ using System.Windows.Controls;
 using System.Windows.Media;
 using InABox.Core;
 
-namespace InABox.DynamicGrid
+namespace InABox.DynamicGrid;
+
+internal class JsonEditorControl : DynamicEditorControl<string, JsonEditor>
 {
-    internal class DynamicJsonGrid<T> : DynamicGrid<T> where T : BaseObject, new()
+    static JsonEditorControl()
     {
-        private int rowindex = -1;
-
-        protected override void Init()
-        {
-        }
-        protected override void DoReconfigure(DynamicGridOptions options)
-        {
-        }
-
-        public override void DeleteItems(params CoreRow[] rows)
-        {
-            var indexes = rows.Select(x => x.Index).OrderBy(x => x).ToArray();
-            foreach (var index in indexes)
-                Data.Rows.RemoveAt(index);
-        }
-
-        public override T LoadItem(CoreRow row)
-        {
-            rowindex = row.Index;
-            return row.ToObject<T>();
-        }
-
-        protected override void Reload(
-            Filters<T> criteria, Columns<T> columns, ref SortOrder<T>? sort, 
-            CancellationToken token, Action<CoreTable?, Exception?> action)
-        {
-        }
+        //DynamicEditorControlFactory.Register<JsonEditorControl, JsonEditor>();
+    }
+    
+    private Button _button;
 
-        public override void SaveItem(T item)
-        {
-            CoreRow row;
-            if (rowindex == -1)
-            {
-                row = Data.NewRow();
-                Data.Rows.Add(row);
-            }
-            else
-            {
-                row = Data.Rows[rowindex];
-            }
+    private string _value = "";
 
-            Data.FillRow(row, item);
-        }
+    public JsonEditorControl()
+    {
+        Width = 150;
     }
 
-    internal class JsonEditorControl : DynamicEditorControl<string, JsonEditor>
+    public override int DesiredHeight()
     {
-        
-                        
-        static JsonEditorControl()
-        {
-            //DynamicEditorControlFactory.Register<JsonEditorControl, JsonEditor>();
-        }
-        
-        private Button _button;
-
-        private string _value = "";
+        return 25;
+    }
 
-        public JsonEditorControl()
-        {
-            Width = 150;
-        }
+    public override int DesiredWidth()
+    {
+        return 150;
+    }
 
-        public override int DesiredHeight()
-        {
-            return 25;
-        }
+    public override void SetColor(Color color)
+    {
+        _button.Background = new SolidColorBrush(color);
+    }
 
-        public override int DesiredWidth()
-        {
-            return 150;
-        }
+    public override void SetFocus()
+    {
+        // Not Sure what to do here?
+    }
+    public override void Configure()
+    {
+    }
 
-        public override void SetColor(Color color)
-        {
-            _button.Background = new SolidColorBrush(color);
-        }
+    protected override FrameworkElement CreateEditor()
+    {
+        _button = new Button();
+        _button.Content = "Edit";
+        _button.Click += _button_Click;
+        return _button;
+    }
 
-        public override void SetFocus()
-        {
-            // Not Sure what to do here?
-        }
-        public override void Configure()
-        {
-        }
+    private void _button_Click(object sender, RoutedEventArgs e)
+    {
+        var type = (EditorDefinition as JsonEditor).Type;
+        var listtype = typeof(List<>).MakeGenericType(type);
+        var list = Activator.CreateInstance(listtype) as IList;
+        if (!string.IsNullOrWhiteSpace(_value))
+            Serialization.DeserializeInto(_value, list);
 
-        protected override FrameworkElement CreateEditor()
-        {
-            _button = new Button();
-            _button.Content = "Edit";
-            _button.Click += _button_Click;
-            return _button;
-        }
+        var array = new object[list.Count];
+        list.CopyTo(array, 0);
 
-        private void _button_Click(object sender, RoutedEventArgs e)
+        var grid = DynamicGridUtils.CreateDynamicGrid(typeof(DynamicGrid<>), type);
+        if (grid.EditItems(array))
         {
-            var type = (EditorDefinition as JsonEditor).Type;
-            var listtype = typeof(List<>).MakeGenericType(type);
-            var list = Activator.CreateInstance(listtype) as IList;
-            if (!string.IsNullOrWhiteSpace(_value))
-                Serialization.DeserializeInto(_value, list);
-
-            var array = new object[list.Count];
-            list.CopyTo(array, 0);
-
-            var data = new CoreTable();
-            data.LoadColumns(type);
-            data.LoadRows(array);
-
-            /*var gridtype = typeof(DynamicJsonGrid<>).MakeGenericType(type);
-            var grid = Activator.CreateInstance(gridtype) as IDynamicGrid;
-            grid.Data = data;
-            if (grid.DirectEdit(data))
-            {
-                var saved = data.Rows.Select(x => x.ToObject(type)).ToArray();
-                _value = Serialization.Serialize(saved, true);
-                CheckChanged();
-            }*/
+            _value = Serialization.Serialize(list, true);
+            CheckChanged();
         }
+    }
 
-        protected override string RetrieveValue()
-        {
-            return _value;
-        }
+    protected override string RetrieveValue()
+    {
+        return _value;
+    }
 
-        protected override void UpdateValue(string value)
-        {
-            _value = "";
-        }
+    protected override void UpdateValue(string value)
+    {
+        _value = "";
     }
 }

+ 6 - 3
inabox.wpf/Grids/PostableSettingsGrid.cs

@@ -9,12 +9,14 @@ using System.Threading.Tasks;
 
 namespace InABox.Wpf;
 
-public class PostableSettingsGrid : DynamicItemsListGrid<PostableSettings>
+public class PostableSettingsGrid : DynamicGrid<PostableSettings>
 {
-    public PostableSettingsGrid()
+    protected override void Init()
     {
         OnCustomiseEditor += PostableSettingsGrid_OnCustomiseEditor;
         OnEditorValueChanged += PostableSettingsGrid_OnEditorValueChanged;
+
+        DataComponent = new DynamicGridItemsListDataComponent<PostableSettings>(this);
     }
 
     private Dictionary<string, object?> PostableSettingsGrid_OnEditorValueChanged(object sender, string name, object value)
@@ -88,7 +90,8 @@ public class PostableSettingsGrid : DynamicItemsListGrid<PostableSettings>
     public static bool ConfigureGlobalPosterSettings(Type globalSettingsType)
     {
         var globalPosterSettings = PosterUtils.LoadGlobalPosterSettings(globalSettingsType);
-        var grid = DynamicGridUtils.CreateDynamicGrid(typeof(DynamicItemsListGrid<>), globalSettingsType);
+        var grid = DynamicGridUtils.CreateDynamicGrid(typeof(DynamicGrid<>), globalSettingsType);
+
         if(grid.EditItems(new object[] { globalPosterSettings }))
         {
             PosterUtils.SaveGlobalPosterSettings(globalSettingsType, globalPosterSettings);

+ 2 - 2
inabox.wpf/Grids/PosterSettingsGrid.cs

@@ -7,10 +7,10 @@ using System.Threading.Tasks;
 
 namespace InABox.Wpf.Grids
 {
-    public class PosterSettingsGrid<TPosterSettings> : DynamicItemsListGrid<TPosterSettings>
+    public class PosterSettingsGrid<TPosterSettings> : DynamicGrid<TPosterSettings>
         where TPosterSettings : PosterSettings, new()
     {
-        public PosterSettingsGrid()
+        protected override void Init()
         {
             OnCustomiseEditor += PosterSettingsGrid_OnCustomiseEditor;
         }

+ 3 - 5
inabox.wpf/Reports/ReportGrid.cs

@@ -93,12 +93,10 @@ namespace InABox.Wpf.Reports
             return columns;
         }
 
-        protected override void Reload(
-            Filters<ReportTemplate> criteria, Columns<ReportTemplate> columns, ref SortOrder<ReportTemplate>? sort,
-            CancellationToken token, Action<CoreTable?, Exception?> action)
+        public override void DefineFilter(Filters<ReportTemplate> filter)
         {
-            criteria.Add(new Filter<ReportTemplate>(x => x.DataModel).IsEqualTo(DataModel.Name).And(x => x.Section).IsEqualTo(Section));
-            base.Reload(criteria, columns, ref sort, token, action);
+            base.DefineFilter(filter);
+            filter.Add(new Filter<ReportTemplate>(x => x.DataModel).IsEqualTo(DataModel.Name).And(x => x.Section).IsEqualTo(Section));
         }
 
         private bool ReportGrid_OnEditItem(object sender, object item)