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:
@@ -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
Reference in New Issue
Block a user