Server-side grid virtualization: DxGrid adapter & API

Implemented MgGridVirtualizedDataSource<T> for DevExpress DxGrid integration, enabling server-side paging/filtering/sorting via SignalR. Added developer-only test page and menu entry. Extended IFruitBankDataControllerCommon and client DTOs for virtualized queries. Renamed ToPagedListAsync to ToGridListAsync and improved AcGridCriteriaTranslator to whitelist only real DB columns. Temporarily relaxed relation-path filtering/sorting for closed testing (see ACCORE-SIGVDS-T-R7X2). Updated documentation, TODOs, icons, CSS, and local dev tooling. Feature is developer-only and not enabled in production grids.
This commit is contained in:
2026-07-20 10:33:35 +02:00
parent 40e6c613cd
commit 1ce86d1371
2 changed files with 83 additions and 1 deletions
@@ -0,0 +1,82 @@
using System.Collections;
using AyCode.Core.Interfaces;
using AyCode.Services.SignalRs;
using DevExpress.Blazor;
using DevExpress.Data.Filtering;
using DevExpress.Data.Filtering.Helpers; // ReferenceEqualsNull extension
namespace AyCode.Blazor.Components.Components.Grids;
/// <summary>
/// DevExpress-facing adapter for <see cref="GridDataMode.ServerVirtualized"/>: translates DxGrid load options
/// into <see cref="AcSignalRVirtualizedDataSource{TDataItem, TId}"/> calls. Composition, not inheritance — the
/// datasource keeps its own base.
/// </summary>
/// <remarks>Contract: <c>AyCode.Services/docs/SIGNALR_VIRTUALIZED_DATASOURCE/README.md</c> (AyCode.Core repo).</remarks>
public class MgGridVirtualizedDataSource<TDataItem, TId> : GridCustomDataSource
where TDataItem : class, IId<TId> where TId : struct
{
private readonly AcSignalRVirtualizedDataSource<TDataItem, TId> _dataSource;
public MgGridVirtualizedDataSource(AcSignalRVirtualizedDataSource<TDataItem, TId> dataSource)
=> _dataSource = dataSource ?? throw new ArgumentNullException(nameof(dataSource));
protected override Type DataItemType => typeof(TDataItem);
/// <summary>The wrapped datasource — for save operations and scope changes (<c>CustomFilterCriteria</c>).</summary>
public AcSignalRVirtualizedDataSource<TDataItem, TId> DataSource => _dataSource;
public override async Task<IList> GetItemsAsync(GridCustomDataSourceItemsOptions options, CancellationToken cancellationToken)
{
var result = await _dataSource.LoadPageAsync(
ToCriteriaString(options.FilterCriteria), ToSortDescriptors(options.SortInfo),
options.StartIndex, ToTake(options.Count));
// Server order IS display order: under a custom data source the grid does not re-sort.
return result.Items!;
}
/// <summary>
/// Count-only probe (<c>Take = 0</c>): the server computes the total and returns an EMPTY page, so this
/// costs a round trip but no rows. Deliberately naive for the first version — correct regardless of the
/// order in which DevExpress calls the two methods. Caching the total from the last page response (keyed by
/// filter, since paging does not change it) is the obvious optimization once the real call pattern is
/// measured.
/// </summary>
public override async Task<int> GetItemCountAsync(GridCustomDataSourceCountOptions options, CancellationToken cancellationToken)
{
var result = await _dataSource.LoadPageAsync(
ToCriteriaString(options.FilterCriteria), sort: null, skip: null, take: 0);
return result.TotalRecords!.Value;
}
#region Option translation
/// <summary>
/// <c>CriteriaOperator</c> overloads <c>==</c>, so <c>criteria == null</c> does NOT do what it looks like —
/// <c>ReferenceEqualsNull()</c> is the correct test.
/// </summary>
private static string? ToCriteriaString(CriteriaOperator criteria)
=> criteria.ReferenceEqualsNull() ? null : CriteriaOperator.ToString(criteria);
/// <summary>
/// A NEGATIVE <c>Count</c> means "no paging, return everything" — passing it through would emit
/// <c>Take(-1)</c>. Maps to <c>null</c> (no page size).
/// </summary>
private static int? ToTake(int count) => count >= 0 ? count : null;
private static List<AcGridSortDescriptor>? ToSortDescriptors(IReadOnlyList<GridCustomDataSourceSortInfo>? sortInfo)
{
if (sortInfo is not { Count: > 0 })
return null;
var result = new List<AcGridSortDescriptor>(sortInfo.Count);
foreach (var info in sortInfo)
result.Add(new AcGridSortDescriptor { FieldName = info.FieldName, Descending = info.DescendingSortOrder });
return result;
}
#endregion
}
File diff suppressed because one or more lines are too long