Forráskód Böngészése

First draft of dashboard system complete.

Kenric Nugteren 6 hónapja
szülő
commit
98b11f5a00

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

@@ -17,7 +17,7 @@ namespace InABox.Clients
 
     public class QueryMultipleResults
     {
-        private readonly Dictionary<string, CoreTable> Results;
+        public Dictionary<string, CoreTable> Results { get; private set; }
 
         internal QueryMultipleResults(Dictionary<string, CoreTable> results)
         {

+ 17 - 0
inabox.wpf/Dashboard/DynamicDashboard.cs

@@ -99,4 +99,21 @@ public static class DynamicDashboardUtils
         LoadTypes();
         return _presenterEditorTypes.GetValueOrDefault(presenterType);
     }
+
+    private static JsonSerializerSettings SerializationSettings()
+    {
+        var settings = Serialization.CreateSerializerSettings();
+        settings.TypeNameHandling = TypeNameHandling.Auto;
+        return settings;
+    }
+
+    public static string Serialize(DynamicDashboard data)
+    {
+        return JsonConvert.SerializeObject(data, typeof(DynamicDashboard), SerializationSettings());
+    }
+
+    public static DynamicDashboard Deserialize(string json)
+    {
+        return JsonConvert.DeserializeObject<DynamicDashboard>(json, SerializationSettings())!;
+    }
 }

+ 9 - 1
inabox.wpf/Dashboard/DynamicDashboardDataComponent.cs

@@ -1,4 +1,5 @@
-using InABox.Core;
+using InABox.Clients;
+using InABox.Core;
 using System;
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
@@ -71,6 +72,13 @@ public class DynamicDashboardDataComponent
         query = Queries.FirstOrDefault(x => x.Key == key);
         return query != null;
     }
+
+    public DynamicDashboardData RunQuery()
+    {
+        var queryDefs = Queries.Select(x => new KeyedQueryDef(x.Key, x.Type, x.Filter, x.Columns, x.SortOrder));
+        var results = Client.QueryMultiple(queryDefs);
+        return new DynamicDashboardData(results.Results);
+    }
 }
 
 public class DynamicDashboardData(Dictionary<string, CoreTable> data)

+ 13 - 3
inabox.wpf/Dashboard/Editor/DynamicDashboardDataEditor.xaml.cs

@@ -1,4 +1,5 @@
-using InABox.DynamicGrid;
+using InABox.Core;
+using InABox.DynamicGrid;
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
@@ -36,11 +37,19 @@ public partial class DynamicDashboardDataEditor : UserControl, INotifyPropertyCh
     {
         InitializeComponent();
 
-        QueryGrid.Items = [new()];
+        QueryGrid.Items = data.Queries.Select(x => new DynamicDashboardDataQueryEditItem(x)).ToList();
+        HasMultipleQueries = QueryGrid.Items.Count > 1;
 
         QueryGrid.Refresh(true, true);
 
-        QueryGrid.InitialiseEditorForm(QueryEditor, [QueryGrid.Items[0]]);
+        if (!HasMultipleQueries)
+        {
+            if(QueryGrid.Items.Count == 0)
+            {
+                QueryGrid.Items = [new()];
+            }
+            QueryGrid.InitialiseEditorForm(QueryEditor, [QueryGrid.Items[0]]);
+        }
     }
 
     private void QueryGrid_OnChanged(object sender, EventArgs e)
@@ -85,6 +94,7 @@ public partial class DynamicDashboardDataEditor : UserControl, INotifyPropertyCh
         };
         if(dlg.ShowDialog() == true)
         {
+            data.Queries = editor.QueryGrid.Items.Select(x => x.ToQuery()).NotNull().ToList();
             return true;
         }
         else

+ 31 - 3
inabox.wpf/Dashboard/Editor/DynamicDashboardDataQueryGrid.cs

@@ -10,12 +10,23 @@ namespace InABox.Wpf.Dashboard.Editor;
 
 internal class DynamicDashboardDataQueryEditItem : BaseObject
 {
+    // Having to do custom OnPropertyChanged stuff because Fody isn't allowed in this project.
+    private Type? _type;
+    [ComboLookupEditor(typeof(TypeLookupGenerator))]
     [EditorSequence(1)]
-    public string Key { get; set; } = "";
+    public Type? Type
+    {
+        get => _type;
+        set
+        {
+            var oldValue = _type;
+            _type = value;
+            OnPropertyChanged(nameof(Type), oldValue, value);
+        }
+    }
 
-    [ComboLookupEditor(typeof(TypeLookupGenerator))]
     [EditorSequence(2)]
-    public Type? Type { get; set; }
+    public string Key { get; set; } = "";
 
     [FilterEditor]
     [EditorSequence(3)]
@@ -29,6 +40,19 @@ internal class DynamicDashboardDataQueryEditItem : BaseObject
     [EditorSequence(5)]
     public string SortOrder { get; set; } = "";
 
+    public DynamicDashboardDataQueryEditItem()
+    {
+    }
+
+    public DynamicDashboardDataQueryEditItem(IDynamicDashboardDataQuery query)
+    {
+        Key = query.Key;
+        Type = query.Type;
+        Filter = Serialization.Serialize(query.Filter);
+        Columns = Serialization.Serialize(query.Columns);
+        SortOrder = Serialization.Serialize(query.SortOrder);
+    }
+
     private class TypeLookupGenerator : LookupGenerator<DynamicDashboardDataQueryEditItem>
     {
         public TypeLookupGenerator(DynamicDashboardDataQueryEditItem[]? items) : base(items)
@@ -53,6 +77,10 @@ internal class DynamicDashboardDataQueryEditItem : BaseObject
             Filter = "";
             Columns = "";
             SortOrder = "";
+            if (Type is not null && (Key.IsNullOrWhiteSpace() || Key == (before as Type)?.Name))
+            {
+                Key = Type.Name;
+            }
         }
     }
 

+ 5 - 2
inabox.wpf/Dashboard/Editor/DynamicDashboardEditor.xaml

@@ -8,6 +8,7 @@
              x:Name="Control">
     <Grid DataContext="{Binding ElementName=Control}">
         <Grid.RowDefinitions>
+            <RowDefinition Height="Auto"/>
             <RowDefinition Height="Auto"/>
             <RowDefinition Height="*"/>
         </Grid.RowDefinitions>
@@ -21,13 +22,15 @@
                       DisplayMemberPath="Item1"
                       SelectedValue="{Binding SelectedPresentationType}"
                       VerticalContentAlignment="Center"
-                      Padding="5" Height="30"/>
+                      Padding="5" Height="30"
+                      MinWidth="100"/>
 
             <Button x:Name="SelectData" Content="Select Data"
                     DockPanel.Dock="Right" Padding="5" Margin="5,0,0,0"
                     Click="SelectData_Click"/>
         </DockPanel>
         <ContentControl x:Name="PresentationEditorControl"
-                        Grid.Row="1"/>
+                        Grid.Row="1" Margin="0,5,0,0"
+                        MinHeight="100"/>
     </Grid>
 </UserControl>

+ 46 - 15
inabox.wpf/Dashboard/Editor/DynamicDashboardEditor.xaml.cs

@@ -46,42 +46,73 @@ public partial class DynamicDashboardEditor : UserControl, INotifyPropertyChange
 
             if(value is not null && value.GetInterfaceDefinition(typeof(IDynamicDashboardDataPresenter<>)) is Type presentationInterface)
             {
-                _presenterPropertiesType = presentationInterface.GenericTypeArguments[0];
-                _presenterProperties = Activator.CreateInstance(_presenterPropertiesType)!;
-
-                if(DynamicDashboardUtils.TryGetPresenterEditor(value, out var editorType))
-                {
-                    var editor = (Activator.CreateInstance(editorType) as IDynamicDashboardDataPresenterEditor)!;
-                    editor.Properties = _presenterProperties;
-                    editor.DataComponent = DataComponent;
-
-                    PresentationEditorControl.Content = editor.Setup();
-                }
+                var propertiesType = presentationInterface.GenericTypeArguments[0];
+                SetProperties(Activator.CreateInstance(propertiesType)!, value);
             }
         }
     }
 
     private DynamicDashboardDataComponent DataComponent;
 
-    private Type? _presenterPropertiesType;
     private object? _presenterProperties;
+    private IDynamicDashboardDataPresenterEditor? _presenterEditor;
 
-    public DynamicDashboardEditor()
+    public DynamicDashboardEditor(DynamicDashboard dashboard)
     {
-        InitializeComponent();
+        DataComponent = dashboard.DataComponent;
+
+        if(dashboard.DataPresenter is not null)
+        {
+            _selectedPresentationType = dashboard.DataPresenter.GetType();
+            SetProperties(dashboard.DataPresenter.Properties, _selectedPresentationType);
+        }
 
-        DataComponent = new();
+        InitializeComponent();
 
         PresentationTypes = DynamicDashboardUtils.GetPresenterTypes()
             .Select(x => new Tuple<string, Type>(x.GetCaption(), x))
             .ToArray();
     }
 
+    private void SetProperties(object properties, Type presentationType)
+    {
+        _presenterProperties = properties;
+
+        if(DynamicDashboardUtils.TryGetPresenterEditor(presentationType, out var editorType))
+        {
+            _presenterEditor = (Activator.CreateInstance(editorType) as IDynamicDashboardDataPresenterEditor)!;
+            _presenterEditor.Properties = _presenterProperties;
+            _presenterEditor.DataComponent = DataComponent;
+
+            PresentationEditorControl.Content = _presenterEditor.Setup();
+        }
+    }
+
     private void SelectData_Click(object sender, RoutedEventArgs e)
     {
         if (DynamicDashboardDataEditor.Execute(DataComponent))
         {
+            if(_presenterEditor is not null)
+            {
+                _presenterEditor.DataComponent = DataComponent;
+            }
+        }
+    }
 
+    public DynamicDashboard GetDashboard()
+    {
+        var dashboard = new DynamicDashboard();
+        dashboard.DataComponent = DataComponent;
+
+        if(SelectedPresentationType is not null)
+        {
+            dashboard.DataPresenter = (Activator.CreateInstance(SelectedPresentationType) as IDynamicDashboardDataPresenter)!;
+            dashboard.DataPresenter.Properties = _presenterProperties!;
+        }
+        else
+        {
+            dashboard.DataPresenter = null;
         }
+        return dashboard;
     }
 }

+ 93 - 3
inabox.wpf/Dashboard/PresenterEditors/DynamicDashboardGridPresenterEditor.cs

@@ -1,19 +1,109 @@
-using System;
+using InABox.DynamicGrid;
+using InABox.WPF;
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using System.Windows;
+using System.Windows.Controls;
 
 namespace InABox.Wpf.Dashboard;
 
 public class DynamicDashboardGridPresenterEditor : IDynamicDashboardDataPresenterEditor<DynamicDashboardGridPresenter, DynamicDashboardGridPresenterProperties>
 {
-    public DynamicDashboardDataComponent DataComponent { get; set; } = null!;
+    private DynamicDashboardDataComponent _dataComponent = null!;
+    public DynamicDashboardDataComponent DataComponent
+    {
+        get => _dataComponent;
+        set
+        {
+            _dataComponent = value;
+
+            QueryBox.ItemsSource = DataComponent.Queries.Select(x => x.Key);
+            if(!DataComponent.TryGetQuery(Properties.Query, out var _query))
+            {
+                Properties.Query = DataComponent.Queries.FirstOrDefault()?.Key ?? "";
+            }
+            QueryBox.SelectedValue = Properties.Query;
+        }
+    }
+
     public DynamicDashboardGridPresenterProperties Properties { get; set; } = null!;
 
+    private ContentControl Content = new();
+    private ComboBox QueryBox = new();
+
     public FrameworkElement? Setup()
     {
-        throw new NotImplementedException();
+        UpdateData();
+
+        var grid = new Grid();
+        grid.AddRow(GridUnitType.Auto);
+        grid.AddRow(GridUnitType.Star);
+
+        var dock = new DockPanel
+        {
+            LastChildFill = false
+        };
+        var label = new Label
+        {
+            Content = "Query:",
+            VerticalAlignment = VerticalAlignment.Center,
+        };
+        DockPanel.SetDock(label, Dock.Left);
+
+        QueryBox.Padding = new(5);
+        QueryBox.MinWidth = 100;
+        QueryBox.SelectionChanged += QueryBox_SelectionChanged;
+        DockPanel.SetDock(QueryBox, Dock.Left);
+
+        dock.Children.Add(label);
+        dock.Children.Add(QueryBox);
+
+        Content.Margin = new(0, 5, 0, 0);
+
+        grid.AddChild(dock, 0, 0);
+        grid.AddChild(Content, 1, 0);
+
+        Content.Height = 100;
+
+        return grid;
+    }
+
+    private void QueryBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
+    {
+        Properties.Query = QueryBox.SelectedValue as string ?? "";
+        UpdateData();
+    }
+
+    private void UpdateData()
+    {
+        if (!DataComponent.TryGetQuery(Properties.Query, out var query)) return;
+
+        var grid = (Activator.CreateInstance(typeof(DynamicItemsListGrid<>).MakeGenericType(query.Type)) as IDynamicGrid)!;
+        grid.Reconfigure(options =>
+        {
+            options.Clear();
+            options.SelectColumns = true;
+        });
+        grid.OnGenerateColumns += (o, e) =>
+        {
+            e.Columns.Clear();
+            if(Properties.Columns is null)
+            {
+                e.Columns.AddRange(grid.ExtractColumns(query.Columns));
+            }
+            else
+            {
+                e.Columns.AddRange(Properties.Columns);
+            }
+        };
+        grid.OnSaveColumns += (o, e) =>
+        {
+            Properties.Columns = grid.VisibleColumns;
+        };
+        grid.Refresh(true, true);
+        Content.Content = grid;
     }
 }

+ 1 - 0
inabox.wpf/Dashboard/PresenterEditors/IDynamicDashboardDataPresenterEditor.cs

@@ -11,6 +11,7 @@ public interface IDynamicDashboardDataPresenterEditor
 {
     /// <summary>
     /// Component for the data of this presenter; can be safely assumed to be non-<see langword="null"/> when <see cref="Setup"/> is called.
+    /// This may be set later, if the user changes the data they've selected.
     /// </summary>
     DynamicDashboardDataComponent DataComponent { get; set; }
 

+ 5 - 0
inabox.wpf/Dashboard/Presenters/DynamicDashboardGridPresenter.cs

@@ -2,6 +2,7 @@
 using InABox.DynamicGrid;
 using System;
 using System.Collections.Generic;
+using System.ComponentModel;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
@@ -82,4 +83,8 @@ public class DynamicDashboardGridPresenter : IDynamicDashboardDataPresenter<Dyna
         }
         Grid.Refresh(false, true);
     }
+
+    public void Shutdown(CancelEventArgs? cancel)
+    {
+    }
 }

+ 3 - 0
inabox.wpf/Dashboard/Presenters/IDynamicDashboardDataPresenter.cs

@@ -1,6 +1,7 @@
 using Newtonsoft.Json;
 using System;
 using System.Collections.Generic;
+using System.ComponentModel;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
@@ -30,6 +31,8 @@ public interface IDynamicDashboardDataPresenter
     /// </summary>
     /// <param name="data">The data to be rendered.</param>
     void Refresh(DynamicDashboardData data);
+
+    void Shutdown(CancelEventArgs? cancel);
 }
 public interface IDynamicDashboardDataPresenter<TProperties> : IDynamicDashboardDataPresenter
     where TProperties: class, new()

+ 66 - 10
inabox.wpf/DigitalForms/Designer/DynamicFormDesignGrid.cs

@@ -22,6 +22,7 @@ using System.Windows.Media.Imaging;
 using InABox.Clients;
 using InABox.Core;
 using InABox.WPF;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
 using Brush = System.Windows.Media.Brush;
 using Color = System.Drawing.Color;
 using Image = System.Windows.Controls.Image;
@@ -32,12 +33,15 @@ namespace InABox.DynamicGrid
 {
     public class DynamicFormCreateElementArgs : EventArgs
     {
-        public DynamicFormCreateElementArgs(DFLayoutElement element)
+        public DynamicFormCreateElementArgs(DFLayoutElement element, string name)
         {
             Element = element;
+            Name = name;
         }
 
         public DFLayoutElement Element { get; }
+
+        public string Name { get; }
     }
 
     public delegate FrameworkElement DynamicFormCreateElementDelegate(object sender, DynamicFormCreateElementArgs e);
@@ -107,6 +111,7 @@ namespace InABox.DynamicGrid
         #region Backing Properties
 
         private readonly List<DynamicFormElement> _elements = new();
+        private readonly List<DynamicFormElementAction> _elementActions = new();
         private bool _showBorders = true;
         private IDigitalFormDataModel? _datamodel;
         private DFLayout form = new();
@@ -274,33 +279,63 @@ namespace InABox.DynamicGrid
             public FrameworkElement? Element { get; set; }
             public bool AllowDuplicate { get; set; }
 
-            public DynamicFormElement(string caption, Type elementType, string category, FrameworkElement? element, bool allowDuplicate)
+            public bool Visible { get; set; }
+
+            public DynamicFormElement(string caption, Type elementType, string category, FrameworkElement? element, bool allowDuplicate, bool visible)
             {
                 Caption = caption;
                 ElementType = elementType;
                 Category = category;
                 Element = element;
                 AllowDuplicate = allowDuplicate;
+                Visible = visible;
+            }
+        }
+
+        class DynamicFormElementAction
+        {
+            public string Caption { get; set; }
+
+            public Bitmap? Image { get; set; }
+
+            public string Category { get; set; }
+
+            public object? Tag { get; set; }
+
+            public Func<object?, DFLayoutElement?> OnClick { get; set; }
+
+            public DynamicFormElementAction(string caption, Bitmap? image, string category, object? tag, Func<object?, DFLayoutElement> onClick)
+            {
+                Caption = caption;
+                Image = image;
+                Category = category;
+                Tag = tag;
+                OnClick = onClick;
             }
         }
 
-        public void AddElement<TElement>(string caption, string category, bool allowduplicate = false)
+        public void AddElement<TElement>(string caption, string category, bool allowduplicate = false, bool visible = true)
             where TElement : DFLayoutElement
         {
-            AddElement(typeof(TElement), caption, category, allowduplicate);
+            AddElement(typeof(TElement), caption, category, allowduplicate, visible: visible);
         }
 
-        public void AddElement(Type TElement, string caption, string category, bool allowduplicate = false)
+        public void AddElement(Type TElement, string caption, string category, bool allowduplicate = false, bool visible = true)
         {
-            _elements.Add(new DynamicFormElement(caption, TElement, category, null, allowduplicate));
+            _elements.Add(new DynamicFormElement(caption, TElement, category, null, allowduplicate, visible));
+        }
+
+        public void AddElementAction<TTag>(string caption, Bitmap? image, string category, TTag tag, Func<TTag, DFLayoutElement?> onClick)
+        {
+            _elementActions.Add(new(caption, image, category, tag, x => onClick((TTag)x)));
         }
 
         internal FrameworkElement? CreateElement(DFLayoutElement element)
         {
             var elementType = element.GetType();
-            if(_elements.Any(x => x.ElementType == elementType))
+            if(_elements.FirstOrDefault(x => x.ElementType == elementType) is DynamicFormElement el)
             {
-                return OnCreateElement?.Invoke(this, new DynamicFormCreateElementArgs(element));
+                return OnCreateElement?.Invoke(this, new DynamicFormCreateElementArgs(element, el.Caption));
             }
             return null;
         }
@@ -1467,6 +1502,17 @@ namespace InABox.DynamicGrid
             method.Invoke(this, new object[] { tuple.Item2 });
         }
 
+        private void ElementActionClick(Tuple<DynamicFormElementAction, CellRange> tuple)
+        {
+            var element = tuple.Item1.OnClick(tuple.Item1.Tag);
+            if(element is not null)
+            {
+                SetControlRange(element, tuple.Item2);
+                form.Elements.Add(element);
+                Render();
+            }
+        }
+
         private void SetControlRange(DFLayoutControl control, CellRange range)
         {
             var minRow = Math.Min(range.StartRow, range.EndRow);
@@ -1575,8 +1621,10 @@ namespace InABox.DynamicGrid
 
             var elements = CreateMenuItem("Add Object", cellRange, null);
 
-            var available = _elements.Where(x => x.AllowDuplicate || !form.Elements.Any(v => (v as DFLayoutElement)?.GetType() == x.ElementType)).ToArray();
-            var cats = available.Select(x => x.Category).Distinct().OrderBy(x => x);
+            var available = _elements.Where(x => x.Visible && (x.AllowDuplicate || !form.Elements.Any(v => (v as DFLayoutElement)?.GetType() == x.ElementType)))
+                .ToArray();
+
+            var cats = available.Select(x => x.Category).Concat(_elementActions.Select(x => x.Category)).Distinct().OrderBy(x => x);
             foreach (var cat in cats)
             {
                 var parentMenu = elements;
@@ -1586,8 +1634,16 @@ namespace InABox.DynamicGrid
                     elements.Items.Add(parentMenu);
                 }
 
+                foreach(var action in _elementActions.Where(x => x.Category == cat))
+                {
+                    parentMenu.AddItem(action.Caption, action.Image, new Tuple<DynamicFormElementAction, CellRange>(action, cellRange), ElementActionClick);
+                }
+
+                parentMenu.AddSeparatorIfNeeded();
+
                 foreach (var element in available.Where(x => string.Equals(x.Category, cat)))
                     parentMenu.AddItem(element.Caption, null, new Tuple<Type, CellRange>(element.ElementType, cellRange), AddElementClick);
+                parentMenu.RemoveUnnecessarySeparators();
             }
 
             if (elements.Items.Count > 0)

+ 1 - 1
inabox.wpf/DynamicGrid/Controls/DynamicContentDialog.xaml

@@ -5,7 +5,7 @@
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
         xmlns:local="clr-namespace:InABox.DynamicGrid"
         mc:Ignorable="d" Height="450" Width="800"
-        x:Name="Window">
+        x:Name="Window" WindowStartupLocation="CenterScreen">
     <DockPanel Margin="5">
         <DockPanel x:Name="Buttons" DockPanel.Dock="Bottom" LastChildFill="False">
             <Button x:Name="CancelButton" Click="CancelButton_Click"

+ 3 - 1
inabox.wpf/DynamicGrid/DynamicEditorForm/DynamicEditorGrid.xaml.cs

@@ -404,7 +404,7 @@ public partial class DynamicEditorGrid : UserControl, IDynamicEditorHost
             {
                 if (columns != null)
                     foreach (var (change, value) in columns)
-                        if (!changededitors.ContainsKey(change) && !change.Equals(sender.ColumnName))
+                        if (!changededitors.ContainsKey(change))
                             changededitors[change] = value;
             }
 
@@ -458,6 +458,8 @@ public partial class DynamicEditorGrid : UserControl, IDynamicEditorHost
             var afterchanged = EditorGrid.OnAfterEditorValueChanged?.Invoke(EditorGrid, new AfterEditorValueChangedArgs(sender.ColumnName, changededitors));
             ExtractChanged(afterchanged);
 
+            changededitors.Remove(sender.ColumnName);
+
             if (changededitors.Count != 0)
                 LoadEditorValues(changededitors);
 

+ 2 - 1
inabox.wpf/DynamicGrid/Editors/ColumnsEditor/ColumnsEditorControl.cs

@@ -160,7 +160,8 @@ public class ColumnsEditorControl : DynamicEditorControl<string, ColumnsEditor>
     protected override void UpdateValue(string value)
     {
         if (ColumnsType != null)
-            Columns = Serialization.Deserialize(typeof(Columns<>).MakeGenericType(ColumnsType), value) as IColumns;
+            Columns = Serialization.Deserialize(typeof(Columns<>).MakeGenericType(ColumnsType), value) as IColumns
+                ?? Core.Columns.Create(ColumnsType, ColumnTypeFlags.IncludeVisible);
         else
             Columns = null;
     }

+ 13 - 1
inabox.wpf/DynamicGrid/Grids/DynamicGrid.cs

@@ -967,6 +967,19 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
 
         return cols;
     }
+    DynamicGridColumns IDynamicGrid.ExtractColumns(IColumns columns)
+    {
+        var cols = new DynamicGridColumns();
+
+        foreach (var col in columns)
+        {
+            var mc = MasterColumns.FirstOrDefault(x => x.ColumnName.Equals(col.Property));
+            if (mc != null && mc.Editor is not NullEditor && mc.Editor.Visible != Visible.Hidden)
+                cols.Add(mc);
+        }
+
+        return cols;
+    }
 
     /// <summary>
     /// Provide a set of columns which is the default for this grid.
@@ -2572,7 +2585,6 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
             VisibleColumns.Clear();
             VisibleColumns.AddRange(editor.Columns);
             SaveColumns(editor.Columns);
-            //OnSaveColumns?.Invoke(this, editor.Columns);
             Refresh(true, true);
         }
     }

+ 2 - 0
inabox.wpf/DynamicGrid/Grids/IDynamicGrid.cs

@@ -103,6 +103,8 @@ public interface IDynamicGrid
     event GenerateColumnsEvent? OnGenerateColumns;
     event SaveColumnsEvent? OnSaveColumns;
 
+    DynamicGridColumns ExtractColumns(IColumns columns);
+
     void AddHiddenColumn(string column);
 
     void UpdateRow<TType>(CoreRow row, string column, TType value, bool refresh = true);

+ 74 - 0
inabox.wpf/FodyWeavers.xsd

@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
+  <xs:element name="Weavers">
+    <xs:complexType>
+      <xs:all>
+        <xs:element name="PropertyChanged" minOccurs="0" maxOccurs="1">
+          <xs:complexType>
+            <xs:attribute name="InjectOnPropertyNameChanged" type="xs:boolean">
+              <xs:annotation>
+                <xs:documentation>Used to control if the On_PropertyName_Changed feature is enabled.</xs:documentation>
+              </xs:annotation>
+            </xs:attribute>
+            <xs:attribute name="TriggerDependentProperties" type="xs:boolean">
+              <xs:annotation>
+                <xs:documentation>Used to control if the Dependent properties feature is enabled.</xs:documentation>
+              </xs:annotation>
+            </xs:attribute>
+            <xs:attribute name="EnableIsChangedProperty" type="xs:boolean">
+              <xs:annotation>
+                <xs:documentation>Used to control if the IsChanged property feature is enabled.</xs:documentation>
+              </xs:annotation>
+            </xs:attribute>
+            <xs:attribute name="EventInvokerNames" type="xs:string">
+              <xs:annotation>
+                <xs:documentation>Used to change the name of the method that fires the notify event. This is a string that accepts multiple values in a comma separated form.</xs:documentation>
+              </xs:annotation>
+            </xs:attribute>
+            <xs:attribute name="CheckForEquality" type="xs:boolean">
+              <xs:annotation>
+                <xs:documentation>Used to control if equality checks should be inserted. If false, equality checking will be disabled for the project.</xs:documentation>
+              </xs:annotation>
+            </xs:attribute>
+            <xs:attribute name="CheckForEqualityUsingBaseEquals" type="xs:boolean">
+              <xs:annotation>
+                <xs:documentation>Used to control if equality checks should use the Equals method resolved from the base class.</xs:documentation>
+              </xs:annotation>
+            </xs:attribute>
+            <xs:attribute name="UseStaticEqualsFromBase" type="xs:boolean">
+              <xs:annotation>
+                <xs:documentation>Used to control if equality checks should use the static Equals method resolved from the base class.</xs:documentation>
+              </xs:annotation>
+            </xs:attribute>
+            <xs:attribute name="SuppressWarnings" type="xs:boolean">
+              <xs:annotation>
+                <xs:documentation>Used to turn off build warnings from this weaver.</xs:documentation>
+              </xs:annotation>
+            </xs:attribute>
+            <xs:attribute name="SuppressOnPropertyNameChangedWarning" type="xs:boolean">
+              <xs:annotation>
+                <xs:documentation>Used to turn off build warnings about mismatched On_PropertyName_Changed methods.</xs:documentation>
+              </xs:annotation>
+            </xs:attribute>
+          </xs:complexType>
+        </xs:element>
+      </xs:all>
+      <xs:attribute name="VerifyAssembly" type="xs:boolean">
+        <xs:annotation>
+          <xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
+        </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="VerifyIgnoreCodes" type="xs:string">
+        <xs:annotation>
+          <xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
+        </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="GenerateXsd" type="xs:boolean">
+        <xs:annotation>
+          <xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
+        </xs:annotation>
+      </xs:attribute>
+    </xs:complexType>
+  </xs:element>
+</xs:schema>

+ 1 - 1
inabox.wpf/Panel/IPanel.cs

@@ -189,7 +189,7 @@ public interface ICorePanel
     /// <summary>
     /// Shutdown the panel.
     /// </summary>
-    /// <param name="cancel">If the operation can be cancelled, this is not <see langword="null"/></param>
+    /// <param name="cancel">If the operation can be cancelled, this is not <see langword="null"/>.</param>
     void Shutdown(CancelEventArgs? cancel);
 
     void Refresh();