Quellcode durchsuchen

Merge branch 'frank' of http://192.168.100.242:3000/PRSDigital/inabox into frank

Frank van den Bos vor 1 Jahr
Ursprung
Commit
50661ed0ad

+ 2 - 1
InABox.Client.Local/LocalClient.cs

@@ -232,7 +232,8 @@ namespace InABox.Clients
             {
                 Version = CoreUtils.GetVersion(),
                 ColorScheme = DbFactory.ColorScheme,
-                Logo = DbFactory.Logo
+                Logo = DbFactory.Logo,
+                DatabaseID = DbFactory.ID
             };
 
         }

+ 2 - 0
InABox.Core/Client/ClientFactory.cs

@@ -36,6 +36,8 @@ namespace InABox.Clients
 
         public static Guid SessionID { get; set; }
 
+        public static Guid DatabaseID { get; set; }
+
 
         public static Platform Platform { get; private set; }
 

+ 6 - 1
InABox.Core/Client/Request.cs

@@ -877,13 +877,15 @@ namespace InABox.Clients
         public int RestPort { get; set; }
         public int RPCPort { get; set; }
 
+        public Guid DatabaseID { get; set; }
+
         [JsonConstructor]
         public DatabaseInfo()
         {
             Version = "";
         }
 
-        public DatabaseInfo(string? colorScheme, byte[]? logo, string version, bool isHTTTPS, int restPort, int rpcPort)
+        public DatabaseInfo(string? colorScheme, byte[]? logo, string version, bool isHTTTPS, int restPort, int rpcPort, Guid databaseID)
         {
             ColorScheme = colorScheme;
             Logo = logo;
@@ -891,6 +893,7 @@ namespace InABox.Clients
             IsHTTPS = isHTTTPS;
             RestPort = restPort;
             RPCPort = rpcPort;
+            DatabaseID = databaseID;
         }
 
         public void SerializeBinary(CoreBinaryWriter writer)
@@ -914,6 +917,7 @@ namespace InABox.Clients
                 writer.Write(RestPort);
                 writer.Write(RPCPort);
             }
+            writer.Write(DatabaseID);
         }
 
         public void DeserializeBinary(CoreBinaryReader reader)
@@ -937,6 +941,7 @@ namespace InABox.Clients
                     Logger.Send(LogType.Error,"","Unable to read RestPort and RPCPort Values");
                 }
             }
+            DatabaseID = reader.ReadGuid();
         }
     }
     

+ 10 - 0
InABox.Core/Column.cs

@@ -273,6 +273,16 @@ namespace InABox.Core
             return this;
         }
 
+        public Columns<T> AddSubColumns<TSub>(Expression<Func<T, TSub>> super, Columns<TSub> sub)
+        {
+            var prefix = CoreUtils.GetFullPropertyName(super, ".") + ".";
+            foreach(var column in sub.ColumnNames())
+            {
+                columns.Add(new Column<T>(prefix + column));
+            }
+            return this;
+        }
+
         public IColumns Add<TEntity>(Expression<Func<TEntity, object?>> expression)
         {
             return Add(CoreUtils.GetFullPropertyName(expression, "."));

+ 2 - 2
InABox.Core/DataModel/AutoDataModel.cs

@@ -29,7 +29,7 @@ namespace InABox.Core
         //private List<Tuple<String,Type>> _lookuptables = new List<Tuple<String,Type>>();
         private readonly List<Tuple<Type, string, bool>> _childtables = new List<Tuple<Type, string, bool>>();
 
-        public AutoDataModel(Filter<T> filter, Columns<T>? columns, SortOrder<T>? sort) : base(filter, columns, sort)
+        public AutoDataModel(Filter<T>? filter, Columns<T>? columns, SortOrder<T>? sort) : base(filter, columns, sort)
         {
             //var props = CoreUtils.PropertyList(typeof(T), x => x.PropertyType.GetInterfaces().Contains(typeof(IEntityLink)));
             //foreach (var prop in props)
@@ -171,7 +171,7 @@ namespace InABox.Core
                     new object?[] { manyToMany1.Item2, manyToMany1.Item3, null, null, false, manyToMany1.Item4, manyToMany1.Item5 });
         }
 
-        public AutoDataModel(Filter<T> filter): this(filter, null, null) { }
+        public AutoDataModel(Filter<T>? filter): this(filter, null, null) { }
 
         public override string Name => typeof(T).EntityName().Split('.').Last();
 

+ 1 - 1
InABox.Core/DataModel/DataModel.cs

@@ -750,7 +750,7 @@ namespace InABox.Core
     public abstract class DataModel<T> : DataModel, IDataModel<T>
         where T : Entity, IRemotable, IPersistent, new()
     {
-        public DataModel(Filter<T> filter, Columns<T>? columns = null, SortOrder<T>? sort = null)
+        public DataModel(Filter<T>? filter, Columns<T>? columns = null, SortOrder<T>? sort = null)
         {
             Filter = filter;
             Columns = columns;

+ 5 - 4
InABox.Core/DocumentCache.cs

@@ -363,8 +363,11 @@ namespace InABox.Core
 
         private string GetFolder()
         {
-            var sanitised = String.Join("_", DocumentCaches.ServerName.Split(System.IO.Path.GetInvalidFileNameChars(), StringSplitOptions.RemoveEmptyEntries) ).TrimEnd('.');
-            return Path.Combine(CoreUtils.GetPath(), sanitised ?? "", "_documentcache", Tag);
+            return Path.Combine(
+                CoreUtils.GetPath(),
+                ClientFactory.DatabaseID.ToString(),
+                "_documentcache",
+                Tag);
         }
 
         private string GetFileName(Guid documentID)
@@ -379,8 +382,6 @@ namespace InABox.Core
     {
         private static readonly Dictionary<Type, IDocumentCache> Caches = new Dictionary<Type, IDocumentCache>();
 
-        public static string? ServerName { get; set; }
-
         #region Registry
 
         public static void RegisterAll()

+ 1 - 1
InABox.Core/ILookupDefinition.cs

@@ -22,7 +22,7 @@ namespace InABox.Core
 
     public interface ILookupDefinition<TLookup, TEntity> where TLookup : Entity
     {
-        Filter<TLookup> DefineFilter(TEntity[] items);
+        Filter<TLookup>? DefineFilter(TEntity[] items);
 
         /// <summary>
         /// Define the columns required for the <c>items</c> parameter of <see cref="DefineFilter(TEntity[])"/>.

+ 194 - 196
InABox.Database/DataUpdater.cs

@@ -9,263 +9,261 @@ using System.Text;
 using System.Text.RegularExpressions;
 using System.Threading.Tasks;
 
-namespace InABox.Database
+namespace InABox.Database;
+
+public class DatabaseVersion : BaseObject, IGlobalConfigurationSettings
 {
+    public string Version { get; set; }
 
-    public class DatabaseVersion : BaseObject, IGlobalConfigurationSettings
+    public DatabaseVersion()
     {
-        public string Version { get; set; }
-
-        public DatabaseVersion()
-        {
-            Version = "0.00";
-        }
+        Version = "0.00";
     }
+}
 
-    public class VersionNumber
-    {
-        public int MajorVersion { get; set; }
-        public int MinorVersion { get; set; }
+public class VersionNumber
+{
+    public int MajorVersion { get; set; }
+    public int MinorVersion { get; set; }
 
-        public string Release { get; set; }
+    public string Release { get; set; }
 
-        public bool IsDevelopmentVersion { get; set; }
+    public bool IsDevelopmentVersion { get; set; }
 
-        public VersionNumber(int majorVersion, int minorVersion, string release = "", bool isDevelopmentVersion = false)
-        {
-            MajorVersion = majorVersion;
-            MinorVersion = minorVersion;
-            Release = release;
-            IsDevelopmentVersion = isDevelopmentVersion;
-        }
+    public VersionNumber(int majorVersion, int minorVersion, string release = "", bool isDevelopmentVersion = false)
+    {
+        MajorVersion = majorVersion;
+        MinorVersion = minorVersion;
+        Release = release;
+        IsDevelopmentVersion = isDevelopmentVersion;
+    }
 
-        private static Regex _format = new(@"^(\d+)\.(\d+)([a-zA-Z]*)$");
-        public static VersionNumber Parse(string versionStr)
+    private static Regex _format = new(@"^(\d+)\.(\d+)([a-zA-Z]*)$");
+    public static VersionNumber Parse(string versionStr)
+    {
+        if(versionStr == "???")
         {
-            if(versionStr == "???")
-            {
-                return new(0, 0, "", true);
-            }
-            var match = _format.Match(versionStr);
-            if (!match.Success)
-                throw new FormatException($"'{versionStr}' is not a valid version!");
-            return new(int.Parse(match.Groups[1].Value), int.Parse(match.Groups[2].Value), match.Groups[3].Value, false);
+            return new(0, 0, "", true);
         }
-        public static bool TryParse(string versionStr, [NotNullWhen(true)] out VersionNumber? version)
+        var match = _format.Match(versionStr);
+        if (!match.Success)
+            throw new FormatException($"'{versionStr}' is not a valid version!");
+        return new(int.Parse(match.Groups[1].Value), int.Parse(match.Groups[2].Value), match.Groups[3].Value, false);
+    }
+    public static bool TryParse(string versionStr, [NotNullWhen(true)] out VersionNumber? version)
+    {
+        if (versionStr == "???")
         {
-            if (versionStr == "???")
-            {
-                version = new(0, 0, "", true);
-                return true;
-            }
-            var match = _format.Match(versionStr);
-            if (!match.Success)
-            {
-                version = null;
-                return false;
-            }
-            version = new(int.Parse(match.Groups[1].Value), int.Parse(match.Groups[2].Value), match.Groups[3].Value, false);
+            version = new(0, 0, "", true);
             return true;
         }
-
-        public static bool operator <(VersionNumber a, VersionNumber b)
+        var match = _format.Match(versionStr);
+        if (!match.Success)
         {
-            if (a.IsDevelopmentVersion)
-            {
-                return false;
-            }
-            else if (b.IsDevelopmentVersion)
-            {
-                return true;
-            }
-            return a.MajorVersion < b.MajorVersion || 
-                (a.MajorVersion == b.MajorVersion &&
-                    (a.MinorVersion < b.MinorVersion || 
-                        (a.MinorVersion == b.MinorVersion && string.Compare(a.Release, b.Release, StringComparison.Ordinal) < 0)));
+            version = null;
+            return false;
         }
-        public static bool operator >(VersionNumber a, VersionNumber b)
+        version = new(int.Parse(match.Groups[1].Value), int.Parse(match.Groups[2].Value), match.Groups[3].Value, false);
+        return true;
+    }
+
+    public static bool operator <(VersionNumber a, VersionNumber b)
+    {
+        if (a.IsDevelopmentVersion)
         {
-            return b < a;
+            return false;
         }
-
-        public static bool operator <=(VersionNumber a, VersionNumber b)
+        else if (b.IsDevelopmentVersion)
         {
-            return !(b < a);
+            return true;
         }
+        return a.MajorVersion < b.MajorVersion || 
+            (a.MajorVersion == b.MajorVersion &&
+                (a.MinorVersion < b.MinorVersion || 
+                    (a.MinorVersion == b.MinorVersion && string.Compare(a.Release, b.Release, StringComparison.Ordinal) < 0)));
+    }
+    public static bool operator >(VersionNumber a, VersionNumber b)
+    {
+        return b < a;
+    }
+
+    public static bool operator <=(VersionNumber a, VersionNumber b)
+    {
+        return !(b < a);
+    }
+
+    public static bool operator >=(VersionNumber a, VersionNumber b)
+    {
+        return !(a < b);
+    }
 
-        public static bool operator >=(VersionNumber a, VersionNumber b)
+    public override bool Equals(object? obj)
+    {
+        if(obj is VersionNumber v)
         {
-            return !(a < b);
+            return this == v;
         }
+        return false;
+    }
 
-        public override bool Equals(object? obj)
-        {
-            if(obj is VersionNumber v)
-            {
-                return this == v;
-            }
+    public override int GetHashCode()
+    {
+        if (IsDevelopmentVersion)
+            return 0;
+        return MajorVersion ^ MinorVersion ^ Release.GetHashCode();
+    }
+
+    public static bool operator ==(VersionNumber a, VersionNumber b)
+    {
+        if (a.IsDevelopmentVersion)
+            return b.IsDevelopmentVersion;
+        if (b.IsDevelopmentVersion)
             return false;
-        }
+        return a.MajorVersion == b.MajorVersion && a.MinorVersion == b.MinorVersion && a.Release == b.Release;
+    }
+    public static bool operator !=(VersionNumber a, VersionNumber b)
+    {
+        if (a.IsDevelopmentVersion)
+            return !b.IsDevelopmentVersion;
+        if (b.IsDevelopmentVersion)
+            return true;
+        return a.MajorVersion != b.MajorVersion || a.MinorVersion != b.MinorVersion || a.Release != b.Release;
+    }
 
-        public override int GetHashCode()
-        {
-            if (IsDevelopmentVersion)
-                return 0;
-            return MajorVersion ^ MinorVersion ^ Release.GetHashCode();
-        }
+    public override string ToString()
+    {
+        return IsDevelopmentVersion ? "???" : $"{MajorVersion}.{MinorVersion:D2}{Release}";
+    }
+}
 
-        public static bool operator ==(VersionNumber a, VersionNumber b)
-        {
-            if (a.IsDevelopmentVersion)
-                return b.IsDevelopmentVersion;
-            if (b.IsDevelopmentVersion)
-                return false;
-            return a.MajorVersion == b.MajorVersion && a.MinorVersion == b.MinorVersion && a.Release == b.Release;
-        }
-        public static bool operator !=(VersionNumber a, VersionNumber b)
-        {
-            if (a.IsDevelopmentVersion)
-                return !b.IsDevelopmentVersion;
-            if (b.IsDevelopmentVersion)
-                return true;
-            return a.MajorVersion != b.MajorVersion || a.MinorVersion != b.MinorVersion || a.Release != b.Release;
-        }
+public static class DataUpdater
+{
+    private static Dictionary<VersionNumber, List<DatabaseUpdateScript>> updateScripts = new();
 
-        public override string ToString()
+    /// <summary>
+    /// Register a migration script to run when updating to this version.
+    /// 
+    /// <para>The <paramref name="action"/> should probably be repeatable; 
+    /// that is, if you run it a second time, it only updates data that needed updating. This way if it accidentally somehow gets run twice, there is no issue.
+    /// </para>
+    /// </summary>
+    /// <param name="version">The version to update to.</param>
+    /// <param name="action">The action to be run.</param>
+    public static void RegisterUpdateScript<TUpdater>()
+        where TUpdater : DatabaseUpdateScript, new()
+    {
+        var updater = new TUpdater();
+        
+        if(!updateScripts.TryGetValue(updater.Version, out var list))
         {
-            return IsDevelopmentVersion ? "???" : $"{MajorVersion}.{MinorVersion:D2}{Release}";
+            list = new();
+            updateScripts[updater.Version] = list;
         }
+        list.Add(updater);
     }
 
-    public static class DataUpdater
+    private static bool MigrateDatabase(VersionNumber fromVersion, VersionNumber toVersion, out VersionNumber newVersion)
     {
-        private static Dictionary<VersionNumber, List<DatabaseUpdateScript>> updateScripts = new();
+        var versionNumbers = updateScripts.Keys.ToList();
+        versionNumbers.Sort((x, y) => x == y ? 0 : x < y ? -1 : 1);
+
+        newVersion = fromVersion;
 
-        /// <summary>
-        /// Register a migration script to run when updating to this version.
-        /// 
-        /// <para>The <paramref name="action"/> should probably be repeatable; 
-        /// that is, if you run it a second time, it only updates data that needed updating. This way if it accidentally somehow gets run twice, there is no issue.
-        /// </para>
-        /// </summary>
-        /// <param name="version">The version to update to.</param>
-        /// <param name="action">The action to be run.</param>
-        public static void RegisterUpdateScript<TUpdater>()
-            where TUpdater : DatabaseUpdateScript, new()
+        int? index = null;
+        foreach (var (i, number) in versionNumbers.Select((x, i) => new Tuple<int, VersionNumber>(i, x)))
         {
-            var updater = new TUpdater();
-            
-            if(!updateScripts.TryGetValue(updater.Version, out var list))
+            if (number > fromVersion)
             {
-                list = new();
-                updateScripts[updater.Version] = list;
+                index = i;
+                break;
             }
-            list.Add(updater);
         }
-
-        private static bool MigrateDatabase(VersionNumber fromVersion, VersionNumber toVersion, out VersionNumber newVersion)
+        if(index != null && fromVersion < toVersion)
         {
-            var versionNumbers = updateScripts.Keys.ToList();
-            versionNumbers.Sort((x, y) => x == y ? 0 : x < y ? -1 : 1);
-
-            newVersion = fromVersion;
-
-            int? index = null;
-            foreach (var (i, number) in versionNumbers.Select((x, i) => new Tuple<int, VersionNumber>(i, x)))
+            Logger.Send(LogType.Information, "", $"Updating database from {fromVersion} to {toVersion}");
+            for (int i = (int)index; i < versionNumbers.Count; i++)
             {
-                if (number > fromVersion)
+                var version = versionNumbers[i];
+                if (toVersion < version)
                 {
-                    index = i;
                     break;
                 }
-            }
-            if(index != null && fromVersion < toVersion)
-            {
-                Logger.Send(LogType.Information, "", $"Updating database from {fromVersion} to {toVersion}");
-                for (int i = (int)index; i < versionNumbers.Count; i++)
+                Logger.Send(LogType.Information, "", $"Executing update to {version}");
+                foreach(var updater in updateScripts[version])
                 {
-                    var version = versionNumbers[i];
-                    if (toVersion < version)
-                    {
-                        break;
-                    }
-                    Logger.Send(LogType.Information, "", $"Executing update to {version}");
-                    foreach(var updater in updateScripts[version])
+                    if (!updater.Update())
                     {
-                        if (!updater.Update())
-                        {
-                            Logger.Send(LogType.Error, "", $"Script failed, cancelling migration");
-                            return false;
-                        }
+                        Logger.Send(LogType.Error, "", $"Script failed, cancelling migration");
+                        return false;
                     }
-                    newVersion = version;
                 }
-                Logger.Send(LogType.Information, "", $"Data migration complete!");
+                newVersion = version;
             }
-            newVersion = toVersion;
-            return true;
+            Logger.Send(LogType.Information, "", $"Data migration complete!");
         }
+        newVersion = toVersion;
+        return true;
+    }
 
-        private static DatabaseVersion GetVersionSettings()
+    private static DatabaseVersion GetVersionSettings()
+    {
+        var result = DbFactory.Provider.Query(new Filter<GlobalSettings>(x => x.Section).IsEqualTo(nameof(DatabaseVersion)))
+            .Rows.FirstOrDefault()?.ToObject<GlobalSettings>();
+        if(result != null)
         {
-            var result = DbFactory.Provider.Query(new Filter<GlobalSettings>(x => x.Section).IsEqualTo(nameof(DatabaseVersion)))
-                .Rows.FirstOrDefault()?.ToObject<GlobalSettings>();
-            if(result != null)
-            {
-                return Serialization.Deserialize<DatabaseVersion>(result.Contents);
-            }
-            var settings = new GlobalSettings() { Section = nameof(DatabaseVersion), Key = "" };
-            var dbVersion = new DatabaseVersion() { Version = "6.30b" };
-            settings.Contents = Serialization.Serialize(dbVersion);
-            DbFactory.Provider.Save(settings);
-            return dbVersion;
+            return Serialization.Deserialize<DatabaseVersion>(result.Contents);
         }
+        var settings = new GlobalSettings() { Section = nameof(DatabaseVersion), Key = "" };
+        var dbVersion = new DatabaseVersion() { Version = "6.30b" };
+        settings.Contents = Serialization.Serialize(dbVersion);
+        DbFactory.Provider.Save(settings);
+        return dbVersion;
+    }
 
-        private static VersionNumber GetDatabaseVersion()
-        {
-            var dbVersion = GetVersionSettings();
-            return VersionNumber.Parse(dbVersion.Version);
-        }
+    private static VersionNumber GetDatabaseVersion()
+    {
+        var dbVersion = GetVersionSettings();
+        return VersionNumber.Parse(dbVersion.Version);
+    }
 
-        private static void UpdateVersionNumber(VersionNumber version)
+    private static void UpdateVersionNumber(VersionNumber version)
+    {
+        if (version.IsDevelopmentVersion)
         {
-            if (version.IsDevelopmentVersion)
-            {
-                return;
-            }
-            var dbVersion = GetVersionSettings();
-            dbVersion.Version = version.ToString();
-
-            var result = DbFactory.Provider.Query(new Filter<GlobalSettings>(x => x.Section).IsEqualTo(nameof(DatabaseVersion)))
-                .Rows.FirstOrDefault()?.ToObject<GlobalSettings>() ?? new GlobalSettings() { Section = nameof(DatabaseVersion), Key = "" };
-            result.OriginalValues["Contents"] = result.Contents;
-            result.Contents = Serialization.Serialize(dbVersion);
-            DbFactory.Provider.Save(result);
+            return;
         }
+        var dbVersion = GetVersionSettings();
+        dbVersion.Version = version.ToString();
+
+        var result = DbFactory.Provider.Query(new Filter<GlobalSettings>(x => x.Section).IsEqualTo(nameof(DatabaseVersion)))
+            .Rows.FirstOrDefault()?.ToObject<GlobalSettings>() ?? new GlobalSettings() { Section = nameof(DatabaseVersion), Key = "" };
+        result.OriginalValues["Contents"] = result.Contents;
+        result.Contents = Serialization.Serialize(dbVersion);
+        DbFactory.Provider.Save(result);
+    }
 
-        /// <summary>
-        /// Migrates the database to the current version.
-        /// </summary>
-        /// <returns><c>false</c> if the migration fails.</returns>
-        public static bool MigrateDatabase()
+    /// <summary>
+    /// Migrates the database to the current version.
+    /// </summary>
+    /// <returns><c>false</c> if the migration fails.</returns>
+    public static bool MigrateDatabase()
+    {
+        try
         {
-            try
-            {
-                var from = GetDatabaseVersion();
-                var to = VersionNumber.Parse(CoreUtils.GetVersion());
+            var from = GetDatabaseVersion();
+            var to = VersionNumber.Parse(CoreUtils.GetVersion());
 
-                var success = MigrateDatabase(from, to, out var newVersion);
-                if (newVersion != from)
-                {
-                    UpdateVersionNumber(newVersion);
-                }
-                return success;
-            }
-            catch(Exception e)
+            var success = MigrateDatabase(from, to, out var newVersion);
+            if (newVersion != from)
             {
-                Logger.Send(LogType.Error, "", $"Error while migrating database: {CoreUtils.FormatException(e)}");
-                return false;
+                UpdateVersionNumber(newVersion);
             }
+            return success;
+        }
+        catch(Exception e)
+        {
+            Logger.Send(LogType.Error, "", $"Error while migrating database: {CoreUtils.FormatException(e)}");
+            return false;
         }
     }
 }

+ 495 - 446
InABox.Database/DbFactory.cs

@@ -4,561 +4,610 @@ using InABox.Configuration;
 using InABox.Core;
 using InABox.Scripting;
 
-namespace InABox.Database
+namespace InABox.Database;
+
+public class DatabaseMetadata : BaseObject, IGlobalConfigurationSettings
 {
-    public static class DbFactory
-    {
-        public static Dictionary<string, ScriptDocument> LoadedScripts = new();
-        
-        private static IProvider? _provider;
-        public static IProvider Provider
-        {
-            get => _provider ?? throw new Exception("Provider is not set");
-            set => _provider = value;
-        }
-        public static bool IsProviderSet => _provider is not null;
-        
-        public static string? ColorScheme { get; set; }
-        public static byte[]? Logo { get; set; }
-        
-        // See notes in Request.DatabaseInfo class
-        // Once RPC transport is stable, these settings need
-        // to be removed
-        public static int RestPort { get; set; }
-        public static int RPCPort { get; set; }
-        
-        //public static Type[] Entities { get { return entities; } set { SetEntityTypes(value); } }
-        public static IEnumerable<Type> Entities
-        {
-            get { return CoreUtils.Entities.Where(x => x.GetInterfaces().Contains(typeof(IPersistent))); }
-        }
+    public Guid DatabaseID { get; set; } = Guid.NewGuid();
+}
 
-        public static Type[] Stores
-        {
-            get => stores;
-            set => SetStoreTypes(value);
-        }
+public static class DbFactory
+{
+    public static Dictionary<string, ScriptDocument> LoadedScripts = new();
 
-        public static DateTime Expiry { get; set; }
+    private static DatabaseMetadata MetaData { get; set; } = new();
 
-        public static void Start()
+    public static Guid ID
+    {
+        get => MetaData.DatabaseID;
+        set
         {
-            CoreUtils.CheckLicensing();
-            
-            var status = ValidateSchema();
-
-            if (status.Equals(SchemaStatus.New))
-                try
-                {
-                    Provider.CreateSchema(ConsolidatedObjectModel().ToArray());
-                    SaveSchema();
-                }
-                catch (Exception err)
-                {
-                    throw new Exception(string.Format("Unable to Create Schema\n\n{0}", err.Message));
-                }
-            else if (status.Equals(SchemaStatus.Changed))
-                try
-                {
-                    Provider.UpgradeSchema(ConsolidatedObjectModel().ToArray());
-                    SaveSchema();
-                }
-                catch (Exception err)
-                {
-                    throw new Exception(string.Format("Unable to Update Schema\n\n{0}", err.Message));
-                }
+            MetaData.DatabaseID = value;
+            SaveMetadata();
+        }
+    }
+    
+    private static IProvider? _provider;
+    public static IProvider Provider
+    {
+        get => _provider ?? throw new Exception("Provider is not set");
+        set => _provider = value;
+    }
+    public static bool IsProviderSet => _provider is not null;
+    
+    public static string? ColorScheme { get; set; }
+    public static byte[]? Logo { get; set; }
+    
+    // See notes in Request.DatabaseInfo class
+    // Once RPC transport is stable, these settings need
+    // to be removed
+    public static int RestPort { get; set; }
+    public static int RPCPort { get; set; }
+    
+    //public static Type[] Entities { get { return entities; } set { SetEntityTypes(value); } }
+    public static IEnumerable<Type> Entities
+    {
+        get { return CoreUtils.Entities.Where(x => x.GetInterfaces().Contains(typeof(IPersistent))); }
+    }
 
-            // Start the provider
-            Provider.Types = ConsolidatedObjectModel();
+    public static Type[] Stores
+    {
+        get => stores;
+        set => SetStoreTypes(value);
+    }
 
-            Provider.OnLog += LogMessage;
+    public static DateTime Expiry { get; set; }
 
-            Provider.Start();
+    public static void Start()
+    {
+        CoreUtils.CheckLicensing();
+        
+        var status = ValidateSchema();
 
-            if (!DataUpdater.MigrateDatabase())
+        if (status.Equals(SchemaStatus.New))
+            try
+            {
+                Provider.CreateSchema(ConsolidatedObjectModel().ToArray());
+                SaveSchema();
+            }
+            catch (Exception err)
+            {
+                throw new Exception(string.Format("Unable to Create Schema\n\n{0}", err.Message));
+            }
+        else if (status.Equals(SchemaStatus.Changed))
+            try
+            {
+                Provider.UpgradeSchema(ConsolidatedObjectModel().ToArray());
+                SaveSchema();
+            }
+            catch (Exception err)
             {
-                throw new Exception("Database migration failed. Aborting startup");
+                throw new Exception(string.Format("Unable to Update Schema\n\n{0}", err.Message));
             }
 
+        // Start the provider
+        Provider.Types = ConsolidatedObjectModel();
 
-            //Load up your custom properties here!
-            // Can't use clients (b/c were inside the database layer already
-            // but we can simply access the store directly :-)
-            //CustomProperty[] props = FindStore<CustomProperty>("", "", "", "").Load(new Filter<CustomProperty>(x=>x.ID).IsNotEqualTo(Guid.Empty),null);
-            var props = Provider.Query<CustomProperty>().Rows.Select(x => x.ToObject<CustomProperty>()).ToArray();
-            DatabaseSchema.Load(props);
+        Provider.OnLog += LogMessage;
 
-            AssertLicense();
-            BeginLicenseCheckTimer();
+        Provider.Start();
 
-            InitStores();
+        CheckMetadata();
 
-            LoadScripts();
+        if (!DataUpdater.MigrateDatabase())
+        {
+            throw new Exception("Database migration failed. Aborting startup");
         }
 
-        #region License
 
-        private enum LicenseValidation
-        {
-            Valid,
-            Missing,
-            Expired,
-            Corrupt,
-            Tampered
-        }
+        //Load up your custom properties here!
+        // Can't use clients (b/c were inside the database layer already
+        // but we can simply access the store directly :-)
+        //CustomProperty[] props = FindStore<CustomProperty>("", "", "", "").Load(new Filter<CustomProperty>(x=>x.ID).IsNotEqualTo(Guid.Empty),null);
+        var props = Provider.Query<CustomProperty>().Rows.Select(x => x.ToObject<CustomProperty>()).ToArray();
+        DatabaseSchema.Load(props);
 
-        private static LicenseValidation CheckLicenseValidity(out License? license, out LicenseData? licenseData)
-        {
-            license = Provider.Load<License>().FirstOrDefault();
-            if (license is null)
-            {
-                licenseData = null;
-                return LicenseValidation.Missing;
-            }
+        AssertLicense();
+        BeginLicenseCheckTimer();
 
-            if (!LicenseUtils.TryDecryptLicense(license.Data, out licenseData, out var error))
-                return LicenseValidation.Corrupt;
+        InitStores();
 
-            if (licenseData.Expiry < DateTime.Now)
-                return LicenseValidation.Expired;
+        LoadScripts();
+    }
 
-            var userTrackingItems = Provider.Query(
-                new Filter<UserTracking>(x => x.ID).InList(licenseData.UserTrackingItems),
-                new Columns<UserTracking>(x => x.ID), log: false).Rows.Select(x => x.Get<UserTracking, Guid>(x => x.ID));
+    #region MetaData
 
-            foreach(var item in licenseData.UserTrackingItems)
-            {
-                if (!userTrackingItems.Contains(item))
-                {
-                    return LicenseValidation.Tampered;
-                }
-            }
-            return LicenseValidation.Valid;
+    private static void SaveMetadata()
+    {
+        var settings = new GlobalSettings
+        {
+            Section = nameof(DatabaseMetadata),
+            Key = "",
+            Contents = Serialization.Serialize(MetaData)
+        };
+        DbFactory.Provider.Save(settings);
+    }
+
+    private static void CheckMetadata()
+    {
+        var result = DbFactory.Provider.Query(new Filter<GlobalSettings>(x => x.Section).IsEqualTo(nameof(DatabaseMetadata)))
+            .Rows.FirstOrDefault()?.ToObject<GlobalSettings>();
+        var data = result is not null ? Serialization.Deserialize<DatabaseMetadata>(result.Contents) : null;
+        if (data is null)
+        {
+            MetaData = new DatabaseMetadata();
+            SaveMetadata();
+        }
+        else
+        {
+            MetaData = data;
         }
+    }
 
-        private static int _expiredLicenseCounter = 0;
-        private static TimeSpan LicenseCheckInterval = TimeSpan.FromMinutes(10);
+    #endregion
 
-        private static bool _readOnly;
-        public static bool IsReadOnly { get => _readOnly; }
+    #region License
 
-        private static System.Timers.Timer LicenseTimer = new System.Timers.Timer(LicenseCheckInterval.TotalMilliseconds) { AutoReset = true };
+    private enum LicenseValidation
+    {
+        Valid,
+        Missing,
+        Expired,
+        Corrupt,
+        Tampered
+    }
 
-        private static void LogRenew(string message)
+    private static LicenseValidation CheckLicenseValidity(out License? license, out LicenseData? licenseData)
+    {
+        license = Provider.Load<License>().FirstOrDefault();
+        if (license is null)
         {
-            LogImportant($"{message} Please renew your license before then, or your database will go into read-only mode; it will be locked for saving anything until you renew your license. For help with renewing your license, please see the documentation at https://prsdigital.com.au/wiki/index.php/License_Renewal.");
+            licenseData = null;
+            return LicenseValidation.Missing;
         }
-        private static void LogLicenseExpiry(DateTime expiry)
+
+        if (!LicenseUtils.TryDecryptLicense(license.Data, out licenseData, out var error))
+            return LicenseValidation.Corrupt;
+
+        if (licenseData.Expiry < DateTime.Now)
+            return LicenseValidation.Expired;
+
+        var userTrackingItems = Provider.Query(
+            new Filter<UserTracking>(x => x.ID).InList(licenseData.UserTrackingItems),
+            new Columns<UserTracking>(x => x.ID), log: false).Rows.Select(x => x.Get<UserTracking, Guid>(x => x.ID));
+
+        foreach(var item in licenseData.UserTrackingItems)
         {
-            if (expiry.Date == DateTime.Today)
-            {
-                LogRenew($"Your database license is expiring today at {expiry.TimeOfDay:HH:mm}!");
-                return;
-            }
-            var diffInDays = (expiry - DateTime.Now).TotalDays;
-            if(diffInDays < 1)
-            {
-                LogRenew($"Your database license will expire in less than a day, on the {expiry:dd MMM yyyy} at {expiry:hh:mm:tt}.");
-            }
-            else if(diffInDays < 3 && (_expiredLicenseCounter * LicenseCheckInterval).TotalHours >= 1)
+            if (!userTrackingItems.Contains(item))
             {
-                LogRenew($"Your database license will expire in less than three days, on the {expiry:dd MMM yyyy} at {expiry:hh:mm:tt}.");
-                _expiredLicenseCounter = 0;
+                return LicenseValidation.Tampered;
             }
-            else if(diffInDays < 7 && (_expiredLicenseCounter * LicenseCheckInterval).TotalHours >= 2)
-            {
-                LogRenew($"Your database license will expire in less than a week, on the {expiry:dd MMM yyyy} at {expiry:hh:mm:tt}.");
-                _expiredLicenseCounter = 0;
-            }
-            ++_expiredLicenseCounter;
         }
+        return LicenseValidation.Valid;
+    }
 
-        public static void LogReadOnly()
-        {
-            LogError("Database is read-only because your license is invalid!");
-        }
+    private static int _expiredLicenseCounter = 0;
+    private static TimeSpan LicenseCheckInterval = TimeSpan.FromMinutes(10);
+
+    private static bool _readOnly;
+    public static bool IsReadOnly { get => _readOnly; }
 
-        private static void BeginReadOnly()
+    private static System.Timers.Timer LicenseTimer = new System.Timers.Timer(LicenseCheckInterval.TotalMilliseconds) { AutoReset = true };
+
+    private static void LogRenew(string message)
+    {
+        LogImportant($"{message} Please renew your license before then, or your database will go into read-only mode; it will be locked for saving anything until you renew your license. For help with renewing your license, please see the documentation at https://prsdigital.com.au/wiki/index.php/License_Renewal.");
+    }
+    private static void LogLicenseExpiry(DateTime expiry)
+    {
+        if (expiry.Date == DateTime.Today)
         {
-            LogImportant("Your database is now in read-only mode, since your license is invalid; you will be unable to save any records to the database until you renew your license. For help with renewing your license, please see the documentation at https://prsdigital.com.au/wiki/index.php/License_Renewal.");
-            _readOnly = true;
+            LogRenew($"Your database license is expiring today at {expiry.TimeOfDay:HH:mm}!");
+            return;
         }
-        private static void EndReadOnly()
+        var diffInDays = (expiry - DateTime.Now).TotalDays;
+        if(diffInDays < 1)
         {
-            LogImportant("Valid license found; the database is no longer read-only.");
-            _readOnly = false;
+            LogRenew($"Your database license will expire in less than a day, on the {expiry:dd MMM yyyy} at {expiry:hh:mm:tt}.");
         }
-
-        private static void BeginLicenseCheckTimer()
+        else if(diffInDays < 3 && (_expiredLicenseCounter * LicenseCheckInterval).TotalHours >= 1)
         {
-            LicenseTimer.Elapsed += LicenseTimer_Elapsed;
-            LicenseTimer.Start();
+            LogRenew($"Your database license will expire in less than three days, on the {expiry:dd MMM yyyy} at {expiry:hh:mm:tt}.");
+            _expiredLicenseCounter = 0;
         }
-
-        private static void LicenseTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
+        else if(diffInDays < 7 && (_expiredLicenseCounter * LicenseCheckInterval).TotalHours >= 2)
         {
-            AssertLicense();
+            LogRenew($"Your database license will expire in less than a week, on the {expiry:dd MMM yyyy} at {expiry:hh:mm:tt}.");
+            _expiredLicenseCounter = 0;
         }
+        ++_expiredLicenseCounter;
+    }
+
+    public static void LogReadOnly()
+    {
+        LogError("Database is read-only because your license is invalid!");
+    }
+
+    private static void BeginReadOnly()
+    {
+        LogImportant("Your database is now in read-only mode, since your license is invalid; you will be unable to save any records to the database until you renew your license. For help with renewing your license, please see the documentation at https://prsdigital.com.au/wiki/index.php/License_Renewal.");
+        _readOnly = true;
+    }
+    private static void EndReadOnly()
+    {
+        LogImportant("Valid license found; the database is no longer read-only.");
+        _readOnly = false;
+    }
+
+    private static void BeginLicenseCheckTimer()
+    {
+        LicenseTimer.Elapsed += LicenseTimer_Elapsed;
+        LicenseTimer.Start();
+    }
 
-        private static Random LicenseIDGenerate = new Random();
-        private static void UpdateValidLicense(License license, LicenseData licenseData)
+    private static void LicenseTimer_Elapsed(object? sender, System.Timers.ElapsedEventArgs e)
+    {
+        AssertLicense();
+    }
+
+    private static Random LicenseIDGenerate = new Random();
+    private static void UpdateValidLicense(License license, LicenseData licenseData)
+    {
+        var ids = Provider.Query(
+            new Filter<UserTracking>(x => x.Created).IsGreaterThanOrEqualTo(licenseData.LastRenewal),
+            new Columns<UserTracking>(x => x.ID), log: false);
+        var newIDList = new List<Guid>();
+        if(ids.Rows.Count > 0)
         {
-            var ids = Provider.Query(
-                new Filter<UserTracking>(x => x.Created).IsGreaterThanOrEqualTo(licenseData.LastRenewal),
-                new Columns<UserTracking>(x => x.ID), log: false);
-            var newIDList = new List<Guid>();
-            if(ids.Rows.Count > 0)
+            for (int i = 0; i < 10; i++)
             {
-                for (int i = 0; i < 10; i++)
-                {
-                    newIDList.Add(ids.Rows[LicenseIDGenerate.Next(0, ids.Rows.Count)].Get<UserTracking, Guid>(x => x.ID));
-                }
+                newIDList.Add(ids.Rows[LicenseIDGenerate.Next(0, ids.Rows.Count)].Get<UserTracking, Guid>(x => x.ID));
             }
-            licenseData.UserTrackingItems = newIDList.ToArray();
+        }
+        licenseData.UserTrackingItems = newIDList.ToArray();
 
-            if(LicenseUtils.TryEncryptLicense(licenseData, out var newData, out var error))
-            {
-                license.Data = newData;
-                Provider.Save(license);
-            }
+        if(LicenseUtils.TryEncryptLicense(licenseData, out var newData, out var error))
+        {
+            license.Data = newData;
+            Provider.Save(license);
         }
+    }
 
-        private static void AssertLicense()
+    private static void AssertLicense()
+    {
+        var result = CheckLicenseValidity(out var license, out var licenseData);
+        if (IsReadOnly)
         {
-            var result = CheckLicenseValidity(out var license, out var licenseData);
-            if (IsReadOnly)
+            if(result == LicenseValidation.Valid)
             {
-                if(result == LicenseValidation.Valid)
-                {
-                    EndReadOnly();
-                }
-                return;
+                EndReadOnly();
             }
+            return;
+        }
 
-            // TODO: Switch to real system
-            if(result != LicenseValidation.Valid)
+        // TODO: Switch to real system
+        if(result != LicenseValidation.Valid)
+        {
+            var newLicense = LicenseUtils.GenerateNewLicense();
+            if (LicenseUtils.TryEncryptLicense(newLicense, out var newData, out var error))
             {
-                var newLicense = LicenseUtils.GenerateNewLicense();
-                if (LicenseUtils.TryEncryptLicense(newLicense, out var newData, out var error))
-                {
-                    if (license == null)
-                        license = new License();
-                    license.Data = newData;
-                    Provider.Save(license);
-                }
-                else
-                {
-                    Logger.Send(LogType.Error, "", $"Error updating license: {error}");
-                }
-                return;
+                if (license == null)
+                    license = new License();
+                license.Data = newData;
+                Provider.Save(license);
             }
             else
             {
-                return;
-            }
-
-            switch (result)
-            {
-                case LicenseValidation.Valid:
-                    LogLicenseExpiry(licenseData!.Expiry);
-                    UpdateValidLicense(license, licenseData);
-                    break;
-                case LicenseValidation.Missing:
-                    LogImportant("Database is unlicensed!");
-                    BeginReadOnly();
-                    break;
-                case LicenseValidation.Expired:
-                    LogImportant("Database license has expired!");
-                    BeginReadOnly();
-                    break;
-                case LicenseValidation.Corrupt:
-                    LogImportant("Database license is corrupt - you will need to renew your license.");
-                    BeginReadOnly();
-                    break;
-                case LicenseValidation.Tampered:
-                    LogImportant("Database license has been tampered with - you will need to renew your license.");
-                    BeginReadOnly();
-                    break;
+                Logger.Send(LogType.Error, "", $"Error updating license: {error}");
             }
+            return;
+        }
+        else
+        {
+            return;
+        }
+
+        switch (result)
+        {
+            case LicenseValidation.Valid:
+                LogLicenseExpiry(licenseData!.Expiry);
+                UpdateValidLicense(license, licenseData);
+                break;
+            case LicenseValidation.Missing:
+                LogImportant("Database is unlicensed!");
+                BeginReadOnly();
+                break;
+            case LicenseValidation.Expired:
+                LogImportant("Database license has expired!");
+                BeginReadOnly();
+                break;
+            case LicenseValidation.Corrupt:
+                LogImportant("Database license is corrupt - you will need to renew your license.");
+                BeginReadOnly();
+                break;
+            case LicenseValidation.Tampered:
+                LogImportant("Database license has been tampered with - you will need to renew your license.");
+                BeginReadOnly();
+                break;
         }
+    }
 
-        #endregion
+    #endregion
 
-        #region Logging
+    #region Logging
 
-        private static void LogMessage(LogType type, string message)
-        {
-            Logger.Send(type, "", message);
-        }
+    private static void LogMessage(LogType type, string message)
+    {
+        Logger.Send(type, "", message);
+    }
 
-        private static void LogInfo(string message)
-        {
-            Logger.Send(LogType.Information, "", message);
-        }
-        private static void LogImportant(string message)
-        {
-            Logger.Send(LogType.Important, "", message);
-        }
-        private static void LogError(string message)
-        {
-            Logger.Send(LogType.Error, "", message);
-        }
+    private static void LogInfo(string message)
+    {
+        Logger.Send(LogType.Information, "", message);
+    }
+    private static void LogImportant(string message)
+    {
+        Logger.Send(LogType.Important, "", message);
+    }
+    private static void LogError(string message)
+    {
+        Logger.Send(LogType.Error, "", message);
+    }
 
-        #endregion
+    #endregion
 
-        public static void InitStores()
+    public static void InitStores()
+    {
+        foreach (var storetype in stores)
         {
-            foreach (var storetype in stores)
-            {
-                var store = (Activator.CreateInstance(storetype) as IStore)!;
-                store.Provider = Provider;
-                store.Init();
-            }
+            var store = (Activator.CreateInstance(storetype) as IStore)!;
+            store.Provider = Provider;
+            store.Init();
         }
+    }
 
-        public static IStore FindStore(Type type, Guid userguid, string userid, Platform platform, string version)
-        {
-            var defType = typeof(Store<>).MakeGenericType(type);
-            Type? subType = Stores.Where(myType => myType.IsSubclassOf(defType)).FirstOrDefault();
-
-            var store = (Activator.CreateInstance(subType ?? defType) as IStore)!;
-            
-            store.Provider = Provider;
-            store.UserGuid = userguid;
-            store.UserID = userid;
-            store.Platform = platform;
-            store.Version = version;
+    public static IStore FindStore(Type type, Guid userguid, string userid, Platform platform, string version)
+    {
+        var defType = typeof(Store<>).MakeGenericType(type);
+        Type? subType = Stores.Where(myType => myType.IsSubclassOf(defType)).FirstOrDefault();
 
-            return store;
-        }
+        var store = (Activator.CreateInstance(subType ?? defType) as IStore)!;
         
-        public static IStore<TEntity> FindStore<TEntity>(Guid userguid, string userid, Platform platform, string version) 
-            where TEntity : Entity, new()
-        {
-            return (FindStore(typeof(TEntity), userguid, userid, platform, version) as IStore<TEntity>)!;
-        }
+        store.Provider = Provider;
+        store.UserGuid = userguid;
+        store.UserID = userid;
+        store.Platform = platform;
+        store.Version = version;
 
-        private static CoreTable DoQueryMultipleQuery<TEntity>(
-            IQueryDef query,
-            Guid userguid, string userid, Platform platform, string version) 
-            where TEntity : Entity, new()
-        {
-            var store = FindStore<TEntity>(userguid, userid, platform, version);
-            return store.Query(query.Filter as Filter<TEntity>, query.Columns as Columns<TEntity>, query.SortOrder as SortOrder<TEntity>);
-        }
+        return store;
+    }
+    
+    public static IStore<TEntity> FindStore<TEntity>(Guid userguid, string userid, Platform platform, string version) 
+        where TEntity : Entity, new()
+    {
+        return (FindStore(typeof(TEntity), userguid, userid, platform, version) as IStore<TEntity>)!;
+    }
 
-        public static Dictionary<string, CoreTable> QueryMultiple(
-            Dictionary<string, IQueryDef> queries, 
-            Guid userguid, string userid, Platform platform, string version)
-        {
-            var result = new Dictionary<string, CoreTable>();
+    private static CoreTable DoQueryMultipleQuery<TEntity>(
+        IQueryDef query,
+        Guid userguid, string userid, Platform platform, string version) 
+        where TEntity : Entity, new()
+    {
+        var store = FindStore<TEntity>(userguid, userid, platform, version);
+        return store.Query(query.Filter as Filter<TEntity>, query.Columns as Columns<TEntity>, query.SortOrder as SortOrder<TEntity>);
+    }
 
-            var queryMethod = typeof(DbFactory).GetMethod(nameof(DoQueryMultipleQuery), BindingFlags.NonPublic | BindingFlags.Static)!;
+    public static Dictionary<string, CoreTable> QueryMultiple(
+        Dictionary<string, IQueryDef> queries, 
+        Guid userguid, string userid, Platform platform, string version)
+    {
+        var result = new Dictionary<string, CoreTable>();
+
+        var queryMethod = typeof(DbFactory).GetMethod(nameof(DoQueryMultipleQuery), BindingFlags.NonPublic | BindingFlags.Static)!;
 
-            var tasks = new List<Task>();
-            foreach (var item in queries)
-                tasks.Add(Task.Run(() =>
+        var tasks = new List<Task>();
+        foreach (var item in queries)
+            tasks.Add(Task.Run(() =>
+            {
+                result[item.Key] = (queryMethod.MakeGenericMethod(item.Value.Type).Invoke(Provider, new object[]
                 {
-                    result[item.Key] = (queryMethod.MakeGenericMethod(item.Value.Type).Invoke(Provider, new object[]
-                    {
-                        item.Value,
-                        userguid, userid, platform, version
-                    }) as CoreTable)!;
-                }));
-
-            Task.WaitAll(tasks.ToArray());
-            return result;
-        }
+                    item.Value,
+                    userguid, userid, platform, version
+                }) as CoreTable)!;
+            }));
 
-        #region Supported Types
+        Task.WaitAll(tasks.ToArray());
+        return result;
+    }
 
-        private class ModuleConfiguration : Dictionary<string, bool>, ILocalConfigurationSettings
-        {
-        }
+    #region Supported Types
 
-        private static Type[]? _dbtypes;
+    private class ModuleConfiguration : Dictionary<string, bool>, ILocalConfigurationSettings
+    {
+    }
 
-        public static IEnumerable<string> SupportedTypes()
-        {
-            _dbtypes ??= LoadSupportedTypes();
-            return _dbtypes.Select(x => x.EntityName().Replace(".", "_"));
-        }
+    private static Type[]? _dbtypes;
 
-        private static Type[] LoadSupportedTypes()
-        {
-            var result = new List<Type>();
+    public static IEnumerable<string> SupportedTypes()
+    {
+        _dbtypes ??= LoadSupportedTypes();
+        return _dbtypes.Select(x => x.EntityName().Replace(".", "_"));
+    }
 
-            var path = Provider.URL.ToLower();
-            var config = new LocalConfiguration<ModuleConfiguration>(Path.GetDirectoryName(path) ?? "", Path.GetFileName(path)).Load();
-            var bChanged = false;
-            foreach (var type in Entities)
-            {
-                var key = type.EntityName();
-                if (config.ContainsKey(key))
-                {
-                    if (config[key])
-                        //Logger.Send(LogType.Information, "", String.Format("{0} is enabled", key));
-                        result.Add(type);
-                    else
-                        Logger.Send(LogType.Information, "", string.Format("Entity [{0}] is disabled", key));
-                }
-                else
-                {
-                    //Logger.Send(LogType.Information, "", String.Format("{0} does not exist - enabling", key));
+    private static Type[] LoadSupportedTypes()
+    {
+        var result = new List<Type>();
 
-                    config[key] = true;
+        var path = Provider.URL.ToLower();
+        var config = new LocalConfiguration<ModuleConfiguration>(Path.GetDirectoryName(path) ?? "", Path.GetFileName(path)).Load();
+        var bChanged = false;
+        foreach (var type in Entities)
+        {
+            var key = type.EntityName();
+            if (config.ContainsKey(key))
+            {
+                if (config[key])
+                    //Logger.Send(LogType.Information, "", String.Format("{0} is enabled", key));
                     result.Add(type);
-                    bChanged = true;
-                }
+                else
+                    Logger.Send(LogType.Information, "", string.Format("Entity [{0}] is disabled", key));
             }
+            else
+            {
+                //Logger.Send(LogType.Information, "", String.Format("{0} does not exist - enabling", key));
 
-            if (bChanged)
-                new LocalConfiguration<ModuleConfiguration>(Path.GetDirectoryName(path) ?? "", Path.GetFileName(path)).Save(config);
-            return result.ToArray();
+                config[key] = true;
+                result.Add(type);
+                bChanged = true;
+            }
         }
 
-        public static bool IsSupported<T>() where T : Entity
-        {
-            _dbtypes ??= LoadSupportedTypes();
-            return _dbtypes.Contains(typeof(T));
-        }
+        if (bChanged)
+            new LocalConfiguration<ModuleConfiguration>(Path.GetDirectoryName(path) ?? "", Path.GetFileName(path)).Save(config);
+        return result.ToArray();
+    }
 
-        #endregion
+    public static bool IsSupported<T>() where T : Entity
+    {
+        _dbtypes ??= LoadSupportedTypes();
+        return _dbtypes.Contains(typeof(T));
+    }
 
-        //public static void OpenSession(bool write)
-        //{
-        //	Provider.OpenSession(write);
-        //}
+    #endregion
 
-        //public static void CloseSession()
-        //{
-        //	Provider.CloseSession();
-        //}
+    //public static void OpenSession(bool write)
+    //{
+    //	Provider.OpenSession(write);
+    //}
 
-        #region Private Methods
+    //public static void CloseSession()
+    //{
+    //	Provider.CloseSession();
+    //}
 
-        public static void LoadScripts()
-        {
-            Logger.Send(LogType.Information, "", "Loading Script Cache...");
-            LoadedScripts.Clear();
-            var scripts = Provider.Load(
-                new Filter<Script>
-                        (x => x.ScriptType).IsEqualTo(ScriptType.BeforeQuery)
-                    .Or(x => x.ScriptType).IsEqualTo(ScriptType.AfterQuery)
-                    .Or(x => x.ScriptType).IsEqualTo(ScriptType.BeforeSave)
-                    .Or(x => x.ScriptType).IsEqualTo(ScriptType.AfterSave)
-                    .Or(x => x.ScriptType).IsEqualTo(ScriptType.BeforeDelete)
-                    .Or(x => x.ScriptType).IsEqualTo(ScriptType.AfterDelete)
-                    .Or(x => x.ScriptType).IsEqualTo(ScriptType.AfterLoad)
-            );
-            foreach (var script in scripts)
+    #region Private Methods
+
+    public static void LoadScripts()
+    {
+        Logger.Send(LogType.Information, "", "Loading Script Cache...");
+        LoadedScripts.Clear();
+        var scripts = Provider.Load(
+            new Filter<Script>
+                    (x => x.ScriptType).IsEqualTo(ScriptType.BeforeQuery)
+                .Or(x => x.ScriptType).IsEqualTo(ScriptType.AfterQuery)
+                .Or(x => x.ScriptType).IsEqualTo(ScriptType.BeforeSave)
+                .Or(x => x.ScriptType).IsEqualTo(ScriptType.AfterSave)
+                .Or(x => x.ScriptType).IsEqualTo(ScriptType.BeforeDelete)
+                .Or(x => x.ScriptType).IsEqualTo(ScriptType.AfterDelete)
+                .Or(x => x.ScriptType).IsEqualTo(ScriptType.AfterLoad)
+        );
+        foreach (var script in scripts)
+        {
+            var key = string.Format("{0} {1}", script.Section, script.ScriptType.ToString());
+            var doc = new ScriptDocument(script.Code);
+            if (doc.Compile())
             {
-                var key = string.Format("{0} {1}", script.Section, script.ScriptType.ToString());
-                var doc = new ScriptDocument(script.Code);
-                if (doc.Compile())
-                {
-                    Logger.Send(LogType.Information, "",
-                        string.Format("- {0}.{1} Compiled Successfully", script.Section, script.ScriptType.ToString()));
-                    LoadedScripts[key] = doc;
-                }
-                else
-                {
-                    Logger.Send(LogType.Error, "",
-                        string.Format("- {0}.{1} Compile Exception:\n{2}", script.Section, script.ScriptType.ToString(), doc.Result));
-                }
+                Logger.Send(LogType.Information, "",
+                    string.Format("- {0}.{1} Compiled Successfully", script.Section, script.ScriptType.ToString()));
+                LoadedScripts[key] = doc;
+            }
+            else
+            {
+                Logger.Send(LogType.Error, "",
+                    string.Format("- {0}.{1} Compile Exception:\n{2}", script.Section, script.ScriptType.ToString(), doc.Result));
             }
-
-            Logger.Send(LogType.Information, "", "Loading Script Cache Complete");
         }
 
-        //private static Type[] entities = null;
-        //private static void SetEntityTypes(Type[] types)
-        //{
-        //	foreach (Type type in types)
-        //	{
-        //		if (!type.IsSubclassOf(typeof(Entity)))
-        //			throw new Exception(String.Format("{0} is not a valid entity", type.Name));
-        //	}
-        //	entities = types;
-        //}
+        Logger.Send(LogType.Information, "", "Loading Script Cache Complete");
+    }
 
-        private static Type[] stores = { };
+    //private static Type[] entities = null;
+    //private static void SetEntityTypes(Type[] types)
+    //{
+    //	foreach (Type type in types)
+    //	{
+    //		if (!type.IsSubclassOf(typeof(Entity)))
+    //			throw new Exception(String.Format("{0} is not a valid entity", type.Name));
+    //	}
+    //	entities = types;
+    //}
 
-        private static void SetStoreTypes(Type[] types)
-        {
-            types = types.Where(
-                myType => myType.IsClass
-                    && !myType.IsAbstract
-                    && !myType.IsGenericType).ToArray();
-            foreach (var type in types)
-                if (!type.GetInterfaces().Contains(typeof(IStore)))
-                    throw new Exception(string.Format("{0} is not a valid store", type.Name));
-            stores = types;
-        }
+    private static Type[] stores = { };
 
-        private static Type[] ConsolidatedObjectModel()
-        {
-            // Add the core types from InABox.Core
-            var types = new List<Type>();
-            //var coreTypes = CoreUtils.TypeList(
-            //	new Assembly[] { typeof(Entity).Assembly },
-            //	myType =>
-            //	myType.IsClass
-            //	&& !myType.IsAbstract
-            //	&& !myType.IsGenericType
-            //	&& myType.IsSubclassOf(typeof(Entity))
-            //	&& myType.GetInterfaces().Contains(typeof(IRemotable))
-            //);
-            //types.AddRange(coreTypes);
-
-            // Now add the end-user object model
-            types.AddRange(Entities.Where(x =>
-                x.GetTypeInfo().IsClass
-                && !x.GetTypeInfo().IsGenericType
-                && x.GetTypeInfo().IsSubclassOf(typeof(Entity))
-            ));
-
-            return types.ToArray();
-        }
+    private static void SetStoreTypes(Type[] types)
+    {
+        types = types.Where(
+            myType => myType.IsClass
+                && !myType.IsAbstract
+                && !myType.IsGenericType).ToArray();
+        foreach (var type in types)
+            if (!type.GetInterfaces().Contains(typeof(IStore)))
+                throw new Exception(string.Format("{0} is not a valid store", type.Name));
+        stores = types;
+    }
 
-        private enum SchemaStatus
-        {
-            New,
-            Changed,
-            Validated
-        }
+    private static Type[] ConsolidatedObjectModel()
+    {
+        // Add the core types from InABox.Core
+        var types = new List<Type>();
+        //var coreTypes = CoreUtils.TypeList(
+        //	new Assembly[] { typeof(Entity).Assembly },
+        //	myType =>
+        //	myType.IsClass
+        //	&& !myType.IsAbstract
+        //	&& !myType.IsGenericType
+        //	&& myType.IsSubclassOf(typeof(Entity))
+        //	&& myType.GetInterfaces().Contains(typeof(IRemotable))
+        //);
+        //types.AddRange(coreTypes);
+
+        // Now add the end-user object model
+        types.AddRange(Entities.Where(x =>
+            x.GetTypeInfo().IsClass
+            && !x.GetTypeInfo().IsGenericType
+            && x.GetTypeInfo().IsSubclassOf(typeof(Entity))
+        ));
+
+        return types.ToArray();
+    }
 
-        private static Dictionary<string, Type> GetSchema()
-        {
-            var model = new Dictionary<string, Type>();
-            var objectmodel = ConsolidatedObjectModel();
-            foreach (var type in objectmodel)
-            {
-                Dictionary<string, Type> thismodel = CoreUtils.PropertyList(type, x => true, true);
-                foreach (var key in thismodel.Keys)
-                    model[type.Name + "." + key] = thismodel[key];
-            }
+    private enum SchemaStatus
+    {
+        New,
+        Changed,
+        Validated
+    }
 
-            return model;
-            //return Serialization.Serialize(model, Formatting.Indented);
+    private static Dictionary<string, Type> GetSchema()
+    {
+        var model = new Dictionary<string, Type>();
+        var objectmodel = ConsolidatedObjectModel();
+        foreach (var type in objectmodel)
+        {
+            Dictionary<string, Type> thismodel = CoreUtils.PropertyList(type, x => true, true);
+            foreach (var key in thismodel.Keys)
+                model[type.Name + "." + key] = thismodel[key];
         }
 
-        private static SchemaStatus ValidateSchema()
-        {
-            var db_schema = Provider.GetSchema();
-            if (db_schema.Count() == 0)
-                return SchemaStatus.New;
+        return model;
+        //return Serialization.Serialize(model, Formatting.Indented);
+    }
 
-            var mdl_json = Serialization.Serialize(GetSchema());
-            var db_json = Serialization.Serialize(db_schema);
-            return mdl_json.Equals(db_json) ? SchemaStatus.Validated : SchemaStatus.Changed;
-        }
+    private static SchemaStatus ValidateSchema()
+    {
+        var db_schema = Provider.GetSchema();
+        if (db_schema.Count() == 0)
+            return SchemaStatus.New;
 
-        private static void SaveSchema()
-        {
-            Provider.SaveSchema(GetSchema());
-        }
+        var mdl_json = Serialization.Serialize(GetSchema());
+        var db_json = Serialization.Serialize(db_schema);
+        return mdl_json.Equals(db_json) ? SchemaStatus.Validated : SchemaStatus.Changed;
+    }
 
-        #endregion
+    private static void SaveSchema()
+    {
+        Provider.SaveSchema(GetSchema());
     }
+
+    #endregion
 }

+ 1 - 1
InABox.Server/RPC/Handlers/Info.cs

@@ -12,7 +12,7 @@ namespace InABox.Rpc
         {
             var response = new RpcInfoResult()
             {
-                Info = new DatabaseInfo(DbFactory.ColorScheme, DbFactory.Logo, CoreUtils.GetVersion(), Sender.IsSecure(), DbFactory.RestPort, DbFactory.RPCPort)
+                Info = new DatabaseInfo(DbFactory.ColorScheme, DbFactory.Logo, CoreUtils.GetVersion(), Sender.IsSecure(), DbFactory.RestPort, DbFactory.RPCPort, DbFactory.ID)
             };
             
             return response;

+ 1 - 1
InABox.Server/RestService.cs

@@ -208,7 +208,7 @@ namespace InABox.API
 
         public static InfoResponse Info(InfoRequest request)
         {
-            var response = new InfoResponse(new DatabaseInfo(DbFactory.ColorScheme, DbFactory.Logo, CoreUtils.GetVersion(), IsHTTPS, DbFactory.RestPort, DbFactory.RPCPort));
+            var response = new InfoResponse(new DatabaseInfo(DbFactory.ColorScheme, DbFactory.Logo, CoreUtils.GetVersion(), IsHTTPS, DbFactory.RestPort, DbFactory.RPCPort, DbFactory.ID));
             response.Status = StatusCode.OK;
             return response;
         }

+ 19 - 2
inabox.wpf/DynamicGrid/BaseDynamicGrid.cs

@@ -97,7 +97,7 @@ namespace InABox.DynamicGrid
             RowStyleSelector = GetRowStyleSelector();
             RowStyleSelector.GetStyle += (row, style) => GetRowStyle(row, style);
             
-            HiddenColumns = new List<Expression<Func<T, object?>>>();
+            HiddenColumns = new HiddenColumnsList();
         }
 
         /// <summary>
@@ -134,7 +134,24 @@ namespace InABox.DynamicGrid
         public bool HasOption(DynamicGridOption option, IEnumerable<DynamicGridOption>? options = null) => (options ?? Options).Contains(option);
         bool IDynamicGrid.HasOption(InABox.DynamicGrid.DynamicGridOption option) => HasOption(option, null);
 
-        public List<Expression<Func<T, object?>>> HiddenColumns { get; }
+        public class HiddenColumnsList
+        {
+            private List<string> Columns { get; set; } = new();
+
+            public IEnumerable<string> ColumnNames => Columns;
+
+            public void Add(Expression<Func<T, object?>> column)
+            {
+                Columns.Add(CoreUtils.GetFullPropertyName(column, "."));
+            }
+
+            public void Add(IColumn column)
+            {
+                Columns.Add(column.Property);
+            }
+        }
+
+        public HiddenColumnsList HiddenColumns { get; }
 
         public void InitialiseEditorForm(IDynamicEditorForm editor, object[] items, Func<Type, CoreTable>? pageDataHandler = null, bool preloadPages = false)
         {

+ 5 - 1
inabox.wpf/DynamicGrid/Columns/DynamicImageColumn.cs

@@ -1,10 +1,14 @@
+using System.Drawing;
 using System.Windows.Media.Imaging;
 using InABox.Core;
+using InABox.WPF;
 
 namespace InABox.DynamicGrid;
 
 public class DynamicImageColumn : DynamicActionColumn
 {
+    private static readonly BitmapImage empty = new Bitmap(32, 32).ToBitmapImage();
+
     public delegate BitmapImage? GetImageDelegate(CoreRow? row);
 
     public DynamicImageColumn(GetImageDelegate image, ActionDelegate? action = null)
@@ -25,5 +29,5 @@ public class DynamicImageColumn : DynamicActionColumn
 
     public bool AllowHeaderClick { get; set; } = false;
         
-    public override object? Data(CoreRow? row) => Image?.Invoke(row);
+    public override object? Data(CoreRow? row) => Image?.Invoke(row) ?? empty;
 }

+ 29 - 29
inabox.wpf/DynamicGrid/DynamicCrossJoinGrid.cs

@@ -6,38 +6,38 @@ using System.Linq.Expressions;
 using System.Text;
 using System.Threading.Tasks;
 
-namespace InABox.DynamicGrid
+namespace InABox.DynamicGrid;
+
+public abstract class DynamicCrossJoinGrid<TEntity, TLeft> : DynamicDataGrid<TEntity>
+    where TEntity : Entity, IRemotable, IPersistent, new()
+    where TLeft : Entity
 {
-    public abstract class DynamicCrossJoinGrid<TEntity, TLeft> : DynamicDataGrid<TEntity>
-        where TEntity : Entity, IRemotable, IPersistent, new()
-        where TLeft : Entity
-    {
-        
-        public TLeft? Left { get; set; }
-        public abstract Expression<Func<TEntity, Guid>> LeftMapping { get; }
-        public abstract Expression<Func<TLeft, Guid>> LeftProperty { get; }
+    
+    public TLeft? Left { get; set; }
+    public abstract Expression<Func<TEntity, Guid>> LeftMapping { get; }
+    public abstract Expression<Func<TLeft, Guid>> LeftProperty { get; }
 
-        protected override void DoReconfigure(FluentList<DynamicGridOption> options)
-        {
-            base.DoReconfigure(options);
-            options.BeginUpdate().Clear().Add(DynamicGridOption.SelectColumns).EndUpdate();
-        }
+    protected override void DoReconfigure(FluentList<DynamicGridOption> options)
+    {
+        base.DoReconfigure(options);
+        options.BeginUpdate().Clear().Add(DynamicGridOption.SelectColumns).EndUpdate();
+    }
 
-        protected override void GenerateColumns(DynamicGridColumns columns)
-        {
-            base.GenerateColumns(columns);
-            var prefix = $"{typeof(TLeft).Name}.";
-            columns.RemoveAll(x => x.ColumnName.StartsWith(prefix));
-        }
+    public override DynamicGridColumns GenerateColumns()
+    {
+        var columns = base.GenerateColumns();
+        var prefix = $"{typeof(TLeft).Name}.";
+        columns.RemoveAll(x => x.ColumnName.StartsWith(prefix));
+        return columns;
+    }
 
-        protected override void Reload(Filters<TEntity> criteria, Columns<TEntity> columns, ref SortOrder<TEntity>? sort, Action<CoreTable?, Exception?> action)
-        {
-            var filter = new Filter<TEntity>();
-            filter.Expression = CoreUtils.ExtractMemberExpression<TEntity, Guid>(LeftMapping);
-            filter.Operator = Operator.IsEqualTo;
-            filter.Value = CoreUtils.GetPropertyValue(Left, CoreUtils.GetFullPropertyName(LeftProperty, "."));
-            criteria.Add(filter);
-            base.Reload(criteria, columns, ref sort, action);
-        }
+    protected override void Reload(Filters<TEntity> criteria, Columns<TEntity> columns, ref SortOrder<TEntity>? sort, Action<CoreTable?, Exception?> action)
+    {
+        var filter = new Filter<TEntity>();
+        filter.Expression = CoreUtils.ExtractMemberExpression<TEntity, Guid>(LeftMapping);
+        filter.Operator = Operator.IsEqualTo;
+        filter.Value = CoreUtils.GetPropertyValue(Left, CoreUtils.GetFullPropertyName(LeftProperty, "."));
+        criteria.Add(filter);
+        base.Reload(criteria, columns, ref sort, action);
     }
 }

+ 1 - 23
inabox.wpf/DynamicGrid/DynamicDataGrid.cs

@@ -80,7 +80,7 @@ namespace InABox.DynamicGrid
                 new UserConfiguration<CoreFilterDefinitions>(GetTag()));
             FilterComponent.OnFilterRefresh += () => Refresh(false, true);
 
-            ColumnsComponent = new DynamicGridCustomColumnsComponent<TEntity>(this, GetTag(), base.LoadColumns);
+            ColumnsComponent = new DynamicGridCustomColumnsComponent<TEntity>(this, GetTag());
 
             MergeBtn = AddButton("Merge", Wpf.Resources.merge.AsBitmapImage(Color.White), DoMerge);
         }
@@ -381,28 +381,6 @@ namespace InABox.DynamicGrid
             new UserConfiguration<DynamicGridSettings>(tag).Save(settings);
         }
 
-        /// <summary>
-        /// Provide a set of columns which is the default for this grid.
-        /// </summary>
-        /// <param name="columns"></param>
-        protected virtual void GenerateColumns(DynamicGridColumns columns)
-        {
-            var cols = new Columns<TEntity>().Default(IsDirectEditMode()
-                ? new[] { ColumnType.IncludeForeignKeys, ColumnType.ExcludeID }
-                : new ColumnType[] {
-                    ColumnType.IncludeLinked, ColumnType.IncludeNestedLinks, ColumnType.IncludeFormulae,
-                    ColumnType.IncludeAggregates, ColumnType.ExcludeID });
-            if (cols != null)
-            {
-                foreach (var col in cols.Items)
-                {
-                    var mc = MasterColumns.FirstOrDefault(x => x.ColumnName.Equals(col.Property));
-                    if (mc != null && !(mc.Editor is NullEditor) && mc.Editor.Visible != Visible.Hidden)
-                        columns.Add(mc);
-                }
-            }
-        }
-
         protected override BaseEditor? GetEditor(object item, DynamicGridColumn column)
         {
             var prop = DatabaseSchema.Properties(typeof(TEntity)).FirstOrDefault(x => x.Name == column.ColumnName);

+ 33 - 14
inabox.wpf/DynamicGrid/DynamicEditorGrid.xaml.cs

@@ -97,6 +97,23 @@ namespace InABox.DynamicGrid
             }
         }
 
+        public IEnumerable<IDynamicEditorControl> Editors
+        {
+            get
+            {
+                foreach (var page in Pages)
+                {
+                    if (page is DynamicEditPage editPage)
+                    {
+                        foreach(var editor in editPage.Editors)
+                        {
+                            yield return editor;
+                        }
+                    }
+                }
+            }
+        }
+
         public bool TryFindEditor(string columnname, [NotNullWhen(true)] out IDynamicEditorControl? editor)
         {
             foreach (var page in Pages)
@@ -203,7 +220,9 @@ namespace InABox.DynamicGrid
             public DynamicEditorGrid EditorGrid { get; set; } = null!; // Set by DynamicEditorGrid
             public bool Ready { get; set; }
 
-            private List<BaseDynamicEditorControl> Editors { get; set; }
+            private List<BaseDynamicEditorControl> EditorList { get; set; }
+
+            public IEnumerable<IDynamicEditorControl> Editors => EditorList;
 
             public PageType PageType => PageType.Editor;
 
@@ -222,7 +241,7 @@ namespace InABox.DynamicGrid
                     if(_readOnly != value)
                     {
                         _readOnly = value;
-                        foreach(var editor in Editors)
+                        foreach(var editor in EditorList)
                         {
                             editor.IsEnabled = !value && editor.EditorDefinition.Editable.IsEditable();
                         }
@@ -234,7 +253,7 @@ namespace InABox.DynamicGrid
             {
                 Header = header;
 
-                Editors = new List<BaseDynamicEditorControl>();
+                EditorList = new List<BaseDynamicEditorControl>();
 
                 InitialiseContent();
             }
@@ -270,7 +289,7 @@ namespace InABox.DynamicGrid
                     element.ColumnName = columnName;
                     element.Color = editor is UniqueCodeEditor ? Color.FromArgb(0xFF, 0xF6, 0xC9, 0xE8) : Colors.LightYellow;
 
-                    Editors.Add(element);
+                    EditorList.Add(element);
 
                     element.Margin = new Thickness(5F, 2.5F, 5F, 2.5F);
 
@@ -349,35 +368,35 @@ namespace InABox.DynamicGrid
 
             public bool TryFindEditor(string columnname, [NotNullWhen(true)] out IDynamicEditorControl? editor)
             {
-                editor = Editors.FirstOrDefault(x => x.ColumnName.Equals(columnname));
-                editor ??= Editors.FirstOrDefault(x => columnname.StartsWith(x.ColumnName + '.'));
+                editor = EditorList.FirstOrDefault(x => x.ColumnName.Equals(columnname));
+                editor ??= EditorList.FirstOrDefault(x => columnname.StartsWith(x.ColumnName + '.'));
                 return editor is not null;
             }
 
             public IEnumerable<BaseDynamicEditorControl> FindEditors(DynamicGridColumn column)
             {
-                return Editors.Where(x => string.Equals(x.ColumnName, column.ColumnName));
+                return EditorList.Where(x => string.Equals(x.ColumnName, column.ColumnName));
             }
 
             #region Configure Editors
 
             private void Lookup_OnUpdateOtherEditor(string columnname, object value)
             {
-                var editor = Editors.FirstOrDefault(x => x.ColumnName.Equals(columnname));
+                var editor = EditorList.FirstOrDefault(x => x.ColumnName.Equals(columnname));
                 if (editor != null)
                     CoreUtils.SetPropertyValue(editor, "Value", value);
             }
 
             private void ConfigureEditors()
             {
-                foreach (var Editor in Editors)
+                foreach (var Editor in EditorList)
                 {
                     var editor = Editor.EditorDefinition;
                     var column = Editor.ColumnName;
 
                     Editor.Configure();
-                    if (!Editors.Any(x => x.ColumnName.Equals(Editor.ColumnName)))
-                        Editors.Add(Editor);
+                    if (!EditorList.Any(x => x.ColumnName.Equals(Editor.ColumnName)))
+                        EditorList.Add(Editor);
                     Editor.Loaded = true;
                 }
             }
@@ -455,7 +474,7 @@ namespace InABox.DynamicGrid
 
             private void LoadEditorValues(Dictionary<string, object?>? changededitors = null)
             {
-                var columnnames = changededitors != null ? changededitors.Keys.ToArray() : Editors.Select(x => x.ColumnName).ToArray();
+                var columnnames = changededitors != null ? changededitors.Keys.ToArray() : EditorList.Select(x => x.ColumnName).ToArray();
                 foreach (var columnname in columnnames)
                 {
                     if (!TryFindEditor(columnname, out var editor))
@@ -494,7 +513,7 @@ namespace InABox.DynamicGrid
                 ConfigureEditors();
                 LoadEditorValues();
 
-                foreach (var editor in Editors)
+                foreach (var editor in EditorList)
                 {
                     foreach(var (column, editorValue) in editor.GetValues())
                     {
@@ -509,7 +528,7 @@ namespace InABox.DynamicGrid
                     }
                 }
 
-                Editors.FirstOrDefault()?.SetFocus();
+                EditorList.FirstOrDefault()?.SetFocus();
 
                 Ready = true;
             }

+ 1 - 1
inabox.wpf/DynamicGrid/DynamicEnclosedListGrid.cs

@@ -38,7 +38,7 @@ namespace InABox.DynamicGrid
             Items = new List<TMany>();
             property = prop;
 
-            ColumnsComponent = new(this, null, base.LoadColumns);
+            ColumnsComponent = new(this, null);
         }
 
         protected override void Init()

+ 28 - 5
inabox.wpf/DynamicGrid/DynamicGrid.cs

@@ -1478,6 +1478,31 @@ namespace InABox.DynamicGrid
             return result;
         }
 
+
+        /// <summary>
+        /// Provide a set of columns which is the default for this grid.
+        /// </summary>
+        public virtual DynamicGridColumns GenerateColumns()
+        {
+            var columns = new DynamicGridColumns();
+
+            var cols = IsDirectEditMode()
+                ? new Columns<T>().Default(ColumnType.IncludeForeignKeys, ColumnType.ExcludeID)
+                : new Columns<T>().Default(ColumnType.IncludeLinked, ColumnType.ExcludeID);
+
+            if (cols != null)
+            {
+                foreach (var col in cols.Items)
+                {
+                    var mc = MasterColumns.FirstOrDefault(x => x.ColumnName.Equals(col.Property));
+                    if (mc != null && mc.Editor is not NullEditor && mc.Editor.Visible != Visible.Hidden)
+                        columns.Add(mc);
+                }
+            }
+            return columns;
+        }
+
+
         private bool SwapRows(int row1, int row2)
         {
 
@@ -2134,8 +2159,8 @@ namespace InABox.DynamicGrid
             var columns = new Columns<T>();
             foreach (var column in VisibleColumns)
                 columns.Add(column.ColumnName);
-            foreach (var column in HiddenColumns)
-                columns.Add(column);
+            foreach (var column in HiddenColumns.ColumnNames)
+                columns.Add(new Column<T>(column));
             return columns;
         }
 
@@ -2605,7 +2630,6 @@ namespace InABox.DynamicGrid
             else if (AddEditClick(null))
             {
                 Refresh(false, true);
-                OnChanged?.Invoke(this, EventArgs.Empty);
             }
         }
 
@@ -2914,7 +2938,6 @@ namespace InABox.DynamicGrid
 
                 var item = CreateItem();
 
-                // Yea, and this won't work, because we're actually usually showing the description of a linked item,
                 // Yea, and this won't work, because we're actually usually showing the description of a linked item,
                 // not the id of the link, and we need to set the ID to have it work properly :-(
 
@@ -3274,7 +3297,7 @@ namespace InABox.DynamicGrid
             {
                 reloadColumns.Add(column);
             }
-            foreach (var column in HiddenColumns)
+            foreach (var column in HiddenColumns.ColumnNames)
             {
                 reloadColumns.Add(column);
             }

+ 11 - 40
inabox.wpf/DynamicGrid/DynamicGridCustomColumnsComponent.cs

@@ -19,60 +19,31 @@ public class DynamicGridCustomColumnsComponent<T>
 {
     private readonly DynamicGrid<T> Grid;
 
-    public Func<DynamicGridColumns> DefaultColumns { get; set; }
-
     public string? Tag { get; set; }
 
-    public DynamicGridCustomColumnsComponent(DynamicGrid<T> grid, string? tag, Func<DynamicGridColumns> defaultColumns)
+    public DynamicGridCustomColumnsComponent(DynamicGrid<T> grid, string? tag)
     {
         Grid = grid;
         Tag = tag;
-        DefaultColumns = defaultColumns;
     }
 
     public DynamicGridColumns LoadColumns()
     {
-        DynamicGridColumns uvc = null;
-        DynamicGridColumns udc = null;
-        DynamicGridColumns gvc = null;
-        DynamicGridColumns gdc = null;
-        var tasks = new Task[]
-        {
-            Task.Run(() => uvc = new UserConfiguration<DynamicGridColumns>(GetTag(false)).Load()),
-            Task.Run(() => udc = new UserConfiguration<DynamicGridColumns>(GetTag(true)).Load()),
-            Task.Run(() => gvc = new GlobalConfiguration<DynamicGridColumns>(GetTag(false)).Load()),
-            Task.Run(() => gdc = new GlobalConfiguration<DynamicGridColumns>(GetTag(true)).Load())
-            
-        };
-        Task.WaitAll(tasks);
-        var columns = Grid.IsDirectEditMode()
-            ? udc?.Any() == true 
-                ? udc 
-                : gdc?.Any() == true
-                    ? gdc
-                    : uvc?.Any() == true
-                        ? uvc
-                        : gvc ?? new DynamicGridColumns()
-            : uvc?.Any() == true
-                ? uvc
-                : gvc ?? new DynamicGridColumns();
+        var tag = GetTag(Grid.IsDirectEditMode());
         
-        // var user = Task.Run(() => new UserConfiguration<DynamicGridColumns>(tag).Load());
-        // user.Wait();
-        // var global = Task.Run(() => new GlobalConfiguration<DynamicGridColumns>(tag).Load());
-        // global.Wait();
-        // //Task.WaitAll(user, global);
-        // var columns = user.Result.Any() ? user.Result : global.Result;
+        var user = Task.Run(() => new UserConfiguration<DynamicGridColumns>(tag).Load());
+        var global = Task.Run(() => new GlobalConfiguration<DynamicGridColumns>(tag).Load());
+        Task.WaitAll(user, global);
+
+        var columns = user.Result.Any() ? user.Result : global.Result;
 
-        // if (!columns.Any())
-        //     GenerateColumns(columns); //override this to provide specific columns on startup
+        if (!columns.Any())
+            columns = Grid.GenerateColumns();
 
         var removes = columns.Where(x => x is null || string.IsNullOrWhiteSpace(x.ColumnName) || DatabaseSchema.Property(typeof(T), x.ColumnName) == null || GetColumnEditor(x) is NullEditor)
            .ToArray();
         foreach (var remove in removes)
             columns.Remove(remove);
-        if (columns.Count == 0)
-            columns.AddRange(DefaultColumns());
 
         foreach (var column in columns)
             try
@@ -118,10 +89,10 @@ public class DynamicGridCustomColumnsComponent<T>
         }
     }
 
-    private string GetTag(bool directedit)
+    private string GetTag(bool directEdit)
     {
         var tag = Tag ?? typeof(T).Name;
-        if (directedit)
+        if (directEdit)
             tag += ":DirectEdit";
         return tag;
     }

+ 1 - 1
inabox.wpf/DynamicGrid/DynamicManyToManyGrid.cs

@@ -62,7 +62,7 @@ namespace InABox.DynamicGrid
             HiddenColumns.Add(x => x.ID);
             HiddenColumns.Add(CoreUtils.CreateLambdaExpression<TManyToMany>(otherproperty.Name + ".ID"));
 
-            ColumnsComponent = new DynamicGridCustomColumnsComponent<TManyToMany>(this, GetTag(), base.LoadColumns);
+            ColumnsComponent = new DynamicGridCustomColumnsComponent<TManyToMany>(this, GetTag());
         }
 
         protected override void Init()

+ 1 - 1
inabox.wpf/DynamicGrid/DynamicOneToManyGrid.cs

@@ -40,7 +40,7 @@ namespace InABox.DynamicGrid
 
             property = CoreUtils.GetOneToManyProperty(typeof(TMany), typeof(TOne));
 
-            ColumnsComponent = new DynamicGridCustomColumnsComponent<TMany>(this, GetTag(), base.LoadColumns);
+            ColumnsComponent = new DynamicGridCustomColumnsComponent<TMany>(this, GetTag());
         }
 
         protected override void Init()

+ 1 - 0
inabox.wpf/Forms/MessageWindow.xaml

@@ -9,6 +9,7 @@
         MinWidth="280"
         SizeToContent="Height"
         x:Name="Window"
+        WindowStartupLocation="CenterScreen"
         DataContext="{Binding ElementName=Window}">
     <Window.Resources>
         <DataTemplate x:Key="ButtonTemplate" DataType="local:MessageWindowButton">