Bladeren bron

Merge commit '25e709bbbc56354614f2a649bdac5b56feb7c12b' into frank

Frank van den Bos 1 jaar geleden
bovenliggende
commit
2b05f57fc0

+ 1 - 1
InABox.Core/Classes/Document/Document.cs

@@ -59,7 +59,7 @@ namespace InABox.Core
             return new Document
             {
                 Data = data,
-                FileName = filename,
+                FileName = Path.GetFileName(filename),
                 CRC = CoreUtils.CalculateCRC(data),
                 TimeStamp = new FileInfo(filename).LastWriteTime
             };

+ 43 - 1
InABox.Core/Filter.cs

@@ -79,7 +79,8 @@ namespace InABox.Core
         InQuery,
         All,
         None,
-        NotInQuery
+        NotInQuery,
+        Not = byte.MaxValue
     }
 
 
@@ -218,6 +219,8 @@ namespace InABox.Core
         Operator Operator { get; set; }
         object? Value { get; set; }
 
+        bool IsNot { get; set; }
+
         IEnumerable<IFilter> Ands { get; }
         IEnumerable<IFilter> Ors { get; }
 
@@ -318,6 +321,8 @@ namespace InABox.Core
         IFilter All();
         IFilter None();
 
+        IFilter Not();
+
         string AsOData();
 
         IEnumerable<string> ColumnNames();
@@ -380,6 +385,8 @@ namespace InABox.Core
         
         public object? Value { get; set; }
 
+        public bool IsNot { get; set; }
+
         IEnumerable<IFilter> IFilter.Ands => Ands;
 
         IEnumerable<IFilter> IFilter.Ors => Ors;
@@ -946,6 +953,16 @@ namespace InABox.Core
 
         #endregion
 
+        #region Not
+
+        public Filter<T> Not()
+        {
+            IsNot = !IsNot;
+            return this;
+        }
+        IFilter IFilter.Not() => Not();
+
+        #endregion
 
         #region All/None
 
@@ -1138,6 +1155,7 @@ namespace InABox.Core
         {
             info.AddValue("Operator", Operator.ToString());
             info.AddValue("Value", Value);
+            info.AddValue("IsNot", IsNot);
             if (Ands.Count > 0)
                 info.AddValue("Ands", Ands, typeof(List<Filter<T>>));
             if (Ors.Count > 0)
@@ -1150,6 +1168,7 @@ namespace InABox.Core
 
             Operator = (Operator)Enum.Parse(typeof(Operator), (string)info.GetValue("Operator", typeof(string)));
             Value = info.GetValue("Value", typeof(object));
+            IsNot = info.GetBoolean("IsNot");
 
             try
             {
@@ -1548,6 +1567,10 @@ namespace InABox.Core
         public void SerializeBinary(CoreBinaryWriter writer)
         {
             writer.SerialiseExpression(typeof(T), Expression, false);
+            if (IsNot)
+            {
+                writer.Write((byte)Operator.Not);
+            }
             writer.Write((byte)Operator);
 
             SerializeValue(writer, Value, true);
@@ -1568,7 +1591,14 @@ namespace InABox.Core
         public void DeserializeBinary(CoreBinaryReader reader)
         {
             Expression = reader.DeserialiseExpression(typeof(T));
+
             Operator = (Operator)reader.ReadByte();
+            if(Operator == Operator.Not)
+            {
+                IsNot = true;
+                Operator = (Operator)reader.ReadByte();
+            }
+
             var val = DeserializeValue(reader);
             var type = (Operator == Operator.InList || Operator == Operator.NotInList)
                 ? Expression.Type.MakeArrayType()
@@ -1659,6 +1689,10 @@ namespace InABox.Core
                     value
                 );
             }
+            if (IsNot)
+            {
+                result = $"not {result}";
+            }
 
             if (Ands != null && Ands.Count > 0)
                 foreach (var and in Ands)
@@ -1933,6 +1967,9 @@ namespace InABox.Core
             writer.WritePropertyName("Operator");
             writer.WriteValue(op);
 
+            writer.WritePropertyName("IsNot");
+            writer.WriteValue(filter.IsNot);
+
             if (val is FilterConstant)
             {
                 writer.WritePropertyName("FilterConstant");
@@ -2051,6 +2088,11 @@ namespace InABox.Core
             var exp = CoreUtils.StringToExpression(prop);
             var op = (Operator)int.Parse(data["Operator"].ToString());
 
+            if(data.TryGetValue("IsNot", out var isNotValue) && isNotValue is bool b)
+            {
+                result.IsNot = b;
+            }
+
             result.Expression = exp;
             result.Operator = op;
             if(data.TryGetValue("Value", out var val))

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

@@ -1399,11 +1399,11 @@ namespace InABox.Database.SQLite
             var result = "";
             if (filter.Operator == Operator.All)
             {
-                result = "1 = 1";
+                result = filter.IsNot ? "1 = 0" : "1 = 1";
             }
             else if (filter.Operator == Operator.None)
             {
-                result = "1 = 0";
+                result = filter.IsNot ? "1 = 1" : "1 = 0";
             }
             else
             {
@@ -1506,6 +1506,11 @@ namespace InABox.Database.SQLite
 
                     }
                 }
+
+                if (filter.IsNot)
+                {
+                    result = $"(NOT {result})";
+                }
             }
 
             var bChanged = false;

+ 2 - 2
inabox.wpf/DynamicGrid/DynamicDocumentGrid.cs

@@ -271,14 +271,14 @@ namespace InABox.DynamicGrid
         private void GetDocuments(Action<Dictionary<string,byte[]>> action)
         {
             var ids = SelectedRows.Select(r => r.Get<IEntityDocument, Guid>(c => c.DocumentLink.ID)).ToArray();
-            var files = new Client<Document>().Query(
+            var files = Client.Query(
                 new Filter<Document>(x => x.ID).InList(ids),
                 new Columns<Document>(x => x.FileName).Add(x => x.Data)
             ).ToDictionary<Document, String, byte[]>(x => x.FileName, x => x.Data);
             action?.Invoke(files);
         }
 
-        private String SanitiseFileName(string filename)
+        private static string SanitiseFileName(string filename)
         {
             var basefilename = Path.GetFileNameWithoutExtension(filename);
             var extension = Path.GetExtension(filename);

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

@@ -1102,7 +1102,12 @@ namespace InABox.DynamicGrid
                 DoChanged();
             bChanged = false;
             _editingObject = null;
-            DataGridItems?.AcceptChanges();
+
+            // Commented out on 19/02/2024 by Kenric. I don't see this being necessary, though I could be wrong. Nevertheless, it was causing a bug when
+            // editing the filter row. It seems that this causes Syncfusion to commit the filter predicates internally, which means that after leaving a 
+            // filter row cell, the filter remained even once it was cleared, meaning a refresh was necessary to get the data back.
+            // I've tested on Bills to see if editing works with this empty, and it seems so.
+            //DataGridItems?.AcceptChanges();
         }
 
         private void UpdateRow(CoreRow row, DataRow? dataRow, Dictionary<string, object?> changes)

+ 8 - 7
inabox.wpf/DynamicGrid/DynamicManyToManyDataGrid.cs

@@ -46,28 +46,29 @@ namespace InABox.DynamicGrid
         {
             var expr = CoreUtils.CreateLambdaExpression<TManyToMany>(prop.Name + ".ID");
             criteria.Add(new Filter<TManyToMany>(expr).IsEqualTo(ID));
-            new Client<TManyToMany>().Query(criteria.Combine(), columns, sort, action);
+            Client.Query(criteria.Combine(), columns, sort, action);
         }
 
         protected override TManyToMany LoadItem(CoreRow row)
         {
             var id = row.Get<TManyToMany, Guid>(x => x.ID);
-            return new Client<TManyToMany>()
-                .Load(
-                    new Filter<TManyToMany>(x => x.ID).IsEqualTo(id))
-                .FirstOrDefault() ?? throw new Exception($"{typeof(TManyToMany)} with ID {id} does not exist!");
+            return Client
+                .Query(
+                    new Filter<TManyToMany>(x => x.ID).IsEqualTo(id),
+                    DynamicGridUtils.LoadEditorColumns(DataColumns()))
+                .ToObjects<TManyToMany>().FirstOrDefault() ?? throw new Exception($"{typeof(TManyToMany)} with ID {id} does not exist!");
         }
 
         protected override void DeleteItems(params CoreRow[] rows)
         {
             var items = LoadItems(rows);
             foreach (var item in items)
-                new Client<TManyToMany>().Delete(item, "");
+                Client.Delete(item, "");
         }
 
         public override void SaveItem(TManyToMany item)
         {
-            new Client<TManyToMany>().Save(item, "");
+            Client.Save(item, "");
         }
     }
 }

+ 354 - 305
inabox.wpf/DynamicGrid/DynamicManyToManyGrid.cs

@@ -10,410 +10,459 @@ using System.Windows.Controls;
 using InABox.Clients;
 using InABox.Configuration;
 using InABox.Core;
+using InABox.Wpf;
 using InABox.WPF;
 
-namespace InABox.DynamicGrid
+namespace InABox.DynamicGrid;
+
+public interface IDynamicManyToManyGrid<TManyToMany, TThis> : IDynamicEditorPage
 {
-    public interface IDynamicManyToManyGrid<TManyToMany, TThis> : IDynamicEditorPage
-    {
-    }
+}
 
-    public class DynamicManyToManyGrid<TManyToMany, TThis> : DynamicGrid<TManyToMany>, IDynamicEditorPage, IDynamicManyToManyGrid<TManyToMany, TThis>
-        where TThis : Entity, new()
-        where TManyToMany : Entity, IPersistent, IRemotable, new()
-    {
-        //private Guid ID = Guid.Empty;
-        protected TThis Item;
-        private TManyToMany[] MasterList = { };
-        protected PropertyInfo otherproperty;
-        protected IEntityLink GetOtherLink(TManyToMany item) => (otherproperty.GetValue(item) as IEntityLink)!;
+public class DynamicManyToManyGrid<TManyToMany, TThis> : DynamicGrid<TManyToMany>, IDynamicEditorPage, IDynamicManyToManyGrid<TManyToMany, TThis>
+    where TThis : Entity, new()
+    where TManyToMany : Entity, IPersistent, IRemotable, new()
+{
+    //private Guid ID = Guid.Empty;
+    protected TThis Item;
+
+    /// <summary>
+    /// Keeps a cache of initially loaded objects, so that we can figure out which guys to delete when we save.
+    /// </summary>
+    private TManyToMany[] MasterList = Array.Empty<TManyToMany>();
 
-        protected PropertyInfo thisproperty;
-        protected IEntityLink GetThisLink(TManyToMany item) => (thisproperty.GetValue(item) as IEntityLink)!;
-        
-        protected List<TManyToMany> WorkingList = new();
+    protected PropertyInfo otherproperty;
+    protected IEntityLink GetOtherLink(TManyToMany item) => (otherproperty.GetValue(item) as IEntityLink)!;
 
-        public PageType PageType => PageType.Other;
+    protected PropertyInfo thisproperty;
+    protected IEntityLink GetThisLink(TManyToMany item) => (thisproperty.GetValue(item) as IEntityLink)!;
+    
+    protected List<TManyToMany> WorkingList = new();
 
-        private bool _readOnly;
-        public bool ReadOnly
+    public PageType PageType => PageType.Other;
+
+    private bool _readOnly;
+    public bool ReadOnly
+    {
+        get => _readOnly;
+        set
         {
-            get => _readOnly;
-            set
+            if(_readOnly != value)
             {
-                if(_readOnly != value)
-                {
-                    _readOnly = value;
-                    Reconfigure();
-                }
+                _readOnly = value;
+                Reconfigure();
             }
         }
+    }
 
-        private static bool IsAutoEntity => typeof(TManyToMany).HasAttribute<AutoEntity>();
+    private static bool IsAutoEntity => typeof(TManyToMany).HasAttribute<AutoEntity>();
 
-        protected DynamicGridCustomColumnsComponent<TManyToMany> ColumnsComponent;
+    protected DynamicGridCustomColumnsComponent<TManyToMany> ColumnsComponent;
 
-        public DynamicManyToManyGrid()
-        {
-            MultiSelect = true;
-            thisproperty = CoreUtils.GetManyToManyThisProperty(typeof(TManyToMany), typeof(TThis));
-            otherproperty = CoreUtils.GetManyToManyOtherProperty(typeof(TManyToMany), typeof(TThis));
+    /// <summary>
+    /// A set of columns representing which columns have been loaded from the database.
+    /// </summary>
+    /// <remarks>
+    /// This is used to refresh the data when the columns change.<br/>
+    /// 
+    /// It is <see langword="null"/> if no data has been loaded from the database (that is, the data was gotten from
+    /// a page data handler instead.)
+    /// </remarks>
+    private HashSet<string>? LoadedColumns;
 
-            HiddenColumns.Add(x => x.ID);
-            HiddenColumns.Add(CoreUtils.CreateLambdaExpression<TManyToMany>(otherproperty.Name + ".ID"));
+    public DynamicManyToManyGrid()
+    {
+        MultiSelect = true;
+        thisproperty = CoreUtils.GetManyToManyThisProperty(typeof(TManyToMany), typeof(TThis));
+        otherproperty = CoreUtils.GetManyToManyOtherProperty(typeof(TManyToMany), typeof(TThis));
 
-            ColumnsComponent = new DynamicGridCustomColumnsComponent<TManyToMany>(this, GetTag());
-        }
+        HiddenColumns.Add(x => x.ID);
+        HiddenColumns.Add(CoreUtils.CreateLambdaExpression<TManyToMany>(otherproperty.Name + ".ID"));
 
-        protected override void Init()
-        {
-        }
+        ColumnsComponent = new DynamicGridCustomColumnsComponent<TManyToMany>(this, GetTag());
+    }
 
-        protected override void DoReconfigure(FluentList<DynamicGridOption> options)
-        {
-            options.BeginUpdate();
-
-            options.Add(DynamicGridOption.RecordCount)
-                .Add(DynamicGridOption.SelectColumns)
-                .Add(DynamicGridOption.MultiSelect);
-
-            if (Security.CanEdit<TManyToMany>() && !ReadOnly)
-                options.Add(DynamicGridOption.AddRows).Add(DynamicGridOption.EditRows);
-            if (Security.CanDelete<TManyToMany>() && !ReadOnly)
-                options.Add(DynamicGridOption.DeleteRows);
-            if (Security.CanImport<TManyToMany>() && !ReadOnly)
-                options.Add(DynamicGridOption.ImportData);
-            if (Security.CanExport<TManyToMany>())
-                options.Add(DynamicGridOption.ExportData);
-            if (Security.CanMerge<TManyToMany>())
-                options.Add(DynamicGridOption.MultiSelect);
-
-            options.EndUpdate();
-        }
+    protected override void Init()
+    {
+    }
+
+    protected override void DoReconfigure(FluentList<DynamicGridOption> options)
+    {
+        options.BeginUpdate();
+
+        options.Add(DynamicGridOption.RecordCount)
+            .Add(DynamicGridOption.SelectColumns)
+            .Add(DynamicGridOption.MultiSelect);
+
+        if (Security.CanEdit<TManyToMany>() && !ReadOnly)
+            options.Add(DynamicGridOption.AddRows).Add(DynamicGridOption.EditRows);
+        if (Security.CanDelete<TManyToMany>() && !ReadOnly)
+            options.Add(DynamicGridOption.DeleteRows);
+        if (Security.CanImport<TManyToMany>() && !ReadOnly)
+            options.Add(DynamicGridOption.ImportData);
+        if (Security.CanExport<TManyToMany>())
+            options.Add(DynamicGridOption.ExportData);
+        if (Security.CanMerge<TManyToMany>())
+            options.Add(DynamicGridOption.MultiSelect);
+
+        options.EndUpdate();
+    }
 
-        public bool MultiSelect { get; set; }
+    public bool MultiSelect { get; set; }
 
-        public DynamicEditorGrid EditorGrid { get; set; }
+    public DynamicEditorGrid EditorGrid { get; set; }
 
-        public string Caption()
-        {
-            //var m2m = typeof(TManyToMany).GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IManyToMany<,>) && i.GenericTypeArguments.Contains(typeof(TThis)));
-            //Type other = m2m.GenericTypeArguments.FirstOrDefault(x => x != typeof(TThis));
-            //var serv = System.Data.Entity PluralizationService.CreateService(new System.Globalization.CultureInfo("en-us"));
-            //var plural = serv.Pluralize(source);
-            //return MvcHtmlString.Create(plural);
-
-            var result = new Inflector.Inflector(new CultureInfo("en")).Pluralize(OtherType().Name);
-            return result;
-        }
+    public string Caption()
+    {
+        //var m2m = typeof(TManyToMany).GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IManyToMany<,>) && i.GenericTypeArguments.Contains(typeof(TThis)));
+        //Type other = m2m.GenericTypeArguments.FirstOrDefault(x => x != typeof(TThis));
+        //var serv = System.Data.Entity PluralizationService.CreateService(new System.Globalization.CultureInfo("en-us"));
+        //var plural = serv.Pluralize(source);
+        //return MvcHtmlString.Create(plural);
+
+        var result = new Inflector.Inflector(new CultureInfo("en")).Pluralize(OtherType().Name);
+        return result;
+    }
 
-        public virtual int Order()
-        {
-            return int.MinValue;
-        }
+    public virtual int Order()
+    {
+        return int.MinValue;
+    }
 
-        public bool Ready { get; set; }
+    public bool Ready { get; set; }
 
-        public void Load(object item, Func<Type, CoreTable?>? PageDataHandler)
-        {
-            Item = (TThis)item;
+    public void Load(object item, Func<Type, CoreTable?>? PageDataHandler)
+    {
+        Item = (TThis)item;
 
-            var data = PageDataHandler?.Invoke(typeof(TManyToMany));
-            if (data != null)
+        var data = PageDataHandler?.Invoke(typeof(TManyToMany));
+        if (data != null)
+        {
+            RefreshData(data);
+        }
+        else
+        {
+            if (Item.ID == Guid.Empty)
             {
+                data = new CoreTable();
+                data.LoadColumns(typeof(TManyToMany));
                 RefreshData(data);
             }
             else
             {
-                if (Item.ID == Guid.Empty)
-                {
-                    data = new CoreTable();
-                    data.LoadColumns(typeof(TManyToMany));
-                    RefreshData(data);
-                }
-                else
+                var exp = CoreUtils.GetPropertyExpression<TManyToMany>(thisproperty.Name + ".ID");
+                var filter = new Filter<TManyToMany>(exp).IsEqualTo(Item.ID).And(exp).IsNotEqualTo(Guid.Empty);
+                var sort = LookupFactory.DefineSort<TManyToMany>();
+
+                var columns = DynamicGridUtils.LoadEditorColumns(DataColumns());
+
+                Client.Query(filter, columns, sort, (o, e) =>
                 {
-                    var exp = CoreUtils.GetPropertyExpression<TManyToMany>(thisproperty.Name + ".ID");
-                    var filter = new Filter<TManyToMany>(exp).IsEqualTo(Item.ID).And(exp).IsNotEqualTo(Guid.Empty);
-                    var sort = LookupFactory.DefineSort<TManyToMany>();
-                    new Client<TManyToMany>().Query(filter, null, sort, (o, e) => {
-                        if(o != null)
-                        {
-                            Dispatcher.Invoke(() => RefreshData(o));
-                        }
-                        else if(e != null)
+                    if (o != null)
+                    {
+                        LoadedColumns = columns.ColumnNames().ToHashSet();
+
+                        Dispatcher.Invoke(() => RefreshData(o));
+                    }
+                    else if(e != null)
+                    {
+                        Dispatcher.Invoke(() =>
                         {
-                            Logger.Send(LogType.Information, ClientFactory.UserID, $"Unknown Error: {CoreUtils.FormatException(e)}");
-                            MessageBox.Show("An error occurred while loading data.");
-                        }
-                    });
-                }
+                            MessageWindow.ShowError("An error occurred while loading data.", e);
+                        });
+                    }
+                });
             }
         }
+    }
 
-        public void BeforeSave(object item)
+    public void BeforeSave(object item)
+    {
+        // Don't need to do anything here
+    }
+
+    public void AfterSave(object item)
+    {
+        if (IsAutoEntity)
         {
-            // Don't need to do anything here
+            return;
         }
+        // First remove any deleted files
+        foreach (var map in MasterList)
+            if (!WorkingList.Contains(map))
+                Client.Delete(map, typeof(TManyToMany).Name + " Deleted by User");
 
-        public void AfterSave(object item)
+        foreach (var map in WorkingList)
         {
-            if (IsAutoEntity)
-            {
-                return;
-            }
-            // First remove any deleted files
-            foreach (var map in MasterList)
-                if (!WorkingList.Contains(map))
-                    new Client<TManyToMany>().Delete(map, typeof(TManyToMany).Name + " Deleted by User");
+            var prop = GetThisLink(map);
+            if (prop.ID != Item.ID)
+                prop.ID = Item.ID;
+        }
 
-            foreach (var map in WorkingList)
-            {
-                var prop = GetThisLink(map);
-                if (prop.ID != Item.ID)
-                    prop.ID = Item.ID;
-            }
+        if (WorkingList.Any(x => x.IsChanged()))
+            Client.Save(WorkingList.Where(x => x.IsChanged()), "Updated by User");
+    }
 
-            if (WorkingList.Any(x => x.IsChanged()))
-                new Client<TManyToMany>().Save(WorkingList.Where(x => x.IsChanged()), "Updated by User");
-        }
+    public Size MinimumSize()
+    {
+        return new Size(400, 400);
+    }
 
-        public Size MinimumSize()
-        {
-            return new Size(400, 400);
-        }
+    private static Type OtherType() =>
+        CoreUtils.GetManyToManyOtherType(typeof(TManyToMany), typeof(TThis));
 
-        private static Type OtherType() =>
-            CoreUtils.GetManyToManyOtherType(typeof(TManyToMany), typeof(TThis));
+    private static string GetTag()
+    {
+        return typeof(TManyToMany).Name + "." + typeof(TThis).Name;
+    }
 
-        private static string GetTag()
-        {
-            return typeof(TManyToMany).Name + "." + typeof(TThis).Name;
-        }
+    public override DynamicGridColumns GenerateColumns()
+    {
+        var cols = new DynamicGridColumns();
+        cols.AddRange(base.GenerateColumns().Where(x => !x.ColumnName.StartsWith(thisproperty.Name + ".")));
+        return cols;
+    }
 
-        public override DynamicGridColumns GenerateColumns()
-        {
-            var cols = new DynamicGridColumns();
-            cols.AddRange(base.GenerateColumns().Where(x => !x.ColumnName.StartsWith(thisproperty.Name + ".")));
-            return cols;
-        }
+    protected override DynamicGridColumns LoadColumns()
+    {
+        return ColumnsComponent.LoadColumns();
+    }
 
-        protected override DynamicGridColumns LoadColumns()
-        {
-            return ColumnsComponent.LoadColumns();
-        }
+    protected override void SaveColumns(DynamicGridColumns columns)
+    {
+        ColumnsComponent.SaveColumns(columns);
+    }
+    protected override void LoadColumnsMenu(ContextMenu menu)
+    {
+        base.LoadColumnsMenu(menu);
+        ColumnsComponent.LoadColumnsMenu(menu);
+    }
 
-        protected override void SaveColumns(DynamicGridColumns columns)
-        {
-            ColumnsComponent.SaveColumns(columns);
-        }
-        protected override void LoadColumnsMenu(ContextMenu menu)
-        {
-            base.LoadColumnsMenu(menu);
-            ColumnsComponent.LoadColumnsMenu(menu);
-        }
+    protected override DynamicGridSettings LoadSettings()
+    {
+        var tag = GetTag();
 
-        protected override DynamicGridSettings LoadSettings()
-        {
-            var tag = GetTag();
+        var user = Task.Run(() => new UserConfiguration<DynamicGridSettings>(tag).Load());
+        user.Wait();
 
-            var user = Task.Run(() => new UserConfiguration<DynamicGridSettings>(tag).Load());
-            user.Wait();
+        //var global = Task.Run(() => new GlobalConfiguration<DynamicGridSettings>(tag).Load());
+        //global.Wait();
+        //Task.WaitAll(user, global);
+        //var columns = user.Result.Any() ? user.Result : global.Result;
 
-            //var global = Task.Run(() => new GlobalConfiguration<DynamicGridSettings>(tag).Load());
-            //global.Wait();
-            //Task.WaitAll(user, global);
-            //var columns = user.Result.Any() ? user.Result : global.Result;
+        return user.Result;
+    }
+    protected override void SaveSettings(DynamicGridSettings settings)
+    {
+        var tag = GetTag();
+        new UserConfiguration<DynamicGridSettings>(tag).Save(settings);
+    }
 
-            return user.Result;
-        }
-        protected override void SaveSettings(DynamicGridSettings settings)
+    protected virtual Guid[] CurrentGuids()
+    {
+        var result = new List<Guid>();
+        foreach (var item in WorkingList)
         {
-            var tag = GetTag();
-            new UserConfiguration<DynamicGridSettings>(tag).Save(settings);
+            //var prop = GetOtherLink(item);
+            var prop = GetThisLink(item);
+            result.Add(prop.ID);
         }
 
-        protected virtual Guid[] CurrentGuids()
-        {
-            var result = new List<Guid>();
-            foreach (var item in WorkingList)
-            {
-                //var prop = GetOtherLink(item);
-                var prop = GetThisLink(item);
-                result.Add(prop.ID);
-            }
-
-            return result.ToArray();
-        }
+        return result.ToArray();
+    }
 
 
-        protected virtual object GetFilter()
-        {
-            var result = LookupFactory.DefineFilter(OtherType(), typeof(TThis), new[] { (TThis)Item });
+    protected virtual IFilter? GetFilter()
+    {
+        var result = LookupFactory.DefineFilter(OtherType(), typeof(TThis), new[] { (TThis)Item });
 
-            var filtertype = typeof(Filter<>).MakeGenericType(OtherType());
+        var filtertype = typeof(Filter<>).MakeGenericType(OtherType());
 
-            var filtermethod = filtertype.GetMethods(BindingFlags.Public | BindingFlags.Static).Where(x =>
-                x.Name.Equals("List") && x.GetParameters().Last().ParameterType.IsAssignableFrom(typeof(IEnumerable<Guid>))).First();
-            var filterexpression = CoreUtils.GetPropertyExpression(OtherType(), "ID");
-            var filtervalues = CurrentGuids();
-            var filter = filtermethod.Invoke(null, new object[] { filterexpression, ListOperator.Excludes, filtervalues }) as IFilter;
+        var filtermethod = filtertype.GetMethods(BindingFlags.Public | BindingFlags.Static).Where(x =>
+            x.Name.Equals("List") && x.GetParameters().Last().ParameterType.IsAssignableFrom(typeof(IEnumerable<Guid>))).First();
+        var filterexpression = CoreUtils.GetPropertyExpression(OtherType(), "ID");
+        var filtervalues = CurrentGuids();
+        var filter = filtermethod.Invoke(null, new object[] { filterexpression, ListOperator.Excludes, filtervalues }) as IFilter;
 
-            if (filter != null)
+        if (filter != null)
+        {
+            if (result != null)
             {
-                if (result != null)
-                {
-                    filter.And(result);
-                }
-
-                return filter;
+                filter.And(result);
             }
 
-            if (result != null) return result;
-
-            return null;
+            return filter;
         }
 
-        protected override void DoAdd(bool OpenEditorOnDirectEdit = false)
+        if (result != null) return result;
+
+        return null;
+    }
+
+    protected override void DoAdd(bool OpenEditorOnDirectEdit = false)
+    {
+        if (MultiSelect)
         {
-            if (MultiSelect)
-            {
-                var filter = GetFilter();
+            var filter = GetFilter();
 
-                var dlgtype = typeof(MultiSelectDialog<>).MakeGenericType(OtherType());
-                var dlg = (Activator.CreateInstance(dlgtype, filter, null, true) as IMultiSelectDialog)!;
-                if (dlg.ShowDialog())
+            var dlgtype = typeof(MultiSelectDialog<>).MakeGenericType(OtherType());
+            var dlg = (Activator.CreateInstance(dlgtype, filter, null, true) as IMultiSelectDialog)!;
+            if (dlg.ShowDialog())
+            {
+                var guids = CurrentGuids();
+                foreach (var entity in dlg.Items(null))
                 {
-                    var guids = CurrentGuids();
-                    var items = dlgtype.GetMethod("Items").Invoke(dlg, new object[] { null }) as IEnumerable;
-                    foreach (var item in items)
+                    if (!guids.Contains(entity.ID))
                     {
-                        var entity = item as Entity;
-                        if (!guids.Contains(entity.ID))
-                        {
-                            var newitem = CreateItem();
-                            var prop = GetOtherLink(newitem);
-                            prop.ID = entity.ID;
-                            prop.Synchronise(entity);
-                            SaveItem(newitem);
-                        }
+                        var newitem = CreateItem();
+                        var prop = GetOtherLink(newitem);
+                        prop.ID = entity.ID;
+                        prop.Synchronise(entity);
+                        SaveItem(newitem);
                     }
-
-                    Refresh(false, true);
                 }
-            }
-            else
-            {
-                base.DoAdd();
-            }
-        }
 
-        protected override TManyToMany CreateItem()
-        {
-            var result = new TManyToMany();
-            if (Item != null)
-            {
-                var prop = GetThisLink(result);
-                prop.ID = Item.ID;
-                prop.Synchronise(Item);
+                Refresh(false, true);
             }
-
-            return result;
         }
-
-        protected override TManyToMany LoadItem(CoreRow row)
+        else
         {
-            return WorkingList[_recordmap[row].Index];
+            base.DoAdd();
         }
+    }
 
-        public override void SaveItem(TManyToMany item)
+    protected override TManyToMany CreateItem()
+    {
+        var result = new TManyToMany();
+        if (Item != null)
         {
-            if (!WorkingList.Contains(item))
-                WorkingList.Add(item);
+            var prop = GetThisLink(result);
+            prop.ID = Item.ID;
+            prop.Synchronise(Item);
         }
 
-        protected override void DeleteItems(params CoreRow[] rows)
-        {
-            foreach (var row in rows)
-            {
-                var id = row.Get<TManyToMany, Guid>(c => c.ID);
-                var item = WorkingList.FirstOrDefault(x => x.ID.Equals(id));
-                if (item != null)
-                    WorkingList.Remove(item);
-            }
-        }
+        return result;
+    }
 
-        private void RefreshData(CoreTable data)
+    protected override TManyToMany LoadItem(CoreRow row)
+    {
+        return WorkingList[_recordmap[row].Index];
+    }
+
+    public override void SaveItem(TManyToMany item)
+    {
+        if (!WorkingList.Contains(item))
+            WorkingList.Add(item);
+    }
+
+    protected override void DeleteItems(params CoreRow[] rows)
+    {
+        foreach (var row in rows)
         {
-            MasterList = data.Rows.Select(x => x.ToObject<TManyToMany>()).ToArray();
-            WorkingList = MasterList.ToList();
-            Refresh(true, true);
-            Ready = true;
+            var id = row.Get<TManyToMany, Guid>(c => c.ID);
+            var item = WorkingList.FirstOrDefault(x => x.ID.Equals(id));
+            if (item != null)
+                WorkingList.Remove(item);
         }
+    }
 
-        protected override void Reload(Filters<TManyToMany> criteria, Columns<TManyToMany> columns, ref SortOrder<TManyToMany>? sort,
-            Action<CoreTable?, Exception?> action)
-        {
-            var results = new CoreTable();
-            results.LoadColumns(typeof(TManyToMany));
+    private void RefreshData(CoreTable data)
+    {
+        MasterList = data.ToArray<TManyToMany>();
+        WorkingList = MasterList.ToList();
+        Refresh(true, true);
+        Ready = true;
+    }
+
+    protected override void Reload(Filters<TManyToMany> criteria, Columns<TManyToMany> columns, ref SortOrder<TManyToMany>? sort,
+        Action<CoreTable?, Exception?> action)
+    {
+        var results = new CoreTable();
+        results.LoadColumns(typeof(TManyToMany));
 
-            if (sort != null)
+        if (LoadedColumns is not null)
+        {
+            // Figure out which columns we still need.
+            var newColumns = columns.Where(x => !LoadedColumns.Contains(x.Property)).ToColumns();
+            if (newColumns.Any() && typeof(TManyToMany).GetCustomAttribute<AutoEntity>() is null)
             {
-                var exp = IQueryableExtensions.ToLambda<TManyToMany>(sort.Expression);
-                var sorted = sort.Direction == SortDirection.Ascending
-                    ? WorkingList.AsQueryable().OrderBy(exp)
-                    : WorkingList.AsQueryable().OrderByDescending(exp);
-                foreach (var then in sort.Thens)
+                var data = Client.Query(
+                    new Filter<TManyToMany>(x => x.ID).InList(WorkingList.Select(x => x.ID).Where(x => x != Guid.Empty).ToArray()),
+                    // We also need to add ID, so we know which item to fill.
+                    newColumns.Add(x => x.ID));
+                foreach (var row in data.Rows)
                 {
-                    var thexp = IQueryableExtensions.ToLambda<TManyToMany>(then.Expression);
-                    sorted = sort.Direction == SortDirection.Ascending ? sorted.ThenBy(exp) : sorted.ThenByDescending(exp);
+                    var item = WorkingList.FirstOrDefault(x => x.ID == row.Get<TManyToMany, Guid>(y => y.ID));
+                    if (item is not null)
+                    {
+                        row.FillObject(item, overrideExisting: false);
+                    }
+                }
+                // Remember that we have now loaded this data.
+                foreach (var column in newColumns)
+                {
+                    LoadedColumns.Add(column.Property);
                 }
-
-                WorkingList = sorted.ToList();
             }
-            results.LoadRows(WorkingList);
-
-            //results.LoadRows(WorkingList);
-            action.Invoke(results, null);
         }
 
-        protected override BaseEditor? GetEditor(object item, DynamicGridColumn column)
+        if (sort != null)
         {
-            var type = CoreUtils.GetProperty(typeof(TManyToMany), column.ColumnName).DeclaringType;
-            if (type.GetInterfaces().Contains(typeof(IEntityLink)) && type.ContainsInheritedGenericType(typeof(TThis)))
-                return new NullEditor();
-            return base.GetEditor(item, column);
-        }
+            var exp = IQueryableExtensions.ToLambda<TManyToMany>(sort.Expression);
+            var sorted = sort.Direction == SortDirection.Ascending
+                ? WorkingList.AsQueryable().OrderBy(exp)
+                : WorkingList.AsQueryable().OrderByDescending(exp);
+            foreach (var then in sort.Thens)
+            {
+                var thexp = IQueryableExtensions.ToLambda<TManyToMany>(then.Expression);
+                sorted = sort.Direction == SortDirection.Ascending ? sorted.ThenBy(exp) : sorted.ThenByDescending(exp);
+            }
 
-        public override void LoadEditorButtons(TManyToMany item, DynamicEditorButtons buttons)
-        {
-            base.LoadEditorButtons(item, buttons);
-            if (ClientFactory.IsSupported<AuditTrail>())
-                buttons.Add("Audit Trail",Wpf.Resources.view.AsBitmapImage(), item, AuditTrailClick);
+            WorkingList = sorted.ToList();
         }
+        results.LoadRows(WorkingList);
 
-        private void AuditTrailClick(object sender, object item)
-        {
-            var entity = (TManyToMany)item;
-            var window = new AuditWindow(entity.ID);
-            window.ShowDialog();
-        }
+        //results.LoadRows(WorkingList);
+        action.Invoke(results, null);
+    }
 
-        public override DynamicEditorPages LoadEditorPages(TManyToMany item)
-        {
-            return item.ID != Guid.Empty ? base.LoadEditorPages(item) : new DynamicEditorPages();
-        }
+    protected override BaseEditor? GetEditor(object item, DynamicGridColumn column)
+    {
+        var type = CoreUtils.GetProperty(typeof(TManyToMany), column.ColumnName).DeclaringType;
+        if (type.GetInterfaces().Contains(typeof(IEntityLink)) && type.ContainsInheritedGenericType(typeof(TThis)))
+            return new NullEditor();
+        return base.GetEditor(item, column);
+    }
+
+    public override void LoadEditorButtons(TManyToMany item, DynamicEditorButtons buttons)
+    {
+        base.LoadEditorButtons(item, buttons);
+        if (ClientFactory.IsSupported<AuditTrail>())
+            buttons.Add("Audit Trail", Wpf.Resources.view.AsBitmapImage(), item, AuditTrailClick);
+    }
 
-        protected override bool BeforePaste(IEnumerable<TManyToMany> items, ClipAction action)
+    private void AuditTrailClick(object sender, object? item)
+    {
+        if (item is not TManyToMany entity) return;
+
+        var window = new AuditWindow(entity.ID);
+        window.ShowDialog();
+    }
+
+    public override DynamicEditorPages LoadEditorPages(TManyToMany item)
+    {
+        return item.ID != Guid.Empty ? base.LoadEditorPages(item) : new DynamicEditorPages();
+    }
+
+    protected override bool BeforePaste(IEnumerable<TManyToMany> items, ClipAction action)
+    {
+        if (action == ClipAction.Copy)
         {
-            if (action == ClipAction.Copy)
+            foreach (var item in items)
             {
-                foreach (var item in items)
-                {
-                    item.ID = Guid.Empty;
-                }
+                item.ID = Guid.Empty;
             }
-            return base.BeforePaste(items, action);
         }
+        return base.BeforePaste(items, action);
     }
 }

+ 5 - 4
inabox.wpf/DynamicGrid/DynamicOneToManyGrid.cs

@@ -49,14 +49,15 @@ namespace InABox.DynamicGrid
 
             property = CoreUtils.GetOneToManyProperty(typeof(TMany), typeof(TOne));
 
+            AddHiddenColumn(property.Name + "." + nameof(IEntityLink.ID));
+            foreach (var col in LookupFactory.RequiredColumns<TMany>())
+                HiddenColumns.Add(col);
+
             ColumnsComponent = new DynamicGridCustomColumnsComponent<TMany>(this, GetTag());
         }
 
         protected override void Init()
         {
-            HiddenColumns.Add(property.Name);
-            foreach (var col in LookupFactory.RequiredColumns<TMany>())
-                HiddenColumns.Add(col);
         }
 
         protected override void DoReconfigure(FluentList<DynamicGridOption> options)
@@ -217,7 +218,7 @@ namespace InABox.DynamicGrid
             {
                 return;
             }
-            new Client<TMany>().Delete(item, typeof(TMany).Name + " Deleted by User");
+            Client.Delete(item, typeof(TMany).Name + " Deleted by User");
         }
 
 

+ 6 - 4
inabox.wpf/DynamicGrid/DynamicRowMovementColumn.cs

@@ -29,26 +29,28 @@ namespace InABox.DynamicGrid
 
         public DynamicRowMovement Direction { get; }
 
-        private bool IsFirst(CoreRow row)
+        private static bool IsFirst(CoreRow? row)
         {
             return row != null && row.Table.Rows.First() == row; // (row.Index == 0);
         }
 
-        private bool IsLast(CoreRow row)
+        private static bool IsLast(CoreRow? row)
         {
             return row != null && row.Table.Rows.Last() == row; //(row.Index == row.Table.Rows.Count - 1);
         }
 
 
-        private BitmapImage GetImage(CoreRow row)
+        private BitmapImage? GetImage(CoreRow? row)
         {
             if (Direction.Equals(DynamicRowMovement.Up))
                 return IsFirst(row) ? null : uparrow;
             return IsLast(row) ? null : downarrow;
         }
 
-        private bool MoveRow(CoreRow row)
+        private bool MoveRow(CoreRow? row)
         {
+            if (row is null) return false;
+
             var tgt = row.Index + (Direction.Equals(DynamicRowMovement.Up) ? -1 : 1);
             if (tgt > -1 && tgt < row.Table.Rows.Count)
                 return Swap(row.Index, tgt);

+ 17 - 6
inabox.wpf/DynamicGrid/Editors/FilterEditor/FilterNode.cs

@@ -74,7 +74,7 @@ public class FilterNode<T> : BaseFilterNode
         this.filterType = filterType;
 
         Property = new(filter.Expression) { Margin = new Thickness(0, 0, 5, 0) };
-        Operator = new(filter.Operator) { Margin = new Thickness(0, 0, 5, 0) };
+        Operator = new(filter.Operator, filter.IsNot) { Margin = new Thickness(0, 0, 5, 0) };
 
         ValuePlaceHolder = CreateValuePlaceHolder();
         
@@ -141,17 +141,28 @@ public class FilterNode<T> : BaseFilterNode
 
     public override Filter<T>? GetFilter()
     {
-        if (Operator.SelectedOperator == Core.Operator.All)
-            return new Filter<T>().All();
+        var op = Operator.SelectedOperator;
+        var isNot = Operator.IsNot;
+        if (op == Core.Operator.All)
+        {
+            return isNot
+                ? new Filter<T>().None()
+                : new Filter<T>().All();
+        }
         if (Operator.SelectedOperator == Core.Operator.None)
-            return new Filter<T>().None();
+        {
+            return isNot
+                ? new Filter<T>().All()
+                : new Filter<T>().None();
+        }
 
         if (Property.SelectedProperty is null)
             return null;
         
         var filter = new Filter<T>();
         filter.Expression = CoreUtils.CreateMemberExpression(typeof(T), Property.SelectedProperty);
-        filter.Operator = Operator.SelectedOperator;
+        filter.Operator = op;
+        filter.IsNot = isNot;
         filter.Value = Constant?.SelectedConstant ?? CustomValue?.Value ?? Value.Value;
 
         foreach(var and in Ands.GetFilters())
@@ -338,7 +349,7 @@ public class FilterNode<T> : BaseFilterNode
         }
     }
 
-    private void Operator_OperatorChanged(Operator op)
+    private void Operator_OperatorChanged(Operator op, bool isNot)
     {
 
         if (Constant is not null)

+ 74 - 13
inabox.wpf/DynamicGrid/Editors/FilterEditor/Nodes/OperatorNode.cs

@@ -1,39 +1,100 @@
 using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Windows;
 using System.Windows.Controls;
 using InABox.Core;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Xceed.Wpf.Toolkit.Primitives;
 
 namespace InABox.DynamicGrid;
 
-public class OperatorNode : ComboBox
+public class OperatorNode : StackPanel
 {
-    public delegate void OperatorChangedHandler(Operator op);
+    public delegate void OperatorChangedHandler(Operator op, bool isNot);
 
     public event OperatorChangedHandler? OperatorChanged;
 
-    public Operator SelectedOperator => (Operator)SelectedItem;
+    public Operator SelectedOperator => (Operator)_boxes.Last().SelectedItem;
 
-    public OperatorNode(Operator op)
-    {
-        SetResourceReference(StyleProperty, typeof(ComboBox));
+    public bool IsNot => _boxes.Count % 2 == 0;
+
+    private List<ComboBox> _boxes = new List<ComboBox>();
 
+    public OperatorNode(Operator op, bool isNot)
+    {
+        Orientation = Orientation.Horizontal;
         VerticalAlignment = System.Windows.VerticalAlignment.Stretch;
-        VerticalContentAlignment = System.Windows.VerticalAlignment.Center;
+
+        if (isNot)
+        {
+            AddBox(Operator.Not);
+            AddBox(op);
+        }
+        else
+        {
+            AddBox(op);
+        }
+    }
+
+    private ComboBox AddBox(Operator selectedItem)
+    {
+        var box = new ComboBox();
+
+        box.VerticalAlignment = System.Windows.VerticalAlignment.Stretch;
+        box.VerticalContentAlignment = System.Windows.VerticalAlignment.Center;
 
         foreach (var value in Enum.GetValues<Operator>())
         {
-            Items.Add(value);
+            box.Items.Add(value);
         }
-        SelectedItem = op;
-        SelectionChanged += ComboBox_SelectionChanged;
+        box.SelectedItem = selectedItem;
+        box.SelectionChanged += ComboBox_SelectionChanged;
+
+        if(_boxes.Count > 0)
+        {
+            box.Margin = new Thickness(5, 0, 0, 0);
+        }
+
+        _boxes.Add(box);
+        Children.Add(box);
+
+        return box;
     }
+
     public void Clear()
     {
-        SelectedIndex = 0;
+        _boxes.Clear();
+        Children.Clear();
+
+        AddBox(default);
+        OperatorChanged?.Invoke(default, false);
     }
 
     private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
     {
-        var selected = (Operator?)SelectedItem ?? Operator.IsEqualTo;
-        OperatorChanged?.Invoke(selected);
+        if (sender is not ComboBox box) return;
+
+        var selected = (Operator?)box.SelectedItem ?? Operator.IsEqualTo;
+
+        if (box == _boxes.Last())
+        {
+            if(selected == Operator.Not)
+            {
+                AddBox(default);
+                OperatorChanged?.Invoke(SelectedOperator, IsNot);
+            }
+        }
+        else
+        {
+            if(selected != Operator.Not)
+            {
+                var idx = _boxes.IndexOf(box);
+                Children.RemoveRange(idx + 1, _boxes.Count - idx);
+                _boxes.RemoveRange(idx + 1, _boxes.Count - (idx + 1));
+                OperatorChanged?.Invoke(SelectedOperator, IsNot);
+            }
+        }
+        OperatorChanged?.Invoke(SelectedOperator, IsNot);
     }
 }

+ 4 - 0
inabox.wpf/DynamicGrid/MultiSelectDialog.cs

@@ -19,6 +19,8 @@ namespace InABox.DynamicGrid
         bool ShowDialog(String? column = null, String? filter = null, FilterType filtertype = FilterType.Contains);
         Guid[] IDs();
         CoreTable Data();
+
+        Entity[] Items(IColumns? columns = null);
     }
 
     public class MultiSelectDialog<T> : IMultiSelectDialog where T : Entity, IRemotable, IPersistent, new()
@@ -214,6 +216,8 @@ namespace InABox.DynamicGrid
             return Array.Empty<T>();
         }
 
+        Entity[] IMultiSelectDialog.Items(IColumns? columns) => Items(columns as Columns<T>);
+
         private void Grid_DoubleClick(object sender, HandledEventArgs args)
         {
             args.Handled = true;

+ 34 - 3
inabox.wpf/Forms/MessageWindow.xaml.cs

@@ -293,8 +293,11 @@ public partial class MessageWindow : Window, INotifyPropertyChanged
             .Message(message ?? exception.Message)
             .Title(title)
             .Details(CoreUtils.FormatException(exception))
-            .Image(image ?? _warning);
-        //    .AddButton(new MessageWindowButton("Show Logs", ShowLogs_Click, MessageWindowButtonPosition.Left));
+            .Image(image ?? _warning)
+            .AddButton(new MessageWindowButton("Email Logs", (window, button) =>
+            {
+                EmailLogs_Click(exception);
+            }, MessageWindowButtonPosition.Left));
 
         var showDetailsButton = new MessageWindowButton("Show Details", (win, button) =>
         {
@@ -308,6 +311,31 @@ public partial class MessageWindow : Window, INotifyPropertyChanged
             .AddOKButton();
     }
 
+    private static void EmailLogs_Click(Exception e)
+    {
+        var logFile = Path.Combine(CoreUtils.GetPath(), string.Format("{0:yyyy-MM-dd}.log", DateTime.Today));
+
+        const int nRead = 1024 * 1024;
+
+        byte[] data;
+        using (var stream = File.OpenRead(logFile))
+        {
+            if (stream.Length > nRead)
+            {
+                stream.Seek(-nRead, SeekOrigin.End);
+            }
+
+            data = new BinaryReader(stream).ReadBytes(Math.Min(nRead, (int)stream.Length));
+        }
+
+        var message = EmailUtils.CreateMessage(
+            subject: "Error logs",
+            to: "support@prsdigital.com.au",
+            body: $"Error logs for PRS:\n\nException: {CoreUtils.FormatException(e)}");
+        message.AddAttachment("Error Logs.txt", data);
+        EmailUtils.OpenEmail(message);
+    }
+
     /// <summary>
     /// Display a message box for a non-exception error, giving options to view the logs.
     /// </summary>
@@ -332,7 +360,10 @@ public partial class MessageWindow : Window, INotifyPropertyChanged
         }
 
         window.Image(image ?? _warning)
-            .AddButton(new MessageWindowButton("Show Logs", ShowLogs_Click, MessageWindowButtonPosition.Left));
+            .AddButton(new MessageWindowButton(
+                "Email Logs",
+                (window, button) => EmailLogs_Click(new Exception(details ?? message)),
+                MessageWindowButtonPosition.Left));
 
         if(details is not null)
         {

+ 163 - 0
inabox.wpf/Utils/EmailUtils.cs

@@ -0,0 +1,163 @@
+using InABox.Clients;
+using InABox.Core;
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Net.Mail;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows.Controls;
+using System.Windows;
+using System.Windows.Forms;
+using System.Drawing;
+using InABox.WPF;
+using MessageBox = System.Windows.Forms.MessageBox;
+using TextBox = System.Windows.Controls.TextBox;
+using InABox.Wpf.Reports;
+
+namespace InABox.Wpf;
+
+public static class EmailUtils
+{
+    /// <summary>
+     /// Creates and opens an email with the default email app - selected by the user.
+     /// This method is for emails with a PDF attachment. Provide the file name and data.
+     /// Optionally provide from, subject and body.
+     /// If from is not provided, an attempt will be made to find the User's email address - if empty it will throw an error (cannot be empty)
+     /// </summary>
+     /// <param name="attachmentname"></param>
+     /// <param name="attachmentdata"></param>
+     /// <param name="from"></param>
+     /// <param name="subject"></param>
+     /// <param name="body"></param>
+    public static void CreateEMLFile(string attachmentname, byte[] attachmentdata, string from = "", string subject = "", string body = "", string to = "")
+    {
+        var message = CreateMessage(from, subject, body, to);
+
+        message = AddAttachment(message, attachmentname, attachmentdata);
+
+        OpenEmail(message, attachmentname);
+    }
+    
+    /// <summary>
+    /// Creates and opens an email with the default email app - selected by the user.
+    /// This method is for emails with multiple PDF attachments. Provide the a Dictionary of names and byte arrays
+    /// Optionally provide from, subject and body.
+    /// If from is not provided, an attempt will be made to find the User's email address - if empty it will throw an error (cannot be empty)
+    /// </summary>
+    /// <param name="attachmentname"></param>
+    /// <param name="attachmentdata"></param>
+    /// <param name="from"></param>
+    /// <param name="subject"></param>
+    /// <param name="body"></param>
+    public static void CreateEMLFile(Dictionary<string,byte[]> attachments, string from = "", string subject = "", string body = "", string to = "")
+    {
+        var message = CreateMessage(from, subject, body, to);
+
+        foreach (var key in attachments.Keys)
+            AddAttachment(message, key, attachments[key]);
+
+        OpenEmail(message);
+    }
+
+    /// <summary>
+    /// Creates and opens an email with the default email app - selected by the user.
+    /// This method is for emails with no attachments.
+    /// Optionally provide from, subject and body.
+    /// If from is not provided, an attempt will be made to find the User's email address - if empty it will throw an error (cannot be empty)
+    /// </summary>
+    /// <param name="from"></param>
+    /// <param name="subject"></param>
+    /// <param name="body"></param>
+
+    public static void CreateEMLFile(string? from, string? subject, string? body)
+    {
+        var message = CreateMessage(from, subject, body);
+
+        OpenEmail(message, "Message from " + message.From);
+    }
+
+    public static void OpenEmail(MailMessage message, string? name = null)
+    {
+        var filename = Path.Combine(
+            Path.GetTempPath(), 
+            Path.ChangeExtension(String.IsNullOrWhiteSpace(name) ? Guid.NewGuid().ToString() : name, ".eml")
+        );
+
+        using (var filestream = File.Open(filename, FileMode.Create))
+        {
+            var binaryWriter = new BinaryWriter(filestream);
+            //Write the Unsent header to the file so the mail client knows this mail must be presented in "New message" mode
+            binaryWriter.Write(Encoding.UTF8.GetBytes("X-Unsent: 1" + Environment.NewLine));
+
+            var assembly = typeof(SmtpClient).Assembly;
+            var mailWriterType = assembly.GetType("System.Net.Mail.MailWriter")!;
+
+            // Get reflection info for MailWriter contructor
+            var mailWriterConstructor =
+                mailWriterType.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(Stream), typeof(bool) }, null)!;
+
+            // Construct MailWriter object with our FileStream
+            var mailWriter = mailWriterConstructor.Invoke(new object[] { filestream, true });
+
+            // Get reflection info for Send() method on MailMessage
+            var sendMethod = typeof(MailMessage).GetMethod("Send", BindingFlags.Instance | BindingFlags.NonPublic)!;
+
+            sendMethod.Invoke(message, BindingFlags.Instance | BindingFlags.NonPublic, null, new[] { mailWriter, true, true }, null);
+
+            // Finally get reflection info for Close() method on our MailWriter
+            var closeMethod = mailWriter.GetType().GetMethod("Close", BindingFlags.Instance | BindingFlags.NonPublic)!;
+
+            // Call close method
+            closeMethod.Invoke(mailWriter, BindingFlags.Instance | BindingFlags.NonPublic, null, new object[] { }, null);
+        }
+
+        // Open the file with the default associated application registered on the local machine
+        Process.Start(new ProcessStartInfo(filename) { UseShellExecute = true });
+    }
+
+    public static MailMessage CreateMessage(string? from = null, string? subject = null, string? body = null, string? to = null)
+    {
+        return new MailMessage(
+            from.NotWhiteSpaceOr(GetAddressFromUser()).NotWhiteSpaceOr("example@outlook.com.au"),
+            to.NotWhiteSpaceOr("example@outlook.com.au"),
+            subject.NotWhiteSpaceOr("Enter subject"),
+            body.NotWhiteSpaceOr("Enter message"))
+        {
+            IsBodyHtml = false
+        };
+    }
+
+    private static string GetAddressFromUser()
+    {
+        CoreTable table = new Client<User>().Query(new Filter<User>(x => x.ID).IsEqualTo(ClientFactory.UserGuid)
+            , new Columns<User>(x => x.EmailAddress));
+        User user = table.Rows.FirstOrDefault().ToObject<User>();
+
+        if (!string.IsNullOrWhiteSpace(user.EmailAddress))
+            return user.EmailAddress;
+        else
+            MessageWindow.ShowMessage("Current User Email Address is blank - please fill in (Human Resources -> User Accounts -> Choose your User -> Email Settings -> Email Address", "Error");
+
+        return "";
+
+    }
+
+    public static MailMessage AddAttachment(this MailMessage message, string attachmentname, byte[] attachmentdata)
+    {
+        var attachment = Path.Combine(
+            Path.GetTempPath(), 
+            String.IsNullOrWhiteSpace(Path.GetExtension(attachmentname)) 
+                ? Path.ChangeExtension(attachmentname, ".pdf") 
+                : attachmentname 
+        );
+        File.WriteAllBytes(attachment, attachmentdata);
+
+        message.Attachments.Add(new Attachment(attachment));
+
+        return message;
+    }
+}