| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 | 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);            }        }    }}
 |