using DevExpress.Blazor;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace AyCode.Blazor.Components.Components.Grids;
///
/// Interface for InfoPanel to support grid access
///
public interface IInfoPanelBase
{
void ClearEditMode();
void SetEditMode(IMgGridBase grid, object editModel);
void RefreshData(IMgGridBase grid, object? dataItem, int visibleIndex = -1);
}
///
/// Context for InfoPanel templates containing data item and edit mode state
///
public record InfoPanelContext(object? DataItem, bool IsEditMode);
///
/// InfoPanel component for displaying and editing grid row details
///
public partial class MgGridInfoPanel : ComponentBase, IAsyncDisposable, IInfoPanelBase
{
[Inject] private IJSRuntime JSRuntime { get; set; } = null!;
///
/// Whether to show readonly fields when in edit mode. Default is false.
///
[Parameter] public bool ShowReadOnlyFieldsInEditMode { get; set; } = false;
///
/// Minimum width for 2 columns layout. Default is 500px.
///
[Parameter] public int TwoColumnBreakpoint { get; set; } = 400;
///
/// Minimum width for 3 columns layout. Default is 800px.
///
[Parameter] public int ThreeColumnBreakpoint { get; set; } = 800;
///
/// Minimum width for 4 columns layout. Default is 1200px.
///
[Parameter] public int FourColumnBreakpoint { get; set; } = 1300;
///
/// Fixed column count. If set (1-4), overrides responsive breakpoints. Default is null (responsive).
///
[Parameter] public int? FixedColumnCount { get; set; }
///
/// Reference to the wrapper component - automatically registers this InfoPanel
///
[CascadingParameter]
public MgGridWithInfoPanel? GridWrapper { get; set; }
private ElementReference _panelElement;
private bool _isJsInitialized;
private const int DefaultTopOffset = 300;
protected IMgGridBase? _currentGrid;
protected object? _currentDataItem;
protected int _focusedRowVisibleIndex = -1;
protected List _allDataColumns = [];
// Edit mode state
protected bool _isEditMode;
protected object? _editModel;
// Type-based caches for performance optimization
private readonly Dictionary<(Type, string), IEditSettings?> _editSettingsCache = [];
private readonly Dictionary> _columnsCache = [];
private readonly Dictionary<(Type, object, object), string?> _comboBoxTextCache = [];
// Track if we need to update UI
private bool _pendingStateChange;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
GridWrapper?.RegisterInfoPanel(this);
await InitializeStickyAsync();
}
}
private async Task InitializeStickyAsync()
{
try
{
await JSRuntime.InvokeVoidAsync(
"MgGridInfoPanel.initSticky",
_panelElement,
DefaultTopOffset);
_isJsInitialized = true;
}
catch (JSException)
{
// JS might not be loaded yet, ignore
}
}
///
/// Refreshes the InfoPanel with data from the specified grid row (view mode)
///
public void RefreshData(IMgGridBase grid, object? dataItem, int visibleIndex = -1)
{
ArgumentNullException.ThrowIfNull(grid);
_currentGrid = grid;
_currentDataItem = dataItem;
_focusedRowVisibleIndex = visibleIndex;
// Clear edit mode when refreshing with new data
_isEditMode = false;
_editModel = null;
// Use cached columns if available
if (_currentGrid != null && _currentDataItem != null)
{
var dataItemType = _currentDataItem.GetType();
_allDataColumns = GetAllDataColumnsCached(dataItemType, _currentGrid);
}
else
{
_allDataColumns = [];
}
// Batch state changes
if (!_pendingStateChange)
{
_pendingStateChange = true;
InvokeAsync(async () =>
{
_pendingStateChange = false;
StateHasChanged();
await OnDataItemChanged.InvokeAsync(dataItem);
});
}
}
///
/// Sets the InfoPanel to edit mode with the given edit model
///
public void SetEditMode(IMgGridBase grid, object editModel)
{
ArgumentNullException.ThrowIfNull(grid);
ArgumentNullException.ThrowIfNull(editModel);
_currentGrid = grid;
_editModel = editModel;
_isEditMode = true;
_currentDataItem = _editModel;
var dataItemType = _editModel.GetType();
_allDataColumns = GetAllDataColumnsCached(dataItemType, _currentGrid);
// Batch state changes
if (!_pendingStateChange)
{
_pendingStateChange = true;
InvokeAsync(() =>
{
_pendingStateChange = false;
StateHasChanged();
});
}
}
///
/// Clears edit mode and returns to view mode
///
public void ClearEditMode()
{
_isEditMode = false;
_editModel = null;
if (!_pendingStateChange)
{
_pendingStateChange = true;
InvokeAsync(() =>
{
_pendingStateChange = false;
StateHasChanged();
});
}
}
///
/// Clears the InfoPanel completely
///
public void Clear()
{
_currentGrid = null;
_currentDataItem = null;
_focusedRowVisibleIndex = -1;
_allDataColumns = [];
_isEditMode = false;
_editModel = null;
StateHasChanged();
}
public async ValueTask DisposeAsync()
{
if (_isJsInitialized)
{
try
{
await JSRuntime.InvokeVoidAsync("MgGridInfoPanel.disposeSticky", _panelElement);
}
catch
{
// Ignore disposal errors
}
}
// Clear all caches on dispose
_editSettingsCache.Clear();
_columnsCache.Clear();
_comboBoxTextCache.Clear();
}
///
/// Gets the data item to display/edit (EditModel in edit mode, otherwise 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.
///
protected string GetDisplayTextFromGrid(DxGridDataColumn column)
{
var dataItem = GetActiveDataItem();
if (_currentGrid == null || dataItem == null || string.IsNullOrWhiteSpace(column.FieldName))
return string.Empty;
try
{
var value = _currentGrid.GetDataItemValue(dataItem, column.FieldName);
if (value == null)
return string.Empty;
var dataItemType = dataItem.GetType();
// Try to resolve display text from EditSettings
var editSettings = GetEditSettingsCached(dataItemType, column.FieldName);
if (editSettings is DxComboBoxSettings comboSettings)
{
var displayText = ResolveComboBoxDisplayTextCached(dataItemType, comboSettings, value);
if (!string.IsNullOrEmpty(displayText))
return displayText;
}
// Apply column's DisplayFormat if available
if (!string.IsNullOrEmpty(column.DisplayFormat))
{
try
{
return string.Format(column.DisplayFormat, value);
}
catch
{
// If format fails, fall through to default formatting
}
}
return FormatValue(value);
}
catch
{
return string.Empty;
}
}
///
/// Gets edit settings for the specified field with Type-based caching
///
private IEditSettings? GetEditSettingsCached(Type dataItemType, string fieldName)
{
if (_currentGrid == null || string.IsNullOrEmpty(fieldName))
return null;
var cacheKey = (dataItemType, fieldName);
if (_editSettingsCache.TryGetValue(cacheKey, out var cached))
return cached;
IEditSettings? settings = null;
try
{
// Try each EditSettings type
settings = _currentGrid.GetColumnEditSettings(fieldName)
?? _currentGrid.GetColumnEditSettings(fieldName)
?? _currentGrid.GetColumnEditSettings(fieldName)
?? _currentGrid.GetColumnEditSettings(fieldName)
?? _currentGrid.GetColumnEditSettings(fieldName)
?? _currentGrid.GetColumnEditSettings(fieldName)
?? (IEditSettings?)_currentGrid.GetColumnEditSettings(fieldName);
}
catch
{
// Ignore errors
}
_editSettingsCache[cacheKey] = settings;
return settings;
}
///
/// Cached version of ResolveComboBoxDisplayText with Type-based key
///
private string? ResolveComboBoxDisplayTextCached(Type dataItemType, DxComboBoxSettings settings, object value)
{
// Use settings object reference and value as cache key
var cacheKey = (dataItemType, (object)settings, value);
if (_comboBoxTextCache.TryGetValue(cacheKey, out var cached))
return cached;
var result = ResolveComboBoxDisplayText(settings, value);
_comboBoxTextCache[cacheKey] = result;
return result;
}
private string? ResolveComboBoxDisplayText(DxComboBoxSettings settings, object value)
{
if (settings.Data == null || string.IsNullOrEmpty(settings.ValueFieldName) || string.IsNullOrEmpty(settings.TextFieldName))
return null;
try
{
foreach (var item in (System.Collections.IEnumerable)settings.Data)
{
if (item == null) continue;
var itemType = item.GetType();
var valueProperty = itemType.GetProperty(settings.ValueFieldName);
var textProperty = itemType.GetProperty(settings.TextFieldName);
if (valueProperty == null || textProperty == null) continue;
var itemValue = valueProperty.GetValue(item);
if (itemValue != null && itemValue.Equals(value))
{
return textProperty.GetValue(item)?.ToString();
}
}
}
catch
{
// If lookup fails, return null and fall back to default formatting
}
return null;
}
private static string FormatValue(object? value)
{
if (value == null)
return string.Empty;
return value switch
{
DateTime dateTime => dateTime.ToString("yyyy-MM-dd HH:mm:ss"),
DateOnly dateOnly => dateOnly.ToString("yyyy-MM-dd"),
TimeOnly timeOnly => timeOnly.ToString("HH:mm:ss"),
TimeSpan timeSpan => timeSpan.ToString(@"hh\:mm\:ss"),
bool boolValue => boolValue ? "Igen" : "Nem",
decimal decValue => decValue.ToString("N2"),
double dblValue => dblValue.ToString("N2"),
float fltValue => fltValue.ToString("N2"),
int or long or short or byte => $"{value:N0}",
_ => value.ToString() ?? string.Empty
};
}
///
/// 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;
}
}
///
/// Cached version of GetAllDataColumns with Type-based key
///
private List GetAllDataColumnsCached(Type dataItemType, IMgGridBase grid)
{
if (_columnsCache.TryGetValue(dataItemType, out var cached))
return cached;
var columns = GetAllDataColumns(grid);
_columnsCache[dataItemType] = columns;
return columns;
}
protected static List GetAllDataColumns(IMgGridBase grid)
{
var columns = new List();
try
{
var allColumns = grid.GetDataColumns();
if (allColumns != null)
{
foreach (var column in allColumns)
{
if (column is DxGridDataColumn dataColumn &&
!string.IsNullOrWhiteSpace(dataColumn.FieldName))
{
columns.Add(dataColumn);
}
}
}
}
catch (Exception)
{
// Ignore errors
}
return columns;
}
///
/// Gets the EditSettings type for rendering logic
///
private EditSettingsType GetEditSettingsType(DxGridDataColumn column)
{
var dataItem = GetActiveDataItem();
if (dataItem == null) return EditSettingsType.None;
var dataItemType = dataItem.GetType();
var settings = GetEditSettingsCached(dataItemType, column.FieldName);
return settings switch
{
DxComboBoxSettings => EditSettingsType.ComboBox,
DxDateEditSettings => EditSettingsType.DateEdit,
DxTimeEditSettings => EditSettingsType.TimeEdit,
DxSpinEditSettings => EditSettingsType.SpinEdit,
DxCheckBoxSettings => EditSettingsType.CheckBox,
DxMemoSettings => EditSettingsType.Memo,
_ => EditSettingsType.None
};
}
private enum EditSettingsType
{
None,
ComboBox,
DateEdit,
TimeEdit,
SpinEdit,
CheckBox,
Memo
}
}