Selaa lähdekoodia

Merge remote-tracking branch 'origin/frank' into kenric

Kenric Nugteren 6 kuukautta sitten
vanhempi
commit
6620f48db0

+ 2 - 1
InABox.Client.RPC/RPCClient.cs

@@ -188,7 +188,8 @@ namespace InABox.Rpc
             {
             {
                 Type = typeof(TEntity),
                 Type = typeof(TEntity),
                 Items = items,
                 Items = items,
-                AuditNote = auditnote
+                AuditNote = auditnote,
+                ExcludeCustomProperties = this.ExcludeCustomProperties
             };
             };
             var result = _transport.Send<RpcSaveCommand, RpcSaveParameters, RpcSaveResult>(parameters);
             var result = _transport.Send<RpcSaveCommand, RpcSaveParameters, RpcSaveResult>(parameters);
             for (int i = 0; i < result.Deltas.Length; i++)
             for (int i = 0; i < result.Deltas.Length; i++)

+ 8 - 0
InABox.Core/Client/BaseClient.cs

@@ -8,6 +8,9 @@ namespace InABox.Clients
 {
 {
     public abstract class BaseClient<TEntity> : IClient<TEntity> where TEntity : Entity, new()
     public abstract class BaseClient<TEntity> : IClient<TEntity> where TEntity : Entity, new()
     {
     {
+        
+        public bool ExcludeCustomProperties { get; set; }
+        
         public TimeSpan Timeout { get; set; } = new TimeSpan(0, 1, 0);
         public TimeSpan Timeout { get; set; } = new TimeSpan(0, 1, 0);
 
 
         public abstract IEnumerable<string> SupportedTypes();
         public abstract IEnumerable<string> SupportedTypes();
@@ -46,6 +49,11 @@ namespace InABox.Clients
             return DoQuery((Filter<TEntity>?)filter, (Columns<TEntity>?)columns, (SortOrder<TEntity>?)sort, range);
             return DoQuery((Filter<TEntity>?)filter, (Columns<TEntity>?)columns, (SortOrder<TEntity>?)sort, range);
         }
         }
 
 
+        public CoreTable Query(IFilter? filter = null, IColumns? columns = null, ISortOrder? sort = null, CoreRange? range = null)
+        {
+            return DoQuery((Filter<TEntity>?)filter, (Columns<TEntity>?)columns, (SortOrder<TEntity>?)sort, range);
+        }
+
         public CoreTable Query(Filter<TEntity>? filter = null, Columns<TEntity>? columns = null, SortOrder<TEntity>? sort = null, CoreRange? range = null)
         public CoreTable Query(Filter<TEntity>? filter = null, Columns<TEntity>? columns = null, SortOrder<TEntity>? sort = null, CoreRange? range = null)
         {
         {
             return DoQuery(filter, columns, sort, range);
             return DoQuery(filter, columns, sort, range);

+ 94 - 9
InABox.Core/Client/Client.cs

@@ -2,8 +2,10 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Linq;
 using System.Linq;
 using System.Reflection;
 using System.Reflection;
+using System.Threading.Tasks;
 using System.Timers;
 using System.Timers;
 using InABox.Core;
 using InABox.Core;
+using IQueryProvider = InABox.Core.IQueryProvider;
 
 
 namespace InABox.Clients
 namespace InABox.Clients
 {
 {
@@ -58,8 +60,93 @@ namespace InABox.Clients
         public CoreTable GetOrDefault(string name) => Results.GetValueOrDefault(name);
         public CoreTable GetOrDefault(string name) => Results.GetValueOrDefault(name);
     }
     }
 
 
+    public class ClientQueryProvider<TEntity> : IQueryProvider<TEntity>
+        where TEntity : Entity, IRemotable, new()
+    {
+        
+        public bool ExcludeCustomProperties { get; set; }
+        
+        #region Non-generic
+
+        public CoreTable Query(IFilter? filter = null, IColumns? columns = null, ISortOrder? sort = null, CoreRange? range = null)
+        {
+            return new Client<TEntity>().Query(filter, columns, sort, range);
+        }
+
+        #endregion
+
+        public CoreTable Query(Filter<TEntity>? filter = null, Columns<TEntity>? columns = null, SortOrder<TEntity>? sort = null, CoreRange? range = null)
+        {
+            return Client.Query(filter, columns, sort, range);
+        }
+
+        public void Query(Filter<TEntity>? filter, Columns<TEntity>? columns, SortOrder<TEntity>? sort, CoreRange? range, Action<CoreTable?, Exception?> action)
+        {
+            Client.Query(filter, columns, sort, range, action);
+        }
+
+        public void Save(TEntity entity, string auditNote)
+        {
+            Client.Save(entity, auditNote);
+        }
+
+        public void Save(IEnumerable<TEntity> entities, string auditNote)
+        {
+            Client.Save(entities, auditNote);
+        }
+
+        public void Save(TEntity entity, string auditnote, Action<TEntity, Exception?> callback)
+        {
+            Client.Save(entity, auditnote, callback);
+        }
+
+        public void Save(IEnumerable<TEntity> entities, string auditnote, Action<IEnumerable<TEntity>, Exception?> callback)
+        {
+            Client.Save(entities, auditnote, callback);
+        }
+
+        public void Delete(TEntity entity, string auditNote)
+        {
+            Client.Delete(entity, auditNote);
+        }
+
+        public void Delete(IEnumerable<TEntity> entities, string auditNote)
+        {
+            Client.Delete(entities, auditNote);
+        }
+
+        public void Delete(TEntity entity, string auditnote, Action<TEntity, Exception?> callback)
+        {
+            Client.Delete(entity, auditnote, callback);
+        }
+
+        public void Delete(IEnumerable<TEntity> entities, string auditnote, Action<IList<TEntity>, Exception?> callback)
+        {
+            Client.Delete(entities, auditnote, callback);
+        }
+    }
+
     public abstract class Client
     public abstract class Client
     {
     {
+        #region IQueryProvider Factory
+
+        private class _Factory : IQueryProviderFactory
+        {
+
+            public bool ExcludeCustomProperties => false;
+            
+            public IQueryProvider Create(Type T)
+            {
+                var type = typeof(ClientQueryProvider<>).MakeGenericType(T);
+                var result = (Activator.CreateInstance(type) as IQueryProvider)!;
+                result.ExcludeCustomProperties = ExcludeCustomProperties;
+                return result;
+            }
+        }
+
+        public static IQueryProviderFactory Factory { get; } = new _Factory();
+
+        #endregion
 
 
         public abstract CoreTable Query(IFilter? filter = null, IColumns? columns = null, ISortOrder? sortOrder = null, CoreRange? range = null);
         public abstract CoreTable Query(IFilter? filter = null, IColumns? columns = null, ISortOrder? sortOrder = null, CoreRange? range = null);
         public abstract void Save(Entity entity, string auditNote);
         public abstract void Save(Entity entity, string auditNote);
@@ -183,6 +270,12 @@ namespace InABox.Clients
             new Client<TEntity>().Delete(entities, auditNote);
             new Client<TEntity>().Delete(entities, auditNote);
         }
         }
 
 
+        public static void Delete<TEntity>(IEnumerable<TEntity> entities, string auditNote, Action<IList<TEntity>, Exception?> callback)
+            where TEntity : Entity, IRemotable, new()
+        {
+            new Client<TEntity>().Delete(entities, auditNote, callback);
+        }
+
         public static void QueryMultiple(
         public static void QueryMultiple(
             Action<Dictionary<string, CoreTable>?, Exception?> callback,
             Action<Dictionary<string, CoreTable>?, Exception?> callback,
             Dictionary<string, IQueryDef> queries)
             Dictionary<string, IQueryDef> queries)
@@ -369,15 +462,7 @@ namespace InABox.Clients
     {
     {
         #region IQueryProvider
         #region IQueryProvider
 
 
-        private class ClientQueryProvider : IQueryProvider<TEntity>
-        {
-            public CoreTable Query(Filter<TEntity>? filter = null, Columns<TEntity>? columns = null, SortOrder<TEntity>? sort = null, CoreRange? range = null)
-            {
-                return Client.Query(filter, columns, sort, range);
-            }
-        }
-
-        public static IQueryProvider<TEntity> Provider { get; private set; } = new ClientQueryProvider();
+        public static IQueryProvider<TEntity> Provider { get; private set; } = new ClientQueryProvider<TEntity>();
 
 
         #endregion
         #endregion
 
 

+ 3 - 29
InABox.Core/Client/ClientFactory.cs

@@ -23,18 +23,15 @@ namespace InABox.Clients
 
 
     public static class ClientFactory
     public static class ClientFactory
     {
     {
-
-
         public static Dictionary<EmailType, Type> MailerTypes = new Dictionary<EmailType, Type>();
         public static Dictionary<EmailType, Type> MailerTypes = new Dictionary<EmailType, Type>();
 
 
-
         public static Guid UserGuid { get; private set; }
         public static Guid UserGuid { get; private set; }
 
 
         public static string UserID { get; private set; }
         public static string UserID { get; private set; }
 
 
         public static Guid UserSecurityID { get; private set; }
         public static Guid UserSecurityID { get; private set; }
 
 
-        public static Guid SessionID { get; set; }
+        public static Guid SessionID { get; private set; }
 
 
         public static Guid DatabaseID { get; set; }
         public static Guid DatabaseID { get; set; }
 
 
@@ -56,7 +53,7 @@ namespace InABox.Clients
 
 
         public static int PINLength { get; } = 4;
         public static int PINLength { get; } = 4;
         
         
-        public static PushHandlers PushHandlers { get; set; } = new PushHandlers();
+        public static PushHandlers PushHandlers { get; private set; } = new PushHandlers();
 
 
         public delegate void RequestErrorHandler(RequestException e);
         public delegate void RequestErrorHandler(RequestException e);
 
 
@@ -67,26 +64,9 @@ namespace InABox.Clients
             OnRequestError?.Invoke(e);
             OnRequestError?.Invoke(e);
         }
         }
 
 
-        private static bool IsSupported(Type t)
-        {
-            return true;
-            //if (SupportedTypes == null)
-            //{
-            //    try
-            //    {
-            //        SupportedTypes = new Client<User>().SupportedTypes();
-            //    }
-            //    catch (Exception e)
-            //    {
-            //        SupportedTypes = new String[] { };
-            //    }
-            //}
-            //return SupportedTypes.Contains(t.EntityName().Replace(".", "_"));
-        }
-
         public static bool IsSupported<T>() where T : Entity, new()
         public static bool IsSupported<T>() where T : Entity, new()
         {
         {
-            return IsSupported(typeof(T));
+            return true;
         }
         }
 
 
         public static bool IsSupported<T1, T2>() where T1 : Entity, new() where T2 : Entity, new()
         public static bool IsSupported<T1, T2>() where T1 : Entity, new() where T2 : Entity, new()
@@ -119,12 +99,6 @@ namespace InABox.Clients
         public static bool IsSupported(params Type[] types)
         public static bool IsSupported(params Type[] types)
         {
         {
             return true;
             return true;
-            //if (SupportedTypes == null)
-            //    SupportedTypes = new Client<User>().SupportedTypes();
-            //bool result = true;
-            //foreach (var type in types)
-            //    result = result && SupportedTypes.Contains(type.EntityName().Replace(".", "_"));
-            //return result;
         }
         }
 
 
         public static void SetClientType(Type type, Platform platform, string? version, params object[]? parameters)
         public static void SetClientType(Type type, Platform platform, string? version, params object[]? parameters)

+ 2 - 16
InABox.Core/Client/IClient.cs

@@ -6,7 +6,7 @@ namespace InABox.Clients
 {
 {
     
     
     
     
-    public interface IClient
+    public interface IClient : IQueryProvider
     {
     {
         IValidationData Validate(string userid, string password, Guid session = default);
         IValidationData Validate(string userid, string password, Guid session = default);
         IValidationData Validate(string pin, Guid session = default);
         IValidationData Validate(string pin, Guid session = default);
@@ -50,24 +50,10 @@ namespace InABox.Clients
 
 
     }
     }
 
 
-    public interface IClient<TEntity> : IClient where TEntity : Entity, new()
+    public interface IClient<TEntity> : IClient, IQueryProvider<TEntity> where TEntity : Entity, new()
     {
     {
-        CoreTable Query(Filter<TEntity>? filter = null, Columns<TEntity>? columns = null, SortOrder<TEntity>? sort = null, CoreRange? range = null);
-        void Query(Filter<TEntity>? filter, Columns<TEntity>? columns, SortOrder<TEntity>? sort, CoreRange? range, Action<CoreTable?, Exception?> callback);
-
         TEntity[] Load(Filter<TEntity>? filter = null, SortOrder<TEntity>? sort = null, CoreRange? range = null);
         TEntity[] Load(Filter<TEntity>? filter = null, SortOrder<TEntity>? sort = null, CoreRange? range = null);
         void Load(Filter<TEntity>? filter, SortOrder<TEntity>? sort, CoreRange? range, Action<TEntity[]?, Exception?> callback);
         void Load(Filter<TEntity>? filter, SortOrder<TEntity>? sort, CoreRange? range, Action<TEntity[]?, Exception?> callback);
-
-        void Save(TEntity entity, string auditnote);
-        void Save(IEnumerable<TEntity> entities, string auditnote);
-        void Save(TEntity entity, string auditnote, Action<TEntity, Exception?> callback);
-        void Save(IEnumerable<TEntity> entity, string auditnote, Action<IEnumerable<TEntity>, Exception?> callback);
-
-        void Delete(TEntity entity, string auditnote);
-        void Delete(IEnumerable<TEntity> entities, string auditnote);
-        void Delete(TEntity entity, string auditnote, Action<TEntity, Exception?> callback);
-        void Delete(IEnumerable<TEntity> entity, string auditnote, Action<IList<TEntity>, Exception?> callback);
-        
     }
     }
 
 
 }
 }

+ 73 - 1
InABox.Core/Client/IQueryProvider.cs

@@ -4,8 +4,80 @@ using System.Text;
 
 
 namespace InABox.Core
 namespace InABox.Core
 {
 {
-    public interface IQueryProvider<T>
+    public interface IQueryProvider
+    {
+        bool ExcludeCustomProperties { get; set; }
+        
+        CoreTable Query(IFilter? filter = null, IColumns? columns = null, ISortOrder? sort = null, CoreRange? range = null);
+    }
+
+    public interface IQueryProvider<T> : IQueryProvider
     {
     {
         CoreTable Query(Filter<T>? filter = null, Columns<T>? columns = null, SortOrder<T>? sort = null, CoreRange? range = null);
         CoreTable Query(Filter<T>? filter = null, Columns<T>? columns = null, SortOrder<T>? sort = null, CoreRange? range = null);
+
+        void Query(Filter<T>? filter, Columns<T>? columns, SortOrder<T>? sort, CoreRange? range, Action<CoreTable?, Exception?> action);
+
+        void Save(T entity, string auditNote);
+
+        void Save(IEnumerable<T> entities, string auditNote);
+
+        void Save(T entity, string auditnote, Action<T, Exception?> callback);
+        void Save(IEnumerable<T> entities, string auditnote, Action<IEnumerable<T>, Exception?> callback);
+
+        void Delete(T entity, string auditNote);
+
+        void Delete(IEnumerable<T> entities, string auditNote);
+
+        void Delete(T entity, string auditnote, Action<T, Exception?> callback);
+        void Delete(IEnumerable<T> entities, string auditnote, Action<IList<T>, Exception?> callback);
+    }
+
+    public interface IQueryProviderFactory
+    {
+
+        bool ExcludeCustomProperties { get; }
+        
+        IQueryProvider Create(Type T);
+
+        IQueryProvider<T> Create<T>()
+            where T : BaseObject, new()
+        {
+            var result = (Create(typeof(T)) as IQueryProvider<T>)!;
+            result.ExcludeCustomProperties = ExcludeCustomProperties;
+            return result;
+        }
+    }
+
+    public static class QueryProviderFactoryExtensions
+    {
+        public static CoreTable Query<T>(this IQueryProviderFactory factory, Filter<T>? filter = null, Columns<T>? columns = null, SortOrder<T>? sort = null, CoreRange? range = null)
+            where T : BaseObject, new()
+        {
+            return factory.Create<T>().Query(filter, columns, sort, range);
+        }
+
+        public static void Query<T>(this IQueryProviderFactory factory, Filter<T>? filter, Columns<T>? columns, SortOrder<T>? sort, CoreRange? range, Action<CoreTable?, Exception?> action)
+            where T : BaseObject, new()
+        {
+            factory.Create<T>().Query(filter, columns, sort, range, action);
+        }
+
+        public static void Save<T>(this IQueryProviderFactory factory, T entity, string auditNote)
+            where T : BaseObject, new()
+        {
+            factory.Create<T>().Save(entity, auditNote);
+        }
+
+        public static void Save<T>(this IQueryProviderFactory factory, IEnumerable<T> entities, string auditNote)
+            where T : BaseObject, new()
+        {
+            factory.Create<T>().Save(entities, auditNote);
+        }
+
+        public static void Delete<T>(this IQueryProviderFactory factory, T entity, string auditNote)
+            where T : BaseObject, new()
+        {
+            factory.Create<T>().Delete(entity, auditNote);
+        }
     }
     }
 }
 }

+ 27 - 3
InABox.Core/CoreTreeNodes.cs

@@ -7,12 +7,28 @@ using System.Linq.Expressions;
 
 
 namespace InABox.Core
 namespace InABox.Core
 {
 {
-    public class CoreTreeNode<TKey> : INotifyPropertyChanged
+    public interface ICoreTreeNode
+    {
+        object? ID { get; set; }
+        object? Parent { get; set; }
+        object? Image { get; set; }
+        CoreRow? Row { get; set; }
+        object? this[string column] { get; set; }
+        string Number { get; }
+    }
+
+    public class CoreTreeNode<TKey> : INotifyPropertyChanged, ICoreTreeNode
     {
     {
 
 
         private CoreTreeNodes<TKey> _owner;
         private CoreTreeNodes<TKey> _owner;
         
         
         public ObservableCollection<CoreTreeNode<TKey>> Children => new ObservableCollection<CoreTreeNode<TKey>>(_owner.GetChildrenOfParent(_id));
         public ObservableCollection<CoreTreeNode<TKey>> Children => new ObservableCollection<CoreTreeNode<TKey>>(_owner.GetChildrenOfParent(_id));
+
+        object? ICoreTreeNode.ID
+        {
+            get => ID;
+            set => ID = (TKey)value!;
+        }
         
         
         private TKey _id;
         private TKey _id;
         public TKey ID 
         public TKey ID 
@@ -25,6 +41,13 @@ namespace InABox.Core
             }
             }
         }
         }
         
         
+        object? ICoreTreeNode.Parent
+        {
+            get => Parent;
+            set => Parent = (TKey)value!;
+        }
+
+        
         private TKey _parent;
         private TKey _parent;
         public TKey Parent 
         public TKey Parent 
         {
         {
@@ -36,8 +59,8 @@ namespace InABox.Core
             }
             }
         }
         }
    
    
-        private object _image;
-        public object Image
+        private object? _image;
+        public object? Image
         {
         {
             get => _image;
             get => _image;
             set
             set
@@ -209,4 +232,5 @@ namespace InABox.Core
             ColumnChanged?.Invoke(node, column);
             ColumnChanged?.Invoke(node, column);
         }
         }
     }
     }
+
 }
 }

+ 5 - 1
InABox.Core/CoreUtils.cs

@@ -1583,7 +1583,11 @@ namespace InABox.Core
                 return File.ReadAllText(curfile);
                 return File.ReadAllText(curfile);
             
             
             return "???";
             return "???";
-            
+        }
+
+        public static string GetLogFile()
+        {
+            return Path.Combine(GetPath(), string.Format("{0:yyyy-MM-dd}.log", DateTime.Today));
         }
         }
 
 
         public static string SplitCamelCase(this string value)
         public static string SplitCamelCase(this string value)

+ 25 - 0
InABox.Core/DatabaseSchema/DatabaseSchema.cs

@@ -271,6 +271,20 @@ namespace InABox.Core
 
 
             _properties[master] = newDict.ToImmutableSortedDictionary();
             _properties[master] = newDict.ToImmutableSortedDictionary();
         }
         }
+        private static void UnregisterProperties(Type master, IEnumerable<IProperty> toRemove)
+        {
+            if (!_properties.TryGetValue(master, out var properties))
+            {
+                properties = ImmutableSortedDictionary<string, IProperty>.Empty;
+            }
+            var newDict = properties.ToDictionary(x => x.Key, x => x.Value);
+            foreach(var prop in toRemove)
+            {
+                newDict.Remove(prop.Name);
+            }
+
+            _properties[master] = newDict.ToImmutableSortedDictionary();
+        }
         
         
         public static void RegisterProperty(IProperty entry)
         public static void RegisterProperty(IProperty entry)
         {
         {
@@ -295,6 +309,17 @@ namespace InABox.Core
             }
             }
         }
         }
 
 
+        public static void Unload(CustomProperty[] customProperties)
+        {
+            var perType = customProperties.GroupBy(x => x.ClassType);
+            foreach(var group in perType)
+            {
+                if (group.Key is null) continue;
+
+                UnregisterProperties(group.Key, group);
+            }
+        }
+
         private static ImmutableSortedDictionary<string, IProperty>? CheckPropertiesInternal(Type type)
         private static ImmutableSortedDictionary<string, IProperty>? CheckPropertiesInternal(Type type)
         {
         {
             try
             try

+ 32 - 14
InABox.Core/Objects/Attributes/Aggregate.cs

@@ -95,8 +95,9 @@ namespace InABox.Core
 
 
     public class ComplexFormulaFieldNode<TType, TResult> : IComplexFormulaNode<TType, TResult>, IComplexFormulaFieldNode
     public class ComplexFormulaFieldNode<TType, TResult> : IComplexFormulaNode<TType, TResult>, IComplexFormulaFieldNode
     {
     {
-        //public Expression<Func<TType, TResult>> Expression { get; set; }
+
         public string Field { get; set; }
         public string Field { get; set; }
+        
 
 
         public ComplexFormulaFieldNode(Expression<Func<TType, TResult>> expression)
         public ComplexFormulaFieldNode(Expression<Func<TType, TResult>> expression)
         {
         {
@@ -109,6 +110,7 @@ namespace InABox.Core
         }
         }
 
 
         string IComplexFormulaFieldNode.GetField() => Field;
         string IComplexFormulaFieldNode.GetField() => Field;
+        
     }
     }
 
 
     #endregion
     #endregion
@@ -322,17 +324,20 @@ namespace InABox.Core
         public IComplexFormulaNode<TType, TCondition> Right { get; set; }
         public IComplexFormulaNode<TType, TCondition> Right { get; set; }
 
 
         public Condition Condition { get; set; }
         public Condition Condition { get; set; }
+        
+        public object? Coalesce { get; set; }
 
 
-        public ComplexFormulaPartial0ConditionNode(IComplexFormulaNode<TType, TCondition> left, IComplexFormulaNode<TType, TCondition> right, Condition condition)
+        public ComplexFormulaPartial0ConditionNode(IComplexFormulaNode<TType, TCondition> left, IComplexFormulaNode<TType, TCondition> right, Condition condition, object? coalesce)
         {
         {
             Left = left;
             Left = left;
             Right = right;
             Right = right;
             Condition = condition;
             Condition = condition;
+            Coalesce = coalesce;
         }
         }
 
 
         public ComplexFormulaPartial1ConditionNode<TType, TCondition, TValue> Then(IComplexFormulaNode<TType, TValue> then)
         public ComplexFormulaPartial1ConditionNode<TType, TCondition, TValue> Then(IComplexFormulaNode<TType, TValue> then)
         {
         {
-            return new ComplexFormulaPartial1ConditionNode<TType, TCondition, TValue>(Left, Right, Condition, then);
+            return new ComplexFormulaPartial1ConditionNode<TType, TCondition, TValue>(Left, Right, Condition, then, Coalesce);
         }
         }
     }
     }
 
 
@@ -345,18 +350,21 @@ namespace InABox.Core
         public IComplexFormulaNode<TType, TValue> True { get; set; }
         public IComplexFormulaNode<TType, TValue> True { get; set; }
 
 
         public Condition Condition { get; set; }
         public Condition Condition { get; set; }
+        
+        public object? Coalesce { get; set; }
 
 
-        public ComplexFormulaPartial1ConditionNode(IComplexFormulaNode<TType, TCondition> left, IComplexFormulaNode<TType, TCondition> right, Condition condition, IComplexFormulaNode<TType, TValue> trueValue)
+        public ComplexFormulaPartial1ConditionNode(IComplexFormulaNode<TType, TCondition> left, IComplexFormulaNode<TType, TCondition> right, Condition condition, IComplexFormulaNode<TType, TValue> trueValue, object? coalesce)
         {
         {
             Left = left;
             Left = left;
             Right = right;
             Right = right;
             Condition = condition;
             Condition = condition;
             True = trueValue;
             True = trueValue;
+            Coalesce = coalesce;
         }
         }
 
 
         public ComplexFormulaConditionNode<TType, TCondition, TValue> Else(IComplexFormulaNode<TType, TValue> elseValue)
         public ComplexFormulaConditionNode<TType, TCondition, TValue> Else(IComplexFormulaNode<TType, TValue> elseValue)
         {
         {
-            return new ComplexFormulaConditionNode<TType, TCondition, TValue>(Left, Right, True, elseValue, Condition);
+            return new ComplexFormulaConditionNode<TType, TCondition, TValue>(Left, Right, True, elseValue, Condition, Coalesce);
         }
         }
     }
     }
 
 
@@ -373,6 +381,9 @@ namespace InABox.Core
         public IComplexFormulaNode False { get; }
         public IComplexFormulaNode False { get; }
 
 
         public Condition Condition { get; }
         public Condition Condition { get; }
+        
+        public object? Coalesce { get; }
+        
     }
     }
     public class ComplexFormulaConditionNode<TType, TCondition, TValue> : IComplexFormulaNode<TType, TValue>, IComplexFormulaConditionNode
     public class ComplexFormulaConditionNode<TType, TCondition, TValue> : IComplexFormulaNode<TType, TValue>, IComplexFormulaConditionNode
     {
     {
@@ -387,14 +398,17 @@ namespace InABox.Core
         public IComplexFormulaNode<TType, TValue> False { get; set; }
         public IComplexFormulaNode<TType, TValue> False { get; set; }
 
 
         public Condition Condition { get; set; }
         public Condition Condition { get; set; }
-
-        public ComplexFormulaConditionNode(IComplexFormulaNode<TType, TCondition> left, IComplexFormulaNode<TType, TCondition> right, IComplexFormulaNode<TType, TValue> trueValue, IComplexFormulaNode<TType, TValue> falseValue, Condition condition)
+        
+        public object? Coalesce { get; }
+        
+        public ComplexFormulaConditionNode(IComplexFormulaNode<TType, TCondition> left, IComplexFormulaNode<TType, TCondition> right, IComplexFormulaNode<TType, TValue> trueValue, IComplexFormulaNode<TType, TValue> falseValue, Condition condition, object? coalesce)
         {
         {
             Left = left;
             Left = left;
             Right = right;
             Right = right;
             True = trueValue;
             True = trueValue;
             False = falseValue;
             False = falseValue;
             Condition = condition;
             Condition = condition;
+            Coalesce = coalesce;
         }
         }
 
 
         IComplexFormulaNode IComplexFormulaConditionNode.Left => Left;
         IComplexFormulaNode IComplexFormulaConditionNode.Left => Left;
@@ -468,9 +482,10 @@ namespace InABox.Core
         public static ComplexFormulaPartial0ConditionNode<TType, TCondition, TValue> If<TType, TCondition, TValue>(
         public static ComplexFormulaPartial0ConditionNode<TType, TCondition, TValue> If<TType, TCondition, TValue>(
             IComplexFormulaNode<TType, TCondition> left,
             IComplexFormulaNode<TType, TCondition> left,
             Condition condition,
             Condition condition,
-            IComplexFormulaNode<TType, TCondition> right)
+            IComplexFormulaNode<TType, TCondition> right,
+            object? coalesce = null)
         {
         {
-            return new ComplexFormulaPartial0ConditionNode<TType, TCondition, TValue>(left, right, condition);
+            return new ComplexFormulaPartial0ConditionNode<TType, TCondition, TValue>(left, right, condition, coalesce);
         }
         }
     }
     }
 
 
@@ -505,7 +520,8 @@ namespace InABox.Core
         ComplexFormulaPartial0ConditionNode<TType, TCondition, TResult> If<TCondition>(
         ComplexFormulaPartial0ConditionNode<TType, TCondition, TResult> If<TCondition>(
             Func<IComplexFormulaGenerator<TType, TCondition>, IComplexFormulaNode<TType, TCondition>> left,
             Func<IComplexFormulaGenerator<TType, TCondition>, IComplexFormulaNode<TType, TCondition>> left,
             Condition condition,
             Condition condition,
-            Func<IComplexFormulaGenerator<TType, TCondition>, IComplexFormulaNode<TType, TCondition>> right);
+            Func<IComplexFormulaGenerator<TType, TCondition>, IComplexFormulaNode<TType, TCondition>> right,
+            object? coalesce);
     }
     }
 
 
     internal class InternalComplexFormulaGenerator<TType, TResult> : IComplexFormulaGenerator<TType, TResult>
     internal class InternalComplexFormulaGenerator<TType, TResult> : IComplexFormulaGenerator<TType, TResult>
@@ -545,13 +561,15 @@ namespace InABox.Core
         public ComplexFormulaPartial0ConditionNode<TType, TCondition, TResult> If<TCondition>(
         public ComplexFormulaPartial0ConditionNode<TType, TCondition, TResult> If<TCondition>(
             Func<IComplexFormulaGenerator<TType, TCondition>, IComplexFormulaNode<TType, TCondition>> left,
             Func<IComplexFormulaGenerator<TType, TCondition>, IComplexFormulaNode<TType, TCondition>> left,
             Condition condition,
             Condition condition,
-            Func<IComplexFormulaGenerator<TType, TCondition>, IComplexFormulaNode<TType, TCondition>> right)
+            Func<IComplexFormulaGenerator<TType, TCondition>, IComplexFormulaNode<TType, TCondition>> right,
+            object? coalesce)
         {
         {
             var generator = new InternalComplexFormulaGenerator<TType, TCondition>();
             var generator = new InternalComplexFormulaGenerator<TType, TCondition>();
             return ComplexFormulaGenerator.If<TType, TCondition, TResult>(
             return ComplexFormulaGenerator.If<TType, TCondition, TResult>(
                 left(generator),
                 left(generator),
                 condition,
                 condition,
-                right(generator));
+                right(generator),
+                coalesce);
         }
         }
 
 
         public IComplexFormulaNode<TType, int> Count<TAggregate, TExpression>(Func<IComplexFormulaGenerator<TAggregate, TExpression>, IComplexFormulaNode<TAggregate, TExpression>> expression, KeyValuePair<Expression<Func<TAggregate, object?>>, Expression<Func<TType, object?>>>[] links, Filter<TAggregate>? filter = null)
         public IComplexFormulaNode<TType, int> Count<TAggregate, TExpression>(Func<IComplexFormulaGenerator<TAggregate, TExpression>, IComplexFormulaNode<TAggregate, TExpression>> expression, KeyValuePair<Expression<Func<TAggregate, object?>>, Expression<Func<TType, object?>>>[] links, Filter<TAggregate>? filter = null)
@@ -607,9 +625,9 @@ namespace InABox.Core
             return ((IComplexFormulaGenerator<TType, TResult>)InternalGenerator).Aggregate(calculation, expression, filter);
             return ((IComplexFormulaGenerator<TType, TResult>)InternalGenerator).Aggregate(calculation, expression, filter);
         }
         }
 
 
-        public ComplexFormulaPartial0ConditionNode<TType, TCondition, TResult> If<TCondition>(Func<IComplexFormulaGenerator<TType, TCondition>, IComplexFormulaNode<TType, TCondition>> left, Condition condition, Func<IComplexFormulaGenerator<TType, TCondition>, IComplexFormulaNode<TType, TCondition>> right)
+        public ComplexFormulaPartial0ConditionNode<TType, TCondition, TResult> If<TCondition>(Func<IComplexFormulaGenerator<TType, TCondition>, IComplexFormulaNode<TType, TCondition>> left, Condition condition, Func<IComplexFormulaGenerator<TType, TCondition>, IComplexFormulaNode<TType, TCondition>> right, object? coalesce = null)
         {
         {
-            return ((IComplexFormulaGenerator<TType, TResult>)InternalGenerator).If(left, condition, right);
+            return ((IComplexFormulaGenerator<TType, TResult>)InternalGenerator).If(left, condition, right, coalesce);
         }
         }
 
 
         public IComplexFormulaNode<TType, int> Count<TAggregate, TExpression>(Func<IComplexFormulaGenerator<TAggregate, TExpression>, IComplexFormulaNode<TAggregate, TExpression>> expression, KeyValuePair<Expression<Func<TAggregate, object?>>, Expression<Func<TType, object?>>>[] links, Filter<TAggregate>? filter = null)
         public IComplexFormulaNode<TType, int> Count<TAggregate, TExpression>(Func<IComplexFormulaGenerator<TAggregate, TExpression>, IComplexFormulaNode<TAggregate, TExpression>> expression, KeyValuePair<Expression<Func<TAggregate, object?>>, Expression<Func<TType, object?>>>[] links, Filter<TAggregate>? filter = null)

+ 4 - 4
InABox.Core/Serialization.cs

@@ -654,9 +654,9 @@ namespace InABox.Core
             return (result != null ? (T)result : default)!;
             return (result != null ? (T)result : default)!;
         }
         }
 
 
-        public static IEnumerable<IProperty> SerializableProperties(Type type) =>
+        public static IEnumerable<IProperty> SerializableProperties(Type type, Predicate<IProperty>? filter = null) =>
             DatabaseSchema.Properties(type)
             DatabaseSchema.Properties(type)
-                .Where(x => !(x is StandardProperty st) || st.IsSerializable);
+                .Where(x => (!(x is StandardProperty st) || st.IsSerializable) && (filter?.Invoke(x) ?? true));
 
 
         private static void WriteOriginalValues<TObject>(this CoreBinaryWriter writer, TObject obj)
         private static void WriteOriginalValues<TObject>(this CoreBinaryWriter writer, TObject obj)
             where TObject : BaseObject
             where TObject : BaseObject
@@ -773,7 +773,7 @@ namespace InABox.Core
         public static void WriteObjects<TObject>(this CoreBinaryWriter writer, ICollection<TObject>? objects)
         public static void WriteObjects<TObject>(this CoreBinaryWriter writer, ICollection<TObject>? objects)
             where TObject : BaseObject, new() => WriteObjects(writer, typeof(TObject), objects);
             where TObject : BaseObject, new() => WriteObjects(writer, typeof(TObject), objects);
 
 
-        public static void WriteObjects<TObject>(this CoreBinaryWriter writer, Type type, ICollection<TObject>? objects)
+        public static void WriteObjects<TObject>(this CoreBinaryWriter writer, Type type, ICollection<TObject>? objects, Predicate<IProperty>? filter = null)
             where TObject : BaseObject
             where TObject : BaseObject
         {
         {
             if (!typeof(TObject).IsAssignableFrom(type))
             if (!typeof(TObject).IsAssignableFrom(type))
@@ -787,7 +787,7 @@ namespace InABox.Core
                 return;
                 return;
             }
             }
 
 
-            var properties = SerializableProperties(type).ToList();
+            var properties = SerializableProperties(type, filter).ToList();
             writer.Write(properties.Count);
             writer.Write(properties.Count);
             foreach (var property in properties)
             foreach (var property in properties)
             {
             {

+ 124 - 3
InABox.Database/IProvider.cs

@@ -108,18 +108,139 @@ public static class ProviderExtensions
         }
         }
     }
     }
 
 
-    private class ProviderQueryProvider<TEntity>(IProvider provider) : IQueryProvider<TEntity>
+    private class ProviderQueryProvider<TEntity>(IProvider provider, string userID) : IQueryProvider<TEntity>
         where TEntity : Entity, new()
         where TEntity : Entity, new()
     {
     {
+
+        public bool ExcludeCustomProperties { get; set; }
+        
         private IProvider Provider = provider;
         private IProvider Provider = provider;
 
 
+        private string UserID = userID;
+
         public CoreTable Query(Filter<TEntity>? filter = null, Columns<TEntity>? columns = null, SortOrder<TEntity>? sort = null, CoreRange? range = null)
         public CoreTable Query(Filter<TEntity>? filter = null, Columns<TEntity>? columns = null, SortOrder<TEntity>? sort = null, CoreRange? range = null)
         {
         {
             return Provider.Query(filter, columns, sort, range);
             return Provider.Query(filter, columns, sort, range);
         }
         }
+
+        public void Query(Filter<TEntity>? filter, Columns<TEntity>? columns, SortOrder<TEntity>? sort, CoreRange? range, Action<CoreTable?, Exception?> action)
+        {
+            Task.Run(() =>
+            {
+                try
+                {
+                    var result = Provider.Query(filter, columns, sort, range);
+                    action(result, null);
+                }
+                catch(Exception e)
+                {
+                    action(null, e);
+                }
+            });
+        }
+
+        public void Delete(TEntity entity, string auditNote)
+        {
+            Provider.Delete(entity, UserID);
+        }
+
+        public void Delete(IEnumerable<TEntity> entities, string auditNote)
+        {
+            Provider.Delete(entities, UserID);
+        }
+
+        public void Delete(TEntity entity, string auditnote, Action<TEntity, Exception?> callback)
+        {
+            Task.Run(() =>
+            {
+                try
+                {
+                    Provider.Delete(entity, UserID);
+                    callback(entity, null);
+                }
+                catch (Exception e)
+                {
+                    callback(entity, e);
+                }
+            });
+        }
+
+        public void Delete(IEnumerable<TEntity> entities, string auditnote, Action<IList<TEntity>, Exception?> callback)
+        {
+            var ents = entities.AsIList();
+            Task.Run(() =>
+            {
+                try
+                {
+                    Provider.Delete(ents, UserID);
+                    callback(ents, null);
+                }
+                catch (Exception e)
+                {
+                    callback(ents, e);
+                }
+            });
+        }
+
+        public void Save(TEntity entity, string auditNote)
+        {
+            Provider.Save(entity);
+        }
+
+        public void Save(IEnumerable<TEntity> entities, string auditNote)
+        {
+            Provider.Save(entities);
+        }
+
+        public void Save(TEntity entity, string auditnote, Action<TEntity, Exception?> callback)
+        {
+            Task.Run(() =>
+            {
+                try
+                {
+                    Provider.Save(entity);
+                    callback(entity, null);
+                }
+                catch (Exception e)
+                {
+                    callback(entity, e);
+                }
+            });
+        }
+
+        public void Save(IEnumerable<TEntity> entities, string auditnote, Action<IEnumerable<TEntity>, Exception?> callback)
+        {
+            var ents = entities.AsIList();
+            Task.Run(() =>
+            {
+                try
+                {
+                    Provider.Save(ents);
+                    callback(ents, null);
+                }
+                catch (Exception e)
+                {
+                    callback(ents, e);
+                }
+            });
+        }
+
+        #region Non-generics
+
+        public CoreTable Query(IFilter? filter = null, IColumns? columns = null, ISortOrder? sort = null, CoreRange? range = null)
+        {
+            return Provider.Query(typeof(TEntity), filter, columns, sort, range);
+        }
+
+        #endregion
+    }
+
+    public static IQueryProvider<TEntity> QueryProvider<TEntity>(this IProvider provider, string userID) where TEntity : Entity, new()
+    {
+        return new ProviderQueryProvider<TEntity>(provider, userID);
     }
     }
-    public static IQueryProvider<TEntity> QueryProvider<TEntity>(this IProvider provider) where TEntity : Entity, new()
+    public static IQueryProvider<TEntity> QueryProvider<TEntity>(this IStore store) where TEntity : Entity, new()
     {
     {
-        return new ProviderQueryProvider<TEntity>(provider);
+        return new ProviderQueryProvider<TEntity>(store.Provider, store.UserID);
     }
     }
 }
 }

+ 4 - 2
InABox.RPC.Shared/Commands/Save/RpcSaveParameters.cs

@@ -9,6 +9,8 @@ namespace InABox.Rpc
         public Type Type { get; set; }
         public Type Type { get; set; }
         public Entity[] Items { get; set; }
         public Entity[] Items { get; set; }
         public string AuditNote { get; set; }
         public string AuditNote { get; set; }
+        
+        public bool ExcludeCustomProperties { get; set; }
 
 
         public string CommandName => "Save";
         public string CommandName => "Save";
 
 
@@ -21,10 +23,10 @@ namespace InABox.Rpc
         public void SerializeBinary(CoreBinaryWriter writer)
         public void SerializeBinary(CoreBinaryWriter writer)
         {
         {
             writer.Write(Type.EntityName());
             writer.Write(Type.EntityName());
-            writer.WriteObjects(Type, Items);
+            writer.WriteObjects(Type, Items, (p) => ExcludeCustomProperties ? p is StandardProperty : true);
             writer.Write(AuditNote);
             writer.Write(AuditNote);
         }
         }
-
+        
         public void DeserializeBinary(CoreBinaryReader reader)
         public void DeserializeBinary(CoreBinaryReader reader)
         {
         {
             var type = reader.ReadString();
             var type = reader.ReadString();

+ 16 - 3
inabox.database.sqlite/SQLiteProvider.cs

@@ -1749,6 +1749,8 @@ public class SQLiteProvider : IProvider
             return string.Format("hex({0})", BitConverter.ToString(Encoding.ASCII.GetBytes(value.ToString() ?? "")).Replace("-", ""));
             return string.Format("hex({0})", BitConverter.ToString(Encoding.ASCII.GetBytes(value.ToString() ?? "")).Replace("-", ""));
         if (value is IColumn col)
         if (value is IColumn col)
             return $"[{col.Property}]";
             return $"[{col.Property}]";
+        if (value is DateTime)
+            return $"'{((DateTime)value):yyyy-MM-dd HH:mm:ss.FFFFFFF}'";
         return value.ToString() ?? "";
         return value.ToString() ?? "";
 
 
     }
     }
@@ -1762,6 +1764,10 @@ public class SQLiteProvider : IProvider
             return string.Format("hex({0})", BitConverter.ToString(Encoding.ASCII.GetBytes(value.ToString() ?? "")).Replace("-", ""));
             return string.Format("hex({0})", BitConverter.ToString(Encoding.ASCII.GetBytes(value.ToString() ?? "")).Replace("-", ""));
         if (type.GetInterface(nameof(IColumn)) != null)
         if (type.GetInterface(nameof(IColumn)) != null)
             return $"[{value}]";
             return $"[{value}]";
+        if (type == typeof(DateTime))
+            return DateTime.TryParse(value, out DateTime _value)
+                ? $"'{_value:yyyy-MM-dd HH:mm:ss.FFFFFFF}'"
+                : $"'{DateTime.MinValue:yyyy-MM-dd HH:mm:ss.FFFFFFF}'";
         return value.ToString() ?? "";
         return value.ToString() ?? "";
 
 
     }
     }
@@ -2264,15 +2270,17 @@ public class SQLiteProvider : IProvider
                 var operands = new List<string>();
                 var operands = new List<string>();
                 var op = formula.GetOperator();
                 var op = formula.GetOperator();
 
 
+                bool bFirst = true;
                 foreach (var field in formula.GetOperands())
                 foreach (var field in formula.GetOperands())
                 {
                 {
                     var operand = LoadComplexFormula(command, type, prefix, fieldmap, tables, columns, field, useparams);
                     var operand = LoadComplexFormula(command, type, prefix, fieldmap, tables, columns, field, useparams);
                     if (op == FormulaOperator.Divide)
                     if (op == FormulaOperator.Divide)
-                        operands.Add($"IFNULL({operand}, 1.00)");
+                        operands.Add($"IFNULL({operand}, {(bFirst ? 0.00 : 1.00)})");
                     else if (op == FormulaOperator.Format)
                     else if (op == FormulaOperator.Format)
                         operands.Add(operand);
                         operands.Add(operand);
                     else
                     else
                         operands.Add($"IFNULL({operand}, 0.00)");
                         operands.Add($"IFNULL({operand}, 0.00)");
+                    bFirst = true;
                 }
                 }
 
 
                 switch (op)
                 switch (op)
@@ -2366,11 +2374,16 @@ public class SQLiteProvider : IProvider
                         throw new Exception($"Invalid formula of type {op}.");
                         throw new Exception($"Invalid formula of type {op}.");
                 }
                 }
             case IComplexFormulaConditionNode condition:
             case IComplexFormulaConditionNode condition:
+                
                 var left = LoadComplexFormula(command, type, prefix, fieldmap, tables, columns, condition.Left, useparams);
                 var left = LoadComplexFormula(command, type, prefix, fieldmap, tables, columns, condition.Left, useparams);
                 var right = LoadComplexFormula(command, type, prefix, fieldmap, tables, columns, condition.Right, useparams);
                 var right = LoadComplexFormula(command, type, prefix, fieldmap, tables, columns, condition.Right, useparams);
                 var trueVal = LoadComplexFormula(command, type, prefix, fieldmap, tables, columns, condition.True, useparams);
                 var trueVal = LoadComplexFormula(command, type, prefix, fieldmap, tables, columns, condition.True, useparams);
                 var falseVal = LoadComplexFormula(command, type, prefix, fieldmap, tables, columns, condition.False, useparams);
                 var falseVal = LoadComplexFormula(command, type, prefix, fieldmap, tables, columns, condition.False, useparams);
-
+                
+                var coalesce = condition.Coalesce != null
+                    ? EscapeValue(condition.Coalesce)
+                    : EscapeValue(condition.TCondition.GetDefault());
+                
                 var conditionOp = condition.Condition switch
                 var conditionOp = condition.Condition switch
                 {
                 {
                     Condition.Equals => "=",
                     Condition.Equals => "=",
@@ -2381,7 +2394,7 @@ public class SQLiteProvider : IProvider
                     Condition.LessThanOrEqualTo => "<=",
                     Condition.LessThanOrEqualTo => "<=",
                     _ => throw new Exception($"{condition.Condition} is not a valid condition")
                     _ => throw new Exception($"{condition.Condition} is not a valid condition")
                 };
                 };
-                return $"(CASE WHEN COALESCE({left}, {condition.TCondition.GetDefault()}) {conditionOp} {right} THEN " +
+                return $"(CASE WHEN COALESCE({left}, {coalesce}) {conditionOp} {right} THEN " +
                     $" {trueVal} ELSE {falseVal} END)";
                     $" {trueVal} ELSE {falseVal} END)";
             default:
             default:
                 throw new Exception($"Unknown ComplexFormula type {node.GetType()}");
                 throw new Exception($"Unknown ComplexFormula type {node.GetType()}");

+ 12 - 1
inabox.wpf/DynamicGrid/DynamicGridCommon.cs

@@ -490,4 +490,15 @@ public class BeforeRefreshEventArgs : CancelEventArgs { }
 public delegate void BeforeRefreshEventHandler(object sender, BeforeRefreshEventArgs args);
 public delegate void BeforeRefreshEventHandler(object sender, BeforeRefreshEventArgs args);
 
 
 public class AfterRefreshEventArgs : EventArgs { }
 public class AfterRefreshEventArgs : EventArgs { }
-public delegate void AfterRefreshEventHandler(object sender, AfterRefreshEventArgs args);
+public delegate void AfterRefreshEventHandler(object sender, AfterRefreshEventArgs args);
+
+public class GenerateColumnsEventArgs
+{
+	public DynamicGridColumns Columns { get; private set; }
+
+	public GenerateColumnsEventArgs(DynamicGridColumns columns)
+	{
+		Columns = columns;
+	}
+}
+public delegate void GenerateColumnsEvent(object sender, GenerateColumnsEventArgs args);

+ 59 - 10
inabox.wpf/DynamicGrid/DynamicGridUtils.cs

@@ -15,6 +15,8 @@ using Syncfusion.Data.Extensions;
 using System.Diagnostics.CodeAnalysis;
 using System.Diagnostics.CodeAnalysis;
 using System.Data;
 using System.Data;
 using System.Windows.Media;
 using System.Windows.Media;
+using AutoProperties;
+using InABox.WPF;
 
 
 namespace InABox.DynamicGrid;
 namespace InABox.DynamicGrid;
 
 
@@ -466,33 +468,80 @@ public static class DynamicGridUtils
         }
         }
         return gridType.MakeGenericType(entityType);
         return gridType.MakeGenericType(entityType);
     }
     }
-
-    public static Window CreateGridWindow(string title, IDynamicGrid dynamicGrid)
+    
+    public static Window CreateGridWindow(string title, IDynamicGrid dynamicGrid, bool showbuttons = false, Func<bool>? okclicked = null)
     {
     {
         dynamicGrid.Margin = new Thickness(5);
         dynamicGrid.Margin = new Thickness(5);
+        
+        var window = new ThemableWindow { Title = title };
+        window.SetValue(WindowBehavior.HideCloseButtonProperty, showbuttons);
+
+        var grid = new Grid();
+        grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) });
+        grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Auto) });
+        grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Auto) });
+        grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Star) });
+        grid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(1, GridUnitType.Auto) });
+        
+        var fe = dynamicGrid as FrameworkElement;
+        fe.SetValue(Grid.RowProperty,0);
+        fe.SetValue(Grid.ColumnProperty, 0);
+        fe.SetValue(Grid.ColumnSpanProperty, 3);
+        grid.Children.Add(fe);
 
 
-        var window = new ThemableWindow { Title = title, Content = dynamicGrid };
-
+        if (showbuttons)
+        {
+            var ok = new Button();
+            ok.Content = "OK";
+            ok.SetValue(Grid.RowProperty,1);
+            ok.SetValue(Grid.ColumnProperty,1);
+            ok.Margin = new Thickness(0,0,5,5);
+            ok.Width = 80;
+            ok.Height = 35;
+            ok.Click += (sender, args) =>
+            {
+                if (okclicked?.Invoke() ?? true)
+                    window.DialogResult = true;
+            };
+            grid.Children.Add(ok);
+            
+            var cancel = new Button();
+            cancel.Content = "Cancel";
+            cancel.SetValue(Grid.RowProperty,1);
+            cancel.SetValue(Grid.ColumnProperty,2);
+            cancel.Margin = new Thickness(0,0,5,5);
+            cancel.Width = 80;
+            cancel.Height = 35;
+            cancel.Click += (sender, args) =>
+            {
+                window.DialogResult = false;
+            };
+            grid.Children.Add(cancel);
+        }
+        
+        window.Content = grid;
+        
         dynamicGrid.Refresh(true, true);
         dynamicGrid.Refresh(true, true);
 
 
         return window;
         return window;
     }
     }
-    public static Window CreateGridWindow(string title, Type entityType, Type? gridType = null)
+    
+    public static Window CreateGridWindow(string title, Type entityType, Type? gridType = null, bool showbuttons = false)
     {
     {
         gridType ??= typeof(DynamicGrid<>);
         gridType ??= typeof(DynamicGrid<>);
         var grid = CreateDynamicGrid(gridType, entityType);
         var grid = CreateDynamicGrid(gridType, entityType);
-        return CreateGridWindow(title, grid);
+        return CreateGridWindow(title, grid, showbuttons);
     }
     }
-    public static Window CreateGridWindow<TGrid, TEntity>(string title)
+    public static Window CreateGridWindow<TGrid, TEntity>(string title, bool showbuttons = false)
         where TEntity : BaseObject
         where TEntity : BaseObject
         where TGrid : IDynamicGrid
         where TGrid : IDynamicGrid
     {
     {
-        return CreateGridWindow(title, typeof(TEntity), typeof(TGrid));
+        return CreateGridWindow(title, typeof(TEntity), typeof(TGrid), showbuttons);
     }
     }
-    public static Window CreateGridWindow<TEntity>(string title)
+    public static Window CreateGridWindow<TEntity>(string title, bool showbuttons = false)
         where TEntity : BaseObject
         where TEntity : BaseObject
     {
     {
-        return CreateGridWindow(title, typeof(TEntity));
+        return CreateGridWindow(title, typeof(TEntity), null, showbuttons);
     }
     }
 
 
     #endregion
     #endregion

+ 5 - 7
inabox.wpf/DynamicGrid/Grids/DynamicDocumentGrid.cs

@@ -9,7 +9,6 @@ using System.Windows.Controls;
 using System.Windows.Data;
 using System.Windows.Data;
 using System.Windows.Media;
 using System.Windows.Media;
 using System.Windows.Media.Imaging;
 using System.Windows.Media.Imaging;
-using InABox.Clients;
 using InABox.Core;
 using InABox.Core;
 using InABox.WPF;
 using InABox.WPF;
 using Microsoft.Win32;
 using Microsoft.Win32;
@@ -497,10 +496,9 @@ public class DynamicDocumentGrid<TDocument, TEntity, TEntityLink> : DynamicManyT
                     columns.Add(string.Join('.', column.ColumnName.Split('.').Skip(1)));
                     columns.Add(string.Join('.', column.ColumnName.Split('.').Skip(1)));
                 }
                 }
             }
             }
-            var docs = new Client<Document>()
-                .Query(
-                    new Filter<Document>(x => x.ID).InList(docIDS),
-                    columns);
+            var docs = Client.Query(
+                new Filter<Document>(x => x.ID).InList(docIDS),
+                columns);
 
 
             foreach (var doc in docs.ToObjects<Document>())
             foreach (var doc in docs.ToObjects<Document>())
             {
             {
@@ -527,7 +525,7 @@ public class DynamicDocumentGrid<TDocument, TEntity, TEntityLink> : DynamicManyT
     {
     {
         if (documents.Any())
         if (documents.Any())
         {
         {
-            new Client<Document>().Save(documents, "Initial Upload");
+            Client.Save(documents, "Initial Upload");
             foreach (var doc in documents)
             foreach (var doc in documents)
             {
             {
                 var newitem = CreateItem();
                 var newitem = CreateItem();
@@ -601,7 +599,7 @@ public class DynamicDocumentGrid<TDocument, TEntity, TEntityLink> : DynamicManyT
                                 row.Set<TDocument, byte[]>(x => x.Thumbnail!, data);
                                 row.Set<TDocument, byte[]>(x => x.Thumbnail!, data);
                             }
                             }
                         }
                         }
-                        Dispatcher.BeginInvoke(() => Refresh(false,false));
+                        Dispatcher.BeginInvoke(() => base.Refresh(false,false));
                     }
                     }
                 );
                 );
             }
             }

+ 6 - 2
inabox.wpf/DynamicGrid/Grids/DynamicGrid.cs

@@ -965,9 +965,13 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
         var cols = IsDirectEditMode()
         var cols = IsDirectEditMode()
             ? new Columns<T>(ColumnTypeFlags.IncludeVisible | ColumnTypeFlags.IncludeForeignKeys)
             ? new Columns<T>(ColumnTypeFlags.IncludeVisible | ColumnTypeFlags.IncludeForeignKeys)
             : new Columns<T>(ColumnTypeFlags.IncludeVisible | ColumnTypeFlags.IncludeLinked);
             : new Columns<T>(ColumnTypeFlags.IncludeVisible | ColumnTypeFlags.IncludeLinked);
-
-        return ExtractColumns(cols);
+        
+        var columns = ExtractColumns(cols);
+        OnGenerateColumns?.Invoke(this, new GenerateColumnsEventArgs(columns));
+        return columns;
     }
     }
+    
+    public event GenerateColumnsEvent OnGenerateColumns;
 
 
     protected virtual void SaveColumns(DynamicGridColumns columns)
     protected virtual void SaveColumns(DynamicGridColumns columns)
     {
     {

+ 3 - 2
inabox.wpf/DynamicGrid/Grids/DynamicManyToManyGrid.cs

@@ -27,6 +27,8 @@ public class DynamicManyToManyGrid<TManyToMany, TThis> : DynamicGrid<TManyToMany
     where TThis : Entity, new()
     where TThis : Entity, new()
     where TManyToMany : Entity, IPersistent, IRemotable, new()
     where TManyToMany : Entity, IPersistent, IRemotable, new()
 {
 {
+    public IQueryProviderFactory Client = Clients.Client.Factory;
+
     //private Guid ID = Guid.Empty;
     //private Guid ID = Guid.Empty;
     protected TThis Item;
     protected TThis Item;
 
 
@@ -410,8 +412,7 @@ public class DynamicManyToManyGrid<TManyToMany, TThis> : DynamicGrid<TManyToMany
     public override void LoadEditorButtons(TManyToMany item, DynamicEditorButtons buttons)
     public override void LoadEditorButtons(TManyToMany item, DynamicEditorButtons buttons)
     {
     {
         base.LoadEditorButtons(item, buttons);
         base.LoadEditorButtons(item, buttons);
-        if (ClientFactory.IsSupported<AuditTrail>())
-            buttons.Add("Audit Trail", Wpf.Resources.view.AsBitmapImage(), item, AuditTrailClick);
+        buttons.Add("Audit Trail", Wpf.Resources.view.AsBitmapImage(), item, AuditTrailClick);
     }
     }
 
 
     private void AuditTrailClick(object sender, object? item)
     private void AuditTrailClick(object sender, object? item)

+ 1 - 1
inabox.wpf/Editors/TextBoxDialog.xaml

@@ -5,7 +5,7 @@
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
         xmlns:wpf="clr-namespace:InABox.Wpf"
         xmlns:wpf="clr-namespace:InABox.Wpf"
         mc:Ignorable="d"
         mc:Ignorable="d"
-        Title="MemoEditor" Height="450" Width="800">
+        Title="MemoEditor" Height="450" Width="800" WindowStartupLocation="CenterScreen">
     <Grid Margin="5">
     <Grid Margin="5">
         <Grid.ColumnDefinitions>
         <Grid.ColumnDefinitions>
             <ColumnDefinition Width="*" />
             <ColumnDefinition Width="*" />

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

@@ -326,7 +326,7 @@ public partial class MessageWindow : Window, INotifyPropertyChanged
 
 
     private static void EmailLogs_Click(Exception e)
     private static void EmailLogs_Click(Exception e)
     {
     {
-        var logFile = Path.Combine(CoreUtils.GetPath(), string.Format("{0:yyyy-MM-dd}.log", DateTime.Today));
+        var logFile = CoreUtils.GetVersion();
 
 
         const int nRead = 1024 * 1024;
         const int nRead = 1024 * 1024;
 
 

+ 117 - 0
inabox.wpf/WindowBehavior.cs

@@ -0,0 +1,117 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Windows;
+using System.Windows.Interop;
+
+namespace InABox.WPF;
+
+public class WindowBehavior
+{
+    private static readonly Type OwnerType = typeof (WindowBehavior);
+
+    #region HideCloseButton (attached property)
+
+    public static readonly DependencyProperty HideCloseButtonProperty =
+        DependencyProperty.RegisterAttached(
+            "HideCloseButton",
+            typeof (bool),
+            OwnerType,
+            new FrameworkPropertyMetadata(false, new PropertyChangedCallback(HideCloseButtonChangedCallback)));
+
+    [AttachedPropertyBrowsableForType(typeof(Window))]
+    public static bool GetHideCloseButton(Window obj) {
+        return (bool)obj.GetValue(HideCloseButtonProperty);
+    }
+
+    [AttachedPropertyBrowsableForType(typeof(Window))]
+    public static void SetHideCloseButton(Window obj, bool value) {
+        obj.SetValue(HideCloseButtonProperty, value);
+    }
+
+    private static void HideCloseButtonChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
+    {
+        var window = d as Window;
+        if (window == null) return;
+
+        var hideCloseButton = (bool)e.NewValue;
+        if (hideCloseButton && !GetIsHiddenCloseButton(window)) {
+            if (!window.IsLoaded) {
+                window.Loaded += HideWhenLoadedDelegate;
+            }
+            else {
+                HideCloseButton(window);
+            }
+            SetIsHiddenCloseButton(window, true);
+        }
+        else if (!hideCloseButton && GetIsHiddenCloseButton(window)) {
+            if (!window.IsLoaded) {
+                window.Loaded -= ShowWhenLoadedDelegate;
+            }
+            else {
+                ShowCloseButton(window);
+            }
+            SetIsHiddenCloseButton(window, false);
+        }
+    }
+
+    #region Win32 imports
+
+    private const int GWL_STYLE = -16;
+    private const int WS_SYSMENU = 0x80000;
+    [DllImport("user32.dll", SetLastError = true)]
+    private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
+    [DllImport("user32.dll")]
+    private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
+
+    #endregion
+
+    private static readonly RoutedEventHandler HideWhenLoadedDelegate = (sender, args) => {
+        if (sender is Window == false) return;
+        var w = (Window)sender;
+        HideCloseButton(w);
+        w.Loaded -= HideWhenLoadedDelegate;
+    };
+
+    private static readonly RoutedEventHandler ShowWhenLoadedDelegate = (sender, args) => {
+        if (sender is Window == false) return;
+        var w = (Window)sender;
+        ShowCloseButton(w);
+        w.Loaded -= ShowWhenLoadedDelegate;
+    };
+
+    private static void HideCloseButton(Window w) {
+        var hwnd = new WindowInteropHelper(w).Handle;
+        SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) & ~WS_SYSMENU);
+    }
+
+    private static void ShowCloseButton(Window w) {
+        var hwnd = new WindowInteropHelper(w).Handle;
+        SetWindowLong(hwnd, GWL_STYLE, GetWindowLong(hwnd, GWL_STYLE) | WS_SYSMENU);
+    }
+
+    #endregion
+
+    #region IsHiddenCloseButton (readonly attached property)
+
+    private static readonly DependencyPropertyKey IsHiddenCloseButtonKey =
+        DependencyProperty.RegisterAttachedReadOnly(
+            "IsHiddenCloseButton",
+            typeof (bool),
+            OwnerType,
+            new FrameworkPropertyMetadata(false));
+
+    public static readonly DependencyProperty IsHiddenCloseButtonProperty =
+        IsHiddenCloseButtonKey.DependencyProperty;
+
+    [AttachedPropertyBrowsableForType(typeof(Window))]
+    public static bool GetIsHiddenCloseButton(Window obj) {
+        return (bool)obj.GetValue(IsHiddenCloseButtonProperty);
+    }
+
+    private static void SetIsHiddenCloseButton(Window obj, bool value) {
+        obj.SetValue(IsHiddenCloseButtonKey, value);
+    }
+
+    #endregion
+
+}