Browse Source

Fix to lookup required columns and for dimensions; report columns loading from template;
Added forms library to PRS Desktop; added duplication of DFLayouts and caused SignaturePad auto-generation to be 200 px;
Fix to FilterConstant.Today; added role forms functionality;

Kenric Nugteren 2 years ago
parent
commit
96df6836ce

+ 29 - 3
InABox.Core/DataModel/DataModel.cs

@@ -189,6 +189,7 @@ namespace InABox.Core
         public IEnumerable<DataTable> Tables => _tables.Select(x => x.Value.Table.ToDataTable(x.Key));
         public IEnumerable<DataTable> DefaultTables => _tables.Where(x => x.Value.IsDefault).Select(x => x.Value.Table.ToDataTable(x.Key));
         public IEnumerable<string> DefaultTableNames => _tables.Where(x => x.Value.IsDefault).Select(x => x.Key);
+        public IEnumerable<string> TableNames => _tables.Select(x => x.Key);
 
         public TType[] ExtractValues<TSource, TType>(Expression<Func<TSource, TType>> column, bool distinct = true, string? alias = null)
         {
@@ -199,7 +200,23 @@ namespace InABox.Core
         {
             var result = new DataSet();
 
-            result.Tables.AddRange(_tables.Select(x => x.Value.Table.ToDataTable(x.Key)).ToArray());
+            foreach(var (key, table) in _tables)
+            {
+                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))
+                {
+                    if (!current.ContainsKey(column))
+                    {
+                        additional.Add(column);
+                    }
+                }
+
+                var dataTable = table.Table.ToDataTable(key, additional);
+                result.Tables.Add(dataTable);
+            }
+
+            //result.Tables.AddRange(_tables.Select(x => x.Value.Table.ToDataTable(x.Key)).ToArray());
             foreach (var relation in _relationships)
             {
                 var childTable = result.Tables[relation.ChildTable];
@@ -306,7 +323,7 @@ namespace InABox.Core
             return TableName(typeof(TType), alias);
         }
 
-        private class DataModelTable
+        public class DataModelTable
         {
             public DataModelTable(Type type, CoreTable table, bool isDefault, IFilter? filter, IColumns? columns, bool shouldLoad = true)
             {
@@ -553,7 +570,11 @@ namespace InABox.Core
             var name = TableName<TType>(alias);
             return _tables[name].Table;
         }
-        private DataModelTable GetDataModelTable<TType>(string? alias = null)
+        public DataModelTable GetDataModelTable(string name)
+        {
+            return _tables[name];
+        }
+        public DataModelTable GetDataModelTable<TType>(string? alias = null)
         {
             CheckTable<TType>(alias);
             var name = TableName<TType>(alias);
@@ -599,6 +620,11 @@ namespace InABox.Core
             var table = GetDataModelTable<TType>(alias);
             return table.Columns as Columns<TType>;
         }
+        public IColumns? GetColumns(string alias)
+        {
+            var table = GetDataModelTable(alias);
+            return table.Columns;
+        }
 
         [Obsolete("Use SetColumns instead")]
         public void SetTableColumns<TType>(Columns<TType> columns, string? alias = null) => SetColumns(columns, alias);

+ 6 - 1
InABox.Core/DataTable.cs

@@ -559,11 +559,16 @@ namespace InABox.Core
             foreach (var row in Rows) dictionary[row.Get(key)] = row.Get(value);
         }
 
-        public DataTable ToDataTable(string name = "")
+        public DataTable ToDataTable(string name = "", IColumns? additionalColumns = null)
         {
             var result = new DataTable(name);
             foreach (var column in Columns)
                 result.Columns.Add(column.ColumnName.Replace('.', '_'), column.DataType);
+            if(additionalColumns != null)
+            {
+                foreach (var (column, type) in additionalColumns.AsDictionary())
+                    result.Columns.Add(column.Replace('.', '_'), type);
+            }
 
             //result.Columns["ID"].Unique = true;
             //result.PrimaryKey = new DataColumn[] { result.Columns["ID"] };

+ 83 - 10
InABox.Core/DigitalForms/DFUtils.cs

@@ -8,24 +8,55 @@ namespace InABox.Core
 {
     public interface IEntityFormUtils
     {
-        public abstract bool CanEditForm(IDigitalFormInstance Form, Entity Entity);
+        public abstract bool CanEditForm(IDigitalFormInstance form, Entity entity);
+
+        public abstract Entity NewEntity(DigitalForm form);
+
+        public abstract void OnSave(IDigitalFormInstance form, Entity entity);
+    }
+
+    public abstract class EntityFormUtils<TForm, TEntity, TEntityLink> : IEntityFormUtils
+        where TForm : EntityForm<TEntity, TEntityLink>
+        where TEntity : Entity, new()
+        where TEntityLink : EntityLink<TEntity>, new()
+    {
+        public abstract bool CanEditForm(TForm form, TEntity entity);
+        public abstract TEntity NewEntity(DigitalForm form);
+        public abstract void OnSave(TForm form, TEntity entity);
+
+        void IEntityFormUtils.OnSave(IDigitalFormInstance form, Entity entity) => OnSave((TForm)form, (TEntity)entity);
+
+        bool IEntityFormUtils.CanEditForm(IDigitalFormInstance form, Entity entity) => CanEditForm((form as TForm)!, (entity as TEntity)!);
+
+        Entity IEntityFormUtils.NewEntity(DigitalForm form) => NewEntity(form);
     }
-    public class EntityFormUtils<TForm, TEntity, TEntityLink> : IEntityFormUtils
+
+    public class DelegateEntityFormUtils<TForm, TEntity, TEntityLink> : EntityFormUtils<TForm, TEntity, TEntityLink>
         where TForm : EntityForm<TEntity, TEntityLink>
         where TEntity : Entity, new()
         where TEntityLink : EntityLink<TEntity>, new()
     {
-        public delegate bool CanEditForm(TForm Form, TEntity Entity);
+        public delegate bool CanEditEvent(TForm form, TEntity entity);
+        public delegate TEntity NewEntityEvent(DigitalForm form);
+        public delegate void OnSaveEvent(TForm form, TEntity entity);
 
-        public CanEditForm CanEdit;
+        public CanEditEvent OnCanEdit;
+        public NewEntityEvent? OnNewEntity;
+        public OnSaveEvent? SaveEvent;
 
-        public EntityFormUtils(CanEditForm canEditForm)
+        public DelegateEntityFormUtils(CanEditEvent canEditForm, NewEntityEvent? onNewEntity = null, OnSaveEvent? onSave = null)
         {
-            CanEdit = canEditForm;
+            OnCanEdit = canEditForm;
+            OnNewEntity = onNewEntity;
+            SaveEvent = onSave;
         }
 
-        bool IEntityFormUtils.CanEditForm(IDigitalFormInstance Form, Entity Entity)
-             => CanEdit((Form as TForm)!, (Entity as TEntity)!);
+        public override bool CanEditForm(TForm form, TEntity entity) => OnCanEdit(form, entity);
+
+        public override TEntity NewEntity(DigitalForm form) => OnNewEntity?.Invoke(form) ?? new TEntity();
+
+        public override void OnSave(TForm form, TEntity entity) => SaveEvent?.Invoke(form, entity);
+        
     }
 
     public static class DFUtils
@@ -84,12 +115,22 @@ namespace InABox.Core
 
         private static Dictionary<Type, IEntityFormUtils> _formUtils = new Dictionary<Type, IEntityFormUtils>();
 
-        public static void AddFormUtils<TForm, TEntity, TEntityLink>(EntityFormUtils<TForm, TEntity, TEntityLink>.CanEditForm editFormFunc)
+        public static void AddFormUtils<TForm, TEntity, TEntityLink>(
+            DelegateEntityFormUtils<TForm, TEntity, TEntityLink>.CanEditEvent editFormFunc,
+            DelegateEntityFormUtils<TForm, TEntity, TEntityLink>.NewEntityEvent? newEntityFunc = null,
+            DelegateEntityFormUtils<TForm, TEntity, TEntityLink>.OnSaveEvent? beforeSaveFunc = null)
+            where TForm : EntityForm<TEntity, TEntityLink>
+            where TEntity : Entity, new()
+            where TEntityLink : EntityLink<TEntity>, new()
+        {
+            _formUtils.Add(typeof(TForm), new DelegateEntityFormUtils<TForm, TEntity, TEntityLink>(editFormFunc, newEntityFunc, beforeSaveFunc));
+        }
+        public static void AddFormUtils<TForm, TEntity, TEntityLink>(EntityFormUtils<TForm, TEntity, TEntityLink> formUtils)
             where TForm : EntityForm<TEntity, TEntityLink>
             where TEntity : Entity, new()
             where TEntityLink : EntityLink<TEntity>, new()
         {
-            _formUtils.Add(typeof(TForm), new EntityFormUtils<TForm, TEntity, TEntityLink>(editFormFunc));
+            _formUtils.Add(typeof(TForm), formUtils);
         }
 
         public static bool CanEditForm(Type TForm, IDigitalFormInstance Form, Entity Entity)
@@ -107,5 +148,37 @@ namespace InABox.Core
         {
             return CanEditForm(typeof(TForm), Form, Entity);
         }
+
+        public static Entity NewEntity(Type TForm, Type TEntity, DigitalForm form)
+        {
+            if (_formUtils.TryGetValue(TForm, out var utils))
+            {
+                return utils.NewEntity(form);
+            }
+            return (Activator.CreateInstance(TEntity) as Entity)!;
+        }
+
+        public static TEntity NewEntity<TForm, TEntity, TEntityLink>(DigitalForm form)
+            where TForm : EntityForm<TEntity, TEntityLink>
+            where TEntity : Entity, new()
+            where TEntityLink : EntityLink<TEntity>, new()
+        {
+            return (TEntity)NewEntity(typeof(TForm), typeof(TEntity), form);
+        }
+
+        public static void OnSave(Type TForm, IDigitalFormInstance form, Entity entity)
+        {
+            if (_formUtils.TryGetValue(TForm, out var utils))
+            {
+                utils.OnSave(form, entity);
+            }
+        }
+        public static void OnSave<TForm, TEntity, TEntityLink>(TForm form, TEntity entity)
+            where TForm : EntityForm<TEntity, TEntityLink>
+            where TEntity : Entity, new()
+            where TEntityLink : EntityLink<TEntity>, new()
+        {
+            OnSave(typeof(TForm), form, entity);
+        }
     }
 }

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

@@ -83,8 +83,8 @@ namespace InABox.Core
             }
 
             var idList = ExtractValues<T, Guid>(x => x.ID);
-            var jsonLists = ExtractValues<T, string>(x => (x as IDigitalFormInstance)!.FormData).ToList();
-            var blobLists = ExtractValues<T, string?>(x => (x as IDigitalFormInstance)!.BlobData).ToList();
+            var jsonLists = ExtractValues<T, string>(x => (x as IDigitalFormInstance)!.FormData, false).ToList();
+            var blobLists = ExtractValues<T, string?>(x => (x as IDigitalFormInstance)!.BlobData, false).ToList();
 
             for(var i = 0; i < jsonLists.Count; ++i)
             {

+ 2 - 1
InABox.Core/DigitalForms/Forms/DigitalForm.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Linq.Expressions;
 using System.Text;
 using Newtonsoft.Json.Linq;
 using TextFieldParserStandard;
@@ -8,7 +9,7 @@ using TextFieldParserStandard;
 namespace InABox.Core
 {
     [UserTracking("Digital Forms")]
-    public class DigitalForm : Entity, IRemotable, IPersistent, ILicense<DigitalFormsLicense>, IDuplicatable
+    public class DigitalForm : Entity, IRemotable, IPersistent, ILicense<DigitalFormsLicense>//, IDuplicatable
     {
         /// <summary>
         ///     The following functions support PNG, BMP and JPEG

+ 5 - 0
InABox.Core/DigitalForms/Forms/DigitalFormLayout.cs

@@ -50,6 +50,11 @@ namespace InABox.Core
             return "L{0:D3}";
         }
 
+        public IEntityDuplicator GetDuplicator()
+        {
+            return new EntityDuplicator<DigitalFormLayout>();
+        }
+
         protected override void Init()
         {
             base.Init();

+ 15 - 2
InABox.Core/DigitalForms/Layouts/DFLayout.cs

@@ -241,6 +241,13 @@ namespace InABox.Core
 
         #region Auto-generated Layouts
 
+        public static string GetLayoutFieldDefaultHeight(DFLayoutField field)
+        {
+            if (field is DFLayoutSignaturePad || field is DFLayoutMultiSignaturePad)
+                return "200";
+            return "Auto";
+        }
+
         public static DFLayoutField? GenerateLayoutFieldFromVariable(DigitalFormVariable variable)
         {
             DFLayoutField? field = Activator.CreateInstance(variable.FieldType()) as DFLayoutField;
@@ -265,7 +272,7 @@ namespace InABox.Core
             int row = 1;
             foreach(var variable in variables)
             {
-                layout.RowHeights.Add("Auto");
+                var rowHeight = "Auto";
 
                 var rowNum = new DFLayoutLabel { Caption = row.ToString(), Row = row, Column = 1 };
                 var label = new DFLayoutLabel { Caption = variable.Code, Row = row, Column = 2 };
@@ -280,7 +287,10 @@ namespace InABox.Core
                     field.Column = 3;
 
                     layout.Elements.Add(field);
+
+                    rowHeight = GetLayoutFieldDefaultHeight(field);
                 }
+                layout.RowHeights.Add(rowHeight);
 
                 ++row;
             }
@@ -300,7 +310,7 @@ namespace InABox.Core
             var i = 0;
             foreach(var variable in variables)
             {
-                layout.RowHeights.Add("Auto");
+                var rowHeight = "Auto";
                 layout.RowHeights.Add("Auto");
 
                 var rowNum = new DFLayoutLabel { Caption = i + 1 + ".", Row = row, Column = 1 };
@@ -317,7 +327,10 @@ namespace InABox.Core
                     field.ColumnSpan = 2;
 
                     layout.Elements.Add(field);
+
+                    rowHeight = GetLayoutFieldDefaultHeight(field);
                 }
+                layout.RowHeights.Add(rowHeight);
 
                 row += 2;
                 ++i;

+ 7 - 0
InABox.Core/Dimensions/Dimensions.cs

@@ -19,36 +19,43 @@ namespace InABox.Core
         [DoubleEditor(Visible = Visible.Hidden)]
         [EditorSequence(2)]
         [Caption("Quantity", IncludePath = false)]
+        [RequiredColumn]
         public abstract double Quantity { get; set; }
         
         [DoubleEditor(Visible = Visible.Hidden)]
         [EditorSequence(3)]
         [Caption("Length", IncludePath = false)]
+        [RequiredColumn]
         public abstract double Length { get; set; }
         
         [DoubleEditor(Visible = Visible.Hidden)]
         [EditorSequence(4)]
         [Caption("Width", IncludePath = false)]
+        [RequiredColumn]
         public abstract double Width { get; set; }
         
         [DoubleEditor(Visible = Visible.Hidden)]
         [EditorSequence(5)]
         [Caption("Height", IncludePath = false)]
+        [RequiredColumn]
         public abstract double Height { get; set; }
         
         [DoubleEditor(Visible = Visible.Hidden)]
         [EditorSequence(6)]
         [Caption("Weight", IncludePath = false)]
+        [RequiredColumn]
         public abstract double Weight { get; set; }
         
         [DoubleEditor(Visible = Visible.Optional, Editable = Editable.Hidden)]
         [Caption("Value", IncludePath = false)]
         [EditorSequence(7)]
+        [RequiredColumn]
         public abstract double Value { get; set; }
         
         [TextBoxEditor(Visible = Visible.Default, Editable=Editable.Hidden)]
         [EditorSequence(8)]
         [Caption("Unit Size", IncludePath = false)]
+        [RequiredColumn]
         public abstract String UnitSize { get; set; }
         
         protected override void Init()

+ 9 - 1
InABox.Core/Entity.cs

@@ -62,13 +62,19 @@ namespace InABox.Core
 
     public interface IEntityDuplicator
     {
-        void Duplicate(IFilter filter);
+        //void Duplicate(IFilter filter);
+
+        void Duplicate(IEnumerable<BaseObject> entities);
     }
 
     public class EntityDuplicator<TEntity> : IEntityDuplicator where TEntity : Entity, IRemotable, IPersistent
     {
         private readonly List<Tuple<Type, Type, Expression>> _relationships = new List<Tuple<Type, Type, Expression>>();
 
+        public void Duplicate(IEnumerable<TEntity> entites) =>
+            Duplicate(typeof(TEntity),
+                new Filter<TEntity>(x => x.ID).InList(entites.Select(x => x.ID).ToArray()));
+
         public void Duplicate(IFilter filter)
         {
             Duplicate(typeof(TEntity), filter);
@@ -100,6 +106,8 @@ namespace InABox.Core
         {
             _relationships.Add(new Tuple<Type, Type, Expression>(typeof(TParent), typeof(TChild), childkey));
         }
+
+        void IEntityDuplicator.Duplicate(IEnumerable<BaseObject> entities) => Duplicate(entities.Cast<TEntity>());
     }
 
     /// <summary>

+ 18 - 3
InABox.Core/Filter.cs

@@ -298,6 +298,23 @@ namespace InABox.Core
             return and;
         }
 
+        public static Filter<T>? And(params Filter<T>?[] filters)
+        {
+            Filter<T>? result = null;
+            foreach (var filter in filters)
+            {
+                if(filter != null)
+                {
+                    if (result is null)
+                        result = filter;
+                    else
+                        result.Ands.Add(filter);
+                }
+            }
+
+            return result;
+        }
+
         public IFilter Or<T1>(Expression<Func<T1, object>> expression)
         {
             return Or(CoreUtils.GetFullPropertyName(expression, "."));
@@ -764,7 +781,7 @@ namespace InABox.Core
         public Filter<T> InQuery<U>(SubQuery<U> value) => ApplyObjectOperator(Operator.InQuery, value);
         IFilter IFilter.InQuery<U>(SubQuery<U> value) => InQuery(value);
 
-        public Filter<T> InQuery<U>(Filter<U> filter, Expression<Func<U, object?>> column) =>  InQuery(new SubQuery<U>(filter, new Column<U>(column)));
+        public Filter<T> InQuery<U>(Filter<U> filter, Expression<Func<U, object?>> column) => InQuery(new SubQuery<U>(filter, new Column<U>(column)));
         IFilter IFilter.InQuery<U>(Filter<U> value, Expression<Func<U, object?>> column) => InQuery(value, column);
 
         #endregion
@@ -1103,8 +1120,6 @@ namespace InABox.Core
 
         #endregion
 
-
-
     }
 
     public class Filters<T> //where T : Entity

+ 1 - 1
InABox.Core/ICoreTable.cs

@@ -25,7 +25,7 @@ namespace InABox.Core
         void LoadRows(CoreRow[] rows);
         void LoadRows(IEnumerable<object> objects);
         CoreRow NewRow(bool populate = false);
-        DataTable ToDataTable(string name = "");
+        DataTable ToDataTable(string name = "", IColumns? additionalColumns = null);
         IDictionary ToDictionary(string keycol, string displaycol, string sortcol = "");
 
         IEnumerable<T> ToObjects<T>() where T : BaseObject, new();

+ 22 - 17
InABox.Core/ILookupDefinition.cs

@@ -163,7 +163,7 @@ namespace InABox.Core
 
         #region General Factory Methods
 
-        private static object DoInvoke(Type TLookup, Type TResult, string Method)
+        private static object? DoInvoke(Type TLookup, Type TResult, string Method)
         {
             var factory = _cache.GetFactory(TLookup);
             if (factory != null)
@@ -192,7 +192,7 @@ namespace InABox.Core
         // If a specific Entity/Lookup match can't be found,
         // this will fall back to the global Lookup filter (if there is one)
         // Otherwise, it will return null
-        private static object DoInvoke(Type TLookup, Type TEntity, IEnumerable items, Type TResult, string Method)
+        private static object? DoInvoke(Type TLookup, Type TEntity, IEnumerable items, Type TResult, string Method)
         {
             var factory = _cache.GetFactory(TLookup, TEntity);
             if (factory != null)
@@ -294,7 +294,7 @@ namespace InABox.Core
 
         #region Filters
 
-        public static IFilter DefineFilter(Type TLookup)
+        public static IFilter? DefineFilter(Type TLookup)
         {
             return DoInvoke(TLookup, typeof(Filter<>), "DefineFilter") as IFilter;
 
@@ -325,22 +325,22 @@ namespace InABox.Core
             //return null;
         }
 
-        public static Filter<TLookup> DefineFilter<TLookup>()
+        public static Filter<TLookup>? DefineFilter<TLookup>()
         {
             return DefineFilter(typeof(TLookup)) as Filter<TLookup>;
         }
 
-        public static IFilter DefineFilter(Type TLookup, Type TEntity, IEnumerable items)
+        public static IFilter? DefineFilter(Type TLookup, Type TEntity, IEnumerable items)
         {
             return DoInvoke(TLookup, TEntity, items, typeof(Filter<>), "DefineFilter") as IFilter;
         }
 
-        public static IFilter DefineFilter<TEntity>(IEnumerable<TEntity> items, Type TLookup)
+        public static IFilter? DefineFilter<TEntity>(IEnumerable<TEntity> items, Type TLookup)
         {
             return DoInvoke(TLookup, typeof(TEntity), items, typeof(Filter<>), "DefineFilter") as IFilter;
         }
 
-        public static Filter<TLookup> DefineFilter<TEntity, TLookup>(TEntity[] items) where TEntity : Entity where TLookup : Entity
+        public static Filter<TLookup>? DefineFilter<TEntity, TLookup>(TEntity[] items) where TEntity : Entity where TLookup : Entity
         {
             return DefineFilter(items, typeof(TLookup)) as Filter<TLookup>;
         }
@@ -371,14 +371,25 @@ namespace InABox.Core
         
         #region RequiredColumns
 
+        public static IColumns DefaultRequiredColumns(Type TLookup)
+        {
+            var result = Columns.Create(TLookup);
+            var props = DatabaseSchema.Properties(TLookup).Where(x => x.Required);
+
+            foreach (var prop in props)
+                result.Add(prop.Name);
+
+            return result;
+        }
+        public static Columns<TLookup> DefaultRequiredColumns<TLookup>()
+            => (DefaultRequiredColumns(typeof(TLookup)) as Columns<TLookup>)!;
+
         public static IColumns RequiredColumns(Type TLookup)
         {
             var result = DoInvoke(TLookup, typeof(Columns<>), "RequiredColumns") as IColumns;
             if (result == null)
             {
-                var type = typeof(Columns<>).MakeGenericType(TLookup);
-                result = Activator.CreateInstance(type) as IColumns;
-                result = result.DefaultColumns();
+                result = DefaultRequiredColumns(TLookup);
             }
 
             return result;
@@ -450,13 +461,7 @@ namespace InABox.Core
 
         public override Columns<TLookup> RequiredColumns()
         {
-            var result = new Columns<TLookup>();
-            var props = DatabaseSchema.Properties(typeof(TLookup)).Where(x => x.Required);
-                
-            //CoreUtils.PropertyList(typeof(TLookup), (p) => p.GetCustomAttribute<RequiredColumnAttribute>() != null, true);
-            foreach (var prop in props)
-                result.Add(prop.Name);
-            return result;
+            return LookupFactory.DefaultRequiredColumns<TLookup>();
         }
     }
 }

+ 12 - 28
InABox.DynamicGrid/DynamicDataGrid.cs

@@ -15,12 +15,20 @@ using Expression = System.Linq.Expressions.Expression;
 
 namespace InABox.DynamicGrid
 {
-    public class DynamicDataGrid<TEntity> : DynamicGrid<TEntity> where TEntity : Entity, IRemotable, IPersistent, new()
+    public interface IDynamicDataGrid : IDynamicGrid
+    {
+        /// <summary>
+        /// The tag the the DynamicGridColumns are stored against. If set to <see langword="null"/>,
+        /// the name of <typeparamref name="TEntity"/> is used as a default.
+        /// </summary>
+        string? ColumnsTag { get; set; }
+    }
+
+    public class DynamicDataGrid<TEntity> : DynamicGrid<TEntity>, IDynamicDataGrid where TEntity : Entity, IRemotable, IPersistent, new()
     {
         public delegate void OnReloadEventHandler(object sender, Filters<TEntity> criteria, Columns<TEntity> columns, ref SortOrder<TEntity>? sortby);
 
         private readonly int ChunkSize = 500;
-        private readonly Button DuplicateBtn;
         private readonly Button MergeBtn;
         private readonly Button FilterBtn;
 
@@ -57,7 +65,6 @@ namespace InABox.DynamicGrid
                 Options.Add(DynamicGridOption.MultiSelect);
             Options.EndUpdate();
 
-            DuplicateBtn = AddButton("Duplicate", Properties.Resources.paste.AsBitmapImage(Color.White), DoDuplicate);
             MergeBtn = AddButton("Merge", Properties.Resources.merge.AsBitmapImage(Color.White), DoMerge);
 
             var fields = DatabaseSchema.Properties(typeof(TEntity));
@@ -96,10 +103,6 @@ namespace InABox.DynamicGrid
             form.ReadOnly = !Security.CanEdit<TEntity>();
         }
 
-        /// <summary>
-        /// The tag the the DynamicGridColumns are stored against. If set to <see langword="null"/>,
-        /// the name of <typeparamref name="TEntity"/> is used as a default.
-        /// </summary>
         public string? ColumnsTag { get; set; }
 
         protected override void OptionsChanged(object sender, EventArgs args)
@@ -107,8 +110,6 @@ namespace InABox.DynamicGrid
             base.OptionsChanged(sender, args);
             if (MergeBtn != null)
                 MergeBtn.Visibility = Visibility.Collapsed;
-            if (DuplicateBtn != null)
-                DuplicateBtn.Visibility = Visibility.Collapsed;
             ShowFilterList = Options.Contains(DynamicGridOption.FilterRows);
         }
 
@@ -118,8 +119,6 @@ namespace InABox.DynamicGrid
             MergeBtn.Visibility = Options.Contains(DynamicGridOption.MultiSelect) && Security.CanMerge<TEntity>() && rows != null && rows.Length > 1
                 ? Visibility.Visible
                 : Visibility.Collapsed;
-            DuplicateBtn.Visibility =
-                typeof(TEntity) is IDuplicatable && rows != null && rows.Length >= 1 ? Visibility.Visible : Visibility.Collapsed;
         }
 
         private Filter<T>? CreateFilter<T>(Dictionary<string, object> criteria) where T : Entity
@@ -751,24 +750,9 @@ namespace InABox.DynamicGrid
             }
         }
 
-        private bool DoDuplicate(Button arg1, CoreRow[] arg2)
+        protected override IEnumerable<TEntity> LoadDuplicatorItems(CoreRow[] rows)
         {
-            var ids = ExtractValues(x => x.ID, Selection.Selected).ToArray();
-            if (!ids.Any())
-            {
-                MessageBox.Show("Please select at least one record to duplicate!");
-                return false;
-            }
-
-            var duplicator = (new TEntity() as IDuplicatable)?.GetDuplicator();
-            if(duplicator is null)
-            {
-                MessageBox.Show($"Cannot duplicate {typeof(TEntity)}");
-                return false;
-            }
-
-            duplicator.Duplicate(new Filter<TEntity>(x => x.ID).InList(ids));
-            return true;
+            return rows.Select(x => x.ToObject<TEntity>());
         }
 
         protected override bool BeforePaste(IEnumerable<TEntity> items, ClipAction action)

+ 47 - 7
InABox.DynamicGrid/DynamicEntityFormGrid.cs

@@ -15,13 +15,14 @@ namespace InABox.DynamicGrid
         where TEntity : Entity
         where TEntityLink : IEntityLink<TEntity>, new()
     {
-        private Guid EntityID { get; set; }
+        private TEntity Entity { get; set; }
 
-        public DynamicEntityFormGrid(Guid entityID)
+        public DynamicEntityFormGrid(TEntity entity)
         {
-            EntityID = entityID;
+            Entity = entity;
 
             OnBeforeSave += BeforeSave;
+            OnCustomiseEditor += DynamicEntityFormGrid_OnCustomiseEditor;
 
             ActionColumns.Add(new DynamicActionColumn(EditImage, EditClick));
             if (DynamicGridUtils.PreviewReport != null)
@@ -30,16 +31,55 @@ namespace InABox.DynamicGrid
             HiddenColumns.Add(x => x.Form.ID);
         }
 
+        private void DynamicEntityFormGrid_OnCustomiseEditor(IDynamicEditorForm sender, TForm[]? items, DynamicGridColumn column, BaseEditor editor)
+        {
+            if(column.ColumnName == "Form.ID")
+            {
+                editor.Editable = Editable.Disabled;
+            }
+        }
+
+        protected override void DoAdd()
+        {
+            var filter = LookupFactory.DefineFilter<TEntity, DigitalForm>(new TEntity[] { Entity })
+                ?? LookupFactory.DefineFilter<TForm, DigitalForm>(Array.Empty<TForm>());
+
+            var select = new MultiSelectDialog<DigitalForm>(
+                filter,
+                LookupFactory.DefineColumns<DigitalForm>()
+                    .Add(x => x.Description),
+                false);
+
+            if (select.ShowDialog() == true)
+            {
+                var digitalForm = select.Items().FirstOrDefault();
+                if(digitalForm is not null)
+                {
+                    var form = new TForm
+                    {
+                        Description = digitalForm.Description
+                    };
+                    form.Form.ID = digitalForm.ID;
+                    form.Parent.ID = Entity.ID;
+
+                    SaveItem(form);
+
+                    Refresh(false, true);
+                    OnChanged?.Invoke(this);
+                }
+            }
+        }
+
         protected override TForm CreateItem()
         {
             var result = base.CreateItem();
-            result.Parent.ID = EntityID;
+            result.Parent.ID = Entity.ID;
             return result;
         }
 
         protected override void Reload(Filters<TForm> criteria, Columns<TForm> columns, ref SortOrder<TForm>? sort, Action<CoreTable?, Exception?> action)
         {
-            criteria.Add(new Filter<TForm>("Parent.ID").IsEqualTo(EntityID));
+            criteria.Add(new Filter<TForm>("Parent.ID").IsEqualTo(Entity.ID));
             base.Reload(criteria, columns, ref sort, action);
         }
 
@@ -47,7 +87,7 @@ namespace InABox.DynamicGrid
         {
             foreach(var item in items)
             {
-                item.Parent.ID = EntityID;
+                item.Parent.ID = Entity.ID;
             }
         }
 
@@ -61,7 +101,7 @@ namespace InABox.DynamicGrid
             if (arg is null) return false;
 
             var formid = arg.Get<TForm, Guid>(x => x.Form.ID);
-            var model = new DigitalFormReportDataModel<TForm>(new Filter<TForm>("Parent.ID").IsEqualTo(EntityID), formid);
+            var model = new DigitalFormReportDataModel<TForm>(new Filter<TForm>("Parent.ID").IsEqualTo(Entity.ID), formid);
             var section = formid.ToString();
 
             // TODO: This is a hack

+ 207 - 151
InABox.DynamicGrid/DynamicGrid.cs

@@ -323,6 +323,7 @@ namespace InABox.DynamicGrid
         private readonly Label EditSpacer;
         private readonly Button Export;
         private readonly Label ExportSpacer;
+        private readonly Button DuplicateBtn;
 
         private readonly GridRowSizingOptions gridRowResizingOptions = new() { CanIncludeHiddenColumns = false, AutoFitMode = AutoFitMode.SmartFit };
         private readonly Button Help;
@@ -382,6 +383,11 @@ namespace InABox.DynamicGrid
             MasterColumns = new DynamicGridColumns();
             MasterColumns.ExtractColumns(typeof(T), "");
 
+            foreach(var column in LookupFactory.RequiredColumns<T>().ColumnNames())
+            {
+                AddHiddenColumn(column);
+            }
+
             ActionColumns = new DynamicActionColumns();
 
             if (IsSequenced)
@@ -548,6 +554,8 @@ namespace InABox.DynamicGrid
             Delete.SetValue(DockPanel.DockProperty, Dock.Right);
             Delete.Click += Delete_Click;
 
+            DuplicateBtn = AddButton("Duplicate", Properties.Resources.paste.AsBitmapImage(Color.White), DoDuplicate);
+
             Count = new Label();
             Count.Height = 30;
             Count.Margin = new Thickness(0, 2, 0, 0);
@@ -662,6 +670,9 @@ namespace InABox.DynamicGrid
                 up.Position = Options.Contains(DynamicGridOption.EditRows) ? DynamicActionColumnPosition.Start : DynamicActionColumnPosition.Hidden;
             if (down != null)
                 down.Position = Options.Contains(DynamicGridOption.EditRows) ? DynamicActionColumnPosition.Start : DynamicActionColumnPosition.Hidden;
+
+            if (DuplicateBtn != null)
+                DuplicateBtn.Visibility = Visibility.Collapsed;
         }
 
         protected override DynamicGridStyleSelector<T> GetStyleSelector()
@@ -1028,6 +1039,9 @@ namespace InABox.DynamicGrid
         protected virtual void SelectItems(CoreRow[]? rows)
         {
             OnSelectItem?.Invoke(this, new DynamicGridSelectionEventArgs(rows));
+
+            DuplicateBtn.Visibility =
+                typeof(T).IsAssignableTo(typeof(IDuplicatable)) && rows != null && rows.Length >= 1 ? Visibility.Visible : Visibility.Collapsed;
         }
 
         private bool bFilterVisible;
@@ -2092,6 +2106,8 @@ namespace InABox.DynamicGrid
 
         #region Item Manipulation
 
+        #region Load/Save/Delete
+
         protected virtual T[] LoadItems(CoreRow[] rows)
         {
             var result = new List<T>();
@@ -2143,6 +2159,10 @@ namespace InABox.DynamicGrid
             DoDelete();
         }
 
+        #endregion
+
+        #region Edit
+
         protected virtual void DoEdit()
         {
             if (!SelectedRows.Any())
@@ -2270,11 +2290,6 @@ namespace InABox.DynamicGrid
             return false;
         }
 
-        protected virtual void ShowHelp(string slug)
-        {
-            Process.Start(new ProcessStartInfo("https://prs-software.com.au/wiki/index.php/" + slug) { UseShellExecute = true });
-        }
-
         protected virtual void DoAdd()
         {
             //CoreRow row = (SelectedRow > -1) && (SelectedRow < Data.Rows.Count) ?  Data.Rows[this.SelectedRow] : null;
@@ -2289,138 +2304,6 @@ namespace InABox.DynamicGrid
         {
             DoAdd();
         }
-
-        protected void ReloadForms<TTargetType, TTargetForm, TSourceForm>(DynamicEditorForm editor, TTargetType item,
-            Expression<Func<TSourceForm, object?>> sourcekey, Guid sourceid)
-            where TTargetType : Entity, new()
-            where TTargetForm : Entity, IRemotable, IPersistent, IDigitalFormInstance, new()
-            where TSourceForm : Entity, IRemotable, IPersistent, IDigitalForm<TTargetType>, new()
-        {
-            var type = typeof(IDynamicOneToManyGrid<,>).MakeGenericType(typeof(TTargetType), typeof(TTargetForm));
-            var page =
-                editor.Pages?.FirstOrDefault(x => x.GetType().GetInterfaces().Contains(type)) as IDynamicOneToManyGrid<TTargetType, TTargetForm>;
-            if (page != null && item != null)
-            {
-                if (!page.Ready)
-                    page.Load(item, null);
-
-                CoreTable table;
-                if (sourceid == Guid.Empty)
-                {
-                    table = new CoreTable();
-                    table.LoadColumns(typeof(TSourceForm));
-                }
-                else
-                {
-                    table = new Client<TSourceForm>().Query(
-                        new Filter<TSourceForm>(sourcekey).IsEqualTo(sourceid).And(x => x.Form.AppliesTo)
-                            .IsEqualTo(typeof(TTargetType).EntityName().Split('.').Last())
-                    );
-                }
-
-                var newforms = new List<TTargetForm>();
-                foreach (var row in table.Rows)
-                {
-                    var sourceform = row.ToObject<TSourceForm>();
-                    var targetform = new TTargetForm();
-
-                    targetform.Form.ID = sourceform.Form.ID;
-                    targetform.Form.Synchronise(sourceform.Form);
-                    newforms.Add(targetform);
-                }
-
-                page.Items.Clear();
-                page.LoadItems(newforms.ToArray());
-            }
-        }
-
-
-        #region ClipBuffer
-
-        private Tuple<ClipAction, CoreRow[]>? ClipBuffer;
-
-        protected void ResetClipBuffer()
-        {
-            ClipBuffer = null;
-        }
-
-        protected void SetClipBuffer(ClipAction action, CoreRow[] rows)
-        {
-            ClipBuffer = new Tuple<ClipAction, CoreRow[]>(action, rows);
-        }
-
-        private void CutToClipBuffer()
-        {
-            SetClipBuffer(ClipAction.Cut, SelectedRows);
-            InvalidateGrid();
-        }
-
-
-        private void CopyToClipBuffer()
-        {
-            SetClipBuffer(ClipAction.Copy, SelectedRows);
-            InvalidateGrid();
-        }
-
-        private void PasteFromClipBuffer()
-        {
-            if (ClipBuffer == null)
-                return;
-
-            if (!IsSequenced)
-                return;
-
-            using (new WaitCursor())
-            {
-                var updates = ClipBuffer.Item2.Select(x => x.ToObject<T>()).ToList();
-                if (BeforePaste(updates, ClipBuffer.Item1))
-                {
-                    var currow = SelectedRows.FirstOrDefault()
-                        ?? Data.Rows.LastOrDefault();
-
-                    var sequence = currow != null ? currow.Get<T, long>(c => ((ISequenceable)c).Sequence) : 0;
-
-                    var postrows = Data.Rows.Where(r => !ClipBuffer.Item2.Contains(r) && r.Get<ISequenceable, long>(x => x.Sequence) >= sequence);
-                    updates.AddRange(LoadItems(postrows.ToArray()));
-
-                    foreach (var update in updates)
-                    {
-                        sequence++;
-                        ((ISequenceable)update).Sequence = sequence;
-                    }
-                }
-
-                if (updates.Any())
-                {
-                    SaveItems(updates.ToArray());
-                    Refresh(false, true);
-                }
-            }
-        }
-
-        protected virtual bool BeforePaste(IEnumerable<T> items, ClipAction action)
-        {
-            return true;
-        }
-
-        #endregion
-
-        private void Cut_Click(object sender, RoutedEventArgs e)
-        {
-            CutToClipBuffer();
-        }
-
-        private void Copy_Click(object sender, RoutedEventArgs e)
-        {
-            CopyToClipBuffer();
-        }
-
-        private void Paste_Click(object sender, RoutedEventArgs e)
-        {
-            PasteFromClipBuffer();
-        }
-
-
         public virtual DynamicEditorPages LoadEditorPages(T item)
         {
             DynamicEditorPages pages = new DynamicEditorPages();
@@ -2442,7 +2325,7 @@ namespace InABox.DynamicGrid
                 (f, i) =>
                 {
                     Process.Start(new ProcessStartInfo("https://prs-software.com.au/wiki/index.php/" + typeof(T).Name.SplitCamelCase().Replace(" ", "_"))
-                        { UseShellExecute = true });
+                    { UseShellExecute = true });
                 }
             );
         }
@@ -2456,14 +2339,14 @@ namespace InABox.DynamicGrid
             var cursor = new WaitCursor();
 
             var pages = items.Length == 1 ? LoadEditorPages(items.First()) : new DynamicEditorPages();
-            
+
             var buttons = new DynamicEditorButtons();
             if (items.Length == 1)
                 LoadEditorButtons(items.First(), buttons);
-            
+
             var editor = new DynamicEditorForm(items.Any() ? items.First().GetType() : typeof(T), pages, buttons, PageDataHandler, PreloadPages);
             editor.SetValue(Panel.ZIndexProperty, 999);
-            
+
             editor.OnCustomiseColumns += (o, c) =>
             {
                 ConfigureColumns(MasterColumns /*, true */);
@@ -2533,7 +2416,7 @@ namespace InABox.DynamicGrid
 
             return editor.ShowDialog() == true;
         }
-        
+
         private void UpdateEditor(DynamicEditorGrid grid, Expression<Func<IDimensioned, object?>> property, bool enabled)
         {
             if (!grid.TryFindEditor(new Column<IDimensioned>(property).Property, out var editor))
@@ -2552,15 +2435,14 @@ namespace InABox.DynamicGrid
         {
             if (items.First() is IDimensioned dimensioned)
             {
-                UpdateEditor(grid,x=>x.Dimensions.Quantity, dimensioned.Dimensions.GetUnit().HasQuantity);
-                UpdateEditor(grid,x=>x.Dimensions.Length, dimensioned.Dimensions.GetUnit().HasLength);
-                UpdateEditor(grid,x=>x.Dimensions.Width, dimensioned.Dimensions.GetUnit().HasWidth);
-                UpdateEditor(grid,x=>x.Dimensions.Height, dimensioned.Dimensions.GetUnit().HasHeight);
-                UpdateEditor(grid,x=>x.Dimensions.Weight, dimensioned.Dimensions.GetUnit().HasWeight);
+                UpdateEditor(grid, x => x.Dimensions.Quantity, dimensioned.Dimensions.GetUnit().HasQuantity);
+                UpdateEditor(grid, x => x.Dimensions.Length, dimensioned.Dimensions.GetUnit().HasLength);
+                UpdateEditor(grid, x => x.Dimensions.Width, dimensioned.Dimensions.GetUnit().HasWidth);
+                UpdateEditor(grid, x => x.Dimensions.Height, dimensioned.Dimensions.GetUnit().HasHeight);
+                UpdateEditor(grid, x => x.Dimensions.Weight, dimensioned.Dimensions.GetUnit().HasWeight);
             }
         }
 
-
         private string[]? ValidateData(T[] items)
         {
             var errors = new List<string>();
@@ -2572,7 +2454,7 @@ namespace InABox.DynamicGrid
         protected virtual void DoValidate(T[] items, List<string> errors)
         {
         }
-        
+
 
 
         protected virtual void AfterLoad(DynamicEditorForm editor, T[] items)
@@ -2592,7 +2474,7 @@ namespace InABox.DynamicGrid
                 foreach (var key in newchanges.Keys)
                     result[key] = newchanges[key];
             }
-            
+
             return result;
         }
 
@@ -2687,7 +2569,6 @@ namespace InABox.DynamicGrid
         {
         }
 
-
         private bool AddEditClick(CoreRow[]? rows)
         {
             if (!IsEnabled || bRefreshing)
@@ -2747,11 +2628,184 @@ namespace InABox.DynamicGrid
             return false;
         }
 
+        #endregion
+
+        #region Duplicate
+
+        protected virtual IEnumerable<T> LoadDuplicatorItems(CoreRow[] rows)
+        {
+            return LoadItems(rows);
+        }
+
+        private bool DoDuplicate(Button button, CoreRow[] rows)
+        {
+            if (!rows.Any())
+            {
+                MessageBox.Show("Please select at least one record to duplicate!");
+                return false;
+            }
+
+            /*var ids = ExtractValues(x => x.ID, Selection.Selected).ToArray();
+            if (!ids.Any())
+            {
+                MessageBox.Show("Please select at least one record to duplicate!");
+                return false;
+            }*/
+
+            var duplicator = (new T() as IDuplicatable)?.GetDuplicator();
+            if (duplicator is null)
+            {
+                MessageBox.Show($"Cannot duplicate {typeof(T)}");
+                return false;
+            }
+
+            duplicator.Duplicate(LoadDuplicatorItems(rows));// new Filter<T>(x => x.ID).InList(ids));
+            return true;
+        }
+
+        #endregion
+
+        protected virtual void ShowHelp(string slug)
+        {
+            Process.Start(new ProcessStartInfo("https://prs-software.com.au/wiki/index.php/" + slug) { UseShellExecute = true });
+        }
+
+        protected void ReloadForms<TTargetType, TTargetForm, TSourceForm>(DynamicEditorForm editor, TTargetType item,
+            Expression<Func<TSourceForm, object?>> sourcekey, Guid sourceid)
+            where TTargetType : Entity, new()
+            where TTargetForm : Entity, IRemotable, IPersistent, IDigitalFormInstance, new()
+            where TSourceForm : Entity, IRemotable, IPersistent, IDigitalForm<TTargetType>, new()
+        {
+            var type = typeof(IDynamicOneToManyGrid<,>).MakeGenericType(typeof(TTargetType), typeof(TTargetForm));
+            var page =
+                editor.Pages?.FirstOrDefault(x => x.GetType().GetInterfaces().Contains(type)) as IDynamicOneToManyGrid<TTargetType, TTargetForm>;
+            if (page != null && item != null)
+            {
+                if (!page.Ready)
+                    page.Load(item, null);
+
+                CoreTable table;
+                if (sourceid == Guid.Empty)
+                {
+                    table = new CoreTable();
+                    table.LoadColumns(typeof(TSourceForm));
+                }
+                else
+                {
+                    table = new Client<TSourceForm>().Query(
+                        new Filter<TSourceForm>(sourcekey).IsEqualTo(sourceid).And(x => x.Form.AppliesTo)
+                            .IsEqualTo(typeof(TTargetType).EntityName().Split('.').Last())
+                    );
+                }
+
+                var newforms = new List<TTargetForm>();
+                foreach (var row in table.Rows)
+                {
+                    var sourceform = row.ToObject<TSourceForm>();
+                    var targetform = new TTargetForm();
+
+                    targetform.Form.ID = sourceform.Form.ID;
+                    targetform.Form.Synchronise(sourceform.Form);
+                    newforms.Add(targetform);
+                }
+
+                page.Items.Clear();
+                page.LoadItems(newforms.ToArray());
+            }
+        }
+
+
+        #region ClipBuffer
+
+        private Tuple<ClipAction, CoreRow[]>? ClipBuffer;
+
+        protected void ResetClipBuffer()
+        {
+            ClipBuffer = null;
+        }
+
+        protected void SetClipBuffer(ClipAction action, CoreRow[] rows)
+        {
+            ClipBuffer = new Tuple<ClipAction, CoreRow[]>(action, rows);
+        }
+
+        private void CutToClipBuffer()
+        {
+            SetClipBuffer(ClipAction.Cut, SelectedRows);
+            InvalidateGrid();
+        }
+
+
+        private void CopyToClipBuffer()
+        {
+            SetClipBuffer(ClipAction.Copy, SelectedRows);
+            InvalidateGrid();
+        }
+
+        private void PasteFromClipBuffer()
+        {
+            if (ClipBuffer == null)
+                return;
+
+            if (!IsSequenced)
+                return;
+
+            using (new WaitCursor())
+            {
+                var updates = ClipBuffer.Item2.Select(x => x.ToObject<T>()).ToList();
+                if (BeforePaste(updates, ClipBuffer.Item1))
+                {
+                    var currow = SelectedRows.FirstOrDefault()
+                        ?? Data.Rows.LastOrDefault();
+
+                    var sequence = currow != null ? currow.Get<T, long>(c => ((ISequenceable)c).Sequence) : 0;
+
+                    var postrows = Data.Rows.Where(r => !ClipBuffer.Item2.Contains(r) && r.Get<ISequenceable, long>(x => x.Sequence) >= sequence);
+                    updates.AddRange(LoadItems(postrows.ToArray()));
+
+                    foreach (var update in updates)
+                    {
+                        sequence++;
+                        ((ISequenceable)update).Sequence = sequence;
+                    }
+                }
+
+                if (updates.Any())
+                {
+                    SaveItems(updates.ToArray());
+                    Refresh(false, true);
+                }
+            }
+        }
+
+        protected virtual bool BeforePaste(IEnumerable<T> items, ClipAction action)
+        {
+            return true;
+        }
+        private void Cut_Click(object sender, RoutedEventArgs e)
+        {
+            CutToClipBuffer();
+        }
+
+        private void Copy_Click(object sender, RoutedEventArgs e)
+        {
+            CopyToClipBuffer();
+        }
+
+        private void Paste_Click(object sender, RoutedEventArgs e)
+        {
+            PasteFromClipBuffer();
+        }
+
+        #endregion
+
         protected virtual void ObjectToRow(T obj, CoreRow row)
         {
             Data.LoadRow(row, obj);
         }
 
+        #region Import / Export
+
         protected virtual Guid GetImportID()
         {
             return Guid.Empty;
@@ -2859,6 +2913,8 @@ namespace InABox.DynamicGrid
             ExcelExporter.DoExport(data, filename);
         }
 
+        #endregion
+
         public void ScrollIntoView(CoreRow row)
         {
             DataGrid.ScrollInView(new RowColumnIndex(row.Index + 1, 0));

+ 23 - 4
InABox.DynamicGrid/DynamicGridUtils.cs

@@ -538,7 +538,12 @@ namespace InABox.DynamicGrid
 
         #endregion
 
-        public static void PopulateFormMenu<TEntityForm, TEntity, TEntityLink>(ItemsControl menu, Guid entityID)
+        public static void PopulateFormMenu<TEntityForm, TEntity, TEntityLink>(
+            ItemsControl menu,
+            Guid entityID,
+            Func<TEntity> loadEntity,
+            bool editOnAdd = false
+            )
             where TEntityForm : EntityForm<TEntity, TEntityLink>, new()
             where TEntity : Entity
             where TEntityLink : IEntityLink<TEntity>, new()
@@ -553,8 +558,12 @@ namespace InABox.DynamicGrid
             var addForm = new MenuItem { Header = "Add Form" };
             addForm.Click += (o, e) =>
             {
+                var entity = loadEntity();
+                var filter = LookupFactory.DefineFilter<TEntity, DigitalForm>(new TEntity[] { entity })
+                    ?? LookupFactory.DefineFilter<TEntityForm, DigitalForm>(Array.Empty<TEntityForm>());
+
                 var select = new MultiSelectDialog<DigitalForm>(
-                    LookupFactory.DefineFilter<TEntityForm, DigitalForm>(Array.Empty<TEntityForm>()),
+                    filter,
                     LookupFactory.DefineColumns<DigitalForm>()
                         .Add(x => x.Description),
                     false);
@@ -570,7 +579,17 @@ namespace InABox.DynamicGrid
                         };
                         form.Parent.ID = entityID;
                         form.Form.ID = digitalForm.ID;
-                        new Client<TEntityForm>().Save(form, "Added by user");
+                        if (editOnAdd)
+                        {
+                            if (DynamicFormEditWindow.EditDigitalForm(form, out var dataModel))
+                            {
+                                dataModel.Update(null);
+                            }
+                        }
+                        else
+                        {
+                            new Client<TEntityForm>().Save(form, "Added by user");
+                        }
                     }
                 };
             };
@@ -580,7 +599,7 @@ namespace InABox.DynamicGrid
             {
                 var window = new ThemableWindow() { Title = $"Manage {typeof(TEntity).Name} Forms" };
 
-                var grid = new DynamicEntityFormGrid<TEntityForm, TEntity, TEntityLink>(entityID);
+                var grid = new DynamicEntityFormGrid<TEntityForm, TEntity, TEntityLink>(loadEntity());
 
                 grid.Refresh(true, true);
                 grid.Margin = new Thickness(5);

+ 1 - 1
InABox.DynamicGrid/FormDesigner/Controls/Fields/DFMultiSignatureControl.cs

@@ -113,7 +113,7 @@ namespace InABox.DynamicGrid
                 {
                     MessageBox.Show("Name cannot be empty!");
                 }
-                if (Images.Children.Cast<UIElement>().Any(x => x is Label lbl && (string)lbl.Content == sigName))
+                else if (Images.Children.Cast<UIElement>().Any(x => x is Label lbl && (string)lbl.Content == sigName))
                 {
                     MessageBox.Show("A signature with that name already exists!");
                 }

+ 42 - 23
InABox.DynamicGrid/FormDesigner/DynamicEditFormWindow.xaml.cs

@@ -65,6 +65,7 @@ namespace InABox.DynamicGrid
 
         private bool IsReopening = false;
         private bool HasChanged = false;
+        private bool HasUnsavedChanges => Grid.IsChanged || HasChanged || DataModel!.Instance.ID == Guid.Empty;
 
         private void RefreshEnabled()
         {
@@ -87,7 +88,7 @@ namespace InABox.DynamicGrid
 
             if (Mode == FormMode.Editing || Mode == FormMode.Filling || Mode == FormMode.Preview)
             {
-                SaveForm.IsEnabled = Grid.IsChanged || HasChanged;
+                SaveForm.IsEnabled = HasUnsavedChanges;
             }
             else
             {
@@ -180,16 +181,17 @@ namespace InABox.DynamicGrid
                 .Add<IDigitalFormInstance>(x => x.Form.Description)
                 .Add("Parent.ID");
         }
-        public static bool EditDigitalForm(IDigitalFormInstance formInstance, [NotNullWhen(true)] out IDigitalFormDataModel? dataModel)
+
+        public static bool EditDigitalForm(
+            IDigitalFormInstance formInstance,
+            [NotNullWhen(true)] out IDigitalFormDataModel? dataModel,
+            Entity? parent = null)
         {
             dataModel = null;
             var formid = formInstance.Form.ID;
 
             var values = DigitalForm.DeserializeFormData(formInstance);
 
-            var parentlink = CoreUtils.HasProperty(formInstance.GetType(), "Parent") ? CoreUtils.GetPropertyValue(formInstance, "Parent") as IEntityLink : null;
-            var parenttype = parentlink?.GetType().BaseType?.GetGenericArguments().FirstOrDefault();
-
             var results = Client.QueryMultiple(
                 new KeyedQueryDef<DigitalFormVariable>(new Filter<DigitalFormVariable>(x => x.Form.ID).IsEqualTo(formid)),
                 new KeyedQueryDef<DigitalFormLayout>(
@@ -198,30 +200,43 @@ namespace InABox.DynamicGrid
                         .And(x => x.Layout).IsNotEqualTo("")));
 
             var variables = results[nameof(DigitalFormVariable)].Rows.Select(x => x.ToObject<DigitalFormVariable>()).ToArray();
-            var layout = results[nameof(DigitalFormLayout)].Rows.FirstOrDefault()?.ToObject<DigitalFormLayout>();
 
-            Entity? parent = null;
-            if (parenttype != null && parentlink != null)
-            {
-                var parentid = parentlink.ID;
-                var filter = Filter.Create(parenttype);
-                filter.Expression = CoreUtils.GetMemberExpression(parenttype, "ID");
-                filter.Operator = Operator.IsEqualTo;
-                filter.Value = parentid;
-
-                var client = (Activator.CreateInstance(typeof(Client<>).MakeGenericType(parenttype)) as Client)!;
-                parent = client.Query(filter, null, null).Rows.FirstOrDefault()?.ToObject(parenttype) as Entity;
-            }
+            var desktopLayout = results[nameof(DigitalFormLayout)]
+                .Rows.FirstOrDefault(x => x.Get<DigitalFormLayout, DFLayoutType>(x => x.Type) == DFLayoutType.Desktop)
+                ?.ToObject<DigitalFormLayout>();
 
+            var layout = desktopLayout ?? results[nameof(DigitalFormLayout)].ToObjects<DigitalFormLayout>().FirstOrDefault();
             if (layout == null)
-                return false;
-            if (parent == null)
             {
-                Logger.Send(LogType.Error, "", $"Form parent is null; Form Type: {formInstance.GetType()}; Parent Type: {parenttype}; Form ID: {formInstance.ID}");
-                MessageBox.Show("An error occurred while loading the form: Form Entity is null");
+                MessageBox.Show("No layout found form form!");
                 return false;
             }
 
+            if (parent is null)
+            {
+                var parentlink = CoreUtils.HasProperty(formInstance.GetType(), "Parent") ? CoreUtils.GetPropertyValue(formInstance, "Parent") as IEntityLink : null;
+                var parenttype = parentlink?.GetType().BaseType?.GetGenericArguments().FirstOrDefault();
+
+                if (parenttype != null && parentlink != null)
+                {
+                    var parentid = parentlink.ID;
+                    var filter = Filter.Create(parenttype);
+                    filter.Expression = CoreUtils.GetMemberExpression(parenttype, "ID");
+                    filter.Operator = Operator.IsEqualTo;
+                    filter.Value = parentid;
+
+                    var client = (Activator.CreateInstance(typeof(Client<>).MakeGenericType(parenttype)) as Client)!;
+                    parent = client.Query(filter, null, null).Rows.FirstOrDefault()?.ToObject(parenttype) as Entity;
+                }
+
+                if (parent == null)
+                {
+                    Logger.Send(LogType.Error, "", $"Form parent is null; Form Type: {formInstance.GetType()}; Parent Type: {parenttype}; Form ID: {formInstance.ID}");
+                    MessageBox.Show("An error occurred while loading the form: Form Entity is null");
+                    return false;
+                }
+            }
+
             var form = new DynamicFormEditWindow
             {
                 Type = layout.Type,
@@ -234,6 +249,10 @@ namespace InABox.DynamicGrid
             try
             {
                 dataModel = formInstance.CreateDataModel(parent);
+                dataModel.OnModelSaved += (model) =>
+                {
+                    DFUtils.OnSave(formInstance.GetType(), formInstance, parent);
+                };
                 form.DataModel = dataModel;
             }
             catch (Exception e)
@@ -295,7 +314,7 @@ namespace InABox.DynamicGrid
 
         private void DynamicFormWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
         {
-            if(DialogResult != true && (HasChanged || Grid.IsChanged))
+            if(DialogResult != true && HasUnsavedChanges)
             {
                 if(MessageBox.Show("This form has unsaved changes. Do you wish to discard them?", "Discard Changes?", MessageBoxButton.YesNo) != MessageBoxResult.Yes)
                 {

+ 43 - 9
InABox.Reports/ReportUtils.cs

@@ -114,6 +114,39 @@ namespace InABox.Reports
                 }
             }
 
+            var report = new Report();
+            report.LoadFromString(templaterdl);
+            report.FileName = template.Name;
+
+            foreach(var tableName in data.TableNames)
+            {
+                var dataSource = report.GetDataSource(tableName);
+                if (dataSource != null)
+                {
+                    var modelTable = data.GetDataModelTable(tableName);
+                    var columnNames = CoreUtils.GetColumnNames(modelTable.Type, x => true);
+                    foreach (var column in dataSource.Columns)
+                    {
+                        if(column is FastReport.Data.Column col && !col.Enabled)
+                        {
+                            //columns.Add(col.Name.Replace('_','.'));
+                            columnNames.Remove(col.Name.Replace('_', '.'));
+                        }
+                        /*if (column is FastReport.Data.Column col && col.DataType != null && col.DataType.IsAssignableTo(typeof(IEnumerable<byte[]>)))
+                        {
+                            col.BindableControl = ColumnBindableControl.Custom;
+                            col.CustomBindableControl = "MultiImageObject";
+                        }*/
+                    }
+                    var columns = Columns.Create(modelTable.Type);
+                    foreach(var column in columnNames)
+                    {
+                        columns.Add(column);
+                    }
+                    modelTable.Columns = columns;
+                }
+            }
+
             var script = new ScriptDocument(template.Script);
             var ScriptOK = false;
             if (!string.IsNullOrWhiteSpace(template.Script))
@@ -132,24 +165,25 @@ namespace InABox.Reports
                 script.Execute("Report", "Populate");
             }
 
-            var report = new Report();
-            report.LoadFromString(templaterdl);
-            report.FileName = template.Name;
-
             var ds = data.AsDataSet();
             report.RegisterData(ds);
 
-            foreach (var table in data.Tables)
+            foreach (var tableName in data.TableNames)
             {
-                var dataSource = report.GetDataSource(table.TableName);
+                var columns = data.GetColumns(tableName)?.AsDictionary() ?? new Dictionary<string, Type>();
+                var dataSource = report.GetDataSource(tableName);
                 if(dataSource != null)
                 {
                     foreach (var column in dataSource.Columns)
                     {
-                        if (column is FastReport.Data.Column col && col.DataType != null && col.DataType.IsAssignableTo(typeof(IEnumerable<byte[]>)))
+                        if (column is FastReport.Data.Column col)
                         {
-                            col.BindableControl = ColumnBindableControl.Custom;
-                            col.CustomBindableControl = "MultiImageObject";
+                            if (col.DataType != null && col.DataType.IsAssignableTo(typeof(IEnumerable<byte[]>)))
+                            {
+                                col.BindableControl = ColumnBindableControl.Custom;
+                                col.CustomBindableControl = "MultiImageObject";
+                            }
+                            col.Enabled = columns.ContainsKey(col.Name.Replace('_', '.'));
                         }
                     }
                 }

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

@@ -1350,7 +1350,7 @@ namespace InABox.Database.SQLite
                 case FilterConstant.Now :
                     return "datetime()";
                 case FilterConstant.Today :
-                    return "date()";
+                    return "strftime('%Y-%m-%d 00:00:00')";
             }
             throw new Exception($"FilterConstant.{constant} is not implemented!");
         }