Compare commits

..

No commits in common. "15776ca53767dd929ff67f28679cb326c9fd0160" and "920bc299aa2ee511317ed0b201f4cbdc4e17ca57" have entirely different histories.

15 changed files with 48 additions and 1951 deletions

View File

@ -1,22 +0,0 @@
namespace AyCode.Blazor.Components.Components.Grids;
/// <summary>
/// Represents the current edit state of the MgGrid
/// </summary>
public enum MgGridEditState
{
/// <summary>
/// No edit operation in progress
/// </summary>
None,
/// <summary>
/// Adding a new row
/// </summary>
New,
/// <summary>
/// Editing an existing row
/// </summary>
Edit
}

View File

@ -8,10 +8,8 @@ using AyCode.Services.SignalRs;
using AyCode.Utils.Extensions;
using DevExpress.Blazor;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using System.ComponentModel;
using System.Reflection;
using DevExpress.Blazor.Internal;
namespace AyCode.Blazor.Components.Components.Grids;
@ -21,48 +19,11 @@ public interface IMgGridBase : IGrid
/// Indicates whether any synchronization operation is in progress
/// </summary>
bool IsSyncing { get; }
string Caption { get; set; }
/// <summary>
/// Current edit state of the grid (None, New, Edit)
/// Event fired when synchronization state changes (true = syncing started, false = syncing ended)
/// </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>
void StepPrevRow();
/// <summary>
/// Navigates to the next row in the grid
/// </summary>
void StepNextRow();
/// <summary>
/// InfoPanel instance for displaying row details (from wrapper)
/// </summary>
IInfoPanelBase? InfoPanelInstance { get; }
/// <summary>
/// Whether the grid/wrapper is currently in fullscreen mode
/// </summary>
bool IsFullscreen { get; }
/// <summary>
/// Toggles fullscreen mode for the grid (or wrapper if available)
/// </summary>
void ToggleFullscreen();
event Action<bool>? OnSyncingStateChanged;
}
public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClient> : DxGrid, IMgGridBase, IAsyncDisposable
@ -84,120 +45,14 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
/// <inheritdoc />
public bool IsSyncing => _dataSource?.IsSyncing ?? false;
/// <inheritdoc />
public MgGridEditState GridEditState { get; private set; } = MgGridEditState.None;
/// <inheritdoc />
[CascadingParameter]
public IMgGridBase? ParentGrid { get; set; }
/// <inheritdoc />
public IMgGridBase GetRootGrid()
{
var current = (IMgGridBase)this;
while (current.ParentGrid != null)
{
current = current.ParentGrid;
}
return current;
}
/// <summary>
/// Reference to the wrapper component for grid-InfoPanel communication
/// </summary>
[CascadingParameter]
public MgGridWithInfoPanel? GridWrapper { get; set; }
private object _focusedDataItem;
/// <summary>
/// InfoPanel instance for displaying row details (from wrapper or direct)
/// </summary>
public IInfoPanelBase? InfoPanelInstance
{
get => GridWrapper?.InfoPanelInstance;
set { /* Set through wrapper */ }
}
/// <inheritdoc />
public bool IsFullscreen => GridWrapper?.IsFullscreen ?? _isStandaloneFullscreen;
public event Action<bool>? OnSyncingStateChanged;
private bool _isStandaloneFullscreen;
/// <inheritdoc />
public void ToggleFullscreen()
{
if (GridWrapper != null)
{
// Ha van wrapper, azt váltjuk fullscreen-be
GridWrapper.ToggleFullscreen();
}
else
{
// Ha nincs wrapper, saját fullscreen állapotot használunk
_isStandaloneFullscreen = !_isStandaloneFullscreen;
InvokeAsync(StateHasChanged);
}
}
public MgGridBase() : base()
{
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
var seq = 0;
// 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 =>
{
if (_isStandaloneFullscreen && GridWrapper == null)
{
// Standalone fullscreen mode - Bootstrap 5 fullscreen overlay
contentBuilder.OpenElement(0, "div");
contentBuilder.AddAttribute(1, "class", "mg-fullscreen-overlay");
// Header
contentBuilder.OpenElement(2, "div");
contentBuilder.AddAttribute(3, "class", "mg-fullscreen-header");
contentBuilder.OpenElement(4, "span");
contentBuilder.AddAttribute(5, "class", "mg-fullscreen-title");
contentBuilder.AddContent(6, Caption);
contentBuilder.CloseElement(); // span
contentBuilder.OpenElement(7, "button");
contentBuilder.AddAttribute(8, "type", "button");
contentBuilder.AddAttribute(9, "class", "btn-close btn-close-white");
contentBuilder.AddAttribute(10, "aria-label", "Close");
contentBuilder.AddAttribute(11, "onclick", EventCallback.Factory.Create<Microsoft.AspNetCore.Components.Web.MouseEventArgs>(this, () =>
{
_isStandaloneFullscreen = false;
InvokeAsync(StateHasChanged);
}));
contentBuilder.CloseElement(); // button
contentBuilder.CloseElement(); // header div
// Body
contentBuilder.OpenElement(12, "div");
contentBuilder.AddAttribute(13, "class", "mg-fullscreen-body");
base.BuildRenderTree(contentBuilder);
contentBuilder.CloseElement(); // body div
contentBuilder.CloseElement(); // overlay div
}
else
{
base.BuildRenderTree(contentBuilder);
}
}));
builder.CloseComponent();
}
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);
@ -208,8 +63,6 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
[Parameter] public string? KeyFieldNameToParentId { get; set; }
[Parameter] public object[]? ContextIds { get; set; }
[Parameter] public string Caption { get; set; } = typeof(TDataItem).Name;
public bool IsMasterGrid => ParentDataItem == null;
protected PropertyInfo? KeyFieldPropertyInfoToParent;
@ -251,9 +104,6 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
protected new EventCallback<GridCustomizeEditModelEventArgs> CustomizeEditModel { get; set; }
[Parameter] public EventCallback<GridCustomizeEditModelEventArgs> OnGridCustomizeEditModel { get; set; }
protected new EventCallback<GridFocusedRowChangedEventArgs> FocusedRowChanged { get; set; }
[Parameter] public EventCallback<GridFocusedRowChangedEventArgs> OnGridFocusedRowChanged { get; set; }
[Parameter] public EventCallback<IList<TDataItem>> OnDataSourceChanged { get; set; }
[Parameter] public EventCallback<GridDataItemChangingEventArgs<TDataItem>> OnGridItemChanging { get; set; }
@ -321,6 +171,7 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
_dataSource = (TSignalRDataSource)Activator.CreateInstance(typeof(TSignalRDataSource), SignalRClient, crudTags, ContextIds)!;
_dataSource.FilterText = FilterText;
//_dataSource = new SignalRDataSource<TDataItem>(SignalRClient, crudTags, ContextIds) { FilterText = FilterText };
SetGridData(_dataSource.GetReferenceInnerList());
@ -336,7 +187,7 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
if (_isDisposed) return;
// Forward the event to external subscribers
//OnSyncingStateChanged?.Invoke(isSyncing);
OnSyncingStateChanged?.Invoke(isSyncing);
// Trigger UI update
InvokeAsync(StateHasChanged);
@ -366,8 +217,12 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
Logger.Debug($"{_gridLogName} OnDataSourceLoaded; Count: {_dataSource?.Count}");
SetGridData(_dataSource!.GetReferenceInnerList());
//if(_dataSourceParam.GetType() == typeof()AcObservableCollection<TDataItem>)
SetGridData(_dataSource!.GetReferenceInnerList());
//else Reload();
//_dataSource.LoadItem(_dataSource.First().Id).Forget();
if (!_isDisposed)
{
await OnDataSourceChanged.InvokeAsync(_dataSource);
@ -450,44 +305,29 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
e.EditModel = editModel;
}
// Set edit state
GridEditState = e.IsNew ? MgGridEditState.New : MgGridEditState.Edit;
await OnGridCustomizeEditModel.InvokeAsync(e);
// Update InfoPanel to edit mode
InfoPanelInstance?.SetEditMode(editModel);
await InvokeAsync(StateHasChanged);
}
private async Task OnEditStart(GridEditStartEventArgs e)
{
var dataItem = (e.DataItem as TDataItem)!;
await OnGridEditStart.InvokeAsync(e);
}
protected virtual async Task OnFocusedRowChanged(GridFocusedRowChangedEventArgs e)
{
_focusedDataItem = e.DataItem;
//void Grid_CustomizeEditModel(GridCustomizeEditModelEventArgs e)
//{
// var model = e.EditModel as EditableWorkOrder;
// if (model == null)
// {
// model = new EditableWorkOrder();
var infoPanelInstance = 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();
}
// Frissítjük az InfoPanel-t az új sor adataival
infoPanelInstance.RefreshData(this, e.DataItem, e.VisibleIndex);
}
await OnGridFocusedRowChanged.InvokeAsync(e);
}
// model.WorkOrderNum = "123";
// model.Description = "hey";
// e.EditModel = model;
// }
//}
private async Task OnItemSaving(GridEditModelSavingEventArgs e)
{
var dataItem = (e.EditModel as TDataItem)!;
@ -495,6 +335,15 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
if (e.IsNew)
{
if (!HasIdValue(dataItem)) SetNewId(dataItem);
//if (ParentDataItem != null && !KeyFieldNameToParentId.IsNullOrWhiteSpace())
//{
// Type examType = typeof(TDataItem);
// // Change the static property value.
// PropertyInfo piShared = examType.GetProperty(KeyFieldNameToParentId);
// piShared.SetValue(dataItem, ParentDataItem.Id);
//}
}
var logText = e.IsNew ? "add" : "update";
@ -515,20 +364,11 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
}
else await UpdateDataItemAsync(dataItem);
GridEditState = MgGridEditState.None;
//var equalityComparer = EqualityComparer<TId>.Default;
//var index = CollectionExtensions.FindIndex(_dataSource, x => equalityComparer.Equals(x.Id, dataItem.Id));
//_dataSource.UpdateCollectionByIndex(index, dataItem, false);
InfoPanelInstance?.ClearEditMode();
await InvokeAsync(StateHasChanged);
}
private async Task OnEditCanceling(GridEditCancelingEventArgs e)
{
GridEditState = MgGridEditState.None;
InfoPanelInstance?.ClearEditMode();
await InvokeAsync(StateHasChanged);
//_dataSource.UpdateCollectionById<TId>(dataItem.Id, false);
}
private Task SaveChangesToServerAsync()
@ -591,27 +431,6 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
e.Column.Visible = AcDomain.IsDeveloperVersion;
e.Column.ShowInColumnChooser = AcDomain.IsDeveloperVersion;
}
// Apply edit mode background to the row being edited
if (e.ElementType == GridElementType.DataRow && GridEditState != MgGridEditState.None)
{
if (e.VisibleIndex == GetFocusedRowIndex())
{
e.Style = string.IsNullOrEmpty(e.Style)
? "background-color: #fffbeb;"
: e.Style + " background-color: #fffbeb;";
}
}
// Apply edit mode background to cells in the edited row
else if (e.ElementType == GridElementType.DataCell && GridEditState != MgGridEditState.None)
{
if (e.VisibleIndex == GetFocusedRowIndex())
{
e.Style = string.IsNullOrEmpty(e.Style)
? "background-color: #fffbeb;"
: e.Style + " background-color: #fffbeb;";
}
}
}
protected override async Task SetParametersAsyncCore(ParameterView parameters)
@ -626,9 +445,8 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
base.DataItemDeleting = EventCallback.Factory.Create<GridDataItemDeletingEventArgs>(this, OnItemDeleting);
base.EditModelSaving = EventCallback.Factory.Create<GridEditModelSavingEventArgs>(this, OnItemSaving);
base.CustomizeEditModel = EventCallback.Factory.Create<GridCustomizeEditModelEventArgs>(this, OnCustomizeEditModel);
base.FocusedRowChanged = EventCallback.Factory.Create<GridFocusedRowChangedEventArgs>(this, OnFocusedRowChanged);
//base.customizecel= EventCallback.Factory.Create<GridCustomizeEditModelEventArgs>(this, OnCustomizeEditModel);
base.EditStart = EventCallback.Factory.Create<GridEditStartEventArgs>(this, OnEditStart);
base.EditCanceling = EventCallback.Factory.Create<GridEditCancelingEventArgs>(this, OnEditCanceling);
CustomizeElement += OnCustomizeElement;
@ -642,6 +460,16 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
HighlightRowOnHover = true;
AutoCollapseDetailRow = true;
AutoExpandAllGroupRows = false;
//KeyboardNavigationEnabled = true;
//var dataColumns = GetDataColumns();
//var idColumn = dataColumns.FirstOrDefault(x => x.FieldName == nameof(IId<TId>.Id));
//if (idColumn != null)
//{
// idColumn.ShowInColumnChooser = AcDomain.IsDeveloperVersion;
// idColumn.Visible = !AcDomain.IsDeveloperVersion;
//}
IsFirstInitializeParameterCore = true;
}
@ -689,31 +517,6 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
return _dataSource.LoadDataSourceAsync(false);
}
/// <summary>
/// Navigates to the previous row in the grid
/// </summary>
public void StepPrevRow()
{
var currentIndex = GetFocusedRowIndex();
if (currentIndex > 0)
{
SetFocusedRowIndex(currentIndex - 1);
}
}
/// <summary>
/// Navigates to the next row in the grid
/// </summary>
public void StepNextRow()
{
var currentIndex = GetFocusedRowIndex();
var visibleRowCount = GetVisibleRowCount();
if (currentIndex >= 0 && currentIndex < visibleRowCount - 1)
{
SetFocusedRowIndex(currentIndex + 1);
}
}
public async ValueTask DisposeAsync()
{
if (_isDisposed) return;

View File

@ -1,28 +0,0 @@
using DevExpress.Blazor;
using Microsoft.AspNetCore.Components;
namespace AyCode.Blazor.Components.Components.Grids;
/// <summary>
/// Extended DxGridDataColumn with additional parameters for InfoPanel support.
/// </summary>
public class MgGridDataColumn : DxGridDataColumn
{
/// <summary>
/// Whether this column should be visible in the InfoPanel. Default is true.
/// </summary>
[Parameter]
public bool ShowInInfoPanel { get; set; } = true;
/// <summary>
/// Custom display format for InfoPanel (overrides DisplayFormat if set).
/// </summary>
[Parameter]
public string? InfoPanelDisplayFormat { get; set; }
/// <summary>
/// Column order in InfoPanel (lower = earlier). Default is int.MaxValue.
/// </summary>
[Parameter]
public int InfoPanelOrder { get; set; } = int.MaxValue;
}

View File

@ -1,10 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace AyCode.Blazor.Components.Components.Grids
{
internal class MgGridHelper
{
}
}

View File

@ -1,383 +0,0 @@
@using DevExpress.Blazor
@using Microsoft.AspNetCore.Components.Rendering
@using System.Reflection
<div @ref="_panelElement" class="mg-grid-info-panel @(_isEditMode ? "edit-mode" : "") @GetColumnCountClass()">
@* Header *@
@if (HeaderTemplate != null)
{
@HeaderTemplate(GetActiveDataItem())
}
else if (_currentGrid != null)
{
<div class="mg-info-panel-header">@_currentGrid.Caption</div>
}
@* Toolbar *@
@if (_currentGrid != null)
{
<div class="mg-info-panel-toolbar">
<MgGridToolbarTemplate Grid="_currentGrid" OnlyGridEditTools="true" ShowOnlyIcon="true" />
</div>
}
@* Content *@
<div class="mg-info-panel-content">
@if (GetActiveDataItem() != null && _currentGrid != null)
{
@if (BeforeColumnsTemplate != null)
{
@BeforeColumnsTemplate(GetActiveDataItem())
}
@if (ColumnsTemplate != null)
{
@ColumnsTemplate(GetActiveDataItem())
}
else
{
@RenderDefaultColumns()
}
@if (AfterColumnsTemplate != null)
{
@AfterColumnsTemplate(GetActiveDataItem())
}
}
else
{
<div class="mg-info-panel-empty">
<p>Válasszon ki egy sort az adatok megtekintéséhez</p>
</div>
}
</div>
@* Footer *@
@if (FooterTemplate != null)
{
@FooterTemplate(GetActiveDataItem())
}
</div>
@code {
[Parameter] public RenderFragment<object?>? HeaderTemplate { get; set; }
[Parameter] public RenderFragment<object?>? BeforeColumnsTemplate { get; set; }
[Parameter] public RenderFragment<object?>? ColumnsTemplate { get; set; }
[Parameter] public RenderFragment<object?>? AfterColumnsTemplate { get; set; }
[Parameter] public RenderFragment<object?>? FooterTemplate { get; set; }
/// <summary>
/// Called when the data item changes (row selection changed)
/// </summary>
[Parameter] public EventCallback<object?> OnDataItemChanged { get; set; }
private string GetColumnCountClass() => FixedColumnCount switch
{
1 => "mg-columns-1",
2 => "mg-columns-2",
3 => "mg-columns-3",
4 => "mg-columns-4",
_ => ""
};
private RenderFragment RenderDefaultColumns() => builder =>
{
var dataItem = GetActiveDataItem();
if (dataItem == null) return;
var dataItemType = dataItem.GetType();
var seq = 0;
builder.OpenElement(seq++, "div");
builder.AddAttribute(seq++, "class", "mg-info-panel-grid");
foreach (var column in GetVisibleColumns())
{
var displayText = GetDisplayTextFromGrid(column);
var value = GetCellValue(column);
var settingsType = GetEditSettingsType(column);
var isEditable = _isEditMode && !column.ReadOnly;
builder.OpenElement(seq++, "div");
builder.AddAttribute(seq++, "class", "mg-info-panel-item");
builder.OpenElement(seq++, "label");
builder.AddAttribute(seq++, "class", isEditable ? "mg-info-panel-label editable" : "mg-info-panel-label");
builder.AddContent(seq++, GetColumnCaption(column));
builder.CloseElement();
builder.OpenElement(seq++, "div");
if (isEditable)
{
RenderEditableCell(column, dataItem, dataItemType, value, displayText, settingsType)(builder);
}
else
{
RenderCellContent(value, displayText)(builder);
}
builder.CloseElement();
builder.CloseElement();
}
builder.CloseElement();
};
private static string GetColumnCaption(DxGridDataColumn column) =>
!string.IsNullOrWhiteSpace(column.Caption) ? column.Caption : column.FieldName;
private RenderFragment RenderEditableCell(DxGridDataColumn column, object dataItem, Type dataItemType, object? value, string displayText, EditSettingsType settingsType)
{
return builder =>
{
var seq = 0;
var propertyInfo = dataItemType.GetProperty(column.FieldName);
if (propertyInfo == null)
{
RenderCellContent(value, displayText)(builder);
return;
}
var underlyingType = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;
if (settingsType == EditSettingsType.ComboBox && GetEditSettings(column.FieldName) is DxComboBoxSettings comboSettings)
{
RenderComboBoxEditor(builder, ref seq, dataItem, propertyInfo, comboSettings);
return;
}
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(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) RenderMemoEditor(builder, ref seq, dataItem, propertyInfo);
else RenderTextBoxEditor(builder, ref seq, dataItem, propertyInfo);
};
}
private void RenderCheckBoxEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo)
{
builder.OpenComponent<DxCheckBox<bool>>(seq++);
builder.AddAttribute(seq++, "Checked", (bool)(propertyInfo.GetValue(dataItem) ?? false));
builder.AddAttribute(seq++, "CheckedChanged", EventCallback.Factory.Create<bool>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
builder.CloseComponent();
}
private void RenderDateTimeEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo, string? displayFormat)
{
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
var value = propertyInfo.GetValue(dataItem);
if (isNullable)
{
builder.OpenComponent<DxDateEdit<DateTime?>>(seq++);
builder.AddAttribute(seq++, "Date", (DateTime?)value);
builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create<DateTime?>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
}
else
{
builder.OpenComponent<DxDateEdit<DateTime>>(seq++);
builder.AddAttribute(seq++, "Date", (DateTime)(value ?? DateTime.MinValue));
builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create<DateTime>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
}
builder.AddAttribute(seq++, "DisplayFormat", displayFormat ?? "yyyy-MM-dd HH:mm");
builder.CloseComponent();
}
private void RenderDateOnlyEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo, string? displayFormat)
{
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
var value = propertyInfo.GetValue(dataItem);
if (isNullable)
{
builder.OpenComponent<DxDateEdit<DateOnly?>>(seq++);
builder.AddAttribute(seq++, "Date", (DateOnly?)value);
builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create<DateOnly?>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
}
else
{
builder.OpenComponent<DxDateEdit<DateOnly>>(seq++);
builder.AddAttribute(seq++, "Date", (DateOnly)(value ?? DateOnly.MinValue));
builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create<DateOnly>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
}
builder.AddAttribute(seq++, "DisplayFormat", displayFormat ?? "yyyy-MM-dd");
builder.CloseComponent();
}
private void RenderSpinIntEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo)
{
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
var value = propertyInfo.GetValue(dataItem);
if (isNullable)
{
builder.OpenComponent<DxSpinEdit<int?>>(seq++);
builder.AddAttribute(seq++, "Value", (int?)value);
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<int?>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
}
else
{
builder.OpenComponent<DxSpinEdit<int>>(seq++);
builder.AddAttribute(seq++, "Value", (int)(value ?? 0));
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<int>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
}
builder.CloseComponent();
}
private void RenderSpinDecimalEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo)
{
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
var value = propertyInfo.GetValue(dataItem);
if (isNullable)
{
builder.OpenComponent<DxSpinEdit<decimal?>>(seq++);
builder.AddAttribute(seq++, "Value", (decimal?)value);
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<decimal?>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
}
else
{
builder.OpenComponent<DxSpinEdit<decimal>>(seq++);
builder.AddAttribute(seq++, "Value", (decimal)(value ?? 0m));
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<decimal>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
}
builder.CloseComponent();
}
private void RenderSpinDoubleEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo)
{
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
var value = propertyInfo.GetValue(dataItem);
if (isNullable)
{
builder.OpenComponent<DxSpinEdit<double?>>(seq++);
builder.AddAttribute(seq++, "Value", (double?)value);
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<double?>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
}
else
{
builder.OpenComponent<DxSpinEdit<double>>(seq++);
builder.AddAttribute(seq++, "Value", (double)(value ?? 0d));
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<double>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
}
builder.CloseComponent();
}
private void RenderTextBoxEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo)
{
builder.OpenComponent<DxTextBox>(seq++);
builder.AddAttribute(seq++, "Text", propertyInfo.GetValue(dataItem)?.ToString() ?? "");
builder.AddAttribute(seq++, "TextChanged", EventCallback.Factory.Create<string>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
builder.CloseComponent();
}
private void RenderMemoEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo)
{
builder.OpenComponent<DxMemo>(seq++);
builder.AddAttribute(seq++, "Text", propertyInfo.GetValue(dataItem)?.ToString() ?? "");
builder.AddAttribute(seq++, "TextChanged", EventCallback.Factory.Create<string>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
builder.AddAttribute(seq++, "Rows", 3);
builder.CloseComponent();
}
private void RenderComboBoxEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo, DxComboBoxSettings settings)
{
var value = propertyInfo.GetValue(dataItem);
var underlyingType = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;
var itemType = settings.Data?.GetType().GetGenericArguments().FirstOrDefault() ?? typeof(object);
if (underlyingType == typeof(int))
RenderComboBoxInt(builder, ref seq, dataItem, propertyInfo, settings, itemType, value);
else if (underlyingType == typeof(long))
RenderComboBoxLong(builder, ref seq, dataItem, propertyInfo, settings, itemType, value);
else if (underlyingType == typeof(Guid))
RenderComboBoxGuid(builder, ref seq, dataItem, propertyInfo, settings, itemType, value);
else
{
builder.OpenComponent<DxTextBox>(seq++);
builder.AddAttribute(seq++, "Text", ResolveComboBoxDisplayText(settings, value ?? new object()) ?? value?.ToString() ?? "");
builder.AddAttribute(seq++, "ReadOnly", true);
builder.CloseComponent();
}
}
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;
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);
builder.AddAttribute(seq++, "Value", isNullable ? currentValue as int? : (currentValue is int intVal ? intVal : 0));
builder.AddAttribute(seq++, "ValueChanged", isNullable
? EventCallback.Factory.Create<int?>(this, v => { propertyInfo.SetValue(dataItem, v); StateHasChanged(); })
: EventCallback.Factory.Create<int>(this, v => { propertyInfo.SetValue(dataItem, v); StateHasChanged(); }));
builder.AddAttribute(seq++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto);
builder.CloseComponent();
}
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 ? 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);
builder.AddAttribute(seq++, "Value", isNullable ? currentValue as long? : (currentValue is long longVal ? longVal : 0L));
builder.AddAttribute(seq++, "ValueChanged", isNullable
? EventCallback.Factory.Create<long?>(this, v => { propertyInfo.SetValue(dataItem, v); StateHasChanged(); })
: EventCallback.Factory.Create<long>(this, v => { propertyInfo.SetValue(dataItem, v); StateHasChanged(); }));
builder.AddAttribute(seq++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto);
builder.CloseComponent();
}
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 ? 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);
builder.AddAttribute(seq++, "Value", isNullable ? currentValue as Guid? : (currentValue is Guid guidVal ? guidVal : Guid.Empty));
builder.AddAttribute(seq++, "ValueChanged", isNullable
? EventCallback.Factory.Create<Guid?>(this, v => { propertyInfo.SetValue(dataItem, v); StateHasChanged(); })
: EventCallback.Factory.Create<Guid>(this, v => { propertyInfo.SetValue(dataItem, v); StateHasChanged(); }));
builder.AddAttribute(seq++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto);
builder.CloseComponent();
}
private RenderFragment RenderCellContent(object? value, string displayText)
{
return builder =>
{
var seq = 0;
builder.OpenElement(seq++, "span");
builder.AddAttribute(seq++, "class", "mg-info-panel-value");
builder.AddAttribute(seq++, "title", displayText);
if (value is bool boolValue)
{
builder.OpenElement(seq++, "span");
builder.AddAttribute(seq++, "class", boolValue ? "dx-icon dx-icon-check" : "dx-icon dx-icon-close");
builder.CloseElement();
}
else
{
builder.AddContent(seq++, displayText);
}
builder.CloseElement();
};
}
}

View File

@ -1,410 +0,0 @@
using DevExpress.Blazor;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace AyCode.Blazor.Components.Components.Grids;
/// <summary>
/// Interface for InfoPanel to support grid access
/// </summary>
public interface IInfoPanelBase
{
void ClearEditMode();
void SetEditMode(object editModel);
void RefreshData(IMgGridBase grid, object? dataItem, int visibleIndex = -1);
}
/// <summary>
/// InfoPanel component for displaying and editing grid row details
/// </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;
/// <summary>
/// Minimum width for 2 columns layout. Default is 500px.
/// </summary>
[Parameter] public int TwoColumnBreakpoint { get; set; } = 400;
/// <summary>
/// Minimum width for 3 columns layout. Default is 800px.
/// </summary>
[Parameter] public int ThreeColumnBreakpoint { get; set; } = 800;
/// <summary>
/// Minimum width for 4 columns layout. Default is 1200px.
/// </summary>
[Parameter] public int FourColumnBreakpoint { get; set; } = 1300;
/// <summary>
/// Fixed column count. If set (1-4), overrides responsive breakpoints. Default is null (responsive).
/// </summary>
[Parameter] public int? FixedColumnCount { get; set; }
/// <summary>
/// Reference to the wrapper component - automatically registers this InfoPanel
/// </summary>
[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
protected IMgGridBase? _currentGrid;
protected object? _currentDataItem;
protected int _focusedRowVisibleIndex = -1;
protected List<DxGridDataColumn> _allDataColumns = [];
// Edit mode state
protected bool _isEditMode;
protected object? _editModel;
// Cache for edit settings to avoid repeated lookups
private readonly Dictionary<string, IEditSettings?> _editSettingsCache = [];
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Register this InfoPanel with the wrapper
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
}
}
/// <summary>
/// Refreshes the InfoPanel with data from the specified grid row (view mode)
/// </summary>
public void RefreshData(IMgGridBase grid, object? dataItem, int visibleIndex = -1)
{
ArgumentNullException.ThrowIfNull(grid);
_currentGrid = grid;
_currentDataItem = dataItem;
_focusedRowVisibleIndex = visibleIndex;
_editSettingsCache.Clear();
// Clear edit mode when refreshing with new data
_isEditMode = false;
_editModel = null;
if (_currentGrid != null && _currentDataItem != null)
{
_allDataColumns = GetAllDataColumns(_currentGrid);
}
else
{
_allDataColumns = [];
}
StateHasChanged();
// Notify subscribers that data item changed
_ = OnDataItemChanged.InvokeAsync(dataItem);
}
/// <summary>
/// Sets the InfoPanel to edit mode with the given edit model
/// </summary>
public void SetEditMode(object editModel)
{
_editModel = editModel;
_isEditMode = true;
_currentDataItem = _editModel;
if (_currentGrid != null)
{
_allDataColumns = GetAllDataColumns(_currentGrid);
}
InvokeAsync(StateHasChanged);
}
/// <summary>
/// Clears edit mode and returns to view mode
/// </summary>
public void ClearEditMode()
{
_isEditMode = false;
_editModel = null;
_editSettingsCache.Clear();
InvokeAsync(StateHasChanged);
}
/// <summary>
/// Clears the InfoPanel completely
/// </summary>
public void Clear()
{
_currentGrid = null;
_currentDataItem = null;
_focusedRowVisibleIndex = -1;
_allDataColumns = [];
_editSettingsCache.Clear();
_isEditMode = false;
_editModel = null;
StateHasChanged();
}
public async ValueTask DisposeAsync()
{
if (_isJsInitialized)
{
try
{
await JSRuntime.InvokeVoidAsync("MgGridInfoPanel.disposeSticky", _panelElement);
}
catch
{
// Ignore disposal errors
}
}
}
/// <summary>
/// Gets the data item to display/edit (EditModel in edit mode, otherwise CurrentDataItem)
/// </summary>
protected object? GetActiveDataItem() => _isEditMode && _editModel != null ? _editModel : _currentDataItem;
/// <summary>
/// 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.
/// </summary>
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;
// Try to resolve display text from EditSettings
var editSettings = GetEditSettings(column.FieldName);
if (editSettings is DxComboBoxSettings comboSettings)
{
var displayText = ResolveComboBoxDisplayText(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;
}
}
/// <summary>
/// Gets edit settings for the specified field (with caching)
/// </summary>
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 = _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
{
// Ignore errors
}
_editSettingsCache[fieldName] = settings;
return settings;
}
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
};
}
/// <summary>
/// Gets the columns to display based on edit mode and ShowReadOnlyFieldsInEditMode setting
/// </summary>
protected IEnumerable<DxGridDataColumn> 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<DxGridDataColumn> GetAllDataColumns(IMgGridBase grid)
{
var columns = new List<DxGridDataColumn>();
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;
}
/// <summary>
/// Gets the EditSettings type for rendering logic
/// </summary>
private EditSettingsType GetEditSettingsType(DxGridDataColumn column)
{
var settings = GetEditSettings(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
}
}

View File

@ -1,148 +0,0 @@
/* Shared edit mode background color configuration - change only here */
/* Grid row background: #fffbeb (see MgGridBase.cs OnCustomizeElement) */
/* InfoPanel background: #fffbeb (see below .edit-mode) */
/* Border color: #f59e0b */
/* Main panel - contained within splitter pane */
.mg-grid-info-panel {
container-type: inline-size;
container-name: infopanel;
background-color: var(--dxbl-bg-secondary, #f8f9fa);
transition: background-color 0.3s ease, border-color 0.3s ease;
display: flex;
flex-direction: column;
overflow: hidden;
/* Prevent panel from pushing out the splitter */
min-height: 0;
max-height: 100%;
/* Default breakpoints - can be overridden via style attribute */
--mg-bp-2col: 500px;
--mg-bp-3col: 800px;
--mg-bp-4col: 1200px;
}
.mg-grid-info-panel.edit-mode {
background-color: #fffbeb !important;
border-left: 3px solid #f59e0b !important;
}
.mg-grid-info-panel.view-mode {
background-color: #f8f9fa !important;
border-left: 3px solid transparent !important;
}
/* Content area - scrollable, takes remaining space */
.mg-info-panel-content {
flex: 1 1 0;
overflow-y: auto;
overflow-x: hidden;
padding: 1rem;
min-height: 0; /* Critical for flex child to allow shrinking */
}
/* Grid layout with responsive column wrapping based on panel width */
.mg-info-panel-grid {
display: grid;
grid-template-columns: 1fr;
gap: 0.75rem;
}
/* Fixed column count classes - override responsive behavior */
.mg-columns-1 .mg-info-panel-grid {
grid-template-columns: 1fr !important;
}
.mg-columns-2 .mg-info-panel-grid {
grid-template-columns: repeat(2, 1fr) !important;
}
.mg-columns-3 .mg-info-panel-grid {
grid-template-columns: repeat(3, 1fr) !important;
}
.mg-columns-4 .mg-info-panel-grid {
grid-template-columns: repeat(4, 1fr) !important;
}
/* Responsive layouts using container queries (when no fixed column count) */
/* 1 column for narrow panels (< 2col breakpoint) - default above */
/* 2 columns for medium width */
@container infopanel (min-width: 500px) {
.mg-grid-info-panel:not(.mg-columns-1):not(.mg-columns-2):not(.mg-columns-3):not(.mg-columns-4) .mg-info-panel-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* 3 columns for wider panels */
@container infopanel (min-width: 800px) {
.mg-grid-info-panel:not(.mg-columns-1):not(.mg-columns-2):not(.mg-columns-3):not(.mg-columns-4) .mg-info-panel-grid {
grid-template-columns: repeat(3, 1fr);
}
}
/* 4 columns for very wide panels */
@container infopanel (min-width: 1200px) {
.mg-grid-info-panel:not(.mg-columns-1):not(.mg-columns-2):not(.mg-columns-3):not(.mg-columns-4) .mg-info-panel-grid {
grid-template-columns: repeat(4, 1fr);
}
}
.mg-info-panel-item {
min-width: 0; /* Prevent grid blowout */
}
/* Fallback styles */
.info-panel-form {
width: 100%;
}
.info-panel-form .fw-semibold {
font-weight: 600;
color: var(--dxbl-text-secondary, #495057);
font-size: 0.875rem;
}
.info-panel-form .fw-semibold.text-primary {
color: var(--dxbl-primary, #0d6efd);
}
/* Text overflow handling - show ellipsis and full text in tooltip */
.info-panel-text-wrapper {
width: 100%;
}
.info-panel-text-wrapper input[readonly] {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
/* View mode value styling - matches DevExpress theme */
.mg-info-panel-value {
padding: 0.375rem 0.75rem;
min-height: 2rem;
display: flex;
align-items: center;
justify-content: flex-start;
background-color: var(--dxbl-bg, #fff);
border: 1px solid var(--dxbl-border-color, #dee2e6);
border-radius: var(--dxbl-border-radius, 0.25rem);
font-size: var(--dxbl-font-size, 0.875rem);
color: var(--dxbl-text, #212529);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mg-info-panel-value-numeric {
font-variant-numeric: tabular-nums;
}
.mg-info-panel-value-bool {
/* Keep left aligned */
}
.mg-info-panel-value-date {
font-variant-numeric: tabular-nums;
}

View File

@ -1,6 +0,0 @@
namespace AyCode.Blazor.Components.Components.Grids;
public class MgGridInfoPanelHelper
{
}

View File

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

View File

@ -1,6 +0,0 @@
namespace AyCode.Blazor.Components.Components.Grids;
public class MgGridToolbarHelper
{
}

View File

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

View File

@ -1,136 +0,0 @@
@using DevExpress.Blazor
<CascadingValue Value="this">
@if (_isFullscreen)
{
<div class="mg-fullscreen-overlay">
<div class="mg-fullscreen-header">
<span class="mg-fullscreen-title">@(_currentGrid?.Caption ?? "Grid")</span>
<button type="button" class="btn-close btn-close-white" aria-label="Close" @onclick="ExitFullscreen"></button>
</div>
<div class="mg-fullscreen-body">
@RenderMainContent()
</div>
</div>
}
else
{
@RenderMainContent()
}
</CascadingValue>
@code {
private IInfoPanelBase? _infoPanelInstance;
private IMgGridBase? _currentGrid;
private bool _isFullscreen;
/// <summary>
/// The grid content to display in the left pane
/// </summary>
[Parameter]
public RenderFragment? GridContent { get; set; }
/// <summary>
/// InfoPanel content (e.g., GridShippingDocumentInfoPanel) to display in the right pane.
/// If not set, the default MgGridInfoPanel is used.
/// </summary>
[Parameter]
public RenderFragment? ChildContent { get; set; }
/// <summary>
/// Initial size of the InfoPanel pane. Default is "400px".
/// </summary>
[Parameter]
public string InfoPanelSize { get; set; } = "400px";
/// <summary>
/// Whether to show the InfoPanel. Default is true.
/// </summary>
[Parameter]
public bool ShowInfoPanel { get; set; } = true;
/// <summary>
/// Whether the wrapper is currently in fullscreen mode
/// </summary>
public bool IsFullscreen => _isFullscreen;
/// <summary>
/// Gets or sets the InfoPanel instance for grid-InfoPanel communication
/// </summary>
public IInfoPanelBase? InfoPanelInstance
{
get => _infoPanelInstance;
set => _infoPanelInstance = value;
}
/// <summary>
/// Registers an InfoPanel instance (called by child InfoPanel components)
/// </summary>
public void RegisterInfoPanel(IInfoPanelBase infoPanel)
{
_infoPanelInstance = infoPanel;
}
/// <summary>
/// Registers the grid instance (called by MgGridBase)
/// </summary>
public void RegisterGrid(IMgGridBase grid)
{
_currentGrid = grid;
}
/// <summary>
/// Toggles fullscreen mode
/// </summary>
public void ToggleFullscreen()
{
_isFullscreen = !_isFullscreen;
StateHasChanged();
}
/// <summary>
/// Enters fullscreen mode
/// </summary>
public void EnterFullscreen()
{
_isFullscreen = true;
StateHasChanged();
}
/// <summary>
/// Exits fullscreen mode
/// </summary>
public void ExitFullscreen()
{
_isFullscreen = false;
StateHasChanged();
}
private RenderFragment RenderMainContent() => __builder =>
{
if (ShowInfoPanel)
{
<DxSplitter Width="100%" Height="@(_isFullscreen ? "100%" : null)" CssClass="mg-grid-with-info-panel" Orientation="Orientation.Horizontal">
<Panes>
<DxSplitterPane>
@GridContent
</DxSplitterPane>
<DxSplitterPane Size="@InfoPanelSize" MinSize="0px" MaxSize="100%" AllowCollapse="true" CssClass="mg-info-panel-pane">
@if (ChildContent != null)
{
@ChildContent
}
else
{
<MgGridInfoPanel />
}
</DxSplitterPane>
</Panes>
</DxSplitter>
}
else
{
@GridContent
}
};
}

View File

@ -1,6 +0,0 @@
namespace AyCode.Blazor.Components.Components;
public class MgComponentsHelper
{
}

View File

@ -1,265 +0,0 @@
/* MgGridInfoPanel styles - DevExpress theme compatible */
/* Main panel - uses DevExpress theme variables */
.mg-grid-info-panel {
container-type: inline-size;
container-name: infopanel;
background-color: var(--dxbl-bg-secondary);
color: var(--dxbl-text);
font-family: var(--dxbl-font-family);
font-size: var(--dxbl-font-size);
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
max-height: 100%;
border-left: 1px solid var(--dxbl-border-color);
}
.mg-grid-info-panel.edit-mode {
background-color: var(--dxbl-warning-bg, #fffbeb);
border-left: 3px solid var(--dxbl-warning, #f59e0b);
}
/* Header styling */
.mg-grid-info-panel .mg-info-panel-header {
padding: var(--dxbl-spacer-sm) var(--dxbl-spacer);
background-color: var(--dxbl-bg);
border-bottom: 1px solid var(--dxbl-border-color);
font-weight: 600;
}
/* Toolbar styling */
.mg-info-panel-toolbar {
padding: var(--dxbl-spacer-xs) var(--dxbl-spacer-sm);
background-color: var(--dxbl-bg);
border-bottom: 1px solid var(--dxbl-border-color);
}
/* Content area - scrollable */
.mg-info-panel-content {
flex: 1 1 0;
overflow-y: auto;
overflow-x: hidden;
padding: var(--dxbl-spacer);
min-height: 0;
}
/* Grid layout for columns */
.mg-info-panel-grid {
display: grid;
grid-template-columns: 1fr;
gap: var(--dxbl-spacer-sm);
}
/* Fixed column count classes */
.mg-columns-1 .mg-info-panel-grid { grid-template-columns: 1fr !important; }
.mg-columns-2 .mg-info-panel-grid { grid-template-columns: repeat(2, 1fr) !important; }
.mg-columns-3 .mg-info-panel-grid { grid-template-columns: repeat(3, 1fr) !important; }
.mg-columns-4 .mg-info-panel-grid { grid-template-columns: repeat(4, 1fr) !important; }
/* Responsive layouts using container queries */
@container infopanel (min-width: 400px) {
.mg-grid-info-panel:not(.mg-columns-1):not(.mg-columns-2):not(.mg-columns-3):not(.mg-columns-4) .mg-info-panel-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@container infopanel (min-width: 800px) {
.mg-grid-info-panel:not(.mg-columns-1):not(.mg-columns-2):not(.mg-columns-3):not(.mg-columns-4) .mg-info-panel-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@container infopanel (min-width: 1300px) {
.mg-grid-info-panel:not(.mg-columns-1):not(.mg-columns-2):not(.mg-columns-3):not(.mg-columns-4) .mg-info-panel-grid {
grid-template-columns: repeat(4, 1fr);
}
}
/* Grid item */
.mg-info-panel-item {
min-width: 0;
}
/* Label styling */
.mg-info-panel-label {
display: block;
margin-bottom: var(--dxbl-spacer-xs);
font-size: calc(var(--dxbl-font-size) * 0.875);
font-weight: 600;
color: var(--dxbl-text-secondary);
}
.mg-info-panel-label.editable {
color: var(--dxbl-primary);
}
/* View mode value styling */
.mg-info-panel-value {
display: block;
padding: var(--dxbl-spacer-xs) 0;
color: var(--dxbl-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mg-info-panel-value-numeric {
font-variant-numeric: tabular-nums;
}
.mg-info-panel-value-date {
font-variant-numeric: tabular-nums;
}
/* Empty state */
.mg-info-panel-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--dxbl-text-muted);
padding: var(--dxbl-spacer-lg);
text-align: center;
}
/* Tables inside info panel */
.mg-info-panel-content table {
width: 100%;
border-collapse: collapse;
font-size: var(--dxbl-font-size);
color: var(--dxbl-text);
margin-bottom: var(--dxbl-spacer);
}
.mg-info-panel-content table th,
.mg-info-panel-content table td {
padding: var(--dxbl-spacer-xs) var(--dxbl-spacer-sm);
border: 1px solid var(--dxbl-border-color);
text-align: left;
white-space: nowrap;
}
.mg-info-panel-content table th {
background-color: var(--dxbl-bg-secondary);
font-weight: 600;
color: var(--dxbl-text-secondary);
}
.mg-info-panel-content table tbody tr:nth-child(odd) {
background-color: var(--dxbl-bg);
}
.mg-info-panel-content table tbody tr:nth-child(even) {
background-color: var(--dxbl-bg-secondary);
}
.mg-info-panel-content table tbody tr:hover {
background-color: var(--dxbl-row-hover-bg);
}
/* Splitter pane styling */
.mg-grid-with-info-panel {
height: 100%;
}
.mg-info-panel-pane {
background-color: var(--dxbl-bg-secondary);
}
/* Fullscreen overlay styling (Bootstrap 5 based) */
.mg-fullscreen-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100vw;
height: 100vh;
z-index: 1050;
background-color: var(--dxbl-bg, #fff);
display: flex;
flex-direction: column;
overflow: hidden;
}
.mg-fullscreen-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
background-color: var(--dxbl-primary, #0d6efd);
color: #fff;
border-bottom: 1px solid var(--dxbl-border-color);
flex-shrink: 0;
}
.mg-fullscreen-title {
font-size: 1.1rem;
font-weight: 600;
margin: 0;
}
.mg-fullscreen-header .btn-close-white {
filter: brightness(0) invert(1);
opacity: 0.8;
}
.mg-fullscreen-header .btn-close-white:hover {
opacity: 1;
}
.mg-fullscreen-body {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.mg-fullscreen-body .mg-grid-with-info-panel,
.mg-fullscreen-body .dxbl-grid {
flex: 1;
height: 100%;
}
/* Legacy DxWindow styling (kept for backwards compatibility) */
.mg-fullscreen-window {
position: fixed !important;
top: 0 !important;
left: 0 !important;
margin: 0 !important;
border-radius: 0 !important;
}
.mg-fullscreen-window .dxbl-window-body {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.mg-fullscreen-content {
flex: 1;
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.mg-fullscreen-content .mg-grid-with-info-panel,
.mg-fullscreen-content .dxbl-grid {
flex: 1;
height: 100%;
}
/* Fullscreen icon classes */
.grid-fullscreen::before {
content: "\e90c";
font-family: 'devextreme-icons';
}
.grid-fullscreen-exit::before {
content: "\e90d";
font-family: 'devextreme-icons';
}

View File

@ -1,120 +0,0 @@
// MgGridInfoPanel - Sticky scroll handling
// Makes the InfoPanel sticky to viewport when scrolling
window.MgGridInfoPanel = {
observers: new Map(),
// Initialize sticky behavior for an InfoPanel element
initSticky: function (element, topOffset) {
if (!element) return;
const elementId = element.id || this.generateId(element);
// Clean up existing observer if any
this.disposeSticky(element);
// Store the initial position of the element (relative to document)
const rect = element.getBoundingClientRect();
const initialTop = rect.top + window.scrollY;
// Calculate and set initial state
this.updatePosition(element, initialTop);
// Handler to update position on scroll and resize
const updateHandler = () => {
this.updatePosition(element, initialTop);
};
// Add event listeners - use passive to not block scrolling
window.addEventListener('resize', updateHandler, { passive: true });
window.addEventListener('scroll', updateHandler, { passive: true });
// Store cleanup info
this.observers.set(elementId, {
element: element,
updateHandler: updateHandler,
initialTop: initialTop
});
return true;
},
// Dispose sticky behavior
disposeSticky: function (element) {
if (!element) return;
const elementId = element.id || this.findElementId(element);
const observer = this.observers.get(elementId);
if (observer) {
window.removeEventListener('resize', observer.updateHandler);
window.removeEventListener('scroll', observer.updateHandler);
// Reset styles
element.style.height = '';
element.style.maxHeight = '';
element.style.transform = '';
this.observers.delete(elementId);
}
},
// Update panel position and height based on scroll
updatePosition: function (element, initialTop) {
if (!element) return;
const scrollY = window.scrollY;
const viewportHeight = window.innerHeight;
const bottomPadding = 30; // 30px from bottom
// Calculate how much we've scrolled past the initial position
const scrolledPast = Math.max(0, scrollY - initialTop);
// Get the splitter pane to know our container limits
const paneContainer = element.closest('.dxbl-splitter-pane');
let maxScrollOffset = Infinity;
if (paneContainer) {
// Don't scroll past the bottom of the pane
const paneHeight = paneContainer.offsetHeight;
const elementHeight = element.offsetHeight;
maxScrollOffset = Math.max(0, paneHeight - elementHeight);
}
// Clamp the scroll offset
const translateY = Math.min(scrolledPast, maxScrollOffset);
// Apply transform to make it "sticky"
element.style.transform = `translateY(${translateY}px)`;
// Calculate height: from current visual position to viewport bottom
const rect = element.getBoundingClientRect();
const visualTop = rect.top; // This already accounts for transform
// Height from current visual top to viewport bottom minus padding
const availableHeight = viewportHeight - visualTop - bottomPadding;
// Clamp height
const finalHeight = Math.max(200, Math.min(availableHeight, viewportHeight - bottomPadding));
element.style.height = finalHeight + 'px';
element.style.maxHeight = finalHeight + 'px';
},
// Generate a unique ID for the element
generateId: function (element) {
const id = 'mg-info-panel-' + Math.random().toString(36).substr(2, 9);
element.id = id;
return id;
},
// Find element ID from stored observers
findElementId: function (element) {
for (const [id, observer] of this.observers.entries()) {
if (observer.element === element) {
return id;
}
}
return null;
}
};