瀏覽代碼

- Converted Dashboard editor to be a preview.
- Constrained dashboard grid columns to data list.
- Added context menu actions to customise dashboard.

Kenric Nugteren 5 月之前
父節點
當前提交
c52a519c73

+ 23 - 0
InABox.Core/Client/Client.cs

@@ -157,6 +157,24 @@ namespace InABox.Clients
             return ClientFactory.CreateClient<User>();
         }
 
+        public static Task<Dictionary<string, CoreTable>> QueryMultipleAsync(Dictionary<string, IQueryDef> queries)
+        {
+            return Task.Run(() =>
+            {
+                try
+                {
+                    using var timer = new Profiler(false);
+                    var result = CheckClient().QueryMultiple(queries);
+                    timer.Log(result.Sum(x => x.Value.Rows.Count));
+                    return result;
+                }
+                catch (RequestException e)
+                {
+                    ClientFactory.RaiseRequestError(e);
+                    throw;
+                }
+            });
+        }
         public static Dictionary<string, CoreTable> QueryMultiple(Dictionary<string, IQueryDef> queries)
         {
             try
@@ -328,6 +346,11 @@ namespace InABox.Clients
                 }
             }, queries.ToDictionary(x => x.Key, x => x as IQueryDef));
 
+        public static async Task<QueryMultipleResults> QueryMultipleAsync(IEnumerable<IKeyedQueryDef> queries)
+        {
+            return new QueryMultipleResults(await QueryMultipleAsync(queries.ToDictionary(x => x.Key, x => x as IQueryDef)));
+        }
+
         public static IValidationData Validate(Guid session)
         {
             try

+ 1 - 18
inabox.wpf/Dashboard/DynamicDashboard.cs

@@ -56,10 +56,9 @@ public class DynamicDashboard
 public static class DynamicDashboardUtils
 {
     private static Type[]? _presenterTypes;
-    private static Dictionary<Type, Type>? _presenterEditorTypes;
     private static bool _loaded;
 
-    [MemberNotNull(nameof(_presenterTypes), nameof(_presenterEditorTypes))]
+    [MemberNotNull(nameof(_presenterTypes))]
     private static void LoadTypes()
     {
         if(!_loaded)
@@ -72,13 +71,8 @@ public static class DynamicDashboardUtils
                 {
                     presenters.Add(entity);
                 }
-                else if(entity.GetInterfaceDefinition(typeof(IDynamicDashboardDataPresenterEditor<,>)) is Type editor)
-                {
-                    presenterEditors[editor.GenericTypeArguments[0]] = entity;
-                }
             }
             _presenterTypes = presenters.ToArray();
-            _presenterEditorTypes = presenterEditors;
             _loaded = true;
         }
     }
@@ -89,17 +83,6 @@ public static class DynamicDashboardUtils
         return _presenterTypes;
     }
 
-    public static bool TryGetPresenterEditor(Type presenterType, [NotNullWhen(true)] out Type? editor)
-    {
-        LoadTypes();
-        return _presenterEditorTypes.TryGetValue(presenterType, out editor);
-    }
-    public static Type? GetPresenterEditor(Type presenterType)
-    {
-        LoadTypes();
-        return _presenterEditorTypes.GetValueOrDefault(presenterType);
-    }
-
     private static JsonSerializerSettings SerializationSettings()
     {
         var settings = Serialization.CreateSerializerSettings();

+ 11 - 2
inabox.wpf/Dashboard/DynamicDashboardDataComponent.cs

@@ -73,12 +73,21 @@ public class DynamicDashboardDataComponent
         return query != null;
     }
 
-    public DynamicDashboardData RunQuery()
+    public DynamicDashboardData RunQuery(int? maxRecords = null)
     {
-        var queryDefs = Queries.Select(x => new KeyedQueryDef(x.Key, x.Type, x.Filter, x.Columns, x.SortOrder));
+        var range = maxRecords.HasValue ? CoreRange.Database(maxRecords.Value) : null;
+        var queryDefs = Queries.Select(x => new KeyedQueryDef(x.Key, x.Type, x.Filter, x.Columns, x.SortOrder, range));
         var results = Client.QueryMultiple(queryDefs);
         return new DynamicDashboardData(results.Results);
     }
+
+    public async Task<DynamicDashboardData> RunQueryAsync(int? maxRecords = null)
+    {
+        var range = maxRecords.HasValue ? CoreRange.Database(maxRecords.Value) : null;
+        var queryDefs = Queries.Select(x => new KeyedQueryDef(x.Key, x.Type, x.Filter, x.Columns, x.SortOrder, range));
+        var results = await Client.QueryMultipleAsync(queryDefs);
+        return new DynamicDashboardData(results.Results);
+    }
 }
 
 public class DynamicDashboardData(Dictionary<string, CoreTable> data)

+ 2 - 9
inabox.wpf/Dashboard/Editor/DynamicDashboardDataEditor.xaml.cs

@@ -82,15 +82,8 @@ public partial class DynamicDashboardDataEditor : UserControl, INotifyPropertyCh
         var dlg = new DynamicContentDialog(editor)
         {
             WindowStartupLocation = WindowStartupLocation.CenterScreen,
-            Title = "Select Dashboard Data"
-        };
-        editor.QueryEditor.OnChanged += (o, e) =>
-        {
-            dlg.CanSave = true;
-        };
-        editor.QueryGrid.OnChanged += (o, e) =>
-        {
-            dlg.CanSave = true;
+            Title = "Select Dashboard Data",
+            CanSave = true
         };
         if(dlg.ShowDialog() == true)
         {

+ 7 - 7
inabox.wpf/Dashboard/Editor/DynamicDashboardDataQueryGrid.cs

@@ -26,7 +26,7 @@ internal class DynamicDashboardDataQueryEditItem : BaseObject
     }
 
     [EditorSequence(2)]
-    public string Key { get; set; } = "";
+    public string Name { get; set; } = "";
 
     [FilterEditor]
     [EditorSequence(3)]
@@ -46,7 +46,7 @@ internal class DynamicDashboardDataQueryEditItem : BaseObject
 
     public DynamicDashboardDataQueryEditItem(IDynamicDashboardDataQuery query)
     {
-        Key = query.Key;
+        Name = query.Key;
         Type = query.Type;
         Filter = Serialization.Serialize(query.Filter);
         Columns = Serialization.Serialize(query.Columns);
@@ -77,9 +77,9 @@ internal class DynamicDashboardDataQueryEditItem : BaseObject
             Filter = "";
             Columns = "";
             SortOrder = "";
-            if (Type is not null && (Key.IsNullOrWhiteSpace() || Key == (before as Type)?.Name))
+            if (Type is not null && (Name.IsNullOrWhiteSpace() || Name == (before as Type)?.Name))
             {
-                Key = Type.Name;
+                Name = Type.Name;
             }
         }
     }
@@ -89,7 +89,7 @@ internal class DynamicDashboardDataQueryEditItem : BaseObject
         if(Type is not null)
         {
             var query = (Activator.CreateInstance(typeof(DynamicDashboardDataQuery<>).MakeGenericType(Type)) as IDynamicDashboardDataQuery)!;
-            query.Key = Key;
+            query.Key = Name;
             query.Filter = Serialization.Deserialize(typeof(Filter<>).MakeGenericType(Type), Filter) as IFilter;
             query.Columns = (Serialization.Deserialize(typeof(Columns<>).MakeGenericType(Type), Columns) as IColumns)
                 ?? Core.Columns.None(Type);
@@ -128,9 +128,9 @@ internal class DynamicDashboardDataQueryGrid : DynamicItemsListGrid<DynamicDashb
 
         foreach(var item in items)
         {
-            if(Items.Any(x => x != item && x.Key == item.Key))
+            if(Items.Any(x => x != item && x.Name == item.Name))
             {
-                errors.Add($"Duplicate key '{item.Key}'");
+                errors.Add($"Duplicate key '{item.Name}'");
             }
         }
     }

+ 55 - 17
inabox.wpf/Dashboard/Editor/DynamicDashboardEditor.xaml

@@ -8,29 +8,67 @@
              x:Name="Control">
     <Grid DataContext="{Binding ElementName=Control}">
         <Grid.RowDefinitions>
+            <!-- Top editor boxes -->
             <RowDefinition Height="Auto"/>
             <RowDefinition Height="Auto"/>
+            <RowDefinition Height="Auto"/>
+            
+            <!-- Separator Row -->
+            <RowDefinition Height="Auto"/>
+            
+            <!-- Presenter Editor -->
+            <RowDefinition Height="Auto"/>
             <RowDefinition Height="*"/>
         </Grid.RowDefinitions>
-        <DockPanel Grid.Row="0"
-                   LastChildFill="False">
-            <Label Content="Presentation Type:"
-                   VerticalAlignment="Center"/>
-            <ComboBox x:Name="PresentationType"
-                      ItemsSource="{Binding PresentationTypes}"
-                      SelectedValuePath="Item2"
-                      DisplayMemberPath="Item1"
-                      SelectedValue="{Binding SelectedPresentationType}"
-                      VerticalContentAlignment="Center"
-                      Padding="5" Height="30"
-                      MinWidth="100"/>
+        <Grid.ColumnDefinitions>
+            <ColumnDefinition Width="Auto"/>
+            <ColumnDefinition Width="*"/>
+            <ColumnDefinition Width="Auto"/>
+        </Grid.ColumnDefinitions>
+        
+        <Label Content="Name:" Grid.Row="0" Grid.Column="0"
+               Margin="0,0,5,0"
+               VerticalAlignment="Center"/>
+        <TextBox x:Name="NameTextBox" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2"
+                 Background="LightYellow"
+                 Height="25"
+                 VerticalContentAlignment="Center"
+                 Text="{Binding DashboardName}"/>
+        
+        <Label Content="Group:" Grid.Row="1" Grid.Column="0"
+               Margin="0,5,5,0"
+               VerticalAlignment="Center"/>
+        <TextBox x:Name="GroupTextBox" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"
+                 Background="LightYellow"
+                 Margin="0,5,0,0"
+                 Height="25"
+                 VerticalContentAlignment="Center"
+                 Text="{Binding DashboardGroup}"/>
+        
+        <Label Content="Type:" Grid.Row="2" Grid.Column="0"
+               Margin="0,5,5,0"
+               VerticalAlignment="Center"/>
+        <ComboBox x:Name="PresentationType"
+                  Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2"
+                  ItemsSource="{Binding PresentationTypes}"
+                  SelectedValuePath="Item2"
+                  DisplayMemberPath="Item1"
+                  SelectedValue="{Binding SelectedPresentationType}"
+                  VerticalContentAlignment="Center"
+                  Height="25"
+                  Margin="0,5,0,0"
+                  MinWidth="100"/>
+
+        <Separator Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3"
+                   Margin="5"/>
 
-            <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" Margin="0,5,0,0"
+                        Grid.Row="4" Grid.RowSpan="2" Grid.ColumnSpan="2"
+                        Margin="0,0,0,0"
                         MinHeight="100"/>
+        <Button x:Name="SelectData" Content="Data"
+                Grid.Row="4" Grid.Column="2"
+                Padding="5" Margin="5,0,0,0"
+                Click="SelectData_Click"/>
     </Grid>
 </UserControl>

+ 40 - 10
inabox.wpf/Dashboard/Editor/DynamicDashboardEditor.xaml.cs

@@ -23,6 +23,28 @@ namespace InABox.Wpf.Dashboard.Editor;
 /// </summary>
 public partial class DynamicDashboardEditor : UserControl, INotifyPropertyChanged
 {
+    private string _dashboardName = "";
+    public string DashboardName
+    {
+        get => _dashboardName;
+        set
+        {
+            _dashboardName = value;
+            OnPropertyChanged();
+        }
+    }
+
+    private string _dashboardGroup = "";
+    public string DashboardGroup
+    {
+        get => _dashboardGroup;
+        set
+        {
+            _dashboardGroup = value;
+            OnPropertyChanged();
+        }
+    }
+
     private Tuple<string, Type>[] _presentationTypes;
     public Tuple<string, Type>[] PresentationTypes
     {
@@ -55,7 +77,7 @@ public partial class DynamicDashboardEditor : UserControl, INotifyPropertyChange
     private DynamicDashboardDataComponent DataComponent;
 
     private object? _presenterProperties;
-    private IDynamicDashboardDataPresenterEditor? _presenterEditor;
+    private IDynamicDashboardDataPresenter? _presenter;
 
     public DynamicDashboardEditor(DynamicDashboard dashboard)
     {
@@ -78,23 +100,31 @@ public partial class DynamicDashboardEditor : UserControl, INotifyPropertyChange
     {
         _presenterProperties = properties;
 
-        if(DynamicDashboardUtils.TryGetPresenterEditor(presentationType, out var editorType))
-        {
-            _presenterEditor = (Activator.CreateInstance(editorType) as IDynamicDashboardDataPresenterEditor)!;
-            _presenterEditor.Properties = _presenterProperties;
-            _presenterEditor.DataComponent = DataComponent;
+        _presenter = (Activator.CreateInstance(presentationType) as IDynamicDashboardDataPresenter)!;
+        _presenter.Properties = _presenterProperties;
+        _presenter.DataComponent = DataComponent;
 
-            PresentationEditorControl.Content = _presenterEditor.Setup();
-        }
+        PresentationEditorControl.Content = _presenter.Setup();
     }
 
     private void SelectData_Click(object sender, RoutedEventArgs e)
     {
         if (DynamicDashboardDataEditor.Execute(DataComponent))
         {
-            if(_presenterEditor is not null)
+            if(_presenter is not null)
             {
-                _presenterEditor.DataComponent = DataComponent;
+                _presenter.DataComponent = DataComponent;
+                DataComponent.RunQueryAsync(100).ContinueWith(data =>
+                {
+                    if(data.Exception is not null)
+                    {
+                        MessageWindow.ShowError("Error loading data.", data.Exception);
+                    }
+                    else
+                    {
+                        _presenter.Refresh(data.Result);
+                    }
+                }, TaskScheduler.FromCurrentSynchronizationContext());
             }
         }
     }

+ 0 - 109
inabox.wpf/Dashboard/PresenterEditors/DynamicDashboardGridPresenterEditor.cs

@@ -1,109 +0,0 @@
-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>
-{
-    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()
-    {
-        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;
-    }
-}

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

@@ -1,43 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using System.Windows;
-
-namespace InABox.Wpf.Dashboard;
-
-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; }
-
-    object Properties { get; set; }
-
-    /// <summary>
-    /// Sets up the data presenter editor; returns the UI element to be rendered to the user.
-    /// </summary>
-    /// <returns>
-    /// <see langword="null"/> if some error occurred and the data cannot be presented; otherwise, returns the control.
-    /// </returns>
-    FrameworkElement? Setup();
-}
-
-public interface IDynamicDashboardDataPresenterEditor<TPresenter, TProperties> : IDynamicDashboardDataPresenterEditor
-    where TPresenter : IDynamicDashboardDataPresenter<TProperties>
-    where TProperties : class, new()
-{
-    /// <summary>
-    /// Component for the data of this presenter; can be safely assumed to be non-<see langword="null"/> when <see cref="Setup"/> is called.
-    /// </summary>
-    new TProperties Properties { get; set; }
-
-    object IDynamicDashboardDataPresenterEditor.Properties
-    {
-        get => Properties;
-        set => Properties = (value as TProperties)!;
-    }
-}

+ 150 - 8
inabox.wpf/Dashboard/Presenters/DynamicDashboardGridPresenter.cs

@@ -1,5 +1,7 @@
 using InABox.Core;
 using InABox.DynamicGrid;
+using InABox.Wpf.Dashboard.Editor;
+using InABox.WPF;
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
@@ -7,6 +9,8 @@ using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Media;
 
 namespace InABox.Wpf.Dashboard;
 
@@ -25,29 +29,136 @@ public class DynamicDashboardGridPresenter : IDynamicDashboardDataPresenter<Dyna
 {
     public DynamicDashboardGridPresenterProperties Properties { get; set; } = null!;
 
-    public DynamicDashboardDataComponent DataComponent { get; set; } = null!;
+    private DynamicDashboardDataComponent _dataComponent = null!;
+    public DynamicDashboardDataComponent DataComponent
+    {
+        get => _dataComponent;
+        set
+        {
+            _dataComponent = value;
+            UpdateData();
+        }
+    }
 
-    private IDynamicItemsListGrid Grid = null!;
+    private IDynamicDashboardGridPresenterGrid? Grid;
+    private ContentControl Content = new()
+    {
+        Height = 300
+    };
+
+    private DynamicDashboardData? _data;
 
     public FrameworkElement? Setup()
     {
-        if (!DataComponent.TryGetQuery(Properties.Query, out var query))
+        UpdateData();
+        return Content;
+    }
+
+    private void UpdateData()
+    {
+        if (Properties.Query.IsNullOrWhiteSpace() || !DataComponent.TryGetQuery(Properties.Query, out var query))
+        {
+            Properties.Query = DataComponent.Queries.FirstOrDefault()?.Key ?? "";
+        }
+
+        if (!DataComponent.TryGetQuery(Properties.Query, out query))
         {
-            return null;
+            var border = new Border
+            {
+                Background = Colors.DimGray.ToBrush()
+            };
+            border.ContextMenuOpening += Border_ContextMenuOpening;
+            Content.Content = border;
+            return;
         }
 
-        Grid = (Activator.CreateInstance(typeof(DynamicItemsListGrid<>).MakeGenericType(query.Type)) as IDynamicItemsListGrid)!;
+        Grid = (Activator.CreateInstance(typeof(DynamicDashboardGridPresenterGrid<>).MakeGenericType(query.Type)) as IDynamicDashboardGridPresenterGrid)!;
         Grid.OnGenerateColumns += Grid_OnGenerateColumns;
         Grid.OnSaveColumns += Grid_OnSaveColumns;
+        Grid.GetAvailableColumns += Grid_GetAvailableColumns;
+        Grid.OnLoadColumnsMenu += Grid_OnLoadColumnsMenu;
 
         Grid.Refresh(true, false);
 
-        return Grid as FrameworkElement;
+        Content.Content = Grid as FrameworkElement;
+    }
+
+    private void CustomiseMenu(ContextMenu menu)
+    {
+        if(DataComponent.Queries.Count > 1)
+        {
+            var queryItem = menu.AddItem("Select Query", null, null);
+            foreach(var query in DataComponent.Queries)
+            {
+                queryItem.AddCheckMenuItem(query.Key, query.Key, SelectQuery_Click, isChecked: Properties.Query == query.Key);
+            }
+        }
+        menu.AddItem("Select Data", null, SelectData_Click);
+    }
+
+    private void SelectData_Click()
+    {
+        if (DynamicDashboardDataEditor.Execute(DataComponent))
+        {
+            UpdateData();
+            DataComponent.RunQueryAsync(100).ContinueWith(data =>
+            {
+                if(data.Exception is not null)
+                {
+                    MessageWindow.ShowError("Error loading data.", data.Exception);
+                }
+                else
+                {
+                    Refresh(data.Result);
+                }
+            }, TaskScheduler.FromCurrentSynchronizationContext());
+        }
+    }
+
+    private void SelectQuery_Click(string key, bool isChecked)
+    {
+        if (!isChecked) return;
+
+        Properties.Query = key;
+        Properties.Columns = null;
+
+        UpdateData();
+        if(_data is not null)
+        {
+            Refresh(_data);
+        }
+    }
+
+    private void Grid_OnLoadColumnsMenu(ContextMenu menu)
+    {
+        menu.AddSeparatorIfNeeded();
+        CustomiseMenu(menu);
+        menu.RemoveUnnecessarySeparators();
+    }
+
+    private void Grid_GetAvailableColumns(GetAvailableColumnsEventArgs args)
+    {
+        if (!DataComponent.TryGetQuery(Properties.Query, out var query))
+        {
+            return;
+        }
+        args.Columns = args.Columns.Where(x => query.Columns.Contains(x.ColumnName));
+    }
+
+    private void Border_ContextMenuOpening(object sender, ContextMenuEventArgs e)
+    {
+        var menu = new ContextMenu();
+
+        CustomiseMenu(menu);
+
+        menu.IsOpen = true;
+
+        e.Handled = true;
     }
 
     private void Grid_OnSaveColumns(object sender, SaveColumnsEventArgs args)
     {
-        Properties.Columns = args.Columns;
+        Properties.Columns = args.Columns.Count > 0 ? args.Columns : null;
     }
 
     private void Grid_OnGenerateColumns(object sender, GenerateColumnsEventArgs args)
@@ -60,11 +171,18 @@ public class DynamicDashboardGridPresenter : IDynamicDashboardDataPresenter<Dyna
                 args.Columns.Add(column.Copy());
             }
         }
+        else if (DataComponent.TryGetQuery(Properties.Query, out var query) && Grid is not null)
+        {
+            args.Columns.Clear();
+            args.Columns.AddRange(Grid.ExtractColumns(query.Columns));
+        }
     }
 
     public void Refresh(DynamicDashboardData data)
     {
-        if (!DataComponent.TryGetQuery(Properties.Query, out var query))
+        _data = data;
+
+        if (!DataComponent.TryGetQuery(Properties.Query, out var query) || Grid is null)
         {
             return;
         }
@@ -88,3 +206,27 @@ public class DynamicDashboardGridPresenter : IDynamicDashboardDataPresenter<Dyna
     {
     }
 }
+
+internal interface IDynamicDashboardGridPresenterGrid : IDynamicItemsListGrid
+{
+    public event Action<ContextMenu>? OnLoadColumnsMenu;
+}
+internal class DynamicDashboardGridPresenterGrid<T> : DynamicItemsListGrid<T>, IDynamicDashboardGridPresenterGrid
+    where T : BaseObject, new()
+{
+    public event Action<ContextMenu>? OnLoadColumnsMenu;
+
+    protected override void DoReconfigure(DynamicGridOptions options)
+    {
+        base.DoReconfigure(options);
+
+        options.Clear();
+        options.SelectColumns = true;
+    }
+
+    protected override void LoadColumnsMenu(ContextMenu menu)
+    {
+        base.LoadColumnsMenu(menu);
+        OnLoadColumnsMenu?.Invoke(menu);
+    }
+}

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

@@ -15,6 +15,7 @@ public interface IDynamicDashboardDataPresenter
 
     /// <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, so the setter should be used to manage data refreshes.
     /// </summary>
     DynamicDashboardDataComponent DataComponent { get; set; }
 

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

@@ -8,6 +8,8 @@ namespace InABox.DynamicGrid;
 
 public class DynamicColumnGrid : DynamicGrid<DynamicGridColumn>
 {
+    public event GetAvailableColumnsEvent? OnProcessColumns;
+
     public DynamicColumnGrid()
     {
         Columns = new DynamicGridColumns();
@@ -147,7 +149,10 @@ public class DynamicColumnGrid : DynamicGrid<DynamicGridColumn>
         }
         result.Sort((a, b) => a.ColumnName.CompareTo(b.ColumnName));
 
-        return result;
+        var args = new GetAvailableColumnsEventArgs(result);
+        OnProcessColumns?.Invoke(args);
+
+        return args.Columns;
     }
 
     protected override void DefineLookups(ILookupEditorControl sender, DynamicGridColumn[] items, bool async = true)

+ 6 - 0
inabox.wpf/DynamicGrid/DynamicGridColumn/DynamicGridColumnsEditor.xaml.cs

@@ -14,6 +14,12 @@ public partial class DynamicGridColumnsEditor : ThemableWindow
 {
     private readonly DynamicColumnGrid ColumnGrid;
 
+    public event GetAvailableColumnsEvent? GetAvailableColumns
+    {
+        add => ColumnGrid.OnProcessColumns += value;
+        remove => ColumnGrid.OnProcessColumns -= value;
+    }
+
     public DynamicGridColumnsEditor(Type type)
     {
         InitializeComponent();

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

@@ -512,4 +512,10 @@ public class SaveColumnsEventArgs
 		Columns = columns;
 	}
 }
-public delegate void SaveColumnsEvent(object sender, SaveColumnsEventArgs args);
+public delegate void SaveColumnsEvent(object sender, SaveColumnsEventArgs args);
+
+public class GetAvailableColumnsEventArgs(IEnumerable<DynamicGridColumn> columns)
+{
+    public IEnumerable<DynamicGridColumn> Columns { get; set; } = columns;
+}
+public delegate void GetAvailableColumnsEvent(GetAvailableColumnsEventArgs args);

+ 5 - 0
inabox.wpf/DynamicGrid/Grids/DynamicGrid.cs

@@ -997,6 +997,7 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
     
     public event GenerateColumnsEvent? OnGenerateColumns;
     public event SaveColumnsEvent? OnSaveColumns;
+    public event GetAvailableColumnsEvent? GetAvailableColumns;
 
     protected virtual void SaveColumns(DynamicGridColumns columns)
     {
@@ -2579,6 +2580,10 @@ public abstract class DynamicGrid<T> : DynamicGrid, IDynamicGridUIComponentParen
         editor.DirectEdit = IsDirectEditMode();
 
         editor.Columns.AddRange(VisibleColumns);
+        editor.GetAvailableColumns += args =>
+        {
+            GetAvailableColumns?.Invoke(args);
+        };
 
         if (editor.ShowDialog().Equals(true))
         {

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

@@ -102,6 +102,7 @@ public interface IDynamicGrid
     
     event GenerateColumnsEvent? OnGenerateColumns;
     event SaveColumnsEvent? OnSaveColumns;
+    event GetAvailableColumnsEvent? GetAvailableColumns;
 
     DynamicGridColumns ExtractColumns(IColumns columns);
 

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

@@ -343,12 +343,12 @@ public interface IPanelHost
 
 public static class IPanelHostExtensions
 {
-    public static void CreateSetupAction(this IPanelHost host, string caption, Bitmap image, Action<PanelAction> onExecute, ContextMenu? menu = null)
+    public static void CreateSetupAction(this IPanelHost host, string caption, Bitmap? image, Action<PanelAction> onExecute, ContextMenu? menu = null)
     {
         host.CreateSetupAction(new PanelAction(caption, image, onExecute) { Menu = menu });
     }
 
-    public static void CreateSetupActionIf(this IPanelHost host, string caption, Bitmap image, Action<PanelAction> onExecute, bool canView, ContextMenu? menu = null)
+    public static void CreateSetupActionIf(this IPanelHost host, string caption, Bitmap? image, Action<PanelAction> onExecute, bool canView, ContextMenu? menu = null)
     {
         if (canView)
         {
@@ -356,13 +356,13 @@ public static class IPanelHostExtensions
         }
     }
 
-    public static void CreateSetupActionIf<TSecurity>(this IPanelHost host, string caption, Bitmap image, Action<PanelAction> onExecute, ContextMenu? menu = null)
+    public static void CreateSetupActionIf<TSecurity>(this IPanelHost host, string caption, Bitmap? image, Action<PanelAction> onExecute, ContextMenu? menu = null)
         where TSecurity : ISecurityDescriptor, new()
     {
         host.CreateSetupActionIf(caption, image, onExecute, Security.IsAllowed<TSecurity>(), menu);
     }
 
-    public static void CreateSetupActionIfCanView<T>(this IPanelHost host, string caption, Bitmap image, Action<PanelAction> onExecute, ContextMenu? menu = null)
+    public static void CreateSetupActionIfCanView<T>(this IPanelHost host, string caption, Bitmap? image, Action<PanelAction> onExecute, ContextMenu? menu = null)
         where T : Entity, new()
     {
         host.CreateSetupActionIf(caption, image, onExecute, Security.CanView<T>(), menu);