Add user layout management to grids with toolbar actions

- Introduced separate auto-save and user-save layout storage keys
- Added IMgGridBase methods for saving, loading, resetting, and checking user layouts
- Updated grid toolbar with "Layout" menu (load, save, reset) and new icons
- Improved layout persistence logic and default layout restore
- Enabled forced grid re-render on layout reset
- Adjusted grid pager and page size defaults
- Updated related components to use new storage keys
- Fixed minor bugs and set RELEASE log level to Debug
This commit is contained in:
Loretta 2025-12-23 11:10:19 +01:00
parent 4dbeb507fe
commit c468583afd
3 changed files with 159 additions and 8 deletions

View File

@ -61,12 +61,36 @@ public interface IMgGridBase : IGrid
/// Whether the grid/wrapper is currently in fullscreen mode
/// </summary>
bool IsFullscreen { get; }
string LayoutStorageKey { get; }
/// <summary>
/// Storage key for automatic layout persistence
/// </summary>
string AutomaticLayoutStorageKey { get; }
/// <summary>
/// Toggles fullscreen mode for the grid (or wrapper if available)
/// </summary>
void ToggleFullscreen();
/// <summary>
/// Saves the current layout to user storage (manual save)
/// </summary>
Task SaveUserLayoutAsync();
/// <summary>
/// Loads layout from user storage (manual load)
/// </summary>
Task LoadUserLayoutAsync();
/// <summary>
/// Resets the layout by clearing auto-saved layout and reloading the page
/// </summary>
Task ResetLayoutAsync();
/// <summary>
/// Checks if a user-saved layout exists without loading it
/// </summary>
Task<bool> HasUserLayoutAsync();
}
public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClient> : DxGrid, IMgGridBase, IAsyncDisposable
@ -81,6 +105,7 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
protected bool IsFirstInitializeParameters;
protected bool IsFirstInitializeParameterCore;
private bool _isDisposed;
private Guid _gridRenderKey = Guid.NewGuid();
private TSignalRDataSource? _dataSource = null!;
private AcObservableCollection<TDataItem>? _dataSourceParam = [];
@ -107,7 +132,12 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
return current;
}
public string LayoutStorageKey
/// <summary>
/// Gets the user layout storage key (replaces AutoSave with UserSave)
/// </summary>
private string UserLayoutStorageKey => AutomaticLayoutStorageKey.Replace("_AutoSave_", "_UserSave_");
public string AutomaticLayoutStorageKey
{
get
{
@ -184,6 +214,7 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
// Standalone fullscreen mode - Bootstrap 5 fullscreen overlay
contentBuilder.OpenElement(0, "div");
contentBuilder.AddAttribute(1, "class", "mg-fullscreen-overlay");
contentBuilder.SetKey(_gridRenderKey);
// Header
contentBuilder.OpenElement(2, "div");
@ -217,7 +248,12 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
}
else
{
// Normal mode - use key for forced re-render on reset
contentBuilder.OpenElement(0, "div");
contentBuilder.SetKey(_gridRenderKey);
contentBuilder.AddAttribute(1, "style", "display: contents;");
base.BuildRenderTree(contentBuilder);
contentBuilder.CloseElement();
}
}));
builder.CloseComponent();
@ -709,21 +745,46 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
/// </summary>
protected virtual int GetLayoutUserId() => 0;
/// <summary>
/// Stores the default layout (before any saved layout is loaded) for reset functionality
/// </summary>
private string? _defaultLayoutJson = null;
/// <summary>
/// Checks if a layout exists in localStorage without loading its content
/// </summary>
protected virtual async Task<string?> GetStorageItem(string localStorageKey)
{
try
{
return await JSRuntime.InvokeAsync<string>("localStorage.getItem", localStorageKey);
}
catch
{
// Mute exceptions for the server prerender stage
}
return null;
}
private async Task Grid_LayoutAutoLoading(GridPersistentLayoutEventArgs e)
{
e.Layout = await LoadLayoutFromLocalStorageAsync(LayoutStorageKey);
// Save the default layout before loading any saved layout
_defaultLayoutJson ??= JsonSerializer.Serialize(SaveLayout());
e.Layout = await LoadLayoutFromLocalStorageAsync(AutomaticLayoutStorageKey);
}
private async Task Grid_LayoutAutoSaving(GridPersistentLayoutEventArgs e)
{
await SaveLayoutToLocalStorageAsync(e.Layout, LayoutStorageKey);
await SaveLayoutToLocalStorageAsync(e.Layout, AutomaticLayoutStorageKey);
}
protected virtual async Task<GridPersistentLayout?> LoadLayoutFromLocalStorageAsync(string localStorageKey)
{
try
{
var json = await JSRuntime.InvokeAsync<string>("localStorage.getItem", localStorageKey);
var json = await GetStorageItem(localStorageKey);
if (!string.IsNullOrWhiteSpace(json))
return JsonSerializer.Deserialize<GridPersistentLayout>(json);
@ -749,10 +810,75 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
}
}
protected virtual async Task RemoveLayoutFromLocalStorageAsync(string localStorageKey)
{
try
{
await JSRuntime.InvokeVoidAsync("localStorage.removeItem", localStorageKey);
}
catch
{
// Mute exceptions for the server prerender stage
}
}
/// <inheritdoc />
public async Task SaveUserLayoutAsync()
{
var layout = SaveLayout();
await SaveLayoutToLocalStorageAsync(layout, UserLayoutStorageKey);
await SaveLayoutToLocalStorageAsync(layout, AutomaticLayoutStorageKey);
}
/// <inheritdoc />
public async Task LoadUserLayoutAsync()
{
var layout = await LoadLayoutFromLocalStorageAsync(UserLayoutStorageKey);
if (layout != null)
{
LoadLayout(layout);
}
}
/// <inheritdoc />
public async Task ResetLayoutAsync()
{
await RemoveLayoutFromLocalStorageAsync(AutomaticLayoutStorageKey);
// Restore the default layout if available
if (!string.IsNullOrWhiteSpace(_defaultLayoutJson))
{
var defaultLayout = JsonSerializer.Deserialize<GridPersistentLayout>(_defaultLayoutJson);
if (defaultLayout != null)
LoadLayout(defaultLayout);
}
}
/// <inheritdoc />
public async Task<bool> HasUserLayoutAsync()
{
return !(await GetStorageItem(UserLayoutStorageKey)).IsNullOrWhiteSpace();
}
#endregion
//public Task AddDataItem(TDataItem dataItem, int messageTag) => PostDataToServerAsync(dataItem, messageTag, TrackingState.Add);
/// <summary>
/// Force grid re-initialization
/// </summary>
/// <returns></returns>
public async Task ForceRenderAsync()
{
// Force grid re-initialization by changing the render key
_gridRenderKey = Guid.NewGuid();
await InvokeAsync(StateHasChanged);
}
public Task UpdateDataItem(TDataItem dataItem) => _dataSource.Update(dataItem, true);
public Task UpdateDataItemAsync(TDataItem dataItem)

View File

@ -14,6 +14,13 @@
@if (!OnlyGridEditTools)
{
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Column Chooser")" BeginGroup="true" Click="ColumnChooserItem_Click" IconCssClass="grid-column-chooser" />
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Layout")" IconCssClass="grid-layout">
<Items>
<DxToolbarItem Text="Load Layout" Click="LoadLayout_Click" IconCssClass="grid-layout-load" Enabled="@_hasUserLayout" />
<DxToolbarItem Text="Save Layout" Click="SaveLayout_Click" IconCssClass="grid-layout-save" />
<DxToolbarItem BeginGroup="true" Text="Reset Layout" Click="ResetLayout_Click" IconCssClass="grid-layout-reset" />
</Items>
</DxToolbarItem>
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Export")" IconCssClass="grid-export" Visible="false" Enabled="@(HasFocusedRow && !IsEditing)">
<Items>
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "To CSV")" Click="ExportCsvItem_Click" IconCssClass="grid-export-xlsx" />
@ -28,7 +35,7 @@
}
</MgGridToolbarBase>
@code {
@code {
[Parameter] public bool OnlyGridEditTools { get; set; } = false;
[Parameter] public IMgGridBase Grid { get; set; } = null!;
[Parameter] public RenderFragment? ToolbarItemsExtended { get; set; }
@ -38,6 +45,7 @@
public MgGridToolbarBase GridToolbar { get; set; } = null!;
const string ExportFileName = "ExportResult";
private bool _hasUserLayout;
private bool _isReloadInProgress;
/// <summary>
@ -70,8 +78,9 @@
/// </summary>
private string FullscreenIconCssClass => IsFullscreenMode ? "grid-fullscreen-exit" : "grid-fullscreen";
protected override void OnInitialized()
protected override async Task OnInitializedAsync()
{
_hasUserLayout = await Grid.HasUserLayoutAsync();
}
async Task ReloadData_Click(ToolbarItemClickEventArgs e)
@ -151,4 +160,20 @@
{
await Grid.ExportToPdfAsync(ExportFileName);
}
async Task LoadLayout_Click()
{
await Grid.LoadUserLayoutAsync();
}
async Task SaveLayout_Click()
{
await Grid.SaveUserLayoutAsync();
_hasUserLayout = true;
}
async Task ResetLayout_Click()
{
await Grid.ResetLayoutAsync();
}
}

View File

@ -116,7 +116,7 @@
}
private string GetStorageKey() => _currentGrid != null
? $"Splitter_{_currentGrid.LayoutStorageKey}"
? $"Splitter_{_currentGrid.AutomaticLayoutStorageKey}"
: null!;
private async Task LoadSavedSizeAsync()