using AyCode.Core; using AyCode.Core.Enums; using AyCode.Core.Helpers; using AyCode.Core.Interfaces; using AyCode.Core.Loggers; using AyCode.Services.Server.SignalRs; using AyCode.Services.SignalRs; using AyCode.Utils.Extensions; using DevExpress.Blazor; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.JSInterop; using System.ComponentModel; using System.Reflection; using System.Text.Json; using DevExpress.Blazor.Internal; using System.Text.RegularExpressions; namespace AyCode.Blazor.Components.Components.Grids; public interface IMgGridBase : IGrid { /// /// Indicates whether any synchronization operation is in progress /// bool IsSyncing { get; } string Caption { get; set; } /// /// Current edit state of the grid (None, New, Edit) /// MgGridEditState GridEditState { get; } /// /// Parent grid in nested grid hierarchy (null if this is a root grid) /// IMgGridBase? ParentGrid { get; } /// /// Gets the root grid in the hierarchy /// IMgGridBase GetRootGrid(); /// /// Navigates to the previous row in the grid /// void StepPrevRow(); /// /// Navigates to the next row in the grid /// void StepNextRow(); /// /// InfoPanel instance for displaying row details (from wrapper) /// IInfoPanelBase? InfoPanelInstance { get; } /// /// Whether the grid/wrapper is currently in fullscreen mode /// bool IsFullscreen { get; } string LayoutStorageKey { get; } /// /// Toggles fullscreen mode for the grid (or wrapper if available) /// void ToggleFullscreen(); } public abstract class MgGridBase : DxGrid, IMgGridBase, IAsyncDisposable where TSignalRDataSource : AcSignalRDataSource> where TDataItem : class, IId where TId : struct where TLoggerClient : AcLoggerBase { private readonly EqualityComparer _equalityComparerId = EqualityComparer.Default; private readonly TypeConverter _typeConverterId = TypeDescriptor.GetConverter(typeof(TId)); protected bool IsFirstInitializeParameters; protected bool IsFirstInitializeParameterCore; private bool _isDisposed; private TSignalRDataSource? _dataSource = null!; private AcObservableCollection? _dataSourceParam = []; private string _gridLogName; /// public bool IsSyncing => _dataSource?.IsSyncing ?? false; /// public MgGridEditState GridEditState { get; private set; } = MgGridEditState.None; /// [CascadingParameter] public IMgGridBase? ParentGrid { get; set; } /// public IMgGridBase GetRootGrid() { var current = (IMgGridBase)this; while (current.ParentGrid != null) { current = current.ParentGrid; } return current; } public string LayoutStorageKey { get { var masterDetailName = IsMasterGrid ? "Master" : ParentDataItem!.GetType().Name; return $"{AutoSaveLayoutName}_{masterDetailName}_AutoSave_{GetLayoutUserId()}"; } } /// /// Reference to the wrapper component for grid-InfoPanel communication /// [CascadingParameter] public MgGridWithInfoPanel? GridWrapper { get; set; } private object _focusedDataItem; /// /// InfoPanel instance for displaying row details. /// First checks own wrapper, then gets InfoPanel from root grid. /// public IInfoPanelBase? InfoPanelInstance { get { // First check if we have a direct wrapper with InfoPanel if (GridWrapper?.InfoPanelInstance != null) return GridWrapper.InfoPanelInstance; // Get InfoPanel from root grid (handles nested grids) var rootGrid = GetRootGrid(); if (rootGrid != this) return rootGrid.InfoPanelInstance; return null; } } /// public bool IsFullscreen => GridWrapper?.IsFullscreen ?? _isStandaloneFullscreen; private bool _isStandaloneFullscreen; /// 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>(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(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); [Inject] protected IJSRuntime JSRuntime { get; set; } = null!; [Parameter] public TLoggerClient Logger { get; set; } [Parameter] public string GridName { get; set; } [Parameter] public IId? ParentDataItem { get; set; } [Parameter] public string? KeyFieldNameToParentId { get; set; } [Parameter] public object[]? ContextIds { get; set; } [Parameter] public string Caption { get; set; } = typeof(TDataItem).Name; /// /// Name for auto-saving/loading grid layout. If not set, defaults to "Grid{TDataItem.Name}" /// [Parameter] public string? AutoSaveLayoutName { get; set; } public bool IsMasterGrid => ParentDataItem == null; protected PropertyInfo? KeyFieldPropertyInfoToParent; private string? _filterText = null; [Parameter] public string? FilterText { get => _filterText; set { _filterText = value; if (_dataSource != null && _dataSource.FilterText != value) { _dataSource.FilterText = value; ReloadDataSourceAsync().Forget(); } } } [Parameter] public AcSignalRClientBase SignalRClient { get; set; } [Parameter] public int GetAllMessageTag { get; set; } [Parameter] public int GetItemMessageTag { get; set; } [Parameter] public int AddMessageTag { get; set; } [Parameter] public int UpdateMessageTag { get; set; } [Parameter] public int RemoveMessageTag { get; set; } protected new EventCallback DataItemDeleting { get; set; } [Parameter] public EventCallback OnGridItemDeleting { get; set; } protected new EventCallback EditModelSaving { get; set; } [Parameter] public EventCallback OnGridEditModelSaving { get; set; } protected new EventCallback EditStart { get; set; } [Parameter] public EventCallback OnGridEditStart { get; set; } protected new EventCallback CustomizeEditModel { get; set; } [Parameter] public EventCallback OnGridCustomizeEditModel { get; set; } protected new EventCallback FocusedRowChanged { get; set; } [Parameter] public EventCallback OnGridFocusedRowChanged { get; set; } [Parameter] public EventCallback> OnDataSourceChanged { get; set; } [Parameter] public EventCallback> OnGridItemChanging { get; set; } /// /// After the server has responded! /// [Parameter] public EventCallback> OnGridItemChanged { get; set; } [Parameter] [DefaultValue(null)] [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "BL0007:Component parameters should be auto properties", Justification = "")] public IList DataSource { get { if (_dataSource == null && Data != null) { Logger.Error($"{_gridLogName} Use the DataSource parameter instead of Data!"); throw new NullReferenceException($"{_gridLogName} Use the DataSource parameter instead of Data!"); } return _dataSource!; } set { _dataSourceParam = value as AcObservableCollection; if (_dataSource != null) // && _dataSourceParam is List workingReferenceList) { SetWorkingReferenceList(_dataSourceParam); } } } private void SetWorkingReferenceList(AcObservableCollection? referenceList) { _dataSource?.SetWorkingReferenceList(referenceList); SetGridData(referenceList); } public void SetGridData(object? data) { if (_isDisposed) return; if (ReferenceEquals(Data, data)) return; BeginUpdate(); Data = data; EndUpdate(); } protected override async Task OnInitializedAsync() { if (Logger == null) throw new NullReferenceException($"[{GetType().Name}] Logger == null"); if (SignalRClient == null) { Logger.Error($"[{GetType().Name}] SignalRClient == null"); throw new NullReferenceException($"[{GetType().Name}] SignalRClient == null"); } var crudTags = new SignalRCrudTags(GetAllMessageTag, GetItemMessageTag, AddMessageTag, UpdateMessageTag, RemoveMessageTag); _dataSource = (TSignalRDataSource)Activator.CreateInstance(typeof(TSignalRDataSource), SignalRClient, crudTags, ContextIds)!; _dataSource.FilterText = FilterText; SetGridData(_dataSource.GetReferenceInnerList()); _dataSource.OnDataSourceLoaded += OnDataSourceLoaded; _dataSource.OnDataSourceItemChanged += OnDataSourceItemChanged; _dataSource.OnSyncingStateChanged += OnDataSourceSyncingStateChanged; await base.OnInitializedAsync(); } private void OnDataSourceSyncingStateChanged(bool isSyncing) { if (_isDisposed) return; // Forward the event to external subscribers //OnSyncingStateChanged?.Invoke(isSyncing); // Trigger UI update InvokeAsync(StateHasChanged); } private async Task OnDataSourceItemChanged(ItemChangedEventArgs args) { if (_isDisposed) return; if (args.TrackingState is TrackingState.GetAll or TrackingState.None) return; Logger.Debug($"{_gridLogName} OnDataSourceItemChanged; trackingState: {args.TrackingState}"); var changedEventArgs = new GridDataItemChangedEventArgs(this, args.Item, args.TrackingState); await OnGridItemChanged.InvokeAsync(changedEventArgs); if (!changedEventArgs.CancelStateChangeInvoke && !_isDisposed) { await InvokeAsync(StateHasChanged); } } private async Task OnDataSourceLoaded() { if (_isDisposed) return; Logger.Debug($"{_gridLogName} OnDataSourceLoaded; Count: {_dataSource?.Count}"); await InvokeAsync(() => SetGridData(_dataSource!.GetReferenceInnerList())); if (!_isDisposed) { await OnDataSourceChanged.InvokeAsync(_dataSource); await InvokeAsync(StateHasChanged); } } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { if (_dataSource == null) return; if (_dataSourceParam != null) await _dataSource.LoadDataSource(_dataSourceParam, true, true); else _dataSource.LoadDataSourceAsync(true).Forget(); } await base.OnAfterRenderAsync(firstRender); } private void SetNewId(TDataItem dataItem) { //TODO: int !!! - J. if (dataItem.Id is Guid) { dataItem.Id = (TId)(_typeConverterId.ConvertTo(Guid.NewGuid(), typeof(TId)))!; } else if (dataItem.Id is int) { var newId = -1 * AcDomain.NextUniqueInt32; dataItem.Id = (TId)(_typeConverterId.ConvertTo(newId, typeof(TId)))!; } } public Task AddDataItem(TDataItem dataItem) { if (!HasIdValue(dataItem)) SetNewId(dataItem); return _dataSource.Add(dataItem, true); } public Task AddDataItemAsync(TDataItem dataItem) { if (!HasIdValue(dataItem)) SetNewId(dataItem); _dataSource.Add(dataItem); return SaveChangesToServerAsync(); } public Task InsertDataItem(int index, TDataItem dataItem) { if (!HasIdValue(dataItem)) SetNewId(dataItem); return _dataSource.Insert(index, dataItem, true); } public Task InsertDataItemAsync(int index, TDataItem dataItem) { if (!HasIdValue(dataItem)) SetNewId(dataItem); _dataSource.Insert(index, dataItem); return SaveChangesToServerAsync(); } protected PropertyInfo? GetDataItemPropertyInfo(string propertyName) => typeof(TDataItem).GetProperty(propertyName); protected virtual async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e) { var editModel = (e.EditModel as TDataItem)!; if (e.IsNew) { if (!HasIdValue(editModel)) SetNewId(editModel); if (ParentDataItem != null && !KeyFieldNameToParentId.IsNullOrWhiteSpace()) { KeyFieldPropertyInfoToParent ??= GetDataItemPropertyInfo(KeyFieldNameToParentId); KeyFieldPropertyInfoToParent!.SetValue(editModel, ParentDataItem.Id); } e.EditModel = editModel; } // Set edit state GridEditState = e.IsNew ? MgGridEditState.New : MgGridEditState.Edit; await OnGridCustomizeEditModel.InvokeAsync(e); // Update InfoPanel to edit mode InfoPanelInstance?.SetEditMode(this, editModel); await InvokeAsync(StateHasChanged); } private async Task OnEditStart(GridEditStartEventArgs e) { await OnGridEditStart.InvokeAsync(e); } protected virtual async Task OnFocusedRowChanged(GridFocusedRowChangedEventArgs e) { _focusedDataItem = e.DataItem; 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); } private async Task OnItemSaving(GridEditModelSavingEventArgs e) { var dataItem = (e.EditModel as TDataItem)!; if (e.IsNew) { if (!HasIdValue(dataItem)) SetNewId(dataItem); } var logText = e.IsNew ? "add" : "update"; Logger.Debug($"{_gridLogName} OnItemSaving {logText}; Id: {dataItem.Id}"); await OnGridEditModelSaving.InvokeAsync(e); if (e.Cancel) { Logger.Debug($"{_gridLogName} OnItemSaving {logText} canceled; Id: {dataItem.Id}"); return; } if (e.IsNew) { if (EditNewRowPosition is GridEditNewRowPosition.FixedOnTop or GridEditNewRowPosition.Top) await AddDataItemAsync(dataItem); else await InsertDataItemAsync(0, dataItem); } else await UpdateDataItemAsync(dataItem); GridEditState = MgGridEditState.None; InfoPanelInstance?.ClearEditMode(); await InvokeAsync(StateHasChanged); } private async Task OnEditCanceling(GridEditCancelingEventArgs e) { GridEditState = MgGridEditState.None; InfoPanelInstance?.ClearEditMode(); await InvokeAsync(StateHasChanged); } private Task SaveChangesToServerAsync() { try { return _dataSource.SaveChangesAsync(); } catch (Exception ex) { Logger.Error($"{_gridLogName} SaveChangesToServerAsync->SaveChangesAsync error!", ex); } return Task.CompletedTask; } private async Task SaveChangesToServer() { var result = false; try { var unsavedItems = await _dataSource.SaveChanges(); if (!(result = unsavedItems.Count == 0)) Logger.Error($"{_gridLogName} SaveChangesToServer->SaveChanges error! unsavedCount: {unsavedItems.Count}"); } catch (Exception ex) { Logger.Error($"{_gridLogName} OnItemSaving", ex); } return result; } private async Task OnItemDeleting(GridDataItemDeletingEventArgs e) { Logger.Debug($"{_gridLogName} OnItemDeleting"); await OnGridItemDeleting.InvokeAsync(e); if (e.Cancel) { Logger.Debug($"{_gridLogName} OnItemDeleting canceled"); return; } var dataItem = (e.DataItem as TDataItem)!; await RemoveDataItem(dataItem); } private void OnCustomizeElement(GridCustomizeElementEventArgs e) { if (e.ElementType == GridElementType.DetailCell) { e.Style = "padding: 0.5rem; opacity: 0.75"; } else if (false && e.ElementType == GridElementType.DataCell && e.Column.Name == nameof(IId.Id)) { 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) { await base.SetParametersAsyncCore(parameters); if (!IsFirstInitializeParameterCore) { //if (typeof(TDataItem) is IId || typeof(TDataItem) is IId) KeyFieldName = "Id"; base.DataItemDeleting = EventCallback.Factory.Create(this, OnItemDeleting); base.EditModelSaving = EventCallback.Factory.Create(this, OnItemSaving); base.CustomizeEditModel = 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; //ShowFilterRow = true; //PageSize = 4; //ShowGroupPanel = true; //AllowSort = false; TextWrapEnabled = false; AllowSelectRowByClick = true; HighlightRowOnHover = true; AutoCollapseDetailRow = true; AutoExpandAllGroupRows = false; IsFirstInitializeParameterCore = true; } } protected override void OnParametersSet() { if (!IsFirstInitializeParameters) { if (GridName.IsNullOrWhiteSpace()) GridName = $"{typeof(TDataItem).Name}Grid"; _gridLogName = $"[{GridName}]"; // Set default AutoSaveLayoutName if not provided if (AutoSaveLayoutName.IsNullOrWhiteSpace()) AutoSaveLayoutName = $"Grid{typeof(TDataItem).Name}"; // Set up layout auto-loading/saving LayoutAutoLoading = Grid_LayoutAutoLoading; LayoutAutoSaving = Grid_LayoutAutoSaving; // Register this grid with the wrapper for splitter size persistence GridWrapper?.RegisterGrid(this); IsFirstInitializeParameters = true; } base.OnParametersSet(); } #region Layout Persistence /// /// Gets the user-specific layout storage key. Override to provide custom user identification. /// protected virtual int GetLayoutUserId() => 0; private async Task Grid_LayoutAutoLoading(GridPersistentLayoutEventArgs e) { e.Layout = await LoadLayoutFromLocalStorageAsync(LayoutStorageKey); } private async Task Grid_LayoutAutoSaving(GridPersistentLayoutEventArgs e) { await SaveLayoutToLocalStorageAsync(e.Layout, LayoutStorageKey); } protected virtual async Task LoadLayoutFromLocalStorageAsync(string localStorageKey) { try { var json = await JSRuntime.InvokeAsync("localStorage.getItem", localStorageKey); if (!string.IsNullOrWhiteSpace(json)) return JsonSerializer.Deserialize(json); } catch { // Mute exceptions for the server prerender stage } return null; } protected virtual async Task SaveLayoutToLocalStorageAsync(GridPersistentLayout layout, string localStorageKey) { try { var json = JsonSerializer.Serialize(layout); await JSRuntime.InvokeVoidAsync("localStorage.setItem", localStorageKey, json); } catch { // Mute exceptions for the server prerender stage } } #endregion //public Task AddDataItem(TDataItem dataItem, int messageTag) => PostDataToServerAsync(dataItem, messageTag, TrackingState.Add); public Task UpdateDataItem(TDataItem dataItem) => _dataSource.Update(dataItem, true); public Task UpdateDataItemAsync(TDataItem dataItem) { _dataSource.Update(dataItem, false); return SaveChangesToServerAsync(); } //public Task UpdateDataItem(TDataItem dataItem, int messageTag) => PostDataToServerAsync(dataItem, messageTag, TrackingState.Update); public Task AddOrUpdateDataItem(TDataItem dataItem) => _dataSource.AddOrUpdate(dataItem, true); public Task RemoveDataItem(TDataItem dataItem) => _dataSource.Remove(dataItem, true); //public Task RemoveDataItem(TDataItem dataItem, int messageTag) => PostDataToServerAsync(dataItem, messageTag, TrackingState.Remove); public Task RemoveDataItem(TId id) => RemoveDataItem(id, RemoveMessageTag); public Task RemoveDataItem(TId id, int messageTag) { return _dataSource.Remove(id, true); } public Task ReloadDataSourceAsync() { return _dataSource.LoadDataSourceAsync(false); } /// /// Navigates to the previous row in the grid /// public void StepPrevRow() { var currentIndex = GetFocusedRowIndex(); if (currentIndex > 0) { SetFocusedRowIndex(currentIndex - 1); } } /// /// Navigates to the next row in the grid /// 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; _isDisposed = true; // Unsubscribe from events to prevent callbacks to disposed component if (_dataSource != null) { _dataSource.OnDataSourceLoaded -= OnDataSourceLoaded; _dataSource.OnDataSourceItemChanged -= OnDataSourceItemChanged; _dataSource.OnSyncingStateChanged -= OnDataSourceSyncingStateChanged; } CustomizeElement -= OnCustomizeElement; // Dispose base if it implements IAsyncDisposable if (this is IAsyncDisposable asyncDisposable && asyncDisposable != this) { await asyncDisposable.DisposeAsync(); } GC.SuppressFinalize(this); } } public class GridDataItemChangingEventArgs : GridDataItemChangedEventArgs where TDataItem : class { internal GridDataItemChangingEventArgs(IMgGridBase grid, TDataItem dataItem, TrackingState trackingState) : base(grid, dataItem, trackingState) { } public bool IsCanceled { get; set; } } public class GridDataItemChangedEventArgs where TDataItem : class { internal GridDataItemChangedEventArgs(IMgGridBase grid, TDataItem dataItem, TrackingState trackingState) { Grid = grid ?? throw new ArgumentNullException(nameof(grid)); DataItem = dataItem; TrackingState = trackingState; } public IMgGridBase Grid { get; } public TDataItem DataItem { get; } public TrackingState TrackingState { get; } public bool CancelStateChangeInvoke { get; set; } }