From 45294199cf61a13411310e99a57f4c0765f282ee Mon Sep 17 00:00:00 2001 From: Loretta Date: Wed, 17 Dec 2025 06:21:21 +0100 Subject: [PATCH] Refactor grid editing, info panel, and toolbar system - Introduce MgEditState enum and expose EditState on IMgGridBase - Replace event-based syncing state with property-based state - Redesign MgGridInfoPanel to support both view and edit modes with dynamic DevExpress editors and two-way binding - Add visual distinction for edit/view modes in info panel - Replace FruitBankToolbarTemplate with generic MgGridToolbarTemplate; toolbar adapts to grid edit/sync state - Update all grid usages to use new toolbar - Improve robustness, error handling, and maintainability throughout grid, info panel, and toolbar code --- .../Components/Grids/GridEditMode.cs | 22 + .../Components/Grids/MgGridBase.cs | 84 +-- .../Components/Grids/MgGridInfoPanel.razor | 678 +++++++++++++++++- .../Components/Grids/MgGridInfoPanel.razor.cs | 308 ++++++-- .../Grids/MgGridInfoPanel.razor.css | 19 +- 5 files changed, 992 insertions(+), 119 deletions(-) create mode 100644 AyCode.Blazor.Components/Components/Grids/GridEditMode.cs diff --git a/AyCode.Blazor.Components/Components/Grids/GridEditMode.cs b/AyCode.Blazor.Components/Components/Grids/GridEditMode.cs new file mode 100644 index 0000000..e0411da --- /dev/null +++ b/AyCode.Blazor.Components/Components/Grids/GridEditMode.cs @@ -0,0 +1,22 @@ +namespace AyCode.Blazor.Components.Components.Grids; + +/// +/// Represents the current edit state of the MgGrid +/// +public enum MgEditState +{ + /// + /// No edit operation in progress + /// + None, + + /// + /// Adding a new row + /// + New, + + /// + /// Editing an existing row + /// + Edit +} diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs b/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs index 867e8f7..930b5af 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs +++ b/AyCode.Blazor.Components/Components/Grids/MgGridBase.cs @@ -23,9 +23,9 @@ public interface IMgGridBase : IGrid bool IsSyncing { get; } /// - /// Event fired when synchronization state changes (true = syncing started, false = syncing ended) + /// Current edit state of the grid (None, New, Edit) /// - event Action? OnSyncingStateChanged; + MgEditState EditState { get; } } public abstract class MgGridBase : DxGrid, IMgGridBase, IAsyncDisposable @@ -49,7 +49,7 @@ public abstract class MgGridBase _dataSource?.IsSyncing ?? false; /// - public event Action? OnSyncingStateChanged; + public MgEditState EditState { get; private set; } = MgEditState.None; [Parameter] public bool ShowInfoPanel { get; set; } = true; @@ -319,7 +319,7 @@ public abstract class MgGridBase) - SetGridData(_dataSource!.GetReferenceInnerList()); - //else Reload(); - //_dataSource.LoadItem(_dataSource.First().Id).Forget(); if (!_isDisposed) { await OnDataSourceChanged.InvokeAsync(_dataSource); @@ -437,37 +433,31 @@ public abstract class MgGridBase.Default; - //var index = CollectionExtensions.FindIndex(_dataSource, x => equalityComparer.Equals(x.Id, dataItem.Id)); - //_dataSource.UpdateCollectionByIndex(index, dataItem, false); + // Kilépés edit módból + EditState = MgEditState.None; + + if (ShowInfoPanel && _infoPanelInstance != null) + { + _infoPanelInstance.ClearEditMode(); + } + } - //_dataSource.UpdateCollectionById(dataItem.Id, false); + private async Task OnEditCanceling(GridEditCancelingEventArgs e) + { + // Kilépés edit módból + EditState = MgEditState.None; + + if (ShowInfoPanel && _infoPanelInstance != null) + { + _infoPanelInstance.ClearEditMode(); + } } private Task SaveChangesToServerAsync() @@ -590,9 +584,9 @@ public abstract class MgGridBase(this, OnItemDeleting); base.EditModelSaving = EventCallback.Factory.Create(this, OnItemSaving); base.CustomizeEditModel = EventCallback.Factory.Create(this, OnCustomizeEditModel); - //base.customizecel= EventCallback.Factory.Create(this, OnCustomizeEditModel); base.FocusedRowChanged = EventCallback.Factory.Create(this, OnFocusedRowChanged); base.EditStart = EventCallback.Factory.Create(this, OnEditStart); + base.EditCanceling = EventCallback.Factory.Create(this, OnEditCanceling); CustomizeElement += OnCustomizeElement; @@ -606,16 +600,6 @@ public abstract class MgGridBase x.FieldName == nameof(IId.Id)); - //if (idColumn != null) - //{ - // idColumn.ShowInColumnChooser = AcDomain.IsDeveloperVersion; - // idColumn.Visible = !AcDomain.IsDeveloperVersion; - //} IsFirstInitializeParameterCore = true; } diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor b/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor index eda2423..2fa1052 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor +++ b/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor @@ -1,28 +1,43 @@ @using DevExpress.Blazor @using Microsoft.AspNetCore.Components.Rendering +@using System.Reflection @typeparam TDataItem where TDataItem : class -
- @if (_currentDataItem != null && _currentGrid != null) +
+ @if (GetActiveDataItem() != null && _currentGrid != null) { - var colSpan = _allDataColumns.Count > 10 ? 6 : 12; + var colSpan = _allDataColumns.Count > 10 ? 6 : 12; + var dataItem = GetActiveDataItem()!; - @foreach (var column in _allDataColumns) { + var displayText = GetDisplayTextFromGrid(column); + var value = GetCellValue(column); + var settingsType = GetEditSettingsType(column); + var isReadOnly = !_isEditMode || column.ReadOnly; + + ColSpanXs="@colSpan"> + + } } @@ -39,4 +54,649 @@ { return !string.IsNullOrWhiteSpace(column.Caption) ? column.Caption : column.FieldName; } + + private string GetCaptionCssClass(bool isReadOnly) + { + return isReadOnly ? "fw-semibold" : "fw-semibold text-primary"; + } + + /// + /// Renders an editable cell with two-way binding to the EditModel + /// + private RenderFragment RenderEditableCell(DxGridDataColumn column, TDataItem dataItem, object? value, string displayText, EditSettingsType settingsType) + { + return builder => + { + var seq = 0; + var fieldName = column.FieldName; + var propertyInfo = typeof(TDataItem).GetProperty(fieldName); + + if (propertyInfo == null) + { + // Fallback to readonly if property not found + RenderCellContent(column, value, displayText, settingsType)(builder); + return; + } + + var propertyType = propertyInfo.PropertyType; + var underlyingType = Nullable.GetUnderlyingType(propertyType) ?? propertyType; + + // ComboBox columns + if (settingsType == EditSettingsType.ComboBox) + { + var comboSettings = GetEditSettings(fieldName) as DxComboBoxSettings; + if (comboSettings != null) + { + RenderComboBoxEditor(builder, ref seq, dataItem, propertyInfo, comboSettings); + return; + } + } + + // Render based on property 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))) + { + 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) + { + var currentValue = (bool)(propertyInfo.GetValue(dataItem) ?? false); + + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Checked", currentValue); + builder.AddAttribute(seq++, "CheckedChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + InvokeAsync(StateHasChanged); + })); + builder.CloseComponent(); + } + + private void RenderDateTimeEditor(RenderTreeBuilder builder, ref int seq, TDataItem dataItem, PropertyInfo propertyInfo, string? displayFormat) + { + var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null; + var currentValue = propertyInfo.GetValue(dataItem); + + if (isNullable) + { + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Date", (DateTime?)currentValue); + builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + InvokeAsync(StateHasChanged); + })); + } + else + { + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Date", (DateTime)(currentValue ?? DateTime.MinValue)); + builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + InvokeAsync(StateHasChanged); + })); + } + builder.AddAttribute(seq++, "DisplayFormat", displayFormat ?? "yyyy-MM-dd HH:mm"); + builder.CloseComponent(); + } + + private void RenderDateOnlyEditor(RenderTreeBuilder builder, ref int seq, TDataItem dataItem, PropertyInfo propertyInfo, string? displayFormat) + { + var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null; + var currentValue = propertyInfo.GetValue(dataItem); + + if (isNullable) + { + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Date", (DateOnly?)currentValue); + builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + InvokeAsync(StateHasChanged); + })); + } + else + { + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Date", (DateOnly)(currentValue ?? DateOnly.MinValue)); + builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + InvokeAsync(StateHasChanged); + })); + } + builder.AddAttribute(seq++, "DisplayFormat", displayFormat ?? "yyyy-MM-dd"); + 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>(seq++); + builder.AddAttribute(seq++, "Time", (TimeOnly?)currentValue); + builder.AddAttribute(seq++, "TimeChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + InvokeAsync(StateHasChanged); + })); + } + else + { + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Time", (TimeOnly)(currentValue ?? TimeOnly.MinValue)); + builder.AddAttribute(seq++, "TimeChanged", EventCallback.Factory.Create(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>(seq++); + builder.AddAttribute(seq++, "Time", (TimeSpan?)currentValue); + builder.AddAttribute(seq++, "TimeChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + InvokeAsync(StateHasChanged); + })); + } + else + { + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Time", (TimeSpan)(currentValue ?? TimeSpan.Zero)); + builder.AddAttribute(seq++, "TimeChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + InvokeAsync(StateHasChanged); + })); + } + builder.CloseComponent(); + } + + private void RenderSpinIntEditor(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>(seq++); + builder.AddAttribute(seq++, "Value", (int?)currentValue); + builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + InvokeAsync(StateHasChanged); + })); + } + else + { + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Value", (int)(currentValue ?? 0)); + builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + InvokeAsync(StateHasChanged); + })); + } + builder.CloseComponent(); + } + + private void RenderSpinDecimalEditor(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>(seq++); + builder.AddAttribute(seq++, "Value", (decimal?)currentValue); + builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + InvokeAsync(StateHasChanged); + })); + } + else + { + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Value", (decimal)(currentValue ?? 0m)); + builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + InvokeAsync(StateHasChanged); + })); + } + builder.CloseComponent(); + } + + private void RenderSpinDoubleEditor(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>(seq++); + builder.AddAttribute(seq++, "Value", (double?)currentValue); + builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + InvokeAsync(StateHasChanged); + })); + } + else + { + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Value", (double)(currentValue ?? 0d)); + builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + InvokeAsync(StateHasChanged); + })); + } + builder.CloseComponent(); + } + + private void RenderTextBoxEditor(RenderTreeBuilder builder, ref int seq, TDataItem dataItem, PropertyInfo propertyInfo) + { + var currentValue = propertyInfo.GetValue(dataItem)?.ToString() ?? string.Empty; + + builder.OpenComponent(seq++); + builder.AddAttribute(seq++, "Text", currentValue); + builder.AddAttribute(seq++, "TextChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + InvokeAsync(StateHasChanged); + })); + builder.CloseComponent(); + } + + private void RenderMemoEditor(RenderTreeBuilder builder, ref int seq, TDataItem dataItem, PropertyInfo propertyInfo) + { + var currentValue = propertyInfo.GetValue(dataItem)?.ToString() ?? string.Empty; + + builder.OpenComponent(seq++); + builder.AddAttribute(seq++, "Text", currentValue); + builder.AddAttribute(seq++, "TextChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + InvokeAsync(StateHasChanged); + })); + builder.AddAttribute(seq++, "Rows", 3); + builder.CloseComponent(); + } + + private void RenderComboBoxEditor(RenderTreeBuilder builder, ref int seq, TDataItem 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) + { + 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(seq++); + builder.AddAttribute(seq++, "Text", displayText); + builder.AddAttribute(seq++, "ReadOnly", true); + builder.CloseComponent(); + } + } + + private void RenderComboBoxInt(RenderTreeBuilder builder, ref int seq, TDataItem dataItem, PropertyInfo propertyInfo, DxComboBoxSettings settings, Type itemType, object? currentValue) + { + var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null; + + // Create the generic ComboBox type: DxComboBox or DxComboBox + var comboType = isNullable + ? typeof(DxComboBox<,>).MakeGenericType(itemType, typeof(int?)) + : typeof(DxComboBox<,>).MakeGenericType(itemType, typeof(int)); + + builder.OpenComponent(seq++, comboType); + 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?); + builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + StateHasChanged(); + })); + } + else + { + builder.AddAttribute(seq++, "Value", currentValue is int intVal ? intVal : 0); + builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + 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) + { + var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null; + + var comboType = isNullable + ? typeof(DxComboBox<,>).MakeGenericType(itemType, typeof(long?)) + : typeof(DxComboBox<,>).MakeGenericType(itemType, typeof(long)); + + builder.OpenComponent(seq++, comboType); + 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?); + builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + StateHasChanged(); + })); + } + else + { + builder.AddAttribute(seq++, "Value", currentValue is long longVal ? longVal : 0L); + builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + 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) + { + var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null; + + var comboType = isNullable + ? typeof(DxComboBox<,>).MakeGenericType(itemType, typeof(Guid?)) + : typeof(DxComboBox<,>).MakeGenericType(itemType, typeof(Guid)); + + builder.OpenComponent(seq++, comboType); + 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?); + builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + StateHasChanged(); + })); + } + else + { + builder.AddAttribute(seq++, "Value", currentValue is Guid guidVal ? guidVal : Guid.Empty); + builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create(this, newValue => + { + propertyInfo.SetValue(dataItem, newValue); + StateHasChanged(); + })); + } + + builder.AddAttribute(seq++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto); + builder.CloseComponent(); + } + + private RenderFragment RenderCellContent(DxGridDataColumn column, object? value, string displayText, EditSettingsType settingsType) + { + return builder => + { + var seq = 0; + + // If column has EditSettings, render based on that + switch (settingsType) + { + case EditSettingsType.ComboBox: + // ComboBox columns show resolved display text + builder.OpenComponent(seq++); + builder.AddAttribute(seq++, "Text", displayText); + builder.AddAttribute(seq++, "ReadOnly", true); + builder.CloseComponent(); + return; + + case EditSettingsType.CheckBox when value is bool boolVal: + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Checked", boolVal); + builder.AddAttribute(seq++, "ReadOnly", true); + 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.OpenComponent(seq++); + builder.AddAttribute(seq++, "Text", displayText); + builder.AddAttribute(seq++, "ReadOnly", true); + builder.AddAttribute(seq++, "CssClass", "text-end"); + builder.CloseComponent(); + return; + + case EditSettingsType.Memo: + builder.OpenComponent(seq++); + builder.AddAttribute(seq++, "Text", displayText); + builder.AddAttribute(seq++, "ReadOnly", true); + builder.AddAttribute(seq++, "Rows", 3); + builder.CloseComponent(); + return; + } + + // Default: render based on value type + switch (value) + { + case bool boolValue: + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Checked", boolValue); + builder.AddAttribute(seq++, "ReadOnly", true); + builder.CloseComponent(); + break; + + case DateTime dateValue: + builder.OpenComponent>(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>(seq++); + builder.AddAttribute(seq++, "Date", dateOnlyValue); + builder.AddAttribute(seq++, "ReadOnly", true); + builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat ?? "yyyy-MM-dd"); + builder.CloseComponent(); + break; + + case TimeOnly timeOnlyValue: + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Time", timeOnlyValue); + builder.AddAttribute(seq++, "ReadOnly", true); + builder.CloseComponent(); + break; + + case TimeSpan timeSpanValue: + builder.OpenComponent>(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.OpenComponent(seq++); + builder.AddAttribute(seq++, "Text", displayText); + builder.AddAttribute(seq++, "ReadOnly", true); + builder.AddAttribute(seq++, "CssClass", "text-end"); + builder.CloseComponent(); + break; + + default: + builder.OpenComponent(seq++); + builder.AddAttribute(seq++, "Text", displayText); + builder.AddAttribute(seq++, "ReadOnly", true); + builder.CloseComponent(); + break; + } + }; + } + + private void RenderDateEditor(RenderTreeBuilder builder, ref int seq, object? value, string? displayFormat) + { + switch (value) + { + case DateTime dateTime: + builder.OpenComponent>(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>(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>(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(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>(seq++); + builder.AddAttribute(seq++, "Time", timeOnly); + builder.AddAttribute(seq++, "ReadOnly", true); + builder.CloseComponent(); + break; + case TimeSpan timeSpan: + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Time", timeSpan); + builder.AddAttribute(seq++, "ReadOnly", true); + builder.CloseComponent(); + break; + case DateTime dateTime: + builder.OpenComponent>(seq++); + builder.AddAttribute(seq++, "Time", dateTime); + builder.AddAttribute(seq++, "ReadOnly", true); + builder.CloseComponent(); + break; + default: + builder.OpenComponent(seq++); + builder.AddAttribute(seq++, "Text", value?.ToString() ?? string.Empty); + builder.AddAttribute(seq++, "ReadOnly", true); + builder.CloseComponent(); + break; + } + } } diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.cs b/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.cs index f75d4dc..f16eb24 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.cs +++ b/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.cs @@ -1,6 +1,5 @@ using DevExpress.Blazor; using Microsoft.AspNetCore.Components; -using System.Reflection; namespace AyCode.Blazor.Components.Components.Grids; @@ -8,41 +7,249 @@ public partial class MgGridInfoPanel : ComponentBase where TDataItem { private DxGrid? _currentGrid; private TDataItem? _currentDataItem; + private int _focusedRowVisibleIndex = -1; private List _allDataColumns = []; + // Edit mode state + private bool _isEditMode; + private TDataItem? _editModel; + + // Cache for edit settings to avoid repeated lookups + private readonly Dictionary _editSettingsCache = []; + /// - /// Refreshes the InfoPanel with data from the specified grid row + /// Refreshes the InfoPanel with data from the specified grid row (view mode) /// - /// The grid instance - /// The data item from the focused row - public void RefreshData(DxGrid grid, TDataItem? dataItem) + public void RefreshData(DxGrid grid, TDataItem? dataItem, int visibleIndex = -1) { ArgumentNullException.ThrowIfNull(grid); _currentGrid = grid; _currentDataItem = dataItem; - - if (_currentGrid != null && _currentDataItem != null) + _focusedRowVisibleIndex = visibleIndex; + _editSettingsCache.Clear(); + + // Ha nem vagyunk edit módban, frissítjük az oszlopokat + if (!_isEditMode) { - _allDataColumns = GetAllDataColumns(_currentGrid); - } - else - { - _allDataColumns = []; + if (_currentGrid != null && _currentDataItem != null) + { + _allDataColumns = GetAllDataColumns(_currentGrid); + } + else + { + _allDataColumns = []; + } } - InvokeAsync(StateHasChanged); + StateHasChanged(); } /// - /// Clears the InfoPanel + /// Sets the InfoPanel to edit mode with the given edit model + /// + public void SetEditMode(TDataItem editModel) + { + _editModel = editModel; + _isEditMode = true; + _currentDataItem = editModel; + + if (_currentGrid != null) + { + _allDataColumns = GetAllDataColumns(_currentGrid); + } + + StateHasChanged(); + } + + /// + /// Clears edit mode and returns to view mode + /// + public void ClearEditMode() + { + _isEditMode = false; + _editModel = null; + StateHasChanged(); + } + + /// + /// Clears the InfoPanel completely /// public void Clear() { _currentGrid = null; _currentDataItem = null; + _focusedRowVisibleIndex = -1; _allDataColumns = []; - InvokeAsync(StateHasChanged); + _editSettingsCache.Clear(); + _isEditMode = false; + _editModel = null; + StateHasChanged(); + } + + /// + /// Gets the data item to display/edit (EditModel in edit mode, otherwise CurrentDataItem) + /// + private TDataItem? 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) + { + 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; + + // Try to resolve display text from EditSettings + var editSettings = GetEditSettings(column.FieldName); + if (editSettings != null) + { + var displayText = ResolveEditSettingsDisplayText(editSettings, 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 caching) + /// + private IEditSettings? GetEditSettings(string fieldName) + { + if (_currentGrid == null || string.IsNullOrEmpty(fieldName)) + return null; + + if (_editSettingsCache.TryGetValue(fieldName, out var cached)) + return cached; + + IEditSettings? settings = null; + try + { + // Try each EditSettings type + settings = TryGetEditSettings(fieldName) + ?? TryGetEditSettings(fieldName) + ?? TryGetEditSettings(fieldName) + ?? TryGetEditSettings(fieldName) + ?? TryGetEditSettings(fieldName) + ?? TryGetEditSettings(fieldName) + ?? TryGetEditSettings(fieldName) + ?? TryGetEditSettings(fieldName) + ?? (IEditSettings?)TryGetEditSettings(fieldName); + } + catch + { + // Ignore errors + } + + _editSettingsCache[fieldName] = settings; + return settings; + } + + private T? TryGetEditSettings(string fieldName) where T : class, IEditSettings + { + try + { + return _currentGrid?.GetColumnEditSettings(fieldName); + } + catch + { + return null; + } + } + + /// + /// Resolves display text based on EditSettings type + /// + private string? ResolveEditSettingsDisplayText(IEditSettings settings, object value) + { + return settings switch + { + DxComboBoxSettings comboSettings => ResolveComboBoxDisplayText(comboSettings, value), + _ => null + }; + } + + /// + /// Resolves the display text from a ComboBox data source + /// + 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 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 => string.Format("{0:N0}", value), + _ => value.ToString() ?? string.Empty + }; } private List GetAllDataColumns(DxGrid grid) @@ -56,8 +263,6 @@ public partial class MgGridInfoPanel : ComponentBase where TDataItem { foreach (var column in allColumns) { - // Minden DxGridDataColumn-t felveszünk, ha van FieldName-je - // NEM vizsgáljuk a Visible property-t! if (column is DxGridDataColumn dataColumn && !string.IsNullOrWhiteSpace(dataColumn.FieldName)) { @@ -76,28 +281,13 @@ public partial class MgGridInfoPanel : ComponentBase where TDataItem private object? GetCellValue(DxGridDataColumn column) { - if (_currentDataItem == null || string.IsNullOrWhiteSpace(column.FieldName)) + var dataItem = GetActiveDataItem(); + if (_currentGrid == null || dataItem == null || string.IsNullOrWhiteSpace(column.FieldName)) return null; try { - var fieldName = column.FieldName; - var properties = fieldName.Split('.'); - object? currentValue = _currentDataItem; - - foreach (var propertyName in properties) - { - if (currentValue == null) - break; - - var propertyInfo = currentValue.GetType().GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance); - if (propertyInfo == null) - return null; - - currentValue = propertyInfo.GetValue(currentValue); - } - - return currentValue; + return _currentGrid.GetDataItemValue(dataItem, column.FieldName); } catch { @@ -105,31 +295,33 @@ public partial class MgGridInfoPanel : ComponentBase where TDataItem } } - private string GetDisplayText(DxGridDataColumn column, object? value) + /// + /// Gets the EditSettings type for rendering logic + /// + private EditSettingsType GetEditSettingsType(DxGridDataColumn column) { - if (value == null) - return string.Empty; - - if (value is DateTime dateTime) + var settings = GetEditSettings(column.FieldName); + + return settings switch { - return dateTime.ToString("yyyy-MM-dd HH:mm:ss"); - } + DxComboBoxSettings => EditSettingsType.ComboBox, + DxDateEditSettings => EditSettingsType.DateEdit, + DxTimeEditSettings => EditSettingsType.TimeEdit, + DxSpinEditSettings => EditSettingsType.SpinEdit, + DxCheckBoxSettings => EditSettingsType.CheckBox, + DxMemoSettings => EditSettingsType.Memo, + _ => EditSettingsType.None + }; + } - if (value is bool boolValue) - { - return boolValue ? "Igen" : "Nem"; - } - - if (value is decimal || value is double || value is float) - { - return string.Format("{0:N2}", value); - } - - if (value is int || value is long || value is short || value is byte) - { - return string.Format("{0:N0}", value); - } - - return value.ToString() ?? string.Empty; + private enum EditSettingsType + { + None, + ComboBox, + DateEdit, + TimeEdit, + SpinEdit, + CheckBox, + Memo } } diff --git a/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.css b/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.css index fd262fb..73ec890 100644 --- a/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.css +++ b/AyCode.Blazor.Components/Components/Grids/MgGridInfoPanel.razor.css @@ -2,7 +2,18 @@ height: 100%; overflow-y: auto; padding: 1rem; + background-color: var(--dxbl-bg-secondary, #f8f9fa); + transition: background-color 0.3s ease, border-color 0.3s ease; +} + +.mg-grid-info-panel.edit-mode { + background-color: #fffbeb; + border-left: 3px solid #f59e0b; +} + +.mg-grid-info-panel.view-mode { background-color: #f8f9fa; + border-left: 3px solid transparent; } .info-panel-form { @@ -11,16 +22,20 @@ .info-panel-form .fw-semibold { font-weight: 600; - color: #495057; + color: var(--dxbl-text-secondary, #495057); font-size: 0.875rem; } +.info-panel-form .fw-semibold.text-primary { + color: var(--dxbl-primary, #0d6efd); +} + .info-panel-empty { display: flex; align-items: center; justify-content: center; height: 100%; - color: #6c757d; + color: var(--dxbl-text-secondary, #6c757d); font-style: italic; }