Przeglądaj źródła

Extracted fancy columns business into IDynamicMemoryEntityGrid

Kenric Nugteren 1 rok temu
rodzic
commit
2ed06d68e8

+ 8 - 36
inabox.wpf/DynamicGrid/DynamicManyToManyGrid.cs

@@ -19,7 +19,10 @@ public interface IDynamicManyToManyGrid<TManyToMany, TThis> : IDynamicEditorPage
 {
 }
 
-public class DynamicManyToManyGrid<TManyToMany, TThis> : DynamicGrid<TManyToMany>, IDynamicEditorPage, IDynamicManyToManyGrid<TManyToMany, TThis>
+public class DynamicManyToManyGrid<TManyToMany, TThis> : DynamicGrid<TManyToMany>,
+    IDynamicEditorPage,
+    IDynamicManyToManyGrid<TManyToMany, TThis>,
+    IDynamicMemoryEntityGrid<TManyToMany>
     where TThis : Entity, new()
     where TManyToMany : Entity, IPersistent, IRemotable, new()
 {
@@ -39,6 +42,8 @@ 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;
@@ -59,16 +64,7 @@ public class DynamicManyToManyGrid<TManyToMany, TThis> : DynamicGrid<TManyToMany
 
     protected DynamicGridCustomColumnsComponent<TManyToMany> ColumnsComponent;
 
-    /// <summary>
-    /// A set of columns representing which columns have been loaded from the database.
-    /// </summary>
-    /// <remarks>
-    /// This is used to refresh the data when the columns change.<br/>
-    /// 
-    /// 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>
-    private HashSet<string>? LoadedColumns;
+    public HashSet<string>? LoadedColumns { get; set; }
 
     public DynamicManyToManyGrid()
     {
@@ -380,31 +376,7 @@ public class DynamicManyToManyGrid<TManyToMany, TThis> : DynamicGrid<TManyToMany
         var results = new CoreTable();
         results.LoadColumns(typeof(TManyToMany));
 
-        if (LoadedColumns is not null)
-        {
-            // Figure out which columns we still need.
-            var newColumns = columns.Where(x => !LoadedColumns.Contains(x.Property)).ToColumns();
-            if (newColumns.Any() && typeof(TManyToMany).GetCustomAttribute<AutoEntity>() is null)
-            {
-                var data = Client.Query(
-                    new Filter<TManyToMany>(x => x.ID).InList(WorkingList.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 = WorkingList.FirstOrDefault(x => x.ID == row.Get<TManyToMany, Guid>(y => y.ID));
-                    if (item is not null)
-                    {
-                        row.FillObject(item, overrideExisting: false);
-                    }
-                }
-                // Remember that we have now loaded this data.
-                foreach (var column in newColumns)
-                {
-                    LoadedColumns.Add(column.Property);
-                }
-            }
-        }
+        this.EnsureColumns(columns);
 
         if (sort != null)
         {

+ 278 - 391
inabox.wpf/DynamicGrid/DynamicOneToManyGrid.cs

@@ -14,485 +14,372 @@ using InABox.Core;
 using InABox.Core.Reports;
 using InABox.WPF;
 
-namespace InABox.DynamicGrid
+namespace InABox.DynamicGrid;
+
+public interface IDynamicOneToManyGrid<TOne, TMany> : IDynamicEditorPage
 {
-    public interface IDynamicOneToManyGrid<TOne, TMany> : IDynamicEditorPage
-    {
-        List<TMany> Items { get; }
-        void LoadItems(TMany[] items);
-    }
+    List<TMany> Items { get; }
+    void LoadItems(TMany[] items);
+}
+
+public class DynamicOneToManyGrid<TOne, TMany> : DynamicGrid<TMany>,
+    IDynamicEditorPage,
+    IDynamicOneToManyGrid<TOne, TMany>,
+    IDynamicMemoryEntityGrid<TMany>
+    where TOne : Entity, new() where TMany : Entity, IPersistent, IRemotable, new()
+{
+    private TMany[] MasterList = Array.Empty<TMany>();
+    private readonly PropertyInfo property;
 
-    public class DynamicOneToManyGrid<TOne, TMany> : DynamicGrid<TMany>, IDynamicEditorPage, 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; }
 
-        /// <summary>
-        /// A set of columns representing which columns have been loaded from the database.
-        /// </summary>
-        /// <remarks>
-        /// This is used to refresh the data when the columns change.<br/>
-        /// 
-        /// 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>
-        private HashSet<string>? LoadedColumns;
+    IEnumerable<TMany> IDynamicMemoryEntityGrid<TMany>.Items => Items;
 
-        protected DynamicGridCustomColumnsComponent<TMany> ColumnsComponent;
+    protected DynamicGridCustomColumnsComponent<TMany> ColumnsComponent;
 
-        public DynamicOneToManyGrid()
-        {
-            Ready = false;
-            Items = new List<TMany>();
-            Criteria = new Filters<TMany>();
+    public DynamicOneToManyGrid()
+    {
+        Ready = false;
+        Items = new List<TMany>();
+        Criteria = new Filters<TMany>();
 
-            property = CoreUtils.GetOneToManyProperty(typeof(TMany), typeof(TOne));
+        property = CoreUtils.GetOneToManyProperty(typeof(TMany), typeof(TOne));
 
-            AddHiddenColumn(property.Name + "." + nameof(IEntityLink.ID));
-            foreach (var col in LookupFactory.RequiredColumns<TMany>())
-                HiddenColumns.Add(col);
+        AddHiddenColumn(property.Name + "." + nameof(IEntityLink.ID));
+        foreach (var col in LookupFactory.RequiredColumns<TMany>())
+            HiddenColumns.Add(col);
 
-            ColumnsComponent = new DynamicGridCustomColumnsComponent<TMany>(this, GetTag());
-        }
+        ColumnsComponent = new DynamicGridCustomColumnsComponent<TMany>(this, GetTag());
+    }
 
-        protected override void Init()
-        {
-        }
+    protected override void Init()
+    {
+    }
 
-        protected override void DoReconfigure(FluentList<DynamicGridOption> options)
-        {
-            options.BeginUpdate();
-
-            options.Add(DynamicGridOption.RecordCount)
-                .Add(DynamicGridOption.SelectColumns);
-            
-            if (Security.CanEdit<TMany>() && !ReadOnly)
-                options.Add(DynamicGridOption.AddRows).Add(DynamicGridOption.EditRows);
-            if (Security.CanDelete<TMany>() && !ReadOnly)
-                options.Add(DynamicGridOption.DeleteRows);
-            if (Security.CanImport<TMany>() && !ReadOnly)
-                options.Add(DynamicGridOption.ImportData);
-            if (Security.CanExport<TMany>())
-                options.Add(DynamicGridOption.ExportData);
-            if (Security.CanMerge<TMany>())
-                options.Add(DynamicGridOption.MultiSelect);
-
-            options.EndUpdate();
-        }
+    protected override void DoReconfigure(FluentList<DynamicGridOption> options)
+    {
+        options.BeginUpdate();
+
+        options.Add(DynamicGridOption.RecordCount)
+            .Add(DynamicGridOption.SelectColumns);
+        
+        if (Security.CanEdit<TMany>() && !ReadOnly)
+            options.Add(DynamicGridOption.AddRows).Add(DynamicGridOption.EditRows);
+        if (Security.CanDelete<TMany>() && !ReadOnly)
+            options.Add(DynamicGridOption.DeleteRows);
+        if (Security.CanImport<TMany>() && !ReadOnly)
+            options.Add(DynamicGridOption.ImportData);
+        if (Security.CanExport<TMany>())
+            options.Add(DynamicGridOption.ExportData);
+        if (Security.CanMerge<TMany>())
+            options.Add(DynamicGridOption.MultiSelect);
+
+        options.EndUpdate();
+    }
 
-        private static bool IsAutoEntity => typeof(TMany).HasAttribute<AutoEntity>();
+    private static bool IsAutoEntity => typeof(TMany).HasAttribute<AutoEntity>();
 
-        protected Filters<TMany> Criteria { get; } = new Filters<TMany>();
+    protected Filters<TMany> Criteria { get; } = new Filters<TMany>();
 
-        public TOne Item { get; protected set; }
+    public TOne Item { get; protected set; }
 
-        public List<TMany> Items { get; private set; }
+    public List<TMany> Items { get; private set; }
 
-        public void LoadItems(TMany[] items)
-        {
-            Items.Clear();
-            Items.AddRange(items);
-            Refresh(false, true);
-        }
+    public void LoadItems(TMany[] items)
+    {
+        Items.Clear();
+        Items.AddRange(items);
+        Refresh(false, true);
+    }
 
-        private static string GetTag()
-        {
-            return typeof(TOne).Name + "." + typeof(TMany).Name;
-        }
+    private static string GetTag()
+    {
+        return typeof(TOne).Name + "." + typeof(TMany).Name;
+    }
 
-        #region IDynamicEditorPage
+    #region IDynamicEditorPage
 
-        public DynamicEditorGrid EditorGrid { get; set; }
+    public DynamicEditorGrid EditorGrid { get; set; }
 
-        public PageType PageType => PageType.Other;
+    public PageType PageType => PageType.Other;
 
-        public bool Ready { get; set; }
+    public bool Ready { get; set; }
 
-        private bool _readOnly;
-        public bool ReadOnly
+    private bool _readOnly;
+    public bool ReadOnly
+    {
+        get => _readOnly;
+        set
         {
-            get => _readOnly;
-            set
+            if (_readOnly != value)
             {
-                if (_readOnly != value)
-                {
-                    _readOnly = value;
-                    Reconfigure();
-                }
+                _readOnly = value;
+                Reconfigure();
             }
         }
+    }
 
-        public virtual void Load(object item, Func<Type, CoreTable?>? PageDataHandler)
-        {
-            Reconfigure();
+    public virtual void Load(object item, Func<Type, CoreTable?>? PageDataHandler)
+    {
+        Reconfigure();
 
-            Item = (TOne)item;
+        Item = (TOne)item;
 
-            Refresh(true, false);
+        Refresh(true, false);
 
-            var data = PageDataHandler?.Invoke(typeof(TMany));
+        var data = PageDataHandler?.Invoke(typeof(TMany));
 
-            if (data == null)
+        if (data == null)
+        {
+            if (Item.ID == Guid.Empty)
             {
-                if (Item.ID == Guid.Empty)
-                {
-                    data = new CoreTable();
-                    data.LoadColumns(typeof(TMany));
-                }
-                else
-                {
-                    var criteria = new Filters<TMany>();
-                    var exp = CoreUtils.GetPropertyExpression<TMany>(property.Name + ".ID");
-                    criteria.Add(new Filter<TMany>(exp).IsEqualTo(Item.ID).And(exp).IsNotEqualTo(Guid.Empty));
-                    criteria.AddRange(Criteria.Items);
-                    var sort = LookupFactory.DefineSort<TMany>();
-
-                    var columns = DynamicGridUtils.LoadEditorColumns(DataColumns());
-
-                    data = Client.Query(criteria.Combine(), columns, sort);
-
-                    LoadedColumns = columns.ColumnNames().ToHashSet();
-                }
+                data = new CoreTable();
+                data.LoadColumns(typeof(TMany));
             }
+            else
+            {
+                var criteria = new Filters<TMany>();
+                var exp = CoreUtils.GetPropertyExpression<TMany>(property.Name + ".ID");
+                criteria.Add(new Filter<TMany>(exp).IsEqualTo(Item.ID).And(exp).IsNotEqualTo(Guid.Empty));
+                criteria.AddRange(Criteria.Items);
+                var sort = LookupFactory.DefineSort<TMany>();
 
-            MasterList = data.Rows.Select(x => x.ToObject<TMany>()).ToArray();
+                var columns = DynamicGridUtils.LoadEditorColumns(DataColumns());
 
-            Items = MasterList.ToList();
-            Refresh(false, true);
-            Ready = true;
-        }
+                data = Client.Query(criteria.Combine(), columns, sort);
 
-        public virtual void BeforeSave(object item)
-        {
-            // Don't need to do anything here
+                LoadedColumns = columns.ColumnNames().ToHashSet();
+            }
         }
 
-        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);
+        MasterList = data.Rows.Select(x => x.ToObject<TMany>()).ToArray();
 
-            foreach (var map in Items)
-            {
-                var prop = (property.GetValue(map) as IEntityLink)!;
-                prop.ID = Item.ID;
-                prop.Synchronise(Item);
-            }
+        Items = MasterList.ToList();
+        Refresh(false, true);
+        Ready = true;
+    }
 
-            new Client<TMany>().Save(Items.Where(x => x.IsChanged()), "Updated by User");
-        }
+    public virtual void BeforeSave(object item)
+    {
+        // Don't need to do anything here
+    }
 
-        public Size MinimumSize()
+    public virtual void AfterSave(object item)
+    {
+        if (IsAutoEntity)
         {
-            return new Size(400, 400);
+            return;
         }
+        // First remove any deleted files
+        foreach (var map in MasterList)
+            if (!Items.Contains(map))
+                OnDeleteItem(map);
 
-        public string Caption()
+        foreach (var map in Items)
         {
-            var caption = typeof(TMany).GetCustomAttribute(typeof(Caption));
-            if (caption != null)
-                return ((Caption)caption).Text;
-            var result = new Inflector.Inflector(new CultureInfo("en")).Pluralize(typeof(TMany).Name);
-            return result;
+            var prop = (property.GetValue(map) as IEntityLink)!;
+            prop.ID = Item.ID;
+            prop.Synchronise(Item);
         }
 
-        public virtual int Order()
-        {
-            return int.MinValue;
-        }
+        new Client<TMany>().Save(Items.Where(x => x.IsChanged()), "Updated by User");
+    }
 
-        #endregion
+    public Size MinimumSize()
+    {
+        return new Size(400, 400);
+    }
 
-        #region DynamicGrid
+    public string Caption()
+    {
+        var caption = typeof(TMany).GetCustomAttribute(typeof(Caption));
+        if (caption != null)
+            return ((Caption)caption).Text;
+        var result = new Inflector.Inflector(new CultureInfo("en")).Pluralize(typeof(TMany).Name);
+        return result;
+    }
 
-        protected virtual void OnDeleteItem(TMany item)
-        {
-            if (IsAutoEntity)
-            {
-                return;
-            }
-            Client.Delete(item, typeof(TMany).Name + " Deleted by User");
-        }
+    public virtual int Order()
+    {
+        return int.MinValue;
+    }
+
+    #endregion
 
+    #region DynamicGrid
 
-        protected override CoreTable LoadImportKeys(string[] fields)
+    protected virtual void OnDeleteItem(TMany item)
+    {
+        if (IsAutoEntity)
         {
-            var result = base.LoadImportKeys(fields);
-            result.LoadRows(MasterList);
-            return result;
+            return;
         }
+        Client.Delete(item, typeof(TMany).Name + " Deleted by User");
+    }
 
-        protected override bool CustomiseImportItem(TMany item)
-        {
-            var result = base.CustomiseImportItem(item);
-            if (result)
-            {
-                var prop = (property.GetValue(item) as IEntityLink)!;
-                prop.ID = Item.ID;
-                prop.Synchronise(Item);
-            }
 
-            return result;
-        }
-        public override DynamicGridColumns GenerateColumns()
-        {
-            var cols = new DynamicGridColumns();
-            cols.AddRange(base.GenerateColumns().Where(x => !x.ColumnName.StartsWith(property.Name + ".")));
-            return cols;
-        }
+    protected override CoreTable LoadImportKeys(string[] fields)
+    {
+        var result = base.LoadImportKeys(fields);
+        result.LoadRows(MasterList);
+        return result;
+    }
 
-        protected override DynamicGridColumns LoadColumns()
+    protected override bool CustomiseImportItem(TMany item)
+    {
+        var result = base.CustomiseImportItem(item);
+        if (result)
         {
-            return ColumnsComponent.LoadColumns();
+            var prop = (property.GetValue(item) as IEntityLink)!;
+            prop.ID = Item.ID;
+            prop.Synchronise(Item);
         }
 
-        protected override void SaveColumns(DynamicGridColumns columns)
-        {
-            ColumnsComponent.SaveColumns(columns);
-        }
-        protected override void LoadColumnsMenu(ContextMenu menu)
-        {
-            base.LoadColumnsMenu(menu);
-            ColumnsComponent.LoadColumnsMenu(menu);
-        }
+        return result;
+    }
+    public override DynamicGridColumns GenerateColumns()
+    {
+        var cols = new DynamicGridColumns();
+        cols.AddRange(base.GenerateColumns().Where(x => !x.ColumnName.StartsWith(property.Name + ".")));
+        return cols;
+    }
 
-        protected override DynamicGridSettings LoadSettings()
-        {
-            var tag = GetTag();
+    protected override DynamicGridColumns LoadColumns()
+    {
+        return ColumnsComponent.LoadColumns();
+    }
 
-            var user = Task.Run(() => new UserConfiguration<DynamicGridSettings>(tag).Load());
-            user.Wait();
+    protected override void SaveColumns(DynamicGridColumns columns)
+    {
+        ColumnsComponent.SaveColumns(columns);
+    }
+    protected override void LoadColumnsMenu(ContextMenu menu)
+    {
+        base.LoadColumnsMenu(menu);
+        ColumnsComponent.LoadColumnsMenu(menu);
+    }
 
-            return user.Result;
-        }
-        protected override void SaveSettings(DynamicGridSettings settings)
-        {
-            var tag = GetTag();
-            new UserConfiguration<DynamicGridSettings>(tag).Save(settings);
-        }
+    protected override DynamicGridSettings LoadSettings()
+    {
+        var tag = GetTag();
 
-        protected override TMany CreateItem()
-        {
-            var result = new TMany();
-            var prop = (property.GetValue(result) as IEntityLink)!;
-            prop.ID = Item.ID;
-            prop.Synchronise(Item);
-            return result;
-        }
+        var user = Task.Run(() => new UserConfiguration<DynamicGridSettings>(tag).Load());
+        user.Wait();
 
-        protected override TMany LoadItem(CoreRow row)
-        {
-            return Items[_recordmap[row].Index];
-        }
+        return user.Result;
+    }
+    protected override void SaveSettings(DynamicGridSettings settings)
+    {
+        var tag = GetTag();
+        new UserConfiguration<DynamicGridSettings>(tag).Save(settings);
+    }
 
-        protected override TMany[] LoadItems(CoreRow[] rows)
-        {
-            var result = new List<TMany>();
-            foreach (var row in rows)
-                result.Add(LoadItem(row));
-            return result.ToArray();
-        }
+    protected override TMany CreateItem()
+    {
+        var result = new TMany();
+        var prop = (property.GetValue(result) as IEntityLink)!;
+        prop.ID = Item.ID;
+        prop.Synchronise(Item);
+        return result;
+    }
 
-        public override void SaveItem(TMany item)
-        {
-            if (!Items.Contains(item))
-                Items.Add(item);
+    protected override TMany LoadItem(CoreRow row)
+    {
+        return Items[_recordmap[row].Index];
+    }
 
-            if (item is ISequenceable) Items = Items.AsQueryable().OrderBy(x => (x as ISequenceable)!.Sequence).ToList();
-        }
+    protected override TMany[] LoadItems(CoreRow[] rows)
+    {
+        var result = new List<TMany>();
+        foreach (var row in rows)
+            result.Add(LoadItem(row));
+        return result.ToArray();
+    }
 
-        protected override void DeleteItems(params CoreRow[] rows)
+    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();
+    }
+
+    protected override void DeleteItems(params CoreRow[] rows)
+    {
+        var items = rows.Select(LoadItem).ToList();
+        foreach (var item in items)
         {
-            var items = rows.Select(LoadItem).ToList();
-            foreach (var item in items)
-            {
-                Items.Remove(item);
-            }
+            Items.Remove(item);
         }
+    }
 
-        /// <summary>
-        /// Load the properties of any <see cref="EntityLink{T}"/>s on this <see cref="TMany"/> where the <see cref="IEntityLink.ID"/> is not <see cref="Guid.Empty"/>.
-        /// This allows us to populate columns of transient objects, as long as they are linked by the ID. What this actually then does is query each
-        /// linked table with the required columns.
-        /// </summary>
-        /// <param name="columns"></param>
-        private void LoadForeignProperties(Columns<TMany> columns)
-        {
-            // Lists of properties that we need, arranged by the entity link property which is their parent.
-            // LinkIDProperty : (Type, Properties: [(columnName, property)], Objects)
-            var newData = new Dictionary<IProperty, Tuple<Type, List<Tuple<string, IProperty>>, HashSet<TMany>>>();
 
-            foreach (var column in columns)
-            {
-                var property = DatabaseSchema.Property(typeof(TMany), column.Property);
-                if (property?.GetOuterParent(x => x.IsEntityLink) is IProperty linkProperty)
-                {
-                    var remaining = column.Property[(linkProperty.Name.Length + 1)..];
-                    if (remaining.Equals(nameof(IEntityLink.ID)))
-                    {
-                        // This guy isn't foreign, so we don't pull him.
-                        continue;
-                    }
-
-                    var idProperty = DatabaseSchema.Property(typeof(TMany), linkProperty.Name + "." + nameof(IEntityLink.ID))!;
-
-                    var linkType = linkProperty.PropertyType.GetInterfaceDefinition(typeof(IEntityLink<>))!.GenericTypeArguments[0];
-                    if (!newData.TryGetValue(idProperty, out var data))
-                    {
-                        data = new Tuple<Type, List<Tuple<string, IProperty>>, HashSet<TMany>>(
-                            linkType,
-                            new List<Tuple<string, IProperty>>(),
-                            new HashSet<TMany>());
-                        newData.Add(idProperty, data);
-                    }
-
-                    var any = false;
-                    foreach (var item in Items)
-                    {
-                        if (!item.LoadedColumns.Contains(column.Property))
-                        {
-                            var linkID = (Guid)idProperty.Getter()(item);
-                            if (linkID != Guid.Empty)
-                            {
-                                any = true;
-                                data.Item3.Add(item);
-                            }
-                        }
-                    }
-                    if (any)
-                    {
-                        data.Item2.Add(new(remaining, property));
-                    }
-                }
-            }
+    protected override void Reload(Filters<TMany> criteria, Columns<TMany> columns, ref SortOrder<TMany>? sort,
+        Action<CoreTable?, Exception?> action)
+    {
+        var results = new CoreTable();
+        results.LoadColumns(typeof(TMany));
 
-            foreach (var (prop, data) in newData)
-            {
-                if (data.Item2.Any())
-                {
-                    var ids = data.Item3.Select(prop.Getter()).Cast<Guid>().ToArray();
-                    var table = Client.Create(data.Item1).Query(
-                        Filter.Create<Entity>(data.Item1, x => x.ID).InList(ids),
-                        Columns.Create(data.Item1, data.Item2.Select(x => x.Item1).ToArray()).Add<Entity>(x => x.ID));
-                    foreach (var entity in data.Item3)
-                    {
-                        var linkID = (Guid)prop.Getter()(entity);
-                        var row = table.Rows.FirstOrDefault(x => x.Get<Entity, Guid>(x => x.ID) == linkID);
-                        if (row is not null)
-                        {
-                            foreach (var (name, property) in data.Item2)
-                            {
-                                if (!entity.LoadedColumns.Contains(property.Name))
-                                {
-                                    property.Setter()(entity, row[name]);
-                                    entity.LoadedColumns.Add(property.Name);
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        }
+        this.EnsureColumns(columns);
 
-        protected override void Reload(Filters<TMany> criteria, Columns<TMany> columns, ref SortOrder<TMany>? sort,
-            Action<CoreTable?, Exception?> action)
+        if (sort != null)
         {
-            var results = new CoreTable();
-            results.LoadColumns(typeof(TMany));
-
-            if (LoadedColumns is not null)
-            {
-                // Figure out which columns we still need.
-                var newColumns = columns.Where(x => !LoadedColumns.Contains(x.Property)).ToColumns();
-                if (newColumns.Any() && typeof(TMany).GetCustomAttribute<AutoEntity>() is null)
-                {
-                    var data = Client.Query(
-                        new Filter<TMany>(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 = Items.FirstOrDefault(x => x.ID == row.Get<TMany, Guid>(y => y.ID));
-                        if (item is not null)
-                        {
-                            row.FillObject(item, overrideExisting: false);
-                        }
-                    }
-                    // Remember that we have now loaded this data.
-                    foreach (var column in newColumns)
-                    {
-                        LoadedColumns.Add(column.Property);
-                    }
-                }
-            }
-            LoadForeignProperties(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 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();
+                var thexp = IQueryableExtensions.ToLambda<TMany>(then.Expression);
+                sorted = sort.Direction == SortDirection.Ascending ? sorted.ThenBy(exp) : sorted.ThenByDescending(exp);
             }
-            results.LoadRows(Items);
-
-            action.Invoke(results, null);
+            Items = sorted.ToList();
         }
+        results.LoadRows(Items);
 
-        protected override BaseEditor? GetEditor(object item, DynamicGridColumn column)
-        {
-            var type = CoreUtils.GetProperty(typeof(TMany), column.ColumnName).DeclaringType;
-            if (type.GetInterfaces().Contains(typeof(IEntityLink)) && type.ContainsInheritedGenericType(typeof(TOne)))
-                return new NullEditor();
-            return base.GetEditor(item, column);
-        }
+        action.Invoke(results, null);
+    }
 
-        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);
-        }
+    protected override BaseEditor? GetEditor(object item, DynamicGridColumn column)
+    {
+        var type = CoreUtils.GetProperty(typeof(TMany), column.ColumnName).DeclaringType;
+        if (type.GetInterfaces().Contains(typeof(IEntityLink)) && type.ContainsInheritedGenericType(typeof(TOne)))
+            return new NullEditor();
+        return base.GetEditor(item, column);
+    }
 
-        private void AuditTrailClick(object sender, object? item)
-        {
-            if (item is not TMany entity) return;
+    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);
+    }
 
-            var window = new AuditWindow(entity.ID);
-            window.ShowDialog();
-        }
+    private void AuditTrailClick(object sender, object? item)
+    {
+        if (item is not TMany entity) return;
 
-        public override DynamicEditorPages LoadEditorPages(TMany item)
-        {
-            return item.ID != Guid.Empty ? base.LoadEditorPages(item) : new DynamicEditorPages();
-        }
+        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)
+    protected override bool BeforePaste(IEnumerable<TMany> items, ClipAction action)
+    {
+        if (action == ClipAction.Copy)
         {
-            if (action == ClipAction.Copy)
+            foreach (var item in items)
             {
-                foreach (var item in items)
-                {
-                    item.ID = Guid.Empty;
-                }
+                item.ID = Guid.Empty;
             }
-            return base.BeforePaste(items, action);
         }
+        return base.BeforePaste(items, action);
+    }
 
-        #endregion
+    #endregion
 
-    }
 }

+ 170 - 0
inabox.wpf/DynamicGrid/IDynamicMemoryEntityGrid.cs

@@ -0,0 +1,170 @@
+using InABox.Clients;
+using InABox.Core;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+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="DynamicMemoryEntryGridExtensions.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>
+    where T : Entity, IRemotable, IPersistent, new()
+{
+    /// <summary>
+    /// A set of columns representing which columns have been loaded from the database.
+    /// </summary>
+    /// <remarks>
+    /// This is used to refresh the data when the columns change.<br/>
+    /// 
+    /// 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 IEnumerable<T> Items { get; }
+}
+
+public static class DynamicMemoryEntryGridExtensions
+{
+    public static void EnsureColumns<T>(this IDynamicMemoryEntityGrid<T> grid, Columns<T> columns)
+        where T : Entity, IRemotable, IPersistent, new()
+    {
+        RequireColumns(grid, columns);
+        LoadForeignProperties(grid, columns);
+    }
+
+    /// <summary>
+    /// Load the properties of any <see cref="EntityLink{T}"/>s on this <see cref="TMany"/> where the <see cref="IEntityLink.ID"/> is not <see cref="Guid.Empty"/>.
+    /// This allows us to populate columns of transient objects, as long as they are linked by the ID. What this actually then does is query each
+    /// 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()
+    {
+        // Lists of properties that we need, arranged by the entity link property which is their parent.
+        // LinkIDProperty : (Type, Properties: [(columnName, property)], Objects)
+        var newData = new Dictionary<IProperty, Tuple<Type, List<Tuple<string, IProperty>>, HashSet<T>>>();
+
+        foreach (var column in columns)
+        {
+            var property = DatabaseSchema.Property(typeof(T), column.Property);
+            if (property?.GetOuterParent(x => x.IsEntityLink) is IProperty linkProperty)
+            {
+                var remaining = column.Property[(linkProperty.Name.Length + 1)..];
+                if (remaining.Equals(nameof(IEntityLink.ID)))
+                {
+                    // This guy isn't foreign, so we don't pull him.
+                    continue;
+                }
+
+                var idProperty = DatabaseSchema.Property(typeof(T), linkProperty.Name + "." + nameof(IEntityLink.ID))!;
+
+                var linkType = linkProperty.PropertyType.GetInterfaceDefinition(typeof(IEntityLink<>))!.GenericTypeArguments[0];
+                if (!newData.TryGetValue(idProperty, out var data))
+                {
+                    data = new Tuple<Type, List<Tuple<string, IProperty>>, HashSet<T>>(
+                        linkType,
+                        new List<Tuple<string, IProperty>>(),
+                        new HashSet<T>());
+                    newData.Add(idProperty, data);
+                }
+
+                var any = false;
+                foreach (var item in grid.Items)
+                {
+                    if (!item.LoadedColumns.Contains(column.Property))
+                    {
+                        var linkID = (Guid)idProperty.Getter()(item);
+                        if (linkID != Guid.Empty)
+                        {
+                            any = true;
+                            data.Item3.Add(item);
+                        }
+                    }
+                }
+                if (any)
+                {
+                    data.Item2.Add(new(remaining, property));
+                }
+            }
+        }
+
+        var queryDefs = new List<IKeyedQueryDef>();
+        foreach (var (prop, data) in newData)
+        {
+            if (data.Item2.Any())
+            {
+                var ids = data.Item3.Select(prop.Getter()).Cast<Guid>().ToArray();
+                queryDefs.Add(new KeyedQueryDef(prop.Name, data.Item1,
+                    Filter.Create<Entity>(data.Item1, x => x.ID).InList(ids),
+                    Columns.Create(data.Item1, data.Item2.Select(x => x.Item1).ToArray()).Add<Entity>(x => x.ID)));
+            }
+        }
+        var results = Client.QueryMultiple(queryDefs);
+        foreach(var (prop, data) in newData)
+        {
+            var table = results.GetOrDefault(prop.Name);
+            if(table is null)
+            {
+                continue;
+            }
+            foreach (var entity in data.Item3)
+            {
+                var linkID = (Guid)prop.Getter()(entity);
+                var row = table.Rows.FirstOrDefault(x => x.Get<Entity, Guid>(x => x.ID) == linkID);
+                if (row is not null)
+                {
+                    foreach (var (name, property) in data.Item2)
+                    {
+                        if (!entity.LoadedColumns.Contains(property.Name))
+                        {
+                            property.Setter()(entity, row[name]);
+                            entity.LoadedColumns.Add(property.Name);
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    public static void RequireColumns<T>(this IDynamicMemoryEntityGrid<T> grid, Columns<T> columns)
+        where T : Entity, IRemotable, IPersistent, new()
+    {
+        if (grid.LoadedColumns is null) return;
+
+        // Figure out which columns we still need.
+        var newColumns = columns.Where(x => !grid.LoadedColumns.Contains(x.Property)).ToColumns();
+        if (newColumns.Any() && 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()),
+                // 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));
+                if (item is not null)
+                {
+                    row.FillObject(item, overrideExisting: false);
+                }
+            }
+            // Remember that we have now loaded this data.
+            foreach (var column in newColumns)
+            {
+                grid.LoadedColumns.Add(column.Property);
+            }
+        }
+    }
+}