Refactor InfoPanel: non-generic, nested grid support

- Replace generic InfoPanel with non-generic version using IInfoPanelBase
- Add ParentGrid, GetRootGrid, and InfoPanelInstance to IMgGridBase for nested grid hierarchy
- Only root grid displays InfoPanel; nested grids inherit context
- InfoPanel now handles any data type via reflection and object
- All grid-to-InfoPanel communication routed through root grid
- Add option to show/hide readonly fields in edit mode
- Improve InfoPanel CSS for up to 4 responsive columns
- Remove redundant code and add debug output for InfoPanel data flow
This commit is contained in:
Loretta 2025-12-17 13:54:07 +01:00
parent 109a4b82b4
commit 90f12a160e
4 changed files with 221 additions and 356 deletions

View File

@ -27,6 +27,16 @@ public interface IMgGridBase : IGrid
/// </summary>
MgGridEditState GridEditState { get; }
/// <summary>
/// Parent grid in nested grid hierarchy (null if this is a root grid)
/// </summary>
IMgGridBase? ParentGrid { get; }
/// <summary>
/// Gets the root grid in the hierarchy
/// </summary>
IMgGridBase GetRootGrid();
/// <summary>
/// Navigates to the previous row in the grid
/// </summary>
@ -36,6 +46,16 @@ public interface IMgGridBase : IGrid
/// Navigates to the next row in the grid
/// </summary>
void StepNextRow();
/// <summary>
/// Whether this grid shows an InfoPanel
/// </summary>
bool ShowInfoPanel { get; set; }
/// <summary>
/// InfoPanel instance for displaying row details
/// </summary>
IInfoPanelBase? InfoPanelInstance { get; set; }
}
public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClient> : DxGrid, IMgGridBase, IAsyncDisposable
@ -61,11 +81,30 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
/// <inheritdoc />
public MgGridEditState GridEditState { get; private set; } = MgGridEditState.None;
[Parameter] public bool ShowInfoPanel { get; set; } = true;
/// <inheritdoc />
[CascadingParameter]
public IMgGridBase? ParentGrid { get; set; }
/// <inheritdoc />
public IMgGridBase GetRootGrid()
{
var current = (IMgGridBase)this;
while (current.ParentGrid != null)
{
current = current.ParentGrid;
}
return current;
}
[Parameter] public bool ShowInfoPanel { get; set; } = false;
private object _focusedDataItem;
private MgGridInfoPanel<TDataItem>? _infoPanelInstance;
/// <summary>
/// InfoPanel instance for displaying row details
/// </summary>
public IInfoPanelBase? InfoPanelInstance { get; set; }
public MgGridBase() : base()
{
}
@ -137,54 +176,65 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
if (!ShowInfoPanel)
{
base.BuildRenderTree(builder);
return;
}
var seq = 0;
builder.OpenComponent<DxSplitter>(seq++);
builder.AddAttribute(seq++, "Width", "100%");
builder.AddAttribute(seq++, "CssClass", "mg-grid-splitter");
builder.AddAttribute(seq++, "Orientation", Orientation.Horizontal);
builder.AddAttribute(seq++, "Panes", (RenderFragment)(panesBuilder =>
// Wrap everything in a CascadingValue to provide this grid as ParentGrid to nested grids
builder.OpenComponent<CascadingValue<IMgGridBase>>(seq++);
builder.AddAttribute(seq++, "Value", (IMgGridBase)this);
builder.AddAttribute(seq++, "ChildContent", (RenderFragment)(contentBuilder =>
{
var paneSeq = 0;
// Left pane - Grid
panesBuilder.OpenComponent<DxSplitterPane>(paneSeq++);
panesBuilder.AddAttribute(paneSeq++, "ChildContent", (RenderFragment)(gridBuilder =>
// Nested grids or root without InfoPanel: render base grid only
if (ParentGrid != null || !ShowInfoPanel)
{
base.BuildRenderTree(gridBuilder);
}));
panesBuilder.CloseComponent();
base.BuildRenderTree(contentBuilder);
return;
}
// Right pane - InfoPanel (sticky to viewport)
panesBuilder.OpenComponent<DxSplitterPane>(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");
// Root grid with InfoPanel enabled: render splitter with grid + InfoPanel
var innerSeq = 0;
panesBuilder.AddAttribute(paneSeq++, "ChildContent", (RenderFragment)(infoPanelBuilder =>
contentBuilder.OpenComponent<DxSplitter>(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 infoPanelSeq = 0;
infoPanelBuilder.OpenComponent<MgGridInfoPanel<TDataItem>>(infoPanelSeq++);
infoPanelBuilder.AddComponentReferenceCapture(infoPanelSeq++, instance =>
var paneSeq = 0;
// Left pane - Grid
panesBuilder.OpenComponent<DxSplitterPane>(paneSeq++);
panesBuilder.AddAttribute(paneSeq++, "ChildContent", (RenderFragment)(gridBuilder =>
{
_infoPanelInstance = (MgGridInfoPanel<TDataItem>)instance;
});
infoPanelBuilder.CloseComponent();
base.BuildRenderTree(gridBuilder);
}));
panesBuilder.CloseComponent();
// Right pane - InfoPanel
panesBuilder.OpenComponent<DxSplitterPane>(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;
infoPanelBuilder.OpenComponent<MgGridInfoPanel>(infoPanelSeq++);
infoPanelBuilder.AddComponentReferenceCapture(infoPanelSeq++, instance =>
{
InfoPanelInstance = (MgGridInfoPanel)instance;
});
infoPanelBuilder.CloseComponent();
}));
panesBuilder.CloseComponent();
}));
panesBuilder.CloseComponent();
contentBuilder.CloseComponent();
}));
builder.CloseComponent();
builder.CloseComponent(); // Close CascadingValue
}
//protected override Task RaiseFocusedRowChangedAsync(GridFocusedRowChangedEventArgsBase args)
@ -311,6 +361,7 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
throw new NullReferenceException($"[{GetType().Name}] SignalRClient == null");
}
ShowInfoPanel = IsMasterGrid;
var crudTags = new SignalRCrudTags(GetAllMessageTag, GetItemMessageTag, AddMessageTag, UpdateMessageTag, RemoveMessageTag);
_dataSource = (TSignalRDataSource)Activator.CreateInstance(typeof(TSignalRDataSource), SignalRClient, crudTags, ContextIds)!;
@ -450,40 +501,44 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
await OnGridCustomizeEditModel.InvokeAsync(e);
// Frissítjük az InfoPanel-t edit módba - itt az EditModel már elérhető
if (ShowInfoPanel && _infoPanelInstance != null)
// Update InfoPanel to edit mode
var rootGrid = GetRootGrid();
if (rootGrid.ShowInfoPanel && rootGrid.InfoPanelInstance != null)
{
_infoPanelInstance.SetEditMode(editModel);
rootGrid.InfoPanelInstance.SetEditMode(editModel);
}
// Force grid refresh to apply edit mode styling
await InvokeAsync(StateHasChanged);
}
private async Task OnEditStart(GridEditStartEventArgs e)
{
await OnGridEditStart.InvokeAsync(e);
// Az InfoPanel-t és az EditMode-ot a CustomizeEditModel-ben frissítjük, mert ott az EditModel már elérhető
}
protected virtual async Task OnFocusedRowChanged(GridFocusedRowChangedEventArgs e)
{
_focusedDataItem = e.DataItem;
var rootGrid = GetRootGrid();
if (ShowInfoPanel && _infoPanelInstance != null)
if (!rootGrid.ShowInfoPanel) return;
// Get the root grid's InfoPanel (the visible one)
var infoPanelInstance = rootGrid.InfoPanelInstance;
if (infoPanelInstance != null && e.DataItem != null)
{
// Ha edit módban vagyunk, de a felhasználó egy másik sorra kattintott,
// akkor kilépünk az edit módból
if (GridEditState != MgGridEditState.None)
{
_infoPanelInstance.ClearEditMode();
infoPanelInstance.ClearEditMode();
}
// Frissítjük az InfoPanel-t az új sor adataival
if (e.DataItem is TDataItem dataItem)
{
_infoPanelInstance.RefreshData(this, dataItem, e.VisibleIndex);
}
// The InfoPanel is now non-generic and works with any data type!
infoPanelInstance.RefreshData(this, e.DataItem, e.VisibleIndex);
}
await OnGridFocusedRowChanged.InvokeAsync(e);
@ -516,29 +571,27 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
}
else await UpdateDataItemAsync(dataItem);
// Kilépés edit módból
GridEditState = MgGridEditState.None;
if (ShowInfoPanel && _infoPanelInstance != null)
var rootGrid = GetRootGrid();
if (rootGrid.ShowInfoPanel && rootGrid.InfoPanelInstance != null)
{
_infoPanelInstance.ClearEditMode();
rootGrid.InfoPanelInstance.ClearEditMode();
}
// Force grid refresh to remove edit mode styling
await InvokeAsync(StateHasChanged);
}
private async Task OnEditCanceling(GridEditCancelingEventArgs e)
{
// Kilépés edit módból
GridEditState = MgGridEditState.None;
if (ShowInfoPanel && _infoPanelInstance != null)
var rootGrid = GetRootGrid();
if (rootGrid.ShowInfoPanel && rootGrid.InfoPanelInstance != null)
{
_infoPanelInstance.ClearEditMode();
rootGrid.InfoPanelInstance.ClearEditMode();
}
// Force grid refresh to remove edit mode styling
await InvokeAsync(StateHasChanged);
}

View File

@ -1,22 +1,22 @@
@using DevExpress.Blazor
@using Microsoft.AspNetCore.Components.Rendering
@using System.Reflection
@typeparam TDataItem where TDataItem : class
<div @ref="_panelElement" class="mg-grid-info-panel @(_isEditMode ? "edit-mode" : "view-mode")">
@* Header - matches grid toolbar height *@
@* Header *@
<div class="dxbl-grid-header-panel px-3 py-2 border-bottom">
<span class="fw-semibold">Sor részletei</span>
</div>
@* Content - scrollable area *@
@* Content *@
<div class="mg-info-panel-content">
@if (GetActiveDataItem() != null && _currentGrid != null)
{
var dataItem = GetActiveDataItem()!;
var dataItemType = dataItem.GetType();
<div class="mg-info-panel-grid">
@foreach (var column in _allDataColumns)
@foreach (var column in GetVisibleColumns())
{
var displayText = GetDisplayTextFromGrid(column);
var value = GetCellValue(column);
@ -31,7 +31,7 @@
<div class="dxbl-fl-ec">
@if (_isEditMode && !column.ReadOnly)
{
@RenderEditableCell(column, dataItem, value, displayText, settingsType)
@RenderEditableCell(column, dataItem, dataItemType, value, displayText, settingsType)
}
else
{
@ -53,30 +53,40 @@
</div>
@code {
private string GetColumnCaption(DxGridDataColumn column)
/// <summary>
/// Gets the columns to display based on edit mode and ShowReadOnlyFieldsInEditMode setting
/// </summary>
private IEnumerable<DxGridDataColumn> GetVisibleColumns()
{
if (!_isEditMode || ShowReadOnlyFieldsInEditMode)
{
return _allDataColumns;
}
// In edit mode with ShowReadOnlyFieldsInEditMode=false, hide readonly columns
return _allDataColumns.Where(c => !c.ReadOnly);
}
private static string GetColumnCaption(DxGridDataColumn column)
{
return !string.IsNullOrWhiteSpace(column.Caption) ? column.Caption : column.FieldName;
}
private string GetCaptionCssClass(bool isReadOnly)
private static string GetCaptionCssClass(bool isReadOnly)
{
return isReadOnly ? "fw-semibold" : "fw-semibold text-primary";
}
/// <summary>
/// Renders an editable cell with two-way binding to the EditModel
/// </summary>
private RenderFragment RenderEditableCell(DxGridDataColumn column, TDataItem dataItem, object? value, string displayText, EditSettingsType settingsType)
private RenderFragment RenderEditableCell(DxGridDataColumn column, object dataItem, Type dataItemType, object? value, string displayText, EditSettingsType settingsType)
{
return builder =>
{
var seq = 0;
var fieldName = column.FieldName;
var propertyInfo = typeof(TDataItem).GetProperty(fieldName);
var propertyInfo = dataItemType.GetProperty(fieldName);
if (propertyInfo == null)
{
// Fallback to readonly if property not found
RenderCellContent(column, value, displayText, settingsType)(builder);
return;
}
@ -84,10 +94,10 @@
var propertyType = propertyInfo.PropertyType;
var underlyingType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
// ComboBox columns
// ComboBox
if (settingsType == EditSettingsType.ComboBox)
{
var comboSettings = GetEditSettings(fieldName) as DxComboBoxSettings;
var comboSettings = GetEditSettings(column.FieldName) as DxComboBoxSettings;
if (comboSettings != null)
{
RenderComboBoxEditor(builder, ref seq, dataItem, propertyInfo, comboSettings);
@ -95,54 +105,29 @@
}
}
// Render based on property type
// Render based on type
if (underlyingType == typeof(bool))
{
RenderCheckBoxEditor(builder, ref seq, dataItem, propertyInfo);
}
else if (underlyingType == typeof(DateTime))
{
RenderDateTimeEditor(builder, ref seq, dataItem, propertyInfo, column.DisplayFormat);
}
else if (underlyingType == typeof(DateOnly))
{
RenderDateOnlyEditor(builder, ref seq, dataItem, propertyInfo, column.DisplayFormat);
}
else if (underlyingType == typeof(TimeOnly))
{
RenderTimeOnlyEditor(builder, ref seq, dataItem, propertyInfo);
}
else if (underlyingType == typeof(TimeSpan))
{
RenderTimeSpanEditor(builder, ref seq, dataItem, propertyInfo);
}
else if (underlyingType == typeof(int))
{
RenderSpinIntEditor(builder, ref seq, dataItem, propertyInfo);
}
else if (underlyingType == typeof(decimal))
{
RenderSpinDecimalEditor(builder, ref seq, dataItem, propertyInfo);
}
else if (underlyingType == typeof(double))
{
RenderSpinDoubleEditor(builder, ref seq, dataItem, propertyInfo);
}
else if (settingsType == EditSettingsType.Memo || (propertyType == typeof(string) && fieldName.Contains("Comment", StringComparison.OrdinalIgnoreCase)))
{
else if (settingsType == EditSettingsType.Memo)
RenderMemoEditor(builder, ref seq, dataItem, propertyInfo);
}
else
{
RenderTextBoxEditor(builder, ref seq, dataItem, propertyInfo);
}
};
}
private void RenderCheckBoxEditor(RenderTreeBuilder builder, ref int seq, TDataItem dataItem, PropertyInfo propertyInfo)
private void RenderCheckBoxEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo)
{
var currentValue = (bool)(propertyInfo.GetValue(dataItem) ?? false);
builder.OpenComponent<DxCheckBox<bool>>(seq++);
builder.AddAttribute(seq++, "Checked", currentValue);
builder.AddAttribute(seq++, "CheckedChanged", EventCallback.Factory.Create<bool>(this, newValue =>
@ -153,7 +138,7 @@
builder.CloseComponent();
}
private void RenderDateTimeEditor(RenderTreeBuilder builder, ref int seq, TDataItem dataItem, PropertyInfo propertyInfo, string? displayFormat)
private void RenderDateTimeEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo, string? displayFormat)
{
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
var currentValue = propertyInfo.GetValue(dataItem);
@ -182,7 +167,7 @@
builder.CloseComponent();
}
private void RenderDateOnlyEditor(RenderTreeBuilder builder, ref int seq, TDataItem dataItem, PropertyInfo propertyInfo, string? displayFormat)
private void RenderDateOnlyEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo, string? displayFormat)
{
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
var currentValue = propertyInfo.GetValue(dataItem);
@ -211,63 +196,7 @@
builder.CloseComponent();
}
private void RenderTimeOnlyEditor(RenderTreeBuilder builder, ref int seq, TDataItem dataItem, PropertyInfo propertyInfo)
{
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
var currentValue = propertyInfo.GetValue(dataItem);
if (isNullable)
{
builder.OpenComponent<DxTimeEdit<TimeOnly?>>(seq++);
builder.AddAttribute(seq++, "Time", (TimeOnly?)currentValue);
builder.AddAttribute(seq++, "TimeChanged", EventCallback.Factory.Create<TimeOnly?>(this, newValue =>
{
propertyInfo.SetValue(dataItem, newValue);
InvokeAsync(StateHasChanged);
}));
}
else
{
builder.OpenComponent<DxTimeEdit<TimeOnly>>(seq++);
builder.AddAttribute(seq++, "Time", (TimeOnly)(currentValue ?? TimeOnly.MinValue));
builder.AddAttribute(seq++, "TimeChanged", EventCallback.Factory.Create<TimeOnly>(this, newValue =>
{
propertyInfo.SetValue(dataItem, newValue);
InvokeAsync(StateHasChanged);
}));
}
builder.CloseComponent();
}
private void RenderTimeSpanEditor(RenderTreeBuilder builder, ref int seq, TDataItem dataItem, PropertyInfo propertyInfo)
{
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
var currentValue = propertyInfo.GetValue(dataItem);
if (isNullable)
{
builder.OpenComponent<DxTimeEdit<TimeSpan?>>(seq++);
builder.AddAttribute(seq++, "Time", (TimeSpan?)currentValue);
builder.AddAttribute(seq++, "TimeChanged", EventCallback.Factory.Create<TimeSpan?>(this, newValue =>
{
propertyInfo.SetValue(dataItem, newValue);
InvokeAsync(StateHasChanged);
}));
}
else
{
builder.OpenComponent<DxTimeEdit<TimeSpan>>(seq++);
builder.AddAttribute(seq++, "Time", (TimeSpan)(currentValue ?? TimeSpan.Zero));
builder.AddAttribute(seq++, "TimeChanged", EventCallback.Factory.Create<TimeSpan>(this, newValue =>
{
propertyInfo.SetValue(dataItem, newValue);
InvokeAsync(StateHasChanged);
}));
}
builder.CloseComponent();
}
private void RenderSpinIntEditor(RenderTreeBuilder builder, ref int seq, TDataItem dataItem, PropertyInfo propertyInfo)
private void RenderSpinIntEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo)
{
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
var currentValue = propertyInfo.GetValue(dataItem);
@ -295,7 +224,7 @@
builder.CloseComponent();
}
private void RenderSpinDecimalEditor(RenderTreeBuilder builder, ref int seq, TDataItem dataItem, PropertyInfo propertyInfo)
private void RenderSpinDecimalEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo)
{
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
var currentValue = propertyInfo.GetValue(dataItem);
@ -323,7 +252,7 @@
builder.CloseComponent();
}
private void RenderSpinDoubleEditor(RenderTreeBuilder builder, ref int seq, TDataItem dataItem, PropertyInfo propertyInfo)
private void RenderSpinDoubleEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo)
{
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
var currentValue = propertyInfo.GetValue(dataItem);
@ -351,10 +280,9 @@
builder.CloseComponent();
}
private void RenderTextBoxEditor(RenderTreeBuilder builder, ref int seq, TDataItem dataItem, PropertyInfo propertyInfo)
private void RenderTextBoxEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo)
{
var currentValue = propertyInfo.GetValue(dataItem)?.ToString() ?? string.Empty;
builder.OpenComponent<DxTextBox>(seq++);
builder.AddAttribute(seq++, "Text", currentValue);
builder.AddAttribute(seq++, "TextChanged", EventCallback.Factory.Create<string>(this, newValue =>
@ -365,10 +293,9 @@
builder.CloseComponent();
}
private void RenderMemoEditor(RenderTreeBuilder builder, ref int seq, TDataItem dataItem, PropertyInfo propertyInfo)
private void RenderMemoEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo)
{
var currentValue = propertyInfo.GetValue(dataItem)?.ToString() ?? string.Empty;
builder.OpenComponent<DxMemo>(seq++);
builder.AddAttribute(seq++, "Text", currentValue);
builder.AddAttribute(seq++, "TextChanged", EventCallback.Factory.Create<string>(this, newValue =>
@ -380,40 +307,29 @@
builder.CloseComponent();
}
private void RenderComboBoxEditor(RenderTreeBuilder builder, ref int seq, TDataItem dataItem, PropertyInfo propertyInfo, DxComboBoxSettings settings)
private void RenderComboBoxEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo, DxComboBoxSettings settings)
{
var currentValue = propertyInfo.GetValue(dataItem);
var propertyType = propertyInfo.PropertyType;
var underlyingType = Nullable.GetUnderlyingType(propertyType) ?? propertyType;
// Determine TData type from settings.Data
var dataType = settings.Data?.GetType();
var itemType = typeof(object);
if (dataType != null && dataType.IsGenericType)
if (dataType?.IsGenericType == true)
{
var genericArgs = dataType.GetGenericArguments();
if (genericArgs.Length > 0)
{
itemType = genericArgs[0];
}
}
// Handle common value types for FK fields
if (underlyingType == typeof(int))
{
RenderComboBoxInt(builder, ref seq, dataItem, propertyInfo, settings, itemType, currentValue);
}
else if (underlyingType == typeof(long))
{
RenderComboBoxLong(builder, ref seq, dataItem, propertyInfo, settings, itemType, currentValue);
}
else if (underlyingType == typeof(Guid))
{
RenderComboBoxGuid(builder, ref seq, dataItem, propertyInfo, settings, itemType, currentValue);
}
else
{
// Fallback: render as TextBox with display text
var displayText = ResolveComboBoxDisplayText(settings, currentValue ?? new object()) ?? currentValue?.ToString() ?? string.Empty;
builder.OpenComponent<DxTextBox>(seq++);
builder.AddAttribute(seq++, "Text", displayText);
@ -422,12 +338,10 @@
}
}
private void RenderComboBoxInt(RenderTreeBuilder builder, ref int seq, TDataItem dataItem, PropertyInfo propertyInfo, DxComboBoxSettings settings, Type itemType, object? currentValue)
private void RenderComboBoxInt(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo, DxComboBoxSettings settings, Type itemType, object? currentValue)
{
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
// Create the generic ComboBox type: DxComboBox<TItem, int> or DxComboBox<TItem, int?>
var comboType = isNullable
var comboType = isNullable
? typeof(DxComboBox<,>).MakeGenericType(itemType, typeof(int?))
: typeof(DxComboBox<,>).MakeGenericType(itemType, typeof(int));
@ -435,7 +349,7 @@
builder.AddAttribute(seq++, "Data", settings.Data);
builder.AddAttribute(seq++, "ValueFieldName", settings.ValueFieldName);
builder.AddAttribute(seq++, "TextFieldName", settings.TextFieldName);
if (isNullable)
{
builder.AddAttribute(seq++, "Value", currentValue as int?);
@ -454,16 +368,14 @@
StateHasChanged();
}));
}
builder.AddAttribute(seq++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto);
builder.CloseComponent();
}
private void RenderComboBoxLong(RenderTreeBuilder builder, ref int seq, TDataItem dataItem, PropertyInfo propertyInfo, DxComboBoxSettings settings, Type itemType, object? currentValue)
private void RenderComboBoxLong(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo, DxComboBoxSettings settings, Type itemType, object? currentValue)
{
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
var comboType = isNullable
var comboType = isNullable
? typeof(DxComboBox<,>).MakeGenericType(itemType, typeof(long?))
: typeof(DxComboBox<,>).MakeGenericType(itemType, typeof(long));
@ -471,7 +383,7 @@
builder.AddAttribute(seq++, "Data", settings.Data);
builder.AddAttribute(seq++, "ValueFieldName", settings.ValueFieldName);
builder.AddAttribute(seq++, "TextFieldName", settings.TextFieldName);
if (isNullable)
{
builder.AddAttribute(seq++, "Value", currentValue as long?);
@ -490,16 +402,14 @@
StateHasChanged();
}));
}
builder.AddAttribute(seq++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto);
builder.CloseComponent();
}
private void RenderComboBoxGuid(RenderTreeBuilder builder, ref int seq, TDataItem dataItem, PropertyInfo propertyInfo, DxComboBoxSettings settings, Type itemType, object? currentValue)
private void RenderComboBoxGuid(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo, DxComboBoxSettings settings, Type itemType, object? currentValue)
{
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
var comboType = isNullable
var comboType = isNullable
? typeof(DxComboBox<,>).MakeGenericType(itemType, typeof(Guid?))
: typeof(DxComboBox<,>).MakeGenericType(itemType, typeof(Guid));
@ -507,7 +417,7 @@
builder.AddAttribute(seq++, "Data", settings.Data);
builder.AddAttribute(seq++, "ValueFieldName", settings.ValueFieldName);
builder.AddAttribute(seq++, "TextFieldName", settings.TextFieldName);
if (isNullable)
{
builder.AddAttribute(seq++, "Value", currentValue as Guid?);
@ -526,7 +436,6 @@
StateHasChanged();
}));
}
builder.AddAttribute(seq++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto);
builder.CloseComponent();
}
@ -537,11 +446,9 @@
{
var seq = 0;
// If column has EditSettings, render based on that
switch (settingsType)
{
case EditSettingsType.ComboBox:
// ComboBox columns show resolved display text
builder.OpenElement(seq++, "div");
builder.AddAttribute(seq++, "title", displayText);
builder.AddAttribute(seq++, "class", "info-panel-text-wrapper");
@ -559,26 +466,6 @@
builder.CloseComponent();
return;
case EditSettingsType.DateEdit:
RenderDateEditor(builder, ref seq, value, column.DisplayFormat);
return;
case EditSettingsType.TimeEdit:
RenderTimeEditor(builder, ref seq, value, column.DisplayFormat);
return;
case EditSettingsType.SpinEdit:
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();
return;
case EditSettingsType.Memo:
builder.OpenElement(seq++, "div");
builder.AddAttribute(seq++, "title", displayText);
@ -591,7 +478,7 @@
return;
}
// Default: render based on value type
// Default by value type
switch (value)
{
case bool boolValue:
@ -617,20 +504,6 @@
builder.CloseComponent();
break;
case TimeOnly timeOnlyValue:
builder.OpenComponent<DxTimeEdit<TimeOnly>>(seq++);
builder.AddAttribute(seq++, "Time", timeOnlyValue);
builder.AddAttribute(seq++, "ReadOnly", true);
builder.CloseComponent();
break;
case TimeSpan timeSpanValue:
builder.OpenComponent<DxTimeEdit<TimeSpan>>(seq++);
builder.AddAttribute(seq++, "Time", timeSpanValue);
builder.AddAttribute(seq++, "ReadOnly", true);
builder.CloseComponent();
break;
case decimal or double or float or int or long or short:
builder.OpenElement(seq++, "div");
builder.AddAttribute(seq++, "title", displayText);
@ -656,69 +529,4 @@
}
};
}
private void RenderDateEditor(RenderTreeBuilder builder, ref int seq, object? value, string? displayFormat)
{
switch (value)
{
case DateTime dateTime:
builder.OpenComponent<DxDateEdit<DateTime>>(seq++);
builder.AddAttribute(seq++, "Date", dateTime);
builder.AddAttribute(seq++, "ReadOnly", true);
builder.AddAttribute(seq++, "DisplayFormat", displayFormat ?? "yyyy-MM-dd HH:mm");
builder.CloseComponent();
break;
case DateOnly dateOnly:
builder.OpenComponent<DxDateEdit<DateOnly>>(seq++);
builder.AddAttribute(seq++, "Date", dateOnly);
builder.AddAttribute(seq++, "ReadOnly", true);
builder.AddAttribute(seq++, "DisplayFormat", displayFormat ?? "yyyy-MM-dd");
builder.CloseComponent();
break;
case DateTimeOffset dateTimeOffset:
builder.OpenComponent<DxDateEdit<DateTimeOffset>>(seq++);
builder.AddAttribute(seq++, "Date", dateTimeOffset);
builder.AddAttribute(seq++, "ReadOnly", true);
builder.AddAttribute(seq++, "DisplayFormat", displayFormat ?? "yyyy-MM-dd HH:mm");
builder.CloseComponent();
break;
default:
builder.OpenComponent<DxTextBox>(seq++);
builder.AddAttribute(seq++, "Text", value?.ToString() ?? string.Empty);
builder.AddAttribute(seq++, "ReadOnly", true);
builder.CloseComponent();
break;
}
}
private void RenderTimeEditor(RenderTreeBuilder builder, ref int seq, object? value, string? displayFormat)
{
switch (value)
{
case TimeOnly timeOnly:
builder.OpenComponent<DxTimeEdit<TimeOnly>>(seq++);
builder.AddAttribute(seq++, "Time", timeOnly);
builder.AddAttribute(seq++, "ReadOnly", true);
builder.CloseComponent();
break;
case TimeSpan timeSpan:
builder.OpenComponent<DxTimeEdit<TimeSpan>>(seq++);
builder.AddAttribute(seq++, "Time", timeSpan);
builder.AddAttribute(seq++, "ReadOnly", true);
builder.CloseComponent();
break;
case DateTime dateTime:
builder.OpenComponent<DxTimeEdit<DateTime>>(seq++);
builder.AddAttribute(seq++, "Time", dateTime);
builder.AddAttribute(seq++, "ReadOnly", true);
builder.CloseComponent();
break;
default:
builder.OpenComponent<DxTextBox>(seq++);
builder.AddAttribute(seq++, "Text", value?.ToString() ?? string.Empty);
builder.AddAttribute(seq++, "ReadOnly", true);
builder.CloseComponent();
break;
}
}
}

View File

@ -4,23 +4,40 @@ using Microsoft.JSInterop;
namespace AyCode.Blazor.Components.Components.Grids;
public partial class MgGridInfoPanel<TDataItem> : ComponentBase, IAsyncDisposable where TDataItem : class
/// <summary>
/// Interface for InfoPanel to support grid access
/// </summary>
public interface IInfoPanelBase
{
void ClearEditMode();
void SetEditMode(object editModel);
void RefreshData(DxGrid grid, object? dataItem, int visibleIndex = -1);
}
/// <summary>
/// Non-generic version of the InfoPanel component
/// </summary>
public partial class MgGridInfoPanel : ComponentBase, IAsyncDisposable, IInfoPanelBase
{
[Inject] private IJSRuntime JSRuntime { get; set; } = null!;
/// <summary>
/// Whether to show readonly fields when in edit mode. Default is false.
/// </summary>
[Parameter] public bool ShowReadOnlyFieldsInEditMode { get; set; } = false;
private ElementReference _panelElement;
private IJSObjectReference? _jsModule;
private bool _isJsInitialized;
private const int DefaultTopOffset = 300; // Increased from 180 to account for header + tabs + toolbar
private DxGrid? _currentGrid;
private TDataItem? _currentDataItem;
private object? _currentDataItem;
private int _focusedRowVisibleIndex = -1;
private List<DxGridDataColumn> _allDataColumns = [];
// Edit mode state
private bool _isEditMode;
private TDataItem? _editModel;
private object? _editModel;
// Cache for edit settings to avoid repeated lookups
private readonly Dictionary<string, IEditSettings?> _editSettingsCache = [];
@ -52,39 +69,46 @@ public partial class MgGridInfoPanel<TDataItem> : ComponentBase, IAsyncDisposabl
/// <summary>
/// Refreshes the InfoPanel with data from the specified grid row (view mode)
/// </summary>
public void RefreshData(DxGrid grid, TDataItem? dataItem, int visibleIndex = -1)
public void RefreshData(DxGrid grid, object? dataItem, int visibleIndex = -1)
{
ArgumentNullException.ThrowIfNull(grid);
System.Diagnostics.Debug.WriteLine($"[InfoPanel] RefreshData called - dataItem type: {dataItem?.GetType().Name ?? "null"}, visibleIndex: {visibleIndex}");
_currentGrid = grid;
_currentDataItem = dataItem;
_focusedRowVisibleIndex = visibleIndex;
_editSettingsCache.Clear();
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)
{
_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();
System.Diagnostics.Debug.WriteLine($"[InfoPanel] RefreshData - StateHasChanged called");
}
/// <summary>
/// Sets the InfoPanel to edit mode with the given edit model
/// </summary>
public void SetEditMode(TDataItem editModel)
public void SetEditMode(object editModel)
{
_editModel = editModel;
_isEditMode = true;
_currentDataItem = editModel;
_currentDataItem = _editModel;
if (_currentGrid != null)
{
@ -101,6 +125,7 @@ public partial class MgGridInfoPanel<TDataItem> : ComponentBase, IAsyncDisposabl
{
_isEditMode = false;
_editModel = null;
_editSettingsCache.Clear();
StateHasChanged();
}
@ -132,17 +157,12 @@ public partial class MgGridInfoPanel<TDataItem> : ComponentBase, IAsyncDisposabl
// Ignore disposal errors
}
}
if (_jsModule != null)
{
await _jsModule.DisposeAsync();
}
}
/// <summary>
/// Gets the data item to display/edit (EditModel in edit mode, otherwise CurrentDataItem)
/// </summary>
private TDataItem? GetActiveDataItem() => _isEditMode && _editModel != null ? _editModel : _currentDataItem;
private object? GetActiveDataItem() => _isEditMode && _editModel != null ? _editModel : _currentDataItem;
/// <summary>
/// Gets the display text for a field using the grid's internal formatting.
@ -163,9 +183,9 @@ public partial class MgGridInfoPanel<TDataItem> : ComponentBase, IAsyncDisposabl
// Try to resolve display text from EditSettings
var editSettings = GetEditSettings(column.FieldName);
if (editSettings != null)
if (editSettings is DxComboBoxSettings comboSettings)
{
var displayText = ResolveEditSettingsDisplayText(editSettings, value);
var displayText = ResolveComboBoxDisplayText(comboSettings, value);
if (!string.IsNullOrEmpty(displayText))
return displayText;
}
@ -206,15 +226,13 @@ public partial class MgGridInfoPanel<TDataItem> : ComponentBase, IAsyncDisposabl
try
{
// Try each EditSettings type
settings = TryGetEditSettings<DxComboBoxSettings>(fieldName)
?? TryGetEditSettings<DxDateEditSettings>(fieldName)
?? TryGetEditSettings<DxTimeEditSettings>(fieldName)
?? TryGetEditSettings<DxSpinEditSettings>(fieldName)
?? TryGetEditSettings<DxCheckBoxSettings>(fieldName)
?? TryGetEditSettings<DxMemoSettings>(fieldName)
?? TryGetEditSettings<DxMaskedInputSettings>(fieldName)
?? TryGetEditSettings<DxTextBoxSettings>(fieldName)
?? (IEditSettings?)TryGetEditSettings<DxDropDownEditSettings>(fieldName);
settings = _currentGrid.GetColumnEditSettings<DxComboBoxSettings>(fieldName)
?? _currentGrid.GetColumnEditSettings<DxDateEditSettings>(fieldName)
?? _currentGrid.GetColumnEditSettings<DxTimeEditSettings>(fieldName)
?? _currentGrid.GetColumnEditSettings<DxSpinEditSettings>(fieldName)
?? _currentGrid.GetColumnEditSettings<DxCheckBoxSettings>(fieldName)
?? _currentGrid.GetColumnEditSettings<DxMemoSettings>(fieldName)
?? (IEditSettings?)_currentGrid.GetColumnEditSettings<DxTextBoxSettings>(fieldName);
}
catch
{
@ -225,33 +243,6 @@ public partial class MgGridInfoPanel<TDataItem> : ComponentBase, IAsyncDisposabl
return settings;
}
private T? TryGetEditSettings<T>(string fieldName) where T : class, IEditSettings
{
try
{
return _currentGrid?.GetColumnEditSettings<T>(fieldName);
}
catch
{
return null;
}
}
/// <summary>
/// Resolves display text based on EditSettings type
/// </summary>
private string? ResolveEditSettingsDisplayText(IEditSettings settings, object value)
{
return settings switch
{
DxComboBoxSettings comboSettings => ResolveComboBoxDisplayText(comboSettings, value),
_ => null
};
}
/// <summary>
/// Resolves the display text from a ComboBox data source
/// </summary>
private string? ResolveComboBoxDisplayText(DxComboBoxSettings settings, object value)
{
if (settings.Data == null || string.IsNullOrEmpty(settings.ValueFieldName) || string.IsNullOrEmpty(settings.TextFieldName))
@ -284,7 +275,7 @@ public partial class MgGridInfoPanel<TDataItem> : ComponentBase, IAsyncDisposabl
return null;
}
private string FormatValue(object? value)
private static string FormatValue(object? value)
{
if (value == null)
return string.Empty;
@ -304,13 +295,16 @@ public partial class MgGridInfoPanel<TDataItem> : ComponentBase, IAsyncDisposabl
};
}
private List<DxGridDataColumn> GetAllDataColumns(DxGrid grid)
private static List<DxGridDataColumn> GetAllDataColumns(DxGrid grid)
{
var columns = new List<DxGridDataColumn>();
try
{
var allColumns = grid.GetDataColumns();
System.Diagnostics.Debug.WriteLine($"[InfoPanel] GetAllDataColumns - grid type: {grid.GetType().Name}, columns count: {allColumns?.Count() ?? 0}");
if (allColumns != null)
{
foreach (var column in allColumns)
@ -319,15 +313,17 @@ public partial class MgGridInfoPanel<TDataItem> : ComponentBase, IAsyncDisposabl
!string.IsNullOrWhiteSpace(dataColumn.FieldName))
{
columns.Add(dataColumn);
System.Diagnostics.Debug.WriteLine($"[InfoPanel] - Column: {dataColumn.FieldName}");
}
}
}
}
catch (Exception)
catch (Exception ex)
{
// Fallback: empty list if GetDataColumns fails
System.Diagnostics.Debug.WriteLine($"[InfoPanel] GetAllDataColumns error: {ex.Message}");
}
System.Diagnostics.Debug.WriteLine($"[InfoPanel] GetAllDataColumns result: {columns.Count} columns");
return columns;
}

View File

@ -6,6 +6,7 @@
/* Breakpoint configuration - CHANGE ONLY THESE VALUES */
/* 2 column breakpoint: 500px */
/* 3 column breakpoint: 800px */
/* 4 column breakpoint: 1200px (for 1920px+ screens) */
/* Main panel - contained within splitter pane */
.mg-grid-info-panel {
@ -61,13 +62,20 @@
}
}
/* 3 columns for wider panels (>= 800px) */
@container infopanel (min-width: 800px) {
/* 3 columns for wider panels (800px - 1199px) */
@container infopanel (min-width: 800px) and (max-width: 1199px) {
.mg-info-panel-grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* 4 columns for very wide panels (>= 1200px, typically on 1920px+ screens) */
@container infopanel (min-width: 1200px) {
.mg-info-panel-grid {
grid-template-columns: repeat(4, 1fr);
}
}
.mg-info-panel-item {
min-width: 0; /* Prevent grid blowout */
}