using AyCode.Core.Helpers;
using AyCode.Core.Interfaces;
using AyCode.Core.Loggers;
using AyCode.Services.Server.SignalRs;
using AyCode.Services.SignalRs;
using DevExpress.Blazor;
using DevExpress.Data.Filtering;
using DevExpress.Data.Linq;
using DevExpress.Data.Linq.Helpers;
using System.Collections;
namespace AyCode.Blazor.Components.Components.Grids;
#region Models
///
/// Sorting information for a single field
///
public class SignalRGridSortInfo
{
public string FieldName { get; set; } = "";
public bool Descending { get; set; }
}
#endregion
///
/// GridCustomDataSource implementation that wraps AcSignalRDataSource.
/// Provides instant local filtering for previously seen filter criteria,
/// while refreshing data in background using SignalR callback pattern.
///
/// Key features:
/// - Uses AcSignalRDataSource for caching and background populate
/// - Tracks seen filter criteria - if already seen, returns local data instantly
/// - Background refresh with callback/populate pattern (no UI blocking)
/// - Full GridCustomDataSource support (filter, sort, page, group, summary)
///
/// Entity type implementing IId
/// ID type (int, Guid, long, etc.)
public class MgGridSignalRDataSource : GridCustomDataSource
where TDataItem : class, IId
where TId : struct
{
private readonly AcSignalRDataSource> _innerDataSource;
private readonly AcLoggerBase? _logger;
// DevExpress CriteriaOperator to Expression converter
private readonly CriteriaToExpressionConverter _criteriaConverter = new();
// Track filter criteria that have been seen before
private readonly HashSet _knownFilterCriteria = new(StringComparer.Ordinal);
// Lock for thread-safe operations
private readonly object _syncLock = new();
// Event fired when background refresh completes
public event Action? OnBackgroundRefreshCompleted;
///
/// Creates a new MgGridSignalRDataSource wrapping an existing AcSignalRDataSource
///
/// The underlying AcSignalRDataSource that handles caching and SignalR communication
/// Optional logger for debugging
public MgGridSignalRDataSource(
AcSignalRDataSource> innerDataSource,
AcLoggerBase? logger = null)
{
_innerDataSource = innerDataSource ?? throw new ArgumentNullException(nameof(innerDataSource));
_logger = logger;
// Subscribe to data source events
_innerDataSource.OnDataSourceLoaded += OnInnerDataSourceLoaded;
}
///
/// Specifies the data item type for the grid
///
protected override Type DataItemType => typeof(TDataItem);
///
/// Gets the inner AcSignalRDataSource for direct access if needed
///
public AcSignalRDataSource> InnerDataSource => _innerDataSource;
#region GridCustomDataSource Implementation
///
/// Gets the total count of items matching the current filter.
/// If filter was seen before, returns local count instantly and refreshes in background.
///
public override async Task GetItemCountAsync(
GridCustomDataSourceCountOptions options,
CancellationToken cancellationToken)
{
var filterKey = GetFilterKey(options.FilterCriteria);
_logger?.Debug($"[MgGridSignalRDataSource] GetItemCountAsync - Filter: {filterKey}");
// If we have local data and this filter was seen before, return local count
if (_innerDataSource.Count > 0 && IsKnownFilter(filterKey))
{
var localCount = ApplyLocalFilter(_innerDataSource.ToList(), options.FilterCriteria).Count;
_logger?.Debug($"[MgGridSignalRDataSource] Returning local count: {localCount}, refreshing in background");
// Refresh in background (fire-and-forget)
_ = RefreshInBackgroundAsync(filterKey);
return localCount;
}
// First time seeing this filter - must wait for server
_logger?.Debug("[MgGridSignalRDataSource] New filter, waiting for server data");
await LoadFromServerAsync();
MarkFilterAsKnown(filterKey);
return ApplyLocalFilter(_innerDataSource.ToList(), options.FilterCriteria).Count;
}
///
/// Gets items for the current page with filtering and sorting applied.
/// If filter was seen before, returns local data instantly and refreshes in background.
///
public override async Task GetItemsAsync(
GridCustomDataSourceItemsOptions options,
CancellationToken cancellationToken)
{
var filterKey = GetFilterKey(options.FilterCriteria);
_logger?.Debug($"[MgGridSignalRDataSource] GetItemsAsync - Skip: {options.StartIndex}, Take: {options.Count}, Filter: {filterKey}");
// If we have local data and this filter was seen before, return local data
if (_innerDataSource.Count > 0 && IsKnownFilter(filterKey))
{
var localResult = GetLocalItems(options);
_logger?.Debug($"[MgGridSignalRDataSource] Returning {localResult.Count} local items, refreshing in background");
// Refresh in background (fire-and-forget)
RefreshInBackgroundAsync(filterKey).Forget();
return localResult;
}
// First time seeing this filter - must wait for server
_logger?.Debug("[MgGridSignalRDataSource] New filter, waiting for server data");
await LoadFromServerAsync();
MarkFilterAsKnown(filterKey);
return GetLocalItems(options);
}
///
/// Gets unique values for a column (used in filter dropdowns).
/// Always returns from local data.
///
public override Task