From 739d0fa8085f8d927d959d8422d09ef25497d09b Mon Sep 17 00:00:00 2001 From: Loretta Date: Thu, 18 Dec 2025 11:02:53 +0100 Subject: [PATCH] Refactor: decouple InfoPanel using MgGridWithInfoPanel Major refactor to decouple InfoPanel logic from grid base. Introduces MgGridWithInfoPanel wrapper component to manage grid and InfoPanel layout and communication. InfoPanels are now customizable via Razor templates with named slots (header, footer, etc.), and grid-to-InfoPanel communication is routed through the wrapper. Removes legacy C#-only InfoPanel base classes and parameters from grid base. This improves flexibility, composability, and maintainability of grid+InfoPanel UIs. --- .../Components/Grids/MgGridBase.cs | 182 ++---------------- .../Components/Grids/MgGridInfoPanel.razor | 136 ++++++++----- .../Components/Grids/MgGridInfoPanel.razor.cs | 116 ++++++----- .../Grids/MgGridWithInfoPanel.razor | 74 +++++++ .../Grids/MgInfoPanelTemplateBase.cs | 47 ----- 5 files changed, 246 insertions(+), 309 deletions(-) create mode 100644 AyCode.Blazor.Components/Components/Grids/MgGridWithInfoPanel.razor delete mode 100644 AyCode.Blazor.Components/Components/Grids/MgInfoPanelTemplateBase.cs diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs b/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs index 2a03c8c..e033bea 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs +++ b/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs @@ -23,6 +23,7 @@ public interface IMgGridBase : IGrid bool IsSyncing { get; } string Caption { get; set; } + /// /// Current edit state of the grid (None, New, Edit) /// @@ -47,16 +48,11 @@ public interface IMgGridBase : IGrid /// Navigates to the next row in the grid /// void StepNextRow(); - - /// - /// Whether this grid shows an InfoPanel - /// - bool ShowInfoPanel { get; set; } /// - /// InfoPanel instance for displaying row details + /// InfoPanel instance for displaying row details (from wrapper) /// - IInfoPanelBase? InfoPanelInstance { get; set; } + IInfoPanelBase? InfoPanelInstance { get; } } public abstract class MgGridBase : DxGrid, IMgGridBase, IAsyncDisposable @@ -97,90 +93,27 @@ public abstract class MgGridBase - /// Custom InfoPanel component type. Must inherit from MgInfoPanelTemplateBase. - /// If not set, the default MgGridInfoPanel is used. + /// Reference to the wrapper component for grid-InfoPanel communication /// - [Parameter] public Type? InfoPanelType { get; set; } + [CascadingParameter] + public MgGridWithInfoPanel? GridWrapper { get; set; } private object _focusedDataItem; /// - /// InfoPanel instance for displaying row details + /// InfoPanel instance for displaying row details (from wrapper or direct) /// - public IInfoPanelBase? InfoPanelInstance { get; set; } + public IInfoPanelBase? InfoPanelInstance + { + get => GridWrapper?.InfoPanelInstance; + set { /* Set through wrapper */ } + } public MgGridBase() : base() { } - //protected override RenderFragment CreateRootComponent() - //{ - // System.Diagnostics.Debug.WriteLine($"[MgGridBase] CreateRootComponent - ShowInfoPanel: {ShowInfoPanel}, GridName: {GridName}"); - - // if (!ShowInfoPanel) - // { - // // Ha nincs InfoPanel, használjuk az alapértelmezett renderelést - // System.Diagnostics.Debug.WriteLine("[MgGridBase] Using base CreateRootComponent"); - - // return base.CreateRootComponent(); - // } - - // // Ha van InfoPanel, akkor splitter-rel burkoljuk be - // System.Diagnostics.Debug.WriteLine("[MgGridBase] Creating splitter wrapper"); - - // return (RenderFragment)(content => (RenderFragment)(builder => - // { - // var seq = 0; - - // // DxSplitter - // builder.OpenComponent(seq++); - // builder.AddAttribute(seq++, "Width", "100%"); - // builder.AddAttribute(seq++, "Height", "100%"); - - // // Panes - // builder.AddAttribute(seq++, "Panes", (RenderFragment)(panesBuilder => - // { - // var paneSeq = 0; - - // // Bal pane - Grid - // panesBuilder.OpenComponent(paneSeq++); - - // panesBuilder.AddAttribute(paneSeq++, "ChildContent", (RenderFragment)(gridBuilder => - // { - // // A grid eredeti content-jét rendereljük - // var baseRootComponent = base.CreateRootComponent(); - // baseRootComponent(content)(gridBuilder); - // })); - - // panesBuilder.CloseComponent(); - - // // Jobb pane - Egyelőre üres - // panesBuilder.OpenComponent(paneSeq++); - - // panesBuilder.AddAttribute(paneSeq++, "Size", "0px"); - // panesBuilder.AddAttribute(paneSeq++, "MinSize", "0px"); - // panesBuilder.AddAttribute(paneSeq++, "AllowCollapse", true); - // panesBuilder.AddAttribute(paneSeq++, "Collapsed", true); - - // panesBuilder.AddAttribute(paneSeq++, "ChildContent", (RenderFragment)(infoPanelBuilder => - // { - // infoPanelBuilder.OpenElement(0, "div"); - // infoPanelBuilder.AddAttribute(1, "style", "padding: 1rem;"); - // infoPanelBuilder.AddContent(2, "Info Panel - Coming soon..."); - // infoPanelBuilder.CloseElement(); - // })); - - // panesBuilder.CloseComponent(); - - // })); - - // builder.CloseComponent(); - // })); - //} - protected override void BuildRenderTree(RenderTreeBuilder builder) { var seq = 0; @@ -190,70 +123,11 @@ public abstract class MgGridBase { - // Nested grids or root without InfoPanel: render base grid only - if (ParentGrid != null || !ShowInfoPanel) - { - base.BuildRenderTree(contentBuilder); - return; - } - - // Root grid with InfoPanel enabled: render splitter with grid + InfoPanel - var innerSeq = 0; - - contentBuilder.OpenComponent(innerSeq++); - contentBuilder.AddAttribute(innerSeq++, "Width", "100%"); - contentBuilder.AddAttribute(innerSeq++, "CssClass", "mg-grid-splitter"); - contentBuilder.AddAttribute(innerSeq++, "Orientation", Orientation.Horizontal); - - contentBuilder.AddAttribute(innerSeq++, "Panes", (RenderFragment)(panesBuilder => - { - var paneSeq = 0; - - // Left pane - Grid - panesBuilder.OpenComponent(paneSeq++); - panesBuilder.AddAttribute(paneSeq++, "ChildContent", (RenderFragment)(gridBuilder => - { - base.BuildRenderTree(gridBuilder); - })); - panesBuilder.CloseComponent(); - - // Right pane - InfoPanel - panesBuilder.OpenComponent(paneSeq++); - panesBuilder.AddAttribute(paneSeq++, "Size", "400px"); - panesBuilder.AddAttribute(paneSeq++, "MinSize", "0px"); - panesBuilder.AddAttribute(paneSeq++, "MaxSize", "100%"); - panesBuilder.AddAttribute(paneSeq++, "AllowCollapse", true); - panesBuilder.AddAttribute(paneSeq++, "CssClass", "mg-info-panel-pane"); - - panesBuilder.AddAttribute(paneSeq++, "ChildContent", (RenderFragment)(infoPanelBuilder => - { - var infoPanelSeq = 0; - - // Use custom InfoPanel type if provided, otherwise use default MgGridInfoPanel - var panelType = InfoPanelType ?? typeof(MgGridInfoPanel); - infoPanelBuilder.OpenComponent(infoPanelSeq++, panelType); - infoPanelBuilder.AddComponentReferenceCapture(infoPanelSeq++, instance => - { - InfoPanelInstance = instance as IInfoPanelBase; - }); - infoPanelBuilder.CloseComponent(); - })); - - panesBuilder.CloseComponent(); - })); - - contentBuilder.CloseComponent(); + base.BuildRenderTree(contentBuilder); })); - builder.CloseComponent(); // Close CascadingValue + builder.CloseComponent(); } - //protected override Task RaiseFocusedRowChangedAsync(GridFocusedRowChangedEventArgsBase args) - //{ - // _focusedDataItem = args.DataItem; - // InvokeAsync(StateHasChanged); - // return base.RaiseFocusedRowChangedAsync(args); - //} - protected bool HasIdValue(TDataItem dataItem) => HasIdValue(dataItem.Id); protected bool HasIdValue(TId id) => !_equalityComparerId.Equals(id, default); protected bool IdEquals(TId id1, TId id2) => _equalityComparerId.Equals(id1, id2); @@ -373,12 +247,10 @@ public abstract class MgGridBase(SignalRClient, crudTags, ContextIds) { FilterText = FilterText }; SetGridData(_dataSource.GetReferenceInnerList()); @@ -514,11 +386,7 @@ public abstract class MgGridBase - @((_currentGrid as IMgGridBase)?.Caption ?? "") + @(_currentGrid?.Caption ?? "") + } - @* Toolbar *@ - @if (_currentGrid != null) - { -
- -
- } + @* Toolbar *@ + @if (_currentGrid != null) + { +
+ +
} @* Content *@
@if (GetActiveDataItem() != null && _currentGrid != null) { - var dataItem = GetActiveDataItem()!; - var dataItemType = dataItem.GetType(); + @* Before Columns *@ + @if (BeforeColumnsTemplate != null) + { + @BeforeColumnsTemplate(GetActiveDataItem()) + } + @* Columns *@ @if (ColumnsTemplate != null) { - @ColumnsTemplate + @ColumnsTemplate(GetActiveDataItem()) } else { -
- @foreach (var column in GetVisibleColumns()) - { - var displayText = GetDisplayTextFromGrid(column); - var value = GetCellValue(column); - var settingsType = GetEditSettingsType(column); - var isReadOnly = !_isEditMode || column.ReadOnly; + @RenderDefaultColumns() + } -
-
- -
- @if (_isEditMode && !column.ReadOnly) - { - @RenderEditableCell(column, dataItem, dataItemType, value, displayText, settingsType) - } - else - { - @RenderCellContent(column, value, displayText, settingsType) - } -
-
-
- } -
+ @* After Columns *@ + @if (AfterColumnsTemplate != null) + { + @AfterColumnsTemplate(GetActiveDataItem()) } } else @@ -76,24 +60,82 @@ @* Footer *@ @if (FooterTemplate != null) { - @FooterTemplate + @FooterTemplate(GetActiveDataItem()) }
@code { /// - /// Gets the columns to display based on edit mode and ShowReadOnlyFieldsInEditMode setting + /// Custom header template. Receives the current data item. /// - private IEnumerable GetVisibleColumns() + [Parameter] public RenderFragment? HeaderTemplate { get; set; } + + /// + /// Content to render before the columns. + /// + [Parameter] public RenderFragment? BeforeColumnsTemplate { get; set; } + + /// + /// Custom columns template. If not set, columns are auto-generated. + /// + [Parameter] public RenderFragment? ColumnsTemplate { get; set; } + + /// + /// Content to render after the columns. + /// + [Parameter] public RenderFragment? AfterColumnsTemplate { get; set; } + + /// + /// Custom footer template. + /// + [Parameter] public RenderFragment? FooterTemplate { get; set; } + + private RenderFragment RenderDefaultColumns() => builder => { - if (!_isEditMode || ShowReadOnlyFieldsInEditMode) + var dataItem = GetActiveDataItem()!; + var dataItemType = dataItem.GetType(); + var seq = 0; + + builder.OpenElement(seq++, "div"); + builder.AddAttribute(seq++, "class", "mg-info-panel-grid"); + + foreach (var column in GetVisibleColumns()) { - return _allDataColumns; + var displayText = GetDisplayTextFromGrid(column); + var value = GetCellValue(column); + var settingsType = GetEditSettingsType(column); + var isReadOnly = !_isEditMode || column.ReadOnly; + + builder.OpenElement(seq++, "div"); + builder.AddAttribute(seq++, "class", "mg-info-panel-item"); + + builder.OpenElement(seq++, "div"); + builder.AddAttribute(seq++, "class", "dxbl-form-layout-item"); + + builder.OpenElement(seq++, "label"); + builder.AddAttribute(seq++, "class", $"dxbl-fl-lc {GetCaptionCssClass(isReadOnly)} d-block mb-1 small"); + builder.AddContent(seq++, GetColumnCaption(column)); + builder.CloseElement(); + + builder.OpenElement(seq++, "div"); + builder.AddAttribute(seq++, "class", "dxbl-fl-ec"); + + if (_isEditMode && !column.ReadOnly) + { + RenderEditableCell(column, dataItem, dataItemType, value, displayText, settingsType)(builder); + } + else + { + RenderCellContent(column, value, displayText, settingsType)(builder); + } + + builder.CloseElement(); + builder.CloseElement(); + builder.CloseElement(); } - - // In edit mode with ShowReadOnlyFieldsInEditMode=false, hide readonly columns - return _allDataColumns.Where(c => !c.ReadOnly); - } + + builder.CloseElement(); + }; private static string GetColumnCaption(DxGridDataColumn column) { diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.cs b/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.cs index e8e7a4f..97d4ae0 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.cs +++ b/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.cs @@ -27,32 +27,23 @@ public partial class MgGridInfoPanel : ComponentBase, IAsyncDisposable, IInfoPan [Parameter] public bool ShowReadOnlyFieldsInEditMode { get; set; } = false; /// - /// Custom header template. If not set, default header is used. + /// Reference to the wrapper component - automatically registers this InfoPanel /// - [Parameter] public RenderFragment? HeaderTemplate { get; set; } - - /// - /// Custom columns template. If not set, columns are auto-generated. - /// - [Parameter] public RenderFragment? ColumnsTemplate { get; set; } - - /// - /// Custom footer template. If not set, no footer is rendered. - /// - [Parameter] public RenderFragment? FooterTemplate { get; set; } + [CascadingParameter] + public MgGridWithInfoPanel? GridWrapper { get; set; } private ElementReference _panelElement; private bool _isJsInitialized; private const int DefaultTopOffset = 300; // Increased from 180 to account for header + tabs + toolbar - private IMgGridBase? _currentGrid; - private object? _currentDataItem; - private int _focusedRowVisibleIndex = -1; - private List _allDataColumns = []; + protected IMgGridBase? _currentGrid; + protected object? _currentDataItem; + protected int _focusedRowVisibleIndex = -1; + protected List _allDataColumns = []; // Edit mode state - private bool _isEditMode; - private object? _editModel; + protected bool _isEditMode; + protected object? _editModel; // Cache for edit settings to avoid repeated lookups private readonly Dictionary _editSettingsCache = []; @@ -61,6 +52,9 @@ public partial class MgGridInfoPanel : ComponentBase, IAsyncDisposable, IInfoPan { if (firstRender) { + // Register this InfoPanel with the wrapper + GridWrapper?.RegisterInfoPanel(this); + await InitializeStickyAsync(); } } @@ -95,21 +89,21 @@ public partial class MgGridInfoPanel : ComponentBase, IAsyncDisposable, IInfoPan _focusedRowVisibleIndex = visibleIndex; _editSettingsCache.Clear(); + // Clear edit mode when refreshing with new data + _isEditMode = false; + _editModel = null; + System.Diagnostics.Debug.WriteLine($"[InfoPanel] RefreshData - _currentDataItem is null: {_currentDataItem == null}, cast success: {dataItem != null && _currentDataItem != null}"); - // Ha nem vagyunk edit módban, frissítjük az oszlopokat - if (!_isEditMode) + if (_currentGrid != null && _currentDataItem != null) { - if (_currentGrid != null && _currentDataItem != null) - { - _allDataColumns = GetAllDataColumns(_currentGrid); - System.Diagnostics.Debug.WriteLine($"[InfoPanel] RefreshData - loaded {_allDataColumns.Count} columns"); - } - else - { - _allDataColumns = []; - System.Diagnostics.Debug.WriteLine($"[InfoPanel] RefreshData - cleared columns (grid or dataItem is null)"); - } + _allDataColumns = GetAllDataColumns(_currentGrid); + System.Diagnostics.Debug.WriteLine($"[InfoPanel] RefreshData - loaded {_allDataColumns.Count} columns"); + } + else + { + _allDataColumns = []; + System.Diagnostics.Debug.WriteLine($"[InfoPanel] RefreshData - cleared columns (grid or dataItem is null)"); } StateHasChanged(); @@ -121,6 +115,8 @@ public partial class MgGridInfoPanel : ComponentBase, IAsyncDisposable, IInfoPan /// public void SetEditMode(object editModel) { + System.Diagnostics.Debug.WriteLine($"[InfoPanel] SetEditMode called - editModel type: {editModel?.GetType().Name ?? "null"}, grid.GridEditState: {_currentGrid?.GridEditState}"); + _editModel = editModel; _isEditMode = true; _currentDataItem = _editModel; @@ -130,7 +126,8 @@ public partial class MgGridInfoPanel : ComponentBase, IAsyncDisposable, IInfoPan _allDataColumns = GetAllDataColumns(_currentGrid); } - StateHasChanged(); + InvokeAsync(StateHasChanged); + System.Diagnostics.Debug.WriteLine($"[InfoPanel] SetEditMode - InvokeAsync(StateHasChanged) called, _isEditMode: {_isEditMode}"); } /// @@ -138,10 +135,13 @@ public partial class MgGridInfoPanel : ComponentBase, IAsyncDisposable, IInfoPan /// public void ClearEditMode() { + System.Diagnostics.Debug.WriteLine($"[InfoPanel] ClearEditMode called, grid.GridEditState: {_currentGrid?.GridEditState}"); + _isEditMode = false; _editModel = null; _editSettingsCache.Clear(); - StateHasChanged(); + InvokeAsync(StateHasChanged); + System.Diagnostics.Debug.WriteLine($"[InfoPanel] ClearEditMode - InvokeAsync(StateHasChanged) called"); } /// @@ -177,13 +177,13 @@ public partial class MgGridInfoPanel : ComponentBase, IAsyncDisposable, IInfoPan /// /// Gets the data item to display/edit (EditModel in edit mode, otherwise CurrentDataItem) /// - private object? GetActiveDataItem() => _isEditMode && _editModel != null ? _editModel : _currentDataItem; + protected object? GetActiveDataItem() => _isEditMode && _editModel != null ? _editModel : _currentDataItem; /// /// Gets the display text for a field using the grid's internal formatting. /// For ComboBox columns, tries to get the text from the lookup data source. /// - private string GetDisplayTextFromGrid(DxGridDataColumn column) + protected string GetDisplayTextFromGrid(DxGridDataColumn column) { var dataItem = GetActiveDataItem(); if (_currentGrid == null || dataItem == null || string.IsNullOrWhiteSpace(column.FieldName)) @@ -310,7 +310,37 @@ public partial class MgGridInfoPanel : ComponentBase, IAsyncDisposable, IInfoPan }; } - private static List GetAllDataColumns(IMgGridBase grid) + /// + /// Gets the columns to display based on edit mode and ShowReadOnlyFieldsInEditMode setting + /// + protected IEnumerable GetVisibleColumns() + { + if (!_isEditMode || ShowReadOnlyFieldsInEditMode) + { + return _allDataColumns; + } + + // In edit mode with ShowReadOnlyFieldsInEditMode=false, hide readonly columns + return _allDataColumns.Where(c => !c.ReadOnly); + } + + protected object? GetCellValue(DxGridDataColumn column) + { + var dataItem = GetActiveDataItem(); + if (_currentGrid == null || dataItem == null || string.IsNullOrWhiteSpace(column.FieldName)) + return null; + + try + { + return _currentGrid.GetDataItemValue(dataItem, column.FieldName); + } + catch + { + return null; + } + } + + protected static List GetAllDataColumns(IMgGridBase grid) { var columns = new List(); @@ -342,22 +372,6 @@ public partial class MgGridInfoPanel : ComponentBase, IAsyncDisposable, IInfoPan return columns; } - private object? GetCellValue(DxGridDataColumn column) - { - var dataItem = GetActiveDataItem(); - if (_currentGrid == null || dataItem == null || string.IsNullOrWhiteSpace(column.FieldName)) - return null; - - try - { - return _currentGrid.GetDataItemValue(dataItem, column.FieldName); - } - catch - { - return null; - } - } - /// /// Gets the EditSettings type for rendering logic /// diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridWithInfoPanel.razor b/AyCode.Blazor.Components/Components/Grids/MgGridWithInfoPanel.razor new file mode 100644 index 0000000..80155bc --- /dev/null +++ b/AyCode.Blazor.Components/Components/Grids/MgGridWithInfoPanel.razor @@ -0,0 +1,74 @@ +@using DevExpress.Blazor + + + @if (ShowInfoPanel) + { + + + + @GridContent + + + @if (ChildContent != null) + { + @ChildContent + } + else + { + + } + + + + } + else + { + @GridContent + } + + +@code { + private IInfoPanelBase? _infoPanelInstance; + + /// + /// The grid content to display in the left pane + /// + [Parameter] + public RenderFragment? GridContent { get; set; } + + /// + /// InfoPanel content (e.g., GridShippingDocumentInfoPanel) to display in the right pane. + /// If not set, the default MgGridInfoPanel is used. + /// + [Parameter] + public RenderFragment? ChildContent { get; set; } + + /// + /// Initial size of the InfoPanel pane. Default is "400px". + /// + [Parameter] + public string InfoPanelSize { get; set; } = "400px"; + + /// + /// Whether to show the InfoPanel. Default is true. + /// + [Parameter] + public bool ShowInfoPanel { get; set; } = true; + + /// + /// Gets or sets the InfoPanel instance for grid-InfoPanel communication + /// + public IInfoPanelBase? InfoPanelInstance + { + get => _infoPanelInstance; + set => _infoPanelInstance = value; + } + + /// + /// Registers an InfoPanel instance (called by child InfoPanel components) + /// + public void RegisterInfoPanel(IInfoPanelBase infoPanel) + { + _infoPanelInstance = infoPanel; + } +} diff --git a/AyCode.Blazor.Components/Components/Grids/MgInfoPanelTemplateBase.cs b/AyCode.Blazor.Components/Components/Grids/MgInfoPanelTemplateBase.cs deleted file mode 100644 index d314f46..0000000 --- a/AyCode.Blazor.Components/Components/Grids/MgInfoPanelTemplateBase.cs +++ /dev/null @@ -1,47 +0,0 @@ -using Microsoft.AspNetCore.Components; - -namespace AyCode.Blazor.Components.Components.Grids; - -/// -/// Base class for custom InfoPanel templates. -/// Inherit from this class to create a custom InfoPanel for a specific grid. -/// -public abstract class MgInfoPanelTemplateBase : MgGridInfoPanel -{ - /// - /// Override this to provide custom header content. - /// Return null to use default header. - /// - protected virtual RenderFragment? GetHeaderTemplate() => null; - - /// - /// Override this to provide custom columns/content. - /// Return null to use auto-generated columns. - /// - protected virtual RenderFragment? GetColumnsTemplate() => null; - - /// - /// Override this to provide custom footer content. - /// Return null to hide footer. - /// - protected virtual RenderFragment? GetFooterTemplate() => null; - - protected override void OnInitialized() - { - SetTemplates(); - base.OnInitialized(); - } - - protected override void OnParametersSet() - { - SetTemplates(); - base.OnParametersSet(); - } - - private void SetTemplates() - { - HeaderTemplate = GetHeaderTemplate(); - ColumnsTemplate = GetColumnsTemplate(); - FooterTemplate = GetFooterTemplate(); - } -}