Refactor grid toolbar and InfoPanel for reusability

Introduce reusable MgGridToolbarBase and MgGridToolbarTemplate components, replacing old toolbar implementations. InfoPanel now displays grid captions and includes a toolbar for quick actions. Refactor InfoPanel value rendering for improved DevExpress-like appearance. Update IMgGridBase and related classes to support captions and use new abstractions. Update usings and project structure accordingly.
This commit is contained in:
Loretta 2025-12-17 18:31:59 +01:00
parent 90f12a160e
commit fe1a59a0bd
6 changed files with 218 additions and 83 deletions

View File

@ -22,6 +22,7 @@ public interface IMgGridBase : IGrid
/// </summary>
bool IsSyncing { get; }
string Caption { get; set; }
/// <summary>
/// Current edit state of the grid (None, New, Edit)
/// </summary>
@ -254,6 +255,8 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
[Parameter] public string? KeyFieldNameToParentId { get; set; }
[Parameter] public object[]? ContextIds { get; set; }
[Parameter] public string Caption { get; set; } = typeof(TDataItem).Name;
public bool IsMasterGrid => ParentDataItem == null;
protected PropertyInfo? KeyFieldPropertyInfoToParent;

View File

@ -5,9 +5,17 @@
<div @ref="_panelElement" class="mg-grid-info-panel @(_isEditMode ? "edit-mode" : "view-mode")">
@* Header *@
<div class="dxbl-grid-header-panel px-3 py-2 border-bottom">
<span class="fw-semibold">Sor részletei</span>
<span class="fw-semibold">@((_currentGrid as IMgGridBase)?.Caption ?? "")</span>
</div>
@* Toolbar *@
@if (_currentGrid != null)
{
<div class="mg-info-panel-toolbar border-bottom">
<MgGridToolbarTemplate Grid="_currentGrid as IMgGridBase" OnlyGridEditTools="true" ShowOnlyIcon="true" />
</div>
}
@* Content *@
<div class="mg-info-panel-content">
@if (GetActiveDataItem() != null && _currentGrid != null)
@ -446,87 +454,37 @@
{
var seq = 0;
switch (settingsType)
// View mode: simple text display with DevExpress-like styling (no editors)
builder.OpenElement(seq++, "div");
builder.AddAttribute(seq++, "class", GetViewModeCssClass(value, settingsType));
builder.AddAttribute(seq++, "title", displayText);
// Special handling for boolean - show checkbox icon
if (value is bool boolValue)
{
case EditSettingsType.ComboBox:
builder.OpenElement(seq++, "div");
builder.AddAttribute(seq++, "title", displayText);
builder.AddAttribute(seq++, "class", "info-panel-text-wrapper");
builder.OpenComponent<DxTextBox>(seq++);
builder.AddAttribute(seq++, "Text", displayText);
builder.AddAttribute(seq++, "ReadOnly", true);
builder.CloseComponent();
builder.CloseElement();
return;
case EditSettingsType.CheckBox when value is bool boolVal:
builder.OpenComponent<DxCheckBox<bool>>(seq++);
builder.AddAttribute(seq++, "Checked", boolVal);
builder.AddAttribute(seq++, "ReadOnly", true);
builder.CloseComponent();
return;
case EditSettingsType.Memo:
builder.OpenElement(seq++, "div");
builder.AddAttribute(seq++, "title", displayText);
builder.OpenComponent<DxMemo>(seq++);
builder.AddAttribute(seq++, "Text", displayText);
builder.AddAttribute(seq++, "ReadOnly", true);
builder.AddAttribute(seq++, "Rows", 3);
builder.CloseComponent();
builder.CloseElement();
return;
builder.OpenElement(seq++, "span");
builder.AddAttribute(seq++, "class", boolValue ? "dx-icon dx-icon-check text-success" : "dx-icon dx-icon-close text-muted");
builder.CloseElement();
}
// Default by value type
switch (value)
else
{
case bool boolValue:
builder.OpenComponent<DxCheckBox<bool>>(seq++);
builder.AddAttribute(seq++, "Checked", boolValue);
builder.AddAttribute(seq++, "ReadOnly", true);
builder.CloseComponent();
break;
case DateTime dateValue:
builder.OpenComponent<DxDateEdit<DateTime>>(seq++);
builder.AddAttribute(seq++, "Date", dateValue);
builder.AddAttribute(seq++, "ReadOnly", true);
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat ?? "yyyy-MM-dd HH:mm");
builder.CloseComponent();
break;
case DateOnly dateOnlyValue:
builder.OpenComponent<DxDateEdit<DateOnly>>(seq++);
builder.AddAttribute(seq++, "Date", dateOnlyValue);
builder.AddAttribute(seq++, "ReadOnly", true);
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat ?? "yyyy-MM-dd");
builder.CloseComponent();
break;
case decimal or double or float or int or long or short:
builder.OpenElement(seq++, "div");
builder.AddAttribute(seq++, "title", displayText);
builder.AddAttribute(seq++, "class", "info-panel-text-wrapper");
builder.OpenComponent<DxTextBox>(seq++);
builder.AddAttribute(seq++, "Text", displayText);
builder.AddAttribute(seq++, "ReadOnly", true);
builder.AddAttribute(seq++, "CssClass", "text-end");
builder.CloseComponent();
builder.CloseElement();
break;
default:
builder.OpenElement(seq++, "div");
builder.AddAttribute(seq++, "title", displayText);
builder.AddAttribute(seq++, "class", "info-panel-text-wrapper");
builder.OpenComponent<DxTextBox>(seq++);
builder.AddAttribute(seq++, "Text", displayText);
builder.AddAttribute(seq++, "ReadOnly", true);
builder.CloseComponent();
builder.CloseElement();
break;
builder.AddContent(seq++, displayText);
}
builder.CloseElement();
};
}
private static string GetViewModeCssClass(object? value, EditSettingsType settingsType)
{
var baseCss = "mg-info-panel-value";
return value switch
{
bool => $"{baseCss} mg-info-panel-value-bool",
decimal or double or float or int or long or short => $"{baseCss} mg-info-panel-value-numeric",
DateTime or DateOnly or TimeOnly => $"{baseCss} mg-info-panel-value-date",
_ => baseCss
};
}
}

View File

@ -11,7 +11,7 @@ public interface IInfoPanelBase
{
void ClearEditMode();
void SetEditMode(object editModel);
void RefreshData(DxGrid grid, object? dataItem, int visibleIndex = -1);
void RefreshData(IMgGridBase grid, object? dataItem, int visibleIndex = -1);
}
/// <summary>
@ -30,7 +30,7 @@ public partial class MgGridInfoPanel : ComponentBase, IAsyncDisposable, IInfoPan
private bool _isJsInitialized;
private const int DefaultTopOffset = 300; // Increased from 180 to account for header + tabs + toolbar
private DxGrid? _currentGrid;
private IMgGridBase? _currentGrid;
private object? _currentDataItem;
private int _focusedRowVisibleIndex = -1;
private List<DxGridDataColumn> _allDataColumns = [];
@ -69,7 +69,7 @@ public partial class MgGridInfoPanel : ComponentBase, IAsyncDisposable, IInfoPan
/// <summary>
/// Refreshes the InfoPanel with data from the specified grid row (view mode)
/// </summary>
public void RefreshData(DxGrid grid, object? dataItem, int visibleIndex = -1)
public void RefreshData(IMgGridBase grid, object? dataItem, int visibleIndex = -1)
{
ArgumentNullException.ThrowIfNull(grid);
@ -290,12 +290,12 @@ public partial class MgGridInfoPanel : ComponentBase, IAsyncDisposable, IInfoPan
decimal decValue => decValue.ToString("N2"),
double dblValue => dblValue.ToString("N2"),
float fltValue => fltValue.ToString("N2"),
int or long or short or byte => string.Format("{0:N0}", value),
int or long or short or byte => $"{value:N0}",
_ => value.ToString() ?? string.Empty
};
}
private static List<DxGridDataColumn> GetAllDataColumns(DxGrid grid)
private static List<DxGridDataColumn> GetAllDataColumns(IMgGridBase grid)
{
var columns = new List<DxGridDataColumn>();

View File

@ -105,3 +105,31 @@
overflow: hidden;
white-space: nowrap;
}
/* View mode value styling - matches DevExpress theme */
.mg-info-panel-value {
padding: 0.375rem 0.75rem;
min-height: 2rem;
display: flex;
align-items: center;
justify-content: flex-start; /* Always align left */
background-color: var(--dxbl-bg, #fff);
border: 1px solid var(--dxbl-border-color, #dee2e6);
border-radius: var(--dxbl-border-radius, 0.25rem);
font-size: var(--dxbl-font-size, 0.875rem);
color: var(--dxbl-text, #212529);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mg-info-panel-value-numeric {
font-variant-numeric: tabular-nums;
}
.mg-info-panel-value-bool {
/* Keep left aligned */
}
.mg-info-panel-value-date {
font-variant-numeric: tabular-nums;
}

View File

@ -0,0 +1,12 @@
using DevExpress.Blazor;
using Microsoft.AspNetCore.Components;
namespace AyCode.Blazor.Components.Components.Grids
{
public class MgGridToolbarBase : DxToolbar
{
[Parameter] public IMgGridBase Grid { get; set; }
[Parameter] public Func<ToolbarItemClickEventArgs, Task> RefreshClick { get; set; }
[Parameter] public bool ShowOnlyIcon { get; set; } = false;
}
}

View File

@ -0,0 +1,134 @@
@using AyCode.Blazor.Components.Components.Grids
<MgGridToolbarBase @ref="GridToolbar" Grid="Grid" ItemRenderStyleMode="ToolbarRenderStyleMode.Plain" ShowOnlyIcon="ShowOnlyIcon">
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "New")" Click="NewItem_Click" IconCssClass="grid-new-row" Visible="@(!IsEditing)" Enabled="@(!IsSyncing)" />
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Edit")" Click="EditItem_Click" IconCssClass="grid-edit-row" Visible="@(!IsEditing)" Enabled="@(HasFocusedRow && !IsSyncing)" />
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Delete")" Click="DeleteItem_Click" IconCssClass="grid-delete-row" Visible="@(!IsEditing)" Enabled="@(false && HasFocusedRow && !IsSyncing)" />
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Save")" Click="SaveItem_Click" IconCssClass="grid-save" Visible="@IsEditing" RenderStyle="ButtonRenderStyle.Primary" />
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Cancel")" Click="CancelEdit_Click" IconCssClass="grid-cancel" Visible="@IsEditing" RenderStyle="ButtonRenderStyle.Secondary" />
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Prev Row")" BeginGroup="true" Click="PrevRow_Click" IconCssClass="grid-chevron-up" Enabled="@(HasFocusedRow && !IsSyncing && !IsEditing)" />
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Next Row")" Click="NextRow_Click" IconCssClass="grid-chevron-down" Enabled="@(HasFocusedRow && !IsSyncing && !IsEditing)" />
@if (!OnlyGridEditTools)
{
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Column Chooser")" BeginGroup="true" Click="ColumnChooserItem_Click" IconCssClass="grid-column-chooser" />
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Export")" IconCssClass="grid-export" Enabled="@(false && HasFocusedRow && !IsEditing)">
<Items>
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "To CSV")" Click="ExportCsvItem_Click" IconCssClass="grid-export-xlsx" />
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "To XLSX")" Click="ExportXlsxItem_Click" IconCssClass="grid-export-xlsx" />
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "To XLS")" Click="ExportXlsItem_Click" IconCssClass="grid-export-xlsx" />
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "To PDF")" Click="ExportPdfItem_Click" IconCssClass="grid-export-pdf" />
</Items>
</DxToolbarItem>
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Reload data")" BeginGroup="true" Click="ReloadData_Click" IconCssClass="grid-refresh" Enabled="@(!IsSyncing && !_isReloadInProgress && !IsEditing)" />
<DxToolbarItem BeginGroup="true"></DxToolbarItem>
@ToolbarItemsExtended
}
</MgGridToolbarBase>
@code {
[Parameter] public bool OnlyGridEditTools { get; set; } = false;
[Parameter] public IMgGridBase Grid { get; set; } = null!;
[Parameter] public RenderFragment? ToolbarItemsExtended { get; set; }
[Parameter] public EventCallback<ToolbarItemClickEventArgs> OnReloadDataClick { get; set; }
[Parameter] public bool ShowOnlyIcon { get; set; } = false;
public MgGridToolbarBase GridToolbar { get; set; } = null!;
const string ExportFileName = "ExportResult";
private bool _isReloadInProgress;
/// <summary>
/// Whether the grid is currently in edit mode (New or Edit)
/// </summary>
private bool IsEditing => Grid?.GridEditState != MgGridEditState.None;
/// <summary>
/// Whether the grid is currently syncing data
/// </summary>
private bool IsSyncing => Grid?.IsSyncing ?? false;
/// <summary>
/// Whether there is a focused row in the grid
/// </summary>
private bool HasFocusedRow => Grid?.GetFocusedRowIndex() >= 0;
protected override void OnInitialized()
{
}
async Task ReloadData_Click(ToolbarItemClickEventArgs e)
{
_isReloadInProgress = true;
try
{
await OnReloadDataClick.InvokeAsync(e);
}
finally
{
_isReloadInProgress = false;
}
}
async Task NewItem_Click()
{
await Grid.StartEditNewRowAsync();
}
async Task EditItem_Click()
{
await Grid.StartEditRowAsync(Grid.GetFocusedRowIndex());
}
void DeleteItem_Click()
{
Grid.ShowRowDeleteConfirmation(Grid.GetFocusedRowIndex());
}
async Task SaveItem_Click()
{
await Grid.SaveChangesAsync();
}
async Task CancelEdit_Click()
{
await Grid.CancelEditAsync();
}
void PrevRow_Click()
{
Grid.StepPrevRow();
}
void NextRow_Click()
{
Grid.StepNextRow();
}
void ColumnChooserItem_Click(ToolbarItemClickEventArgs e)
{
Grid.ShowColumnChooser();
}
async Task ExportXlsxItem_Click()
{
await Grid.ExportToXlsxAsync(ExportFileName);
}
async Task ExportXlsItem_Click()
{
await Grid.ExportToXlsAsync(ExportFileName);
}
async Task ExportCsvItem_Click()
{
await Grid.ExportToCsvAsync(ExportFileName);
}
async Task ExportPdfItem_Click()
{
await Grid.ExportToPdfAsync(ExportFileName);
}
}