Skip to main content

GridCustomDataSource Class

Allows you to implement a custom data source for DxGrid and DxTreeList components.

Namespace: DevExpress.Blazor

Assembly: DevExpress.Blazor.Grid.v26.1.dll

NuGet Package: DevExpress.Blazor

Declaration

public abstract class GridCustomDataSource

Remarks

You can bind the Grid component to data in the following ways:

The TreeList component supports the following data binding scenarios:

If these data binding modes do not meet specific business requirements, you can implement your own data provider based on the GridCustomDataSource class.

Limitations and Considerations

Note the following limitations when using a custom data source.

Grid

TreeList

  • Summary calculation is unavailable.
  • The pager summary may display the number of loaded rows instead of the total row count.
  • AllPages mode of the Select All checkbox is unsupported.
  • Sort and filter operations cancel an active select/deselect all operation.
  • DxTreeList sorts data at the component level instead of requesting sorted data from the data source. As a result, sort settings configured at the data source level do not affect the TreeList sort order.
  • The SelectAllAsync(Boolean) and DeselectAllAsync() methods load all data and can impact overall performance and increase memory consumption.
  • A second call to the SelectAllAsync(Boolean) or DeselectAllAsync() method cancels the operation initiated by the previous call.
  • The TreeList component sends an additional request during initialization to determine the item type. If the item type differs from ExpandoObject, you can specify this type directly in a GridCustomDataSource descendant’s configuration to avoid additional requests:

    private class MyCustomDataSource<T> : GridCustomDataSource {
        protected override Type DataItemType => typeof(T);
        // ...
    }
    

Example: Implement Custom Data Provider Using OData

The following code snippet implements a custom data provider that uses an OData client to fetch data from the Web API Service:

View Example: Bind the Grid to OData Data Source

using System.Collections;
using Blazor.WebAssembly.Models;
using DevExpress.Blazor;
using DevExpress.Data.Filtering;
using DevExpress.Data.Filtering.Helpers;
using Simple.OData.Client;

namespace Blazor.WebAssembly.Services;

public class SimpleODataClientDataSource : GridCustomDataSource {
    private readonly ODataClient _client;
    public SimpleODataClientDataSource(IHttpClientFactory httpClientFactory)
        => _client = new ODataClient(new ODataClientSettings(httpClientFactory.CreateClient("API"), new Uri("odata/", UriKind.Relative)));

    public override async Task<int> GetItemCountAsync(GridCustomDataSourceCountOptions options, CancellationToken cancellationToken)
        => await ApplyFiltering(options.FilterCriteria, _client.For<Post>()).Count()
            .FindScalarAsync<int>(cancellationToken);

    public override async Task<IList> GetItemsAsync(GridCustomDataSourceItemsOptions options, CancellationToken cancellationToken) {
        var filteredClient = ApplyFiltering(options.FilterCriteria, _client.For<Post>().Top(options.Count).Skip(options.StartIndex));
        return (await ApplySorting(options, filteredClient).FindEntriesAsync(cancellationToken)).ToList();
    }

    private static IBoundClient<Post> ApplyFiltering(CriteriaOperator criteria, IBoundClient<Post> boundClient)
        => !criteria.ReferenceEqualsNull() ? boundClient.Filter(ToSimpleClientCriteria(criteria)) : boundClient;

    private static string ToSimpleClientCriteria(CriteriaOperator criteria)
        => $"{criteria}".Replace("[", "").Replace("]", "");

    private static IBoundClient<Post> ApplySorting(GridCustomDataSourceItemsOptions options, IBoundClient<Post> boundClient) {
        return options.SortInfo != null && options.SortInfo.Any() ? boundClient.OrderBy(options.SortInfo
                .Where(info => !info.DescendingSortOrder).Select(info => info.FieldName).ToArray())
            .OrderByDescending(options.SortInfo
                .Where(info => info.DescendingSortOrder).Select(info => info.FieldName).ToArray()) : boundClient;
    }
    public async Task DeleteAsync(Post instance)
        => await _client.For<Post>().Key(instance.ID).DeleteEntryAsync();

    public async Task AddOrUpdateAsync(Post instance, bool update = false) {
        if (!update) {
            await _client.For<Post>().Set(new { instance.Title, instance.Content }).InsertEntryAsync();
        }
        else {

            await _client.For<Post>().Key(instance.ID).Set(instance).UpdateEntryAsync();
        }
    }
}

Example: Implement Custom Data Provider Using Entity Framework Core

The following code snippet implements a custom data provider that uses EF Core to fetch data from a database:

View Example: Custom Data Source Binding using Entity Framework Core

using DevExpress.Blazor;
using DevExpress.Data.Filtering;
using DevExpress.Data.Filtering.Helpers;
using CustomDataSource.Models;
using System.Collections;
using System.Linq.Expressions;
using DevExpress.Data.Linq;
using DevExpress.Data.Linq.Helpers;
using Microsoft.EntityFrameworkCore;

namespace CustomDataSource.Services;

public class OrdersDataSource : GridCustomDataSource, IDisposable {
    protected override Type DataItemType => typeof(Order);
    private readonly NorthwindContext _context;
    private bool _disposed = false;

    public OrdersDataSource(IDbContextFactory<NorthwindContext> contextFactory) {
        _context = contextFactory.CreateDbContext();
    } 

    public override Task<int> GetItemCountAsync(GridCustomDataSourceCountOptions options, CancellationToken cancellationToken) {
        return ApplyFiltering(options.FilterCriteria, _context.Orders)
            .CountAsync(cancellationToken);
    }

    public override async Task<IList> GetItemsAsync(GridCustomDataSourceItemsOptions options, CancellationToken cancellationToken) {
        var filteredQuery = ApplyFiltering(options.FilterCriteria, _context.Orders);

        if (options.Count >= 0) {
            return await ApplySorting(options, filteredQuery)
                .Skip(options.StartIndex)
                .Take(options.Count)
                .ToListAsync(cancellationToken);
        }

        return await ApplySorting(options, filteredQuery).ToListAsync(cancellationToken);
    }

    public override Task<object[]> GetUniqueValuesAsync(GridCustomDataSourceUniqueValuesOptions options, CancellationToken cancellationToken) {
        var filteredQuery = ApplyFiltering(options.FilterCriteria, _context.Orders);
        var lambda = GetTypedLambda(options.FieldName);
        return filteredQuery
            .Select(lambda)
            .Distinct()
            .OrderBy(x => x)
            .ToArrayAsync(cancellationToken);
    }

    #region Summary methods

    public override async Task<IList<GridCustomDataSourceGroupInfo>> GetGroupInfoAsync(GridCustomDataSourceGroupingOptions options,
        CancellationToken cancellationToken) {
        var filteredQuery = ApplyFiltering(options.FilterCriteria, _context.Orders);

        var summaryQuery = GetGroupByAndSelectQuery(options.SummaryInfo, filteredQuery, false, options.FieldName);
        var resultList = await summaryQuery.ToListAsync(cancellationToken);

        var groupInfoList = new List<GridCustomDataSourceGroupInfo>();
        foreach (var result in resultList) {
            var groupInfo = new GridCustomDataSourceGroupInfo() { Value = result["Value"], DataItemCount = (int)result["DataItemCount"] };

            var summaryList = new List<object>();
            foreach (var kvp in result) {
                if (kvp.Key.StartsWith("Summary_")) {
                    summaryList.Add(kvp.Value);
                }
            }

            groupInfo.SummaryValues = summaryList;
            groupInfoList.Add(groupInfo);
        }

        return options.DescendingSortOrder
            ? groupInfoList
                .OrderByDescending(g => g.Value)
                .ToList()
            : groupInfoList
                .OrderBy(g => g.Value)
                .ToList();
    }

    public override async Task<IList> GetTotalSummaryAsync(GridCustomDataSourceTotalSummaryOptions options, CancellationToken cancellationToken) {
        var filteredQuery = ApplyFiltering(options.FilterCriteria, _context.Orders);

        var summaryQuery = GetGroupByAndSelectQuery(options.SummaryInfo, filteredQuery, true);
        var result = await summaryQuery.FirstOrDefaultAsync(cancellationToken);
        return result != null
            ? result
                .Select(d => d.Value)
                .ToList()
            : null;
    }

    #endregion

    #region Helper methods

    private static IQueryable<Order> ApplyFiltering(CriteriaOperator criteria, IQueryable<Order> queryableSource) {
        return !criteria.ReferenceEqualsNull()
            ? (IQueryable<Order>)queryableSource.AppendWhere(
                new CriteriaToEFExpressionConverter(queryableSource.Provider.GetType()), criteria)
            : queryableSource;
    }

    private static IQueryable<Order> ApplySorting(GridCustomDataSourceItemsOptions options, IQueryable<Order> queryableSource) {
        if (options.SortInfo != null) {
            foreach (var sortInfo in options.SortInfo) {
                queryableSource = SortByField(sortInfo.FieldName, sortInfo.DescendingSortOrder, queryableSource);
            }
        }

        return queryableSource;
    }

    private static IQueryable<Order> SortByField(string fieldName, bool descendingOrder, IQueryable<Order> queryableSource) {
        var lambda = GetTypedLambda(fieldName);
        if (queryableSource.Expression.Type != typeof(IOrderedQueryable<Order>)) {
            return !descendingOrder
                ? queryableSource.OrderBy(lambda)
                : queryableSource.OrderByDescending(lambda);
        }

        return !descendingOrder
            ? ((IOrderedQueryable<Order>)queryableSource).ThenBy(lambda)
            : ((IOrderedQueryable<Order>)queryableSource).ThenByDescending(lambda);
    }

    private static Expression<Func<Order, object>> GetTypedLambda(string fieldName) {
        var parameter = Expression.Parameter(typeof(Order));
        var property = Expression.Property(parameter, fieldName);
        var lambda = Expression.Lambda<Func<Order, object>>(Expression.Convert(property, typeof(object)), parameter);
        return lambda;
    }

    private static IQueryable<Dictionary<string, object>> GetGroupByAndSelectQuery(IReadOnlyList<GridCustomDataSourceSummaryInfo> summaryInfoList,
        IQueryable<Order> queryableSource, bool totalSummary, string groupedFieldName = "") {
        IQueryable groupedQuery;
        Type groupedPropertyType;
        if (totalSummary) {
            groupedPropertyType = typeof(int);
            groupedQuery = queryableSource.GroupBy(x => 1);
        } else {
            var parameter = Expression.Parameter(typeof(Order));
            var property = Expression.Property(parameter, groupedFieldName);
            groupedPropertyType = property.Type;
            var groupLambda = Expression.Lambda(property, parameter);

            var groupByCall = Expression.Call(typeof(Queryable), "GroupBy", new[] { typeof(Order), groupedPropertyType },
                queryableSource.Expression, groupLambda);
            groupedQuery = queryableSource.Provider.CreateQuery(groupByCall);
        }

        var groupType = typeof(IGrouping<,>).MakeGenericType(groupedPropertyType, typeof(Order));
        var group = Expression.Parameter(groupType);

        // defining a dictionary that will hold group info
        var dictItems = new List<ElementInit>();
        var addMethod = typeof(Dictionary<string, object>).GetMethod("Add");
        if (!totalSummary) {
            dictItems.AddRange(new List<ElementInit> {
                Expression.ElementInit(addMethod, Expression.Constant("Value"),
                    Expression.Convert(Expression.Property(group, "Key"), typeof(object))
                ),
                Expression.ElementInit(addMethod, Expression.Constant("DataItemCount"),
                    Expression.Convert(Expression.Call(typeof(Enumerable), "Count",
                        new[] { typeof(Order) }, group), typeof(object))
                )
            });
        }

        for (int i = 0; i < summaryInfoList.Count; i++) {
            var summaryInfo = summaryInfoList[i];

            var groupSummaryCall = GetGroupSummaryCall(summaryInfo, group);
            dictItems.Add(Expression.ElementInit(addMethod, Expression.Constant("Summary_" + i),
                Expression.Convert(groupSummaryCall, typeof(object))));
        }

        var dictInit = Expression.ListInit(Expression.New(typeof(Dictionary<string, object>)), dictItems);

        // creating the select query
        Type selectFuncType = typeof(Func<,>).MakeGenericType(groupType, typeof(Dictionary<string, object>));
        var selectLambda = Expression.Lambda(selectFuncType, dictInit, group);
        var selectCall = Expression.Call(typeof(Queryable), "Select",
            new[] { groupedQuery.ElementType, typeof(Dictionary<string, object>) },
            groupedQuery.Expression, selectLambda);

        return groupedQuery.Provider.CreateQuery<Dictionary<string, object>>(selectCall);
    }

    private static Expression GetGroupSummaryCall(GridCustomDataSourceSummaryInfo summaryInfo,
        Expression group) {
        var parameter = Expression.Parameter(typeof(Order));
        var property = Expression.Property(parameter, summaryInfo.FieldName);
        var summaryLambda = Expression.Lambda(property, parameter);

        switch (summaryInfo.SummaryType) {
            case GridSummaryItemType.Avg:
                return Expression.Call(typeof(Enumerable), "Average", new[] { typeof(Order) },
                    group, summaryLambda);
            case GridSummaryItemType.Count:
                return Expression.Call(typeof(Enumerable), "Count", new[] { typeof(Order) },
                    group);
            case GridSummaryItemType.Max:
                return Expression.Call(typeof(Enumerable), "Max", new[] { typeof(Order), property.Type },
                    group, summaryLambda);
            case GridSummaryItemType.Min:
                return Expression.Call(typeof(Enumerable), "Min", new[] { typeof(Order), property.Type },
                    group, summaryLambda);
            case GridSummaryItemType.Sum:
                return Expression.Call(typeof(Enumerable), "Sum", new[] { typeof(Order) },
                    group, summaryLambda);
            default:
                throw new NotSupportedException(summaryInfo.SummaryType.ToString());
        }
    }

    #endregion

    #region IDisposable Implementation

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing) {
        if (!_disposed) {
            if (disposing) {
                _context?.Dispose();
            }
            _disposed = true;
        }
    }

    #endregion
}

Inheritance

See Also