Browse Source

New client save system; also fixing MultiImage report object crash and Form_Data.TableName

Kenric Nugteren 2 years ago
parent
commit
2447a1c59b

+ 5 - 0
InABox.Client.Local/LocalClient.cs

@@ -178,12 +178,17 @@ namespace InABox.Clients
         {
         {
             store = DbFactory.FindStore<TEntity>(ClientFactory.UserGuid, ClientFactory.UserID, ClientFactory.Platform, ClientFactory.Version);
             store = DbFactory.FindStore<TEntity>(ClientFactory.UserGuid, ClientFactory.UserID, ClientFactory.Platform, ClientFactory.Version);
             store.Save(entity, auditnote);
             store.Save(entity, auditnote);
+            entity.CommitChanges();
         }
         }
 
 
         protected override void DoSave(IEnumerable<TEntity> entities, string auditnote)
         protected override void DoSave(IEnumerable<TEntity> entities, string auditnote)
         {
         {
             store = DbFactory.FindStore<TEntity>(ClientFactory.UserGuid, ClientFactory.UserID, ClientFactory.Platform, ClientFactory.Version);
             store = DbFactory.FindStore<TEntity>(ClientFactory.UserGuid, ClientFactory.UserID, ClientFactory.Platform, ClientFactory.Version);
             store.Save(entities, auditnote);
             store.Save(entities, auditnote);
+            foreach(var entity in entities)
+            {
+                entity.CommitChanges();
+            }
         }
         }
 
 
         #endregion
         #endregion

+ 1 - 1
InABox.Client.Remote.Json/JsonClient.cs

@@ -97,7 +97,7 @@ namespace InABox.Clients
             );
             );
             var req = new RestRequest(cmd, Method.POST)
             var req = new RestRequest(cmd, Method.POST)
             {
             {
-                Timeout = Timeout.Milliseconds
+                Timeout = Timeout.Milliseconds,
             };
             };
             //Log("  * {0}{1}() Creating Uri, Client and RestRequest took {2}ms", Action, typeof(TEntity).Name, sw.ElapsedMilliseconds);
             //Log("  * {0}{1}() Creating Uri, Client and RestRequest took {2}ms", Action, typeof(TEntity).Name, sw.ElapsedMilliseconds);
             //sw.Restart();
             //sw.Restart();

+ 34 - 3
InABox.Client.Remote.Shared/RemoteClient.cs

@@ -286,6 +286,7 @@ namespace InABox.Clients
             var request = new SaveRequest<TEntity>();
             var request = new SaveRequest<TEntity>();
             request.Item = entity;
             request.Item = entity;
             request.AuditNote = auditnote;
             request.AuditNote = auditnote;
+            request.ReturnOnlyChanged = true;
 
 
             PrepareRequest(request);
             PrepareRequest(request);
 
 
@@ -293,7 +294,7 @@ namespace InABox.Clients
             switch (response.Status)
             switch (response.Status)
             {
             {
                 case StatusCode.OK:
                 case StatusCode.OK:
-                    var props = CoreUtils.PropertyList(typeof(TEntity), x => true, true);
+                    /*var props = CoreUtils.PropertyList(typeof(TEntity), x => true, true);
                     entity.SetObserving(false);
                     entity.SetObserving(false);
                     foreach (var prop in props.Keys)
                     foreach (var prop in props.Keys)
                     {
                     {
@@ -301,8 +302,20 @@ namespace InABox.Clients
                         CoreUtils.SetPropertyValue(entity, prop, value);
                         CoreUtils.SetPropertyValue(entity, prop, value);
                     }
                     }
 
 
+                    entity.CommitChanges();
+                    entity.SetObserving(true);*/
+
+                    entity.SetObserving(false);
+                    foreach (var (key, value) in response.ChangedValues)
+                    {
+                        if(CoreUtils.TryGetProperty<TEntity>(key, out var property))
+                        {
+                            CoreUtils.SetPropertyValue(entity, key, CoreUtils.ChangeType(value, property.PropertyType));
+                        }
+                    }
                     entity.CommitChanges();
                     entity.CommitChanges();
                     entity.SetObserving(true);
                     entity.SetObserving(true);
+
                     break;
                     break;
                 case StatusCode.Unauthenticated:
                 case StatusCode.Unauthenticated:
                     throw new RemoteException("Client not authenticated", request);
                     throw new RemoteException("Client not authenticated", request);
@@ -317,6 +330,7 @@ namespace InABox.Clients
             var request = new MultiSaveRequest<TEntity>();
             var request = new MultiSaveRequest<TEntity>();
             request.Items = items;
             request.Items = items;
             request.AuditNote = auditnote;
             request.AuditNote = auditnote;
+            request.ReturnOnlyChanged = true;
 
 
             PrepareRequest(request);
             PrepareRequest(request);
 
 
@@ -324,7 +338,24 @@ namespace InABox.Clients
             switch (response.Status)
             switch (response.Status)
             {
             {
                 case StatusCode.OK:
                 case StatusCode.OK:
-                    var props = CoreUtils.PropertyList(typeof(TEntity), x => true, true);
+                    for(int i = 0; i < items.Length; ++i)
+                    {
+                        var entity = items[i];
+                        var changedValues = response.ChangedValues[i];
+
+                        entity.SetObserving(false);
+                        foreach (var (key, value) in changedValues)
+                        {
+                            if (CoreUtils.TryGetProperty<TEntity>(key, out var property))
+                            {
+                                CoreUtils.SetPropertyValue(entity, key, CoreUtils.ChangeType(value, property.PropertyType));
+                            }
+                        }
+                        entity.CommitChanges();
+                        entity.SetObserving(true);
+                    }
+
+                    /*var props = CoreUtils.PropertyList(typeof(TEntity), x => true, true);
                     for (var i = 0; i < items.Length; i++)
                     for (var i = 0; i < items.Length; i++)
                     {
                     {
                         items[i].SetObserving(false);
                         items[i].SetObserving(false);
@@ -337,7 +368,7 @@ namespace InABox.Clients
                         //CoreUtils.DeepClone<TEntity>(response.Items[i], items[i]);
                         //CoreUtils.DeepClone<TEntity>(response.Items[i], items[i]);
                         items[i].CommitChanges();
                         items[i].CommitChanges();
                         items[i].SetObserving(true);
                         items[i].SetObserving(true);
-                    }
+                    }*/
                     break;
                     break;
                 case StatusCode.Unauthenticated:
                 case StatusCode.Unauthenticated:
                     throw new RemoteException("Client not authenticated", request);
                     throw new RemoteException("Client not authenticated", request);

+ 10 - 0
InABox.Core/Client/Request.cs

@@ -179,6 +179,10 @@ namespace InABox.Clients
         public TEntity[] Items { get; set; }
         public TEntity[] Items { get; set; }
         public string AuditNote { get; set; }
         public string AuditNote { get; set; }
 
 
+        [Obsolete("We don't like this; it should always be true.")]
+        // Added 11/04/23 to address incompatibility, remove as soon as possible; the relevant code to update is in RestService; update assuming that ReturnOnlyChanged is always true.
+        public bool ReturnOnlyChanged { get; set; } = false;
+
         public override RequestMethod GetMethod() => RequestMethod.MultiSave;
         public override RequestMethod GetMethod() => RequestMethod.MultiSave;
     }
     }
 
 
@@ -186,6 +190,8 @@ namespace InABox.Clients
     {
     {
         //public Guid[] IDs { get; set; }
         //public Guid[] IDs { get; set; }
         public TEntity[] Items { get; set; }
         public TEntity[] Items { get; set; }
+
+        public List<Dictionary<string, object?>> ChangedValues { get; set; } = new List<Dictionary<string, object?>>();
     }
     }
 
 
     public class SaveRequest<TEntity> : BaseRequest<TEntity> where TEntity : Entity, new()
     public class SaveRequest<TEntity> : BaseRequest<TEntity> where TEntity : Entity, new()
@@ -193,6 +199,8 @@ namespace InABox.Clients
         public TEntity Item { get; set; }
         public TEntity Item { get; set; }
         public string AuditNote { get; set; }
         public string AuditNote { get; set; }
 
 
+        public bool ReturnOnlyChanged { get; set; } = false;
+
         public override RequestMethod GetMethod() => RequestMethod.Save;
         public override RequestMethod GetMethod() => RequestMethod.Save;
     }
     }
 
 
@@ -200,6 +208,8 @@ namespace InABox.Clients
     {
     {
         //public Guid ID { get; set; }
         //public Guid ID { get; set; }
         public TEntity Item { get; set; }
         public TEntity Item { get; set; }
+
+        public Dictionary<string, object?> ChangedValues { get; set; } = new Dictionary<string, object?>();
     }
     }
 
 
     public class DeleteRequest<TEntity> : BaseRequest<TEntity> where TEntity : Entity, new()
     public class DeleteRequest<TEntity> : BaseRequest<TEntity> where TEntity : Entity, new()

+ 3 - 0
InABox.Core/CoreUtils.cs

@@ -684,6 +684,7 @@ namespace InABox.Core
                 return info;
                 return info;
             throw new Exception($"Property {name} does not exist for {t.Name}");
             throw new Exception($"Property {name} does not exist for {t.Name}");
         }
         }
+        public static PropertyInfo GetProperty<T>(string name) => GetProperty(typeof(T), name);
 
 
         public static bool TryGetProperty(Type t, string name, [NotNullWhen(true)] out PropertyInfo? propertyInfo)
         public static bool TryGetProperty(Type t, string name, [NotNullWhen(true)] out PropertyInfo? propertyInfo)
         {
         {
@@ -701,6 +702,8 @@ namespace InABox.Core
                 return false;
                 return false;
             return true;
             return true;
         }
         }
+        public static bool TryGetProperty<T>(string name, [NotNullWhen(true)] out PropertyInfo? propertyInfo) =>
+            TryGetProperty(typeof(T), name, out propertyInfo);
 
 
         public static bool HasProperty(Type t, string name)
         public static bool HasProperty(Type t, string name)
         {
         {

+ 46 - 27
InABox.Core/DataModel/DataModel.cs

@@ -80,6 +80,8 @@ namespace InABox.Core
     {
     {
         IEnumerable<DataTable> DefaultTables { get; }
         IEnumerable<DataTable> DefaultTables { get; }
 
 
+        void AddTable(string alias, CoreTable table, bool isdefault = false);
+
         void AddTable(Type type, CoreTable table, bool isdefault = false, string? alias = null);
         void AddTable(Type type, CoreTable table, bool isdefault = false, string? alias = null);
         /// <summary>
         /// <summary>
         /// Adds a table to the datamodel.
         /// Adds a table to the datamodel.
@@ -96,6 +98,7 @@ namespace InABox.Core
         /// </param>
         /// </param>
         void AddTable<TType>(Filter<TType>? filter, Columns<TType>? columns, bool isdefault = false, string? alias = null, bool shouldLoad = true);
         void AddTable<TType>(Filter<TType>? filter, Columns<TType>? columns, bool isdefault = false, string? alias = null, bool shouldLoad = true);
         void LinkTable(Type parenttype, string parentcolumn, Type childtype, string childcolumn, string? parentalias = null, string? childalias = null, bool isLookup = false);
         void LinkTable(Type parenttype, string parentcolumn, Type childtype, string childcolumn, string? parentalias = null, string? childalias = null, bool isLookup = false);
+        void LinkTable(Type parenttype, string parentcolumn, string childalias, string childcolumn, string? parentalias = null, bool isLookup = false);
 
 
         void LinkTable<TParent, TChild>(Expression<Func<TParent, object>> parent, Expression<Func<TChild, object>> child, string? parentalias = null,
         void LinkTable<TParent, TChild>(Expression<Func<TParent, object>> parent, Expression<Func<TChild, object>> child, string? parentalias = null,
             string? childalias = null, bool isLookup = false);
             string? childalias = null, bool isLookup = false);
@@ -151,6 +154,7 @@ namespace InABox.Core
         CoreTable GetTable<TType>(string? alias = null);
         CoreTable GetTable<TType>(string? alias = null);
         void SetTableData(Type type, CoreTable tableData, string? alias = null);
         void SetTableData(Type type, CoreTable tableData, string? alias = null);
         bool HasTable(Type type, string? alias = null);
         bool HasTable(Type type, string? alias = null);
+        bool HasTable<TType>(string? alias = null);
 
 
         void LoadModel(IEnumerable<string> requiredTables, Dictionary<string, IQueryDef>? requiredQueries = null);
         void LoadModel(IEnumerable<string> requiredTables, Dictionary<string, IQueryDef>? requiredQueries = null);
         void LoadModel(IEnumerable<string> requiredTables, params IDataModelQueryDef[] requiredQueries);
         void LoadModel(IEnumerable<string> requiredTables, params IDataModelQueryDef[] requiredQueries);
@@ -172,16 +176,7 @@ namespace InABox.Core
             AddTable(new Filter<User>(x => x.ID).IsEqualTo(ClientFactory.UserGuid), null, true);
             AddTable(new Filter<User>(x => x.ID).IsEqualTo(ClientFactory.UserGuid), null, true);
         }
         }
 
 
-        public Dictionary<Type, CoreTable> AsDictionary
-        {
-            get
-            {
-                var result = new Dictionary<Type, CoreTable>();
-                foreach (var alias in _tables.Keys)
-                    result[_tables[alias].Type] = _tables[alias].Table;
-                return result;
-            }
-        }
+        public IEnumerable<KeyValuePair<string, DataModelTable>> ModelTables => _tables;
 
 
         public abstract string Name { get; }
         public abstract string Name { get; }
 
 
@@ -203,12 +198,17 @@ namespace InABox.Core
             foreach(var (key, table) in _tables)
             foreach(var (key, table) in _tables)
             {
             {
                 var current = table.Table.Columns.ToDictionary(x => x.ColumnName, x => x);
                 var current = table.Table.Columns.ToDictionary(x => x.ColumnName, x => x);
-                var additional = Columns.Create(table.Type);
-                foreach (var column in CoreUtils.GetColumnNames(table.Type, x => true))
+
+                IColumns? additional = null;
+                if(table.Type != null)
                 {
                 {
-                    if (!current.ContainsKey(column))
+                    additional = Columns.Create(table.Type);
+                    foreach (var column in CoreUtils.GetColumnNames(table.Type, x => true))
                     {
                     {
-                        additional.Add(column);
+                        if (!current.ContainsKey(column))
+                        {
+                            additional.Add(column);
+                        }
                     }
                     }
                 }
                 }
 
 
@@ -325,7 +325,9 @@ namespace InABox.Core
 
 
         public class DataModelTable
         public class DataModelTable
         {
         {
-            public DataModelTable(Type type, CoreTable table, bool isDefault, IFilter? filter, IColumns? columns, bool shouldLoad = true)
+            private bool shouldLoad;
+
+            public DataModelTable(Type? type, CoreTable table, bool isDefault, IFilter? filter, IColumns? columns, bool shouldLoad = true)
             {
             {
                 Type = type;
                 Type = type;
                 Table = table;
                 Table = table;
@@ -335,7 +337,7 @@ namespace InABox.Core
                 ShouldLoad = shouldLoad;
                 ShouldLoad = shouldLoad;
             }
             }
 
 
-            public Type Type { get; }
+            public Type? Type { get; }
 
 
             public CoreTable Table { get; set; }
             public CoreTable Table { get; set; }
 
 
@@ -345,7 +347,14 @@ namespace InABox.Core
 
 
             public IColumns? Columns { get; set; }
             public IColumns? Columns { get; set; }
 
 
-            public bool ShouldLoad { get; set; }
+            public bool ShouldLoad
+            {
+                get => shouldLoad && Type != null;
+                set
+                {
+                    shouldLoad = value;
+                }
+            }
         }
         }
 
 
         #region New Load Methods
         #region New Load Methods
@@ -383,14 +392,19 @@ namespace InABox.Core
             var relation = _relationships.Where(x => x.ChildTable == tableName).FirstOrDefault();
             var relation = _relationships.Where(x => x.ChildTable == tableName).FirstOrDefault();
             if (relation != null)
             if (relation != null)
             {
             {
-                var subFilter = (typeof(DataModel).GetMethod(nameof(GetSubquery), BindingFlags.NonPublic | BindingFlags.Instance)
+                var table = _tables[relation.ParentTable];
+
+                if(table.Type != null)
+                {
+                    var subFilter = (typeof(DataModel).GetMethod(nameof(GetSubquery), BindingFlags.NonPublic | BindingFlags.Instance)
                     .MakeGenericMethod(_tables[relation.ParentTable].Type, typeof(TType))
                     .MakeGenericMethod(_tables[relation.ParentTable].Type, typeof(TType))
                     .Invoke(this, new object?[] { relation, requiredQueries }) as Filter<TType>)!;
                     .Invoke(this, new object?[] { relation, requiredQueries }) as Filter<TType>)!;
 
 
-                if (newFilter != null)
-                    newFilter.And(subFilter);
-                else
-                    newFilter = subFilter;
+                    if (newFilter != null)
+                        newFilter.And(subFilter);
+                    else
+                        newFilter = subFilter;
+                }
             }
             }
 
 
             return newFilter;
             return newFilter;
@@ -465,19 +479,19 @@ namespace InABox.Core
             return _relationships.Any(x => x.ChildTable == tableName);
             return _relationships.Any(x => x.ChildTable == tableName);
         }
         }
 
 
-        public static string TableName(Type type, string? alias = null)
+        public static string TableName(Type? type, string? alias = null)
         {
         {
-            return string.IsNullOrWhiteSpace(alias) ? type.EntityName().Split('.').Last() : alias;
+            return string.IsNullOrWhiteSpace(alias) ? (type ?? throw new Exception("No type or alias given!")).EntityName().Split('.').Last() : alias;
         }
         }
 
 
-        private void CheckTable(Type type, string? alias = null)
+        private void CheckTable(Type? type, string? alias = null)
         {
         {
             var name = TableName(type, alias);
             var name = TableName(type, alias);
             if (!_tables.ContainsKey(name))
             if (!_tables.ContainsKey(name))
                 throw new Exception(string.Format("No Table for {0}", name));
                 throw new Exception(string.Format("No Table for {0}", name));
         }
         }
 
 
-        public void AddTable(Type type, CoreTable table, bool isdefault = false, string? alias = null)
+        public void AddTable(Type? type, CoreTable table, bool isdefault = false, string? alias = null)
         {
         {
             var name = TableName(type, alias);
             var name = TableName(type, alias);
             if (!_tables.ContainsKey(name))
             if (!_tables.ContainsKey(name))
@@ -499,8 +513,10 @@ namespace InABox.Core
                 throw new Exception(string.Format("[{0}] does not exist in this data model!", name));
                 throw new Exception(string.Format("[{0}] does not exist in this data model!", name));
             }
             }
         }
         }
+        public void LinkTable(Type parenttype, string parentcolumn, string childalias, string childcolumn, string? parentalias = null, bool isLookup = false) =>
+            LinkTable(parenttype, parentcolumn, null, childcolumn, parentalias, childalias, isLookup);
 
 
-        public void LinkTable(Type parenttype, string parentcolumn, Type childtype, string childcolumn, string? parentalias = null,
+        public void LinkTable(Type parenttype, string parentcolumn, Type? childtype, string childcolumn, string? parentalias = null,
             string? childalias = null, bool isLookup = false)
             string? childalias = null, bool isLookup = false)
         {
         {
             CheckTable(parenttype, parentalias);
             CheckTable(parenttype, parentalias);
@@ -524,11 +540,14 @@ namespace InABox.Core
             var name = TableName(type, alias);
             var name = TableName(type, alias);
             return _tables.ContainsKey(name);
             return _tables.ContainsKey(name);
         }
         }
+        public bool HasTable<T>(string? alias = null) => HasTable(typeof(T), alias);
 
 
         #endregion
         #endregion
 
 
         #region Adding & Linking Tables
         #region Adding & Linking Tables
 
 
+        public void AddTable(string alias, CoreTable table, bool isdefault = false) => AddTable(null, table, isdefault, alias);
+
         public void AddTable<TType>(Filter<TType>? filter, Columns<TType>? columns, bool isdefault = false, string? alias = null, bool shouldLoad = true)
         public void AddTable<TType>(Filter<TType>? filter, Columns<TType>? columns, bool isdefault = false, string? alias = null, bool shouldLoad = true)
         {
         {
             var name = TableName<TType>(alias);
             var name = TableName<TType>(alias);

+ 9 - 4
InABox.Core/DataTable.cs

@@ -322,12 +322,13 @@ namespace InABox.Core
 
 
         private List<CoreRow>? rows;
         private List<CoreRow>? rows;
         private List<CoreColumn> columns = new List<CoreColumn>();
         private List<CoreColumn> columns = new List<CoreColumn>();
+        private string tableName;
 
 
         #endregion
         #endregion
 
 
         #region Properties
         #region Properties
 
 
-        public string TableName { get; set; }
+        public string TableName { get => tableName; }
 
 
         public IList<CoreColumn> Columns { get => columns; }
         public IList<CoreColumn> Columns { get => columns; }
         public IList<CoreRow> Rows
         public IList<CoreRow> Rows
@@ -345,9 +346,13 @@ namespace InABox.Core
 
 
         #endregion
         #endregion
 
 
-        public CoreTable() : base()
+        public CoreTable() : this("")
         {
         {
-            TableName = "";
+        }
+
+        public CoreTable(string tableName): base()
+        {
+            this.tableName = tableName;
         }
         }
 
 
         public CoreTable(Type type) : this()
         public CoreTable(Type type) : this()
@@ -957,7 +962,7 @@ namespace InABox.Core
 
 
         public void ReadBinary(BinaryReader reader, IList<CoreColumn>? columns)
         public void ReadBinary(BinaryReader reader, IList<CoreColumn>? columns)
         {
         {
-            TableName = reader.ReadString();
+            tableName = reader.ReadString();
 
 
             Columns.Clear();
             Columns.Clear();
             if (columns is null)
             if (columns is null)

+ 2 - 2
InABox.Core/DigitalForms/DigitalFormReportDataModel.cs

@@ -60,8 +60,8 @@ namespace InABox.Core
 
 
             FormDataTable = formDataTable;
             FormDataTable = formDataTable;
 
 
-            AddTable(typeof(CoreTable), formDataTable, true, "Form_Data");
-            LinkTable(typeof(T), "ID", typeof(CoreTable), "Parent.ID", childalias: "Form_Data");
+            AddTable("Form_Data", formDataTable, true);
+            LinkTable(typeof(T), "ID", "Form_Data", "Parent.ID");
         }
         }
 
 
         private void LoadFormDataIntoRow(CoreRow row, Dictionary<string, object?> data)
         private void LoadFormDataIntoRow(CoreRow row, Dictionary<string, object?> data)

+ 1 - 1
InABox.Core/ICoreTable.cs

@@ -11,7 +11,7 @@ namespace InABox.Core
         IList<CoreColumn> Columns { get; }
         IList<CoreColumn> Columns { get; }
         IList<CoreRow> Rows { get; }
         IList<CoreRow> Rows { get; }
         Dictionary<string, IList<Action<object, object>?>> Setters { get; }
         Dictionary<string, IList<Action<object, object>?>> Setters { get; }
-        string TableName { get; set; }
+        string TableName { get; }
 
 
         void CopyTo(CoreTable table);
         void CopyTo(CoreTable table);
         void CopyTo(DataTable table);
         void CopyTo(DataTable table);

+ 2 - 2
InABox.Database/Stores/Store.cs

@@ -600,7 +600,7 @@ namespace InABox.Database
             //UpdateExternalLinks(entity, false);
             //UpdateExternalLinks(entity, false);
 
 
             entity = RunScript(ScriptType.AfterSave, new[] { entity }).First();
             entity = RunScript(ScriptType.AfterSave, new[] { entity }).First();
-            entity.CommitChanges();
+            //entity.CommitChanges();
 
 
             //CloseSession("Save", false);
             //CloseSession("Save", false);
             //}
             //}
@@ -730,7 +730,7 @@ namespace InABox.Database
             {
             {
                 AfterSave(entity);
                 AfterSave(entity);
                 //UpdateExternalLinks(entity, false);
                 //UpdateExternalLinks(entity, false);
-                entity.CommitChanges();
+                //entity.CommitChanges();
             }
             }
 
 
             entities = RunScript(ScriptType.AfterSave, entities);
             entities = RunScript(ScriptType.AfterSave, entities);

+ 75 - 3
InABox.Server/RestService.cs

@@ -340,9 +340,38 @@ namespace InABox.API
             try
             try
             {
             {
                 var e = request.Item;
                 var e = request.Item;
+                var oldValues = new Dictionary<string, object?>();
+                foreach (var key in e.OriginalValues.Keys)
+                {
+                    oldValues.Add(key, CoreUtils.GetPropertyValue(e, key));
+                }
+                    
                 var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version);
                 var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version);
                 store.Save(e, request.AuditNote);
                 store.Save(e, request.AuditNote);
-                response.Item = e;
+
+                if (request.ReturnOnlyChanged)
+                {
+                    foreach(var (key, value) in e.OriginalValues)
+                    {
+                        if(oldValues.TryGetValue(key, out var oldValue))
+                        {
+                            var curValue = CoreUtils.GetPropertyValue(e, key);
+                            if(!Equals(curValue, oldValue))
+                            {
+                                response.ChangedValues.Add(key, CoreUtils.GetPropertyValue(e, key));
+                            }
+                        }
+                        else
+                        {
+                            response.ChangedValues.Add(key, CoreUtils.GetPropertyValue(e, key));
+                        }
+                    }
+                }
+                else
+                {
+                    response.Item = e;
+                }
+
                 response.Status = StatusCode.OK;
                 response.Status = StatusCode.OK;
                 Logger.Send(LogType.Information, userid,
                 Logger.Send(LogType.Information, userid,
                     string.Format("[{0} {1}] [{2:D8}] Save{3} Data=[{4}] Complete", request.Credentials.Platform, request.Credentials.Version,
                     string.Format("[{0} {1}] [{2:D8}] Save{3} Data=[{4}] Complete", request.Credentials.Platform, request.Credentials.Version,
@@ -379,15 +408,58 @@ namespace InABox.API
                     request.Items != null ? string.Join(", ", request.Items.Select(x => x.ToString())) : request + " (null)"));
                     request.Items != null ? string.Join(", ", request.Items.Select(x => x.ToString())) : request + " (null)"));
             try
             try
             {
             {
+
                 var es = request.Items;
                 var es = request.Items;
+
+                var oldValuesList = new List<Dictionary<string, object?>>();
+                foreach(var e in es)
+                {
+                    var oldValues = new Dictionary<string, object?>();
+                    foreach (var key in e.OriginalValues.Keys)
+                    {
+                        oldValues.Add(key, CoreUtils.GetPropertyValue(e, key));
+                    }
+                    oldValuesList.Add(oldValues);
+                }
+
                 var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version);
                 var store = DbFactory.FindStore<TEntity>(userguid, userid, request.Credentials.Platform, request.Credentials.Version);
 
 
                 store.Save(es, request.AuditNote);
                 store.Save(es, request.AuditNote);
-                response.Items = es;
+
+                if (request.ReturnOnlyChanged)
+                {
+                    for(int i = 0; i < es.Length; ++i)
+                    {
+                        var e = es[i];
+
+                        var changedValues = new Dictionary<string, object?>();
+                        var oldValues = oldValuesList[i];
+                        foreach (var (key, value) in e.OriginalValues)
+                        {
+                            if (oldValues.TryGetValue(key, out var oldValue))
+                            {
+                                if (!Equals(value, oldValue))
+                                {
+                                    changedValues.Add(key, value);
+                                }
+                            }
+                            else
+                            {
+                                changedValues.Add(key, value);
+                            }
+                        }
+                        response.ChangedValues.Add(changedValues);
+                    }
+                }
+                else
+                {
+                    response.Items = es;
+                }
+
                 response.Status = StatusCode.OK;
                 response.Status = StatusCode.OK;
                 Logger.Send(LogType.Information, userid,
                 Logger.Send(LogType.Information, userid,
                     string.Format("[{0} {1}] [{2:D8}] MultiSave{3} Count=[{4}] Complete", request.Credentials.Platform, request.Credentials.Version,
                     string.Format("[{0} {1}] [{2:D8}] MultiSave{3} Count=[{4}] Complete", request.Credentials.Platform, request.Credentials.Version,
-                        (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), response.Items.Length));
+                        (int)DateTime.Now.Subtract(start).TotalMilliseconds, SimpleName(typeof(TEntity)), es.Length));
 
 
                 CredentialsCache.Refresh(typeof(TEntity) == typeof(User));
                 CredentialsCache.Refresh(typeof(TEntity) == typeof(User));
             }
             }

+ 35 - 5
inabox.client.ipc/PipeIPCClient.cs

@@ -170,7 +170,7 @@ namespace InABox.Client.IPC
             return result.ToArray();
             return result.ToArray();
         }
         }
 
 
-        protected override CoreTable DoQuery(Filter<TEntity> filter, Columns<TEntity> columns, SortOrder<TEntity> sort = null)
+        protected override CoreTable DoQuery(Filter<TEntity>? filter, Columns<TEntity>? columns, SortOrder<TEntity>? sort = null)
         {
         {
             var request = new QueryRequest<TEntity>
             var request = new QueryRequest<TEntity>
             {
             {
@@ -232,7 +232,8 @@ namespace InABox.Client.IPC
             var request = new SaveRequest<TEntity>
             var request = new SaveRequest<TEntity>
             {
             {
                 Item = entity,
                 Item = entity,
-                AuditNote = auditnote
+                AuditNote = auditnote,
+                ReturnOnlyChanged = true
             };
             };
             PrepareRequest(request);
             PrepareRequest(request);
 
 
@@ -240,7 +241,7 @@ namespace InABox.Client.IPC
             switch (response.Status)
             switch (response.Status)
             {
             {
                 case StatusCode.OK:
                 case StatusCode.OK:
-                    var props = CoreUtils.PropertyList(typeof(TEntity), x => true, true);
+                    /*var props = CoreUtils.PropertyList(typeof(TEntity), x => true, true);
                     entity.SetObserving(false);
                     entity.SetObserving(false);
                     foreach (var prop in props.Keys)
                     foreach (var prop in props.Keys)
                     {
                     {
@@ -248,8 +249,20 @@ namespace InABox.Client.IPC
                         CoreUtils.SetPropertyValue(entity, prop, value);
                         CoreUtils.SetPropertyValue(entity, prop, value);
                     }
                     }
 
 
+                    entity.CommitChanges();
+                    entity.SetObserving(true);*/
+
+                    entity.SetObserving(false);
+                    foreach (var (key, value) in response.ChangedValues)
+                    {
+                        if (CoreUtils.TryGetProperty<TEntity>(key, out var property))
+                        {
+                            CoreUtils.SetPropertyValue(entity, key, CoreUtils.ChangeType(value, property.PropertyType));
+                        }
+                    }
                     entity.CommitChanges();
                     entity.CommitChanges();
                     entity.SetObserving(true);
                     entity.SetObserving(true);
+
                     break;
                     break;
                 case StatusCode.Unauthenticated:
                 case StatusCode.Unauthenticated:
                     throw new IPCException("Client not authenticated");
                     throw new IPCException("Client not authenticated");
@@ -264,7 +277,8 @@ namespace InABox.Client.IPC
             var request = new MultiSaveRequest<TEntity>
             var request = new MultiSaveRequest<TEntity>
             {
             {
                 Items = items,
                 Items = items,
-                AuditNote = auditnote
+                AuditNote = auditnote,
+                ReturnOnlyChanged = true
             };
             };
             PrepareRequest(request);
             PrepareRequest(request);
 
 
@@ -272,7 +286,7 @@ namespace InABox.Client.IPC
             switch (response.Status)
             switch (response.Status)
             {
             {
                 case StatusCode.OK:
                 case StatusCode.OK:
-                    var props = CoreUtils.PropertyList(typeof(TEntity), x => true, true);
+                    /*var props = CoreUtils.PropertyList(typeof(TEntity), x => true, true);
                     for (var i = 0; i < items.Length; i++)
                     for (var i = 0; i < items.Length; i++)
                     {
                     {
                         items[i].SetObserving(false);
                         items[i].SetObserving(false);
@@ -285,6 +299,22 @@ namespace InABox.Client.IPC
                         //CoreUtils.DeepClone<TEntity>(response.Items[i], items[i]);
                         //CoreUtils.DeepClone<TEntity>(response.Items[i], items[i]);
                         items[i].CommitChanges();
                         items[i].CommitChanges();
                         items[i].SetObserving(true);
                         items[i].SetObserving(true);
+                    }*/
+                    for (int i = 0; i < items.Length; ++i)
+                    {
+                        var entity = items[i];
+                        var changedValues = response.ChangedValues[i];
+
+                        entity.SetObserving(false);
+                        foreach (var (key, value) in changedValues)
+                        {
+                            if (CoreUtils.TryGetProperty<TEntity>(key, out var property))
+                            {
+                                CoreUtils.SetPropertyValue(entity, key, CoreUtils.ChangeType(value, property.PropertyType));
+                            }
+                        }
+                        entity.CommitChanges();
+                        entity.SetObserving(true);
                     }
                     }
                     break;
                     break;
                 case StatusCode.Unauthenticated:
                 case StatusCode.Unauthenticated:

+ 1 - 2
inabox.database.sqlite/SQLiteProvider.cs

@@ -2318,8 +2318,7 @@ namespace InABox.Database.SQLite
             var cols = CoreUtils.GetColumns(T, columns);
             var cols = CoreUtils.GetColumns(T, columns);
             //LogStop("GetColumns");
             //LogStop("GetColumns");
 
 
-            var result = new CoreTable();
-            result.TableName = T.EntityName();
+            var result = new CoreTable(T.EntityName());
             foreach (var col in cols.GetColumns())
             foreach (var col in cols.GetColumns())
                 result.Columns.Add(new CoreColumn { ColumnName = col.Property, DataType = col.Type });
                 result.Columns.Add(new CoreColumn { ColumnName = col.Property, DataType = col.Type });
             //LogStop("MakeTable");
             //LogStop("MakeTable");

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

@@ -352,6 +352,7 @@ namespace InABox.DynamicGrid
                             var image = new MultiImageObject
                             var image = new MultiImageObject
                             {
                             {
                                 DataColumn = dataColumn,
                                 DataColumn = dataColumn,
+                                Dock = System.Windows.Forms.DockStyle.Fill
                             };
                             };
                             cell.AddChild(image);
                             cell.AddChild(image);
 
 
@@ -362,6 +363,7 @@ namespace InABox.DynamicGrid
                             var image = new MultiSignatureObject
                             var image = new MultiSignatureObject
                             {
                             {
                                 DataColumn = dataColumn,
                                 DataColumn = dataColumn,
+                                Dock = System.Windows.Forms.DockStyle.Fill
                             };
                             };
                             cell.AddChild(image);
                             cell.AddChild(image);
 
 

+ 56 - 3
inabox.wpf/DynamicGrid/IDynamicEditorHost.cs

@@ -8,6 +8,14 @@ namespace InABox.DynamicGrid
 {
 {
     public interface IDynamicEditorHost
     public interface IDynamicEditorHost
     {
     {
+        /// <summary>
+        /// A list of columns which are defined for this editor; from this are loaded the additional columns for lookups. A useful default is just
+        /// to call <see cref="DynamicGridColumns.ExtractColumns(Type)"/>, if a singular type for the editor is well-defined.
+        /// </summary>
+        /// <remarks>
+        /// I'm still not sure whether this one is actually a good idea, but it seems to be how the editors have functioned for a while. My reasoning is that
+        /// if the lookup defines a column to be loaded, but it doesn't get loaded because it is not in this list, then we would have broken functionality.
+        /// </remarks>
         IEnumerable<DynamicGridColumn> Columns { get; }
         IEnumerable<DynamicGridColumn> Columns { get; }
 
 
         /// <summary>
         /// <summary>
@@ -15,6 +23,7 @@ namespace InABox.DynamicGrid
         /// lookup defined for column EntityLink.ID, <paramref name="columns"/> will contain all EntityLink.* except EntityLink.ID.
         /// lookup defined for column EntityLink.ID, <paramref name="columns"/> will contain all EntityLink.* except EntityLink.ID.
         /// 
         /// 
         /// This essentially gives us the other columns we need to load from the database for lookups.
         /// This essentially gives us the other columns we need to load from the database for lookups.
+        /// See <see cref="DefaultDynamicEditorHost{T}.LoadColumns(string, Dictionary{string, string})"/> for the canonical implementation.
         /// </summary>
         /// </summary>
         /// <remarks>
         /// <remarks>
         /// This is dumb; we don't want it, because the presence of <see cref="Columns"/> kinda makes it redundant.
         /// This is dumb; we don't want it, because the presence of <see cref="Columns"/> kinda makes it redundant.
@@ -24,22 +33,66 @@ namespace InABox.DynamicGrid
         void LoadColumns(string column, Dictionary<string, string> columns);
         void LoadColumns(string column, Dictionary<string, string> columns);
 
 
         /// <summary>
         /// <summary>
-        /// In most cases, calls <see cref="LookupFactory.DefineFilter(Type)"/>.
+        /// In most cases, calls <see cref="LookupFactory.DefineFilter(Type)"/>, and I think this should explain what this method does. Provide a method that
+        /// doesn't do this if you like breaking things (or if you need to provide a filter that differs from the standard lookup; just make sure that you
+        /// check <paramref name="type"/>, and use the <see cref="LookupFactory"/> function if not the type you want).
         /// </summary>
         /// </summary>
-        /// <param name="type"></param>
-        /// <returns></returns>
+        /// <param name="type">The T in <see cref="Filter{T}"/>.</param>
+        /// <returns>A filter (or <see langword="null"/>, being synonymous with <see cref="Filter{T}.All"/>).</returns>
         IFilter? DefineFilter(Type type);
         IFilter? DefineFilter(Type type);
 
 
+        /// <summary>
+        /// Trigger the loading of the lookup values; the canonical implementation calls <see cref="ILookupEditor.Values(string, object[]?)"/>,
+        /// and calls (either sync/async) <see cref="ILookupEditorControl.LoadLookups(CoreTable)"/> when the values are loaded.
+        /// </summary>
+        /// <param name="editor">The editor to load the lookups for.</param>
         void LoadLookups(ILookupEditorControl editor);
         void LoadLookups(ILookupEditorControl editor);
 
 
+        /// <summary>
+        /// Get a document for a given filename.
+        /// </summary>
+        /// <remarks>
+        /// The usual implementation will go through the <see cref="Client{TEntity}"/> interface.
+        /// </remarks>
+        /// <param name="filename">The filename of the document.</param>
+        /// <returns>The document with the right filename, or <see langword="null"/> if not found.</returns>
         Document? FindDocument(string filename);
         Document? FindDocument(string filename);
 
 
+        /// <summary>
+        /// Get a document for a given ID.
+        /// </summary>
+        /// <remarks>
+        /// The usual implementation will go through the <see cref="Client{TEntity}"/> interface.
+        /// </remarks>
+        /// <param name="id">The ID of the document.</param>
+        /// <returns>The document, or <see langword="null"/> if not found.</returns>
         Document? GetDocument(Guid id);
         Document? GetDocument(Guid id);
 
 
+        /// <summary>
+        /// Saves a document.
+        /// </summary>
+        /// <remarks>
+        /// The usual implementation will go through the <see cref="Client{TEntity}"/> interface.
+        /// </remarks>
+        /// <param name="document">The document to save.</param>
         void SaveDocument(Document document);
         void SaveDocument(Document document);
 
 
+        /// <summary>
+        /// Returns a list of the currently edited items; may be an empty array.
+        /// 
+        /// This should probably always be a <see cref="BaseObject"/>[].
+        /// </summary>
+        /// <returns>The items being edited.</returns>
         object?[] GetItems();
         object?[] GetItems();
 
 
+        /// <summary>
+        /// Um... I'm really not sure; achieves the same function as <see cref="DynamicEditorGrid.OnGetEditor"/> - if you know what that does, good job.
+        /// </summary>
+        /// <remarks>
+        /// I think you should be fine to just return <paramref name="column"/>.Editor, as defined by <see cref="DynamicGridColumn"/>.
+        /// </remarks>
+        /// <param name="column">The column to get the editor of.</param>
+        /// <returns>The editor, or <see langword="null"/> if it doesn't exist.</returns>
         BaseEditor? GetEditor(DynamicGridColumn column);
         BaseEditor? GetEditor(DynamicGridColumn column);
     }
     }
 
 

+ 1 - 0
inabox.wpf/Reports/CustomObjects/MultiItemObject.cs

@@ -85,6 +85,7 @@ namespace InABox.Wpf.Reports.CustomObjects
 
 
         public MultiItemObject()
         public MultiItemObject()
         {
         {
+            Padding = Padding.Empty;
             Columns = 3;
             Columns = 3;
             Rows = 1;
             Rows = 1;
             RowHeight = 100;
             RowHeight = 100;

+ 2 - 2
inabox.wpf/Reports/ReportUtils.cs

@@ -134,7 +134,7 @@ namespace InABox.Wpf.Reports
             {
             {
                 var modelTable = data.GetDataModelTable(tableName);
                 var modelTable = data.GetDataModelTable(tableName);
                 var dataSource = report.GetDataSource(tableName);
                 var dataSource = report.GetDataSource(tableName);
-                if (dataSource != null)
+                if (dataSource != null && modelTable.Type is not null)
                 {
                 {
                     var columnNames = CoreUtils.GetColumnNames(modelTable.Type, x => true);
                     var columnNames = CoreUtils.GetColumnNames(modelTable.Type, x => true);
                     foreach (var column in dataSource.Columns)
                     foreach (var column in dataSource.Columns)
@@ -198,7 +198,7 @@ namespace InABox.Wpf.Reports
                                 col.BindableControl = ColumnBindableControl.Custom;
                                 col.BindableControl = ColumnBindableControl.Custom;
                                 col.CustomBindableControl = "MultiImageObject";
                                 col.CustomBindableControl = "MultiImageObject";
                             }
                             }
-                            col.Enabled = columns is null ? true : columns.ContainsKey(col.Name.Replace('_', '.'));
+                            col.Enabled = columns is null || columns.ContainsKey(col.Name.Replace('_', '.'));
                         }
                         }
                     }
                     }
                 }
                 }