server-side Pagination, pager on client side shows only the first page

dear support, i am trying to implement Database-side pagination with service classes.

the client shows only the first page and 50 records. it does not allow to go to other pages.

here is the console output. can you please help ?

***

nfo: Microsoft.EntityFrameworkCore.Database.Command[20101]

      Executed DbCommand (2,422ms) [Parameters=[], CommandType='Text', CommandTimeout='30']

      SELECT COUNT(*)

      FROM [Contacts] AS [c]

info: Microsoft.EntityFrameworkCore.Database.Command[20101]

      Executed DbCommand (22ms) [Parameters=[@__p_0='?' (DbType = Int32), @__p_1='?' (DbType = Int32)], CommandType='Text', CommandTimeout='30']

      SELECT [c].[Id], [c].[Arc], [c].[Comment], [c].[ContactPersonInOrganization], [c].[ContactType], [c].[CreatedAt], [c].[DateOfBirth], [c].[Email], [c].[FirstName], [c].[GdprConsent], [c].[Gender], [c].[Jurisdiction], [c].[LastName], [c].[NationalId], [c].[OrganizationFormationDate], [c].[OrganizationName], [c].[OrganizationRegistrationNumber], [c].[OrganizationType], [c].[OurReference], [c].[Passport], [c].[PhoneHome], [c].[PhoneMobile], [c].[PhoneOther], [c].[PhoneWork], [c].[RowVersion], [c].[Status], [c].[TaxIdentificationCode], [c].[UpdatedAt], [c].[Vat]

      FROM [Contacts] AS [c]

      ORDER BY [c].[Id]

      OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY

LoadData: Retrieved 50 contacts

LoadData: totalRecords=4320620

LoadData: PageCount=86413

LoadData: Grid refreshed******

*****************

@page "/contactspage3"

@inject ToastService ToastService

@inject IContactService ContactService

@using Syncfusion.Blazor

@using Syncfusion.Blazor.Grids

@using Microsoft.EntityFrameworkCore


<PageTitle>Contacts</PageTitle>

<SfGrid Height="100%" Width="100%" EnableAdaptiveUI="true" EnableStickyHeader="true" @ref="gridContacts" PageSettings="@PageSettings" TotalItemsCount="@totalRecords" TValue="Contact" DataSource="@Contacts" AllowPaging="true" EnableVirtualization=false AllowGrouping=false AllowFiltering=true AllowExcelExport=true>

    <GridEvents TValue="Contact" OnActionBegin="OnActionBegin" />

    <GridFilterSettings Type="Syncfusion.Blazor.Grids.FilterType.Menu" />

    <GridFilterSettings Mode="FilterBarMode.Immediate" ImmediateModeDelay="700"></GridFilterSettings>

    <GridSelectionSettings Mode="Syncfusion.Blazor.Grids.SelectionMode.Row" Type="Syncfusion.Blazor.Grids.SelectionType.Multiple" CheckboxOnly="true" PersistSelection="true"></GridSelectionSettings>

    <GridColumns>

        <GridColumn HeaderText="Manage Records" Width="100">

            <GridCommandColumns>

                <GridCommandColumn Type="CommandButtonType.Edit" ButtonOption="@(new CommandButtonOptions() { IconCss = "e-icons e-edit", CssClass = "e-flat" })"></GridCommandColumn>

            </GridCommandColumns>

        </GridColumn>

        <GridColumn Type="ColumnType.CheckBox" Width="30"></GridColumn>

        <GridColumn Field=@nameof(Contact.Id) HeaderText="dbId" TextAlign="TextAlign.Left" IsPrimaryKey="true" Width="120"></GridColumn>

        <GridColumn Field=@nameof(Contact.ContactType) HeaderText="Type" Width="120"></GridColumn>

        <GridColumn Field=@nameof(Contact.LastName) HeaderText=@nameof(Contact.LastName) Width="120"></GridColumn>

        <GridColumn Field=@nameof(Contact.FirstName) HeaderText=@nameof(Contact.FirstName) Width="120"></GridColumn>

        <GridColumn Field=@nameof(Contact.OrganizationName) HeaderText=@nameof(Contact.OrganizationName) Width="120"></GridColumn>

        <GridColumn Field=@nameof(Contact.DateOfBirth) HeaderText=@nameof(Contact.DateOfBirth) Width="120"></GridColumn>

        <GridColumn Field=@nameof(Contact.PhoneMobile) HeaderText="Mobile" Width="120"></GridColumn>

    </GridColumns>

</SfGrid>



@code {

    private SfGrid<Contact>? gridContacts;

    private List<Contact> Contacts { get; set; } = new List<Contact>();

    private GridPageSettings PageSettings { get; set; } = new GridPageSettings { PageSize = 50 };

    private const int PageSize = 50;

    private int totalRecords=5;


    protected override async Task OnInitializedAsync()

    {

        await LoadData(0, PageSize);

    }


    private async Task OnActionBegin(ActionEventArgs<Contact> args)

    {

        if (args.RequestType == Syncfusion.Blazor.Grids.Action.Paging)

        {

            var currentPage = args.CurrentPage;

            var pageSize = PageSize;

            var skip = (currentPage - 1) * pageSize;

            Console.WriteLine($"OnActionBegin: currentPage={currentPage}, pageSize={pageSize}, skip={skip}");

            await LoadData(skip, pageSize);

        }

    }


    private async Task LoadData(int skip, int take)

    {

        Console.WriteLine($"LoadData: skip={skip}, take={take}");

        var result = await ContactService.GetContactsAsyncv2(skip, take);

        Console.WriteLine($"LoadData: Retrieved {result.Contacts.Count} contacts");

        Contacts = result.Contacts.ToList(); // Ensure complete list replacement

        totalRecords = result.TotalCount;

        Console.WriteLine($"LoadData: totalRecords={totalRecords}");


        // Calculate PageCount based on total records and PageSize

        PageSettings.PageCount = (int)Math.Ceiling((double)totalRecords / PageSize);

        Console.WriteLine($"LoadData: PageCount={PageSettings.PageCount}");

        // Force a grid re-render for debugging purposes

        await Task.Delay(100); // Adjust delay as needed

        await gridContacts.Refresh();

        Console.WriteLine($"LoadData: Grid refreshed");

        StateHasChanged();

    }

}



=====

 public interface IContactService

 {

     Task<(List<Contact> Contacts, int TotalCount)> GetContactsAsyncv2(int skip,int take);


    Task<List<Contact>> GetContactsAsync(QuerySpecification<Contact> spec);

     Task<(List<Contact> Contacts, int TotalCount)> GetContactsAsync2(QuerySpecification<Contact> spec);


     Task<Contact?> GetContactByIdAsync(int id);

     Task<Contact> CreateContactAsync(Contact contact);

     Task<Contact> UpdateContactAsync(Contact contact);

     Task DeleteContactAsync(int id);

 }


 public class ContactService : IContactService

 {

     private readonly ApplicationDbContext _context;

     private readonly MyQueryBuilder _myqueryBuilder;


     public ContactService(ApplicationDbContext context, ILogger<MyQueryBuilder> logger)

     {

         _context = context;

         _myqueryBuilder = new MyQueryBuilder(logger);

     }


     public async Task<(List<Contact> Contacts, int TotalCount)> GetContactsAsyncv2(int skip, int take)

     {

         var totalCount = await _context.Contacts.CountAsync();

         var contacts = await _context.Contacts

                                      .OrderBy(c => c.Id)

                                      .Skip(skip)

                                      .Take(take)

                                      .ToListAsync(); // Using ToListAsync() to return List<Contact>

         return (contacts, totalCount);

     }


5 Replies

MS Monisha Saravanan Syncfusion Team August 2, 2024 04:46 AM UTC


Hi Daniel,


Greetings from Syncfusion.


Query: “dear support, i am trying to implement Database-side pagination with service classes. the client shows only the first page and 50 records. it does not allow to go to other pages. here is the console output. can you please help ?”


We suspect that you are encountering issues with pagination on your end. After reviewing your shared code, we observed that only the current page data is being fetched. As a result, the pager element only displays the current page.


To render the pager correctly and allow navigation through all pages, we need to provide the total count of records to the DataGrid. This enables the pager to calculate and display all available pages.


In the example below, we utilized the custom adaptor feature of the DataGrid. With this approach, the DataGrid handles the skip and take requests internally, eliminating the need for external events (OnActionBegin) to manage pagination.


Kindly check the below attached sample and code snippet for your reference.


 

@inject IJSRuntime Runtime

@inject HttpClient Http

 

@if(@IsVisible)

{

 

    <SfGrid ID="Grid" @ref="@Grid"   TValue="Order" Height="315" Toolbar="@(new List<string>() { "Add", "Edit","Update","Delete","Cancel" , "Search", "ExcelExport" , "CsvExport" ,"PdfExport"})" AllowFiltering="true" AllowSelection="true" AllowSorting="true" AllowResizing="true" AllowReordering="true" AllowGrouping="true" AllowPaging="true"  AllowExcelExport="true" AllowPdfExport="true">

        <GridEditSettings AllowAdding="true" AllowDeleting="true" AllowEditing="true" Mode="EditMode.Normal"></GridEditSettings>

        <SfDataManager AdaptorInstance="@typeof(CustomAdaptor)" Adaptor="Adaptors.CustomAdaptor"></SfDataManager>

        <GridFilterSettings Type="Syncfusion.Blazor.Grids.FilterType.Excel"></GridFilterSettings>

        <GridPageSettings PageSize="50"></GridPageSettings>

    </SfGrid>

}

@code{

    SfGrid<Order>?  Grid { get; set; }

    public static List<Order> result = new List<Order>();

    public bool IsVisible { get; set; } = false;

 

    protected override async void OnInitialized()

    {

        var apiUrl = "https://localhost:7157/api/DataGrid";

        result = await Http.GetFromJsonAsync<List<Order>>(apiUrl);

        //result = result.Take(50).ToList(); //if this line is ebabled then it will render only one page in DataGrid

        IsVisible = true;

        StateHasChanged();

 

    }

 

    // Implementing custom adaptor by extending the DataAdaptor class

    public class CustomAdaptor : DataAdaptor

    {

        // Performs data Read operation

        public override object  Read(DataManagerRequest dm, string key = null)

        {

            IEnumerable<Order> DataSource = result;

           

            int count = DataSource.Cast<Order>().Count(); // need to pass the entire DataSource count

            if (dm.Skip != 0)

            {

                //Paging

                DataSource = DataOperations.PerformSkip(DataSource, dm.Skip);

            }

            if (dm.Take != 0)

            {

                DataSource = DataOperations.PerformTake(DataSource, dm.Take);

            }

            return dm.RequiresCounts ? new DataResult() { Result = DataSource, Count = count } : (object)DataSource;

        }

 

  }

  }



Kindly get back to us if we misunderstood your query or if you have further queries.


Regards,

Monisha




Attachment: WASM_90ecbb37.zip


DA DAN August 2, 2024 07:11 AM UTC

hi, if you inspect the console out****

LoadData: Retrieved 50 contacts

LoadData: totalRecords=4320620

LoadData: PageCount=86413

LoadData: Grid refreshed******

total records number is returned 

     public async Task<(List<Contact> Contacts, int TotalCount)> GetContactsAsyncv2(int skip, int take) {

       return (contacts, totalCount);

}

So why the grid is not showing the rest of the pages that must be fetched on demand?

we dont want to use an api but service classed. can you show us a very simple working example in order to achieve the requirements (Database-side pagination,filtering) to load data "on damand when changing page, 




PS Prathap Senthil Syncfusion Team August 5, 2024 06:09 PM UTC

Hi Daniel,

Based on the reported problem, we would like to clarify that the TotalItemsCount property is not binding directly to the DataGrid, which is why you faced the issue with your previous approach using the DataSource property.


Instead of using the DataSource property, we suggest using the CustomAdaptor feature to achieve your requirements. We have already documented a similar SQL  server data binding with the CustomAdaptor feature.please refer to the documentation below.


Reference: https://blazor.syncfusion.com/documentation/datagrid/data-binding#sql-server-data-bindingsql-client


Previously, you used the actionBegin handler. Instead, with the CustomAdaptor, the ReadAsync method should return the current page records and the total record count. This way, the grid will render paging correctly, and the records for the next page will be displayed using this approach. This is the default behavior. Thanks for your understanding.

Regards,
Prathap Senthil



DA DAN August 21, 2024 10:58 AM UTC

i am trying to implement it but it does not work. i am getting not data.

here is my code for beneficiaries page, and custom adapter and service:

====razor page====

@page "/beneficiaries"

@using Syncfusion.Blazor.Grids

@using Syncfusion.Blazor.Data

@using Rad.Models

@using Rad.Services

@inject BeneficiaryDataAdaptor BeneficiaryDataAdaptor


<h1>Beneficiary Management</h1>


<SfGrid TValue="Beneficiary" AllowPaging="true" AllowSorting="true" AllowFiltering="true">

    <GridPageSettings PageSize="10"></GridPageSettings>

    <GridFilterSettings Type="Syncfusion.Blazor.Grids.FilterType.Menu"></GridFilterSettings>

    <GridEditSettings AllowAdding="true" AllowEditing="true" AllowDeleting="true"></GridEditSettings>

    <SfDataManager Adaptor="Adaptors.CustomAdaptor">

        <SfDataManager.AdaptorInstance>

            @BeneficiaryDataAdaptor

        </SfDataManager.AdaptorInstance>

    </SfDataManager>

   @* <SfDataManager AdaptorInstance="@BeneficiaryDataAdaptor">

    </SfDataManager> *@

    <GridColumns>

        <GridColumn Field="@nameof(Beneficiary.Id)" IsPrimaryKey="true" IsIdentity="true" HeaderText="ID" TextAlign="TextAlign.Right" Width="100"></GridColumn>

        <GridColumn Field="@nameof(Beneficiary.FirstName)" HeaderText="First Name" Width="150"></GridColumn>

        <GridColumn Field="@nameof(Beneficiary.LastName)" HeaderText="Last Name" Width="150"></GridColumn>

        <!-- Add more columns as needed based on your Beneficiary model -->

    </GridColumns>

</SfGrid>


@code {


}


=======

//Services> BeneficiaryDataAdaptor.cs

using Syncfusion.Blazor.Data;

using Rad.Services;

using Rad.Models;

using Microsoft.Extensions.Logging;

using Syncfusion.Blazor;


public class BeneficiaryDataAdaptor : DataAdaptor

{

    private readonly IBeneficiaryService _beneficiaryService;

    private readonly ILogger<BeneficiaryDataAdaptor> _logger;


    public BeneficiaryDataAdaptor(IBeneficiaryService beneficiaryService, ILogger<BeneficiaryDataAdaptor> logger)

    {

        _beneficiaryService = beneficiaryService;

        _logger = logger;

    }


    public override async Task<object> ReadAsync(DataManagerRequest dataManagerRequest, string key = null)

    {

        _logger.LogInformation("ReadAsync method called");


        try

        {

            var skip = dataManagerRequest.Skip;

            var take = dataManagerRequest.Take > 0 ? dataManagerRequest.Take : 10;

            var searchText = dataManagerRequest.Search?.FirstOrDefault()?.Key ?? string.Empty;

            var orderBy = dataManagerRequest.Sorted?.FirstOrDefault()?.Name ?? "Id";

            var isAscending = dataManagerRequest.Sorted?.FirstOrDefault()?.Direction == "Ascending";


            _logger.LogInformation($"Fetching data with parameters: skip={skip}, take={take}, searchText='{searchText}', orderBy='{orderBy}', isAscending={isAscending}");


            var result = await _beneficiaryService.GetPaginatedBeneficiariesAsync(skip, take, searchText, orderBy, isAscending);


            _logger.LogInformation($"Fetched {result.Data.Count} records out of {result.Count}");


            return new DataResult() { Result = result.Data, Count = result.Count };

        }

        catch (Exception ex)

        {

            _logger.LogError(ex, "Error occurred while fetching beneficiaries");

            throw;

        }

    }


    public override async Task<object> InsertAsync(DataManager dataManager, object data, string key)

    {

        if (data is Beneficiary beneficiary)

        {

            var createdBeneficiary = await _beneficiaryService.CreateBeneficiaryAsync(beneficiary);

            return createdBeneficiary;

        }

        throw new InvalidCastException("Invalid data type for insertion.");

    }


    public override async Task<object> UpdateAsync(DataManager dataManager, object data, string keyField, string key)

    {

        if (data is Beneficiary beneficiary)

        {

            var updatedBeneficiary = await _beneficiaryService.UpdateBeneficiaryAsync(beneficiary);

            return updatedBeneficiary;

        }

        throw new InvalidCastException("Invalid data type for update.");

    }


    public override async Task<object> RemoveAsync(DataManager dataManager, object data, string keyField, string key)

    {

        if (int.TryParse(key, out int id))

        {

            await _beneficiaryService.DeleteBeneficiaryAsync(id);

            return data;

        }

        throw new ArgumentException("Invalid key for deletion.");

    }

}

=======================

// IBeneficiaryService.cs


using Microsoft.EntityFrameworkCore;

using Rad.Data;

using Rad.Models;

using System.Linq.Expressions;


namespace Rad.Services

{

    public interface IBeneficiaryService

    {

        IQueryable<Beneficiary> GetBeneficiariesQuery();

        Task<(List<Beneficiary> Data, int Count)> GetPaginatedBeneficiariesAsync(int skip, int take, string searchText, string orderBy, bool isAscending);


        Task<List<Beneficiary>> GetAllBeneficiariesAsync(); // New method to get all beneficiaries


        Task<Beneficiary?> GetBeneficiaryByIdAsync(int beneficiaryId);

        Task<Beneficiary> CreateBeneficiaryAsync(Beneficiary beneficiary);

        Task<Beneficiary> UpdateBeneficiaryAsync(Beneficiary beneficiary);

        Task DeleteBeneficiaryAsync(int beneficiaryId);

    }

    public class BeneficiaryService : IBeneficiaryService

    {

        private readonly ApplicationDbContext _context;

        private readonly MyQueryBuilder _myQueryBuilder;


        public BeneficiaryService(ApplicationDbContext context, ILogger<MyQueryBuilder> logger)

        {

            _context = context;

            _myQueryBuilder = new MyQueryBuilder(logger);

        }


        public IQueryable<Beneficiary> GetBeneficiariesQuery()

        {

            return _context.Beneficiaries.AsNoTracking();

        }



        public async Task<(List<Beneficiary> Data, int Count)> GetPaginatedBeneficiariesAsync(

            int skip, int take, string searchText, string orderBy, bool isAscending)

        {

            var query = GetBeneficiariesQuery();


            if (!string.IsNullOrEmpty(searchText))

            {

                query = query.Where(b => b.FirstName.Contains(searchText));

            }


            if (!string.IsNullOrEmpty(orderBy))

            {

                var parameter = Expression.Parameter(typeof(Beneficiary), "b");

                var property = Expression.Property(parameter, orderBy);

                var lambda = Expression.Lambda<Func<Beneficiary, object>>(

                    Expression.Convert(Expression.Coalesce(property, Expression.Constant(string.Empty)), typeof(object)),

                    parameter

                );


                query = isAscending ? query.OrderBy(lambda) : query.OrderByDescending(lambda);

            }


            var count = await query.CountAsync();

            var data = await query.Skip(skip).Take(take).ToListAsync();

            // Debugging/Logging

            Console.WriteLine($"Fetched {data.Count} records out of {count}.");

            return (data, count);

        }



        public async Task<List<Beneficiary>> GetAllBeneficiariesAsync()

        {

            return await _context.Beneficiaries.ToListAsync();

        }


        public async Task<Beneficiary?> GetBeneficiaryByIdAsync(int BeneficiaryId)

        {

            // Implementation for fetching beneficiary by ID

            return await _context.Beneficiaries.FindAsync(BeneficiaryId);

        }


        public async Task<Beneficiary> CreateBeneficiaryAsync(Beneficiary beneficiary)

        {

            _context.Beneficiaries.Add(beneficiary);

            await _context.SaveChangesAsync();

            return beneficiary;

        }


        public async Task<Beneficiary> UpdateBeneficiaryAsync(Beneficiary beneficiary)

        {

            _context.Entry(beneficiary).State = EntityState.Modified;

            await _context.SaveChangesAsync();

            return beneficiary;

        }


        public async Task DeleteBeneficiaryAsync(int beneficiaryId)

        {

            var beneficiary = await _context.Beneficiaries.FindAsync(beneficiaryId);

            if (beneficiary != null)

            {

                _context.Beneficiaries.Remove(beneficiary);

                await _context.SaveChangesAsync();

            }

        }

    }

}


========Program.cs======

builder.Services.AddScoped<IBeneficiaryService, BeneficiaryService>();

builder.Services.AddScoped<BeneficiaryDataAdaptor>();





PS Prathap Senthil Syncfusion Team August 22, 2024 12:25 PM UTC

Based on the reported issue, it appears that the SfDataManager's AdaptorInstance has been incorrectly defined in your code, leading to the problem you're experiencing. We recommend defining the AdaptorInstance correctly. Please refer to the code snippet and sample provided below for guidance. If the issue persists, please share a reproducible sample with duplicate data or try to reproduce the reported issue using the attached sample.

<SfDataManager AdaptorInstance="@typeof(BeneficiaryDataAdaptor)" Adaptor="Adaptors.CustomAdaptor">

             

</SfDataManager>

//This code is not defined correctly.

  <SfDataManager Adaptor="Adaptors.CustomAdaptor">

 

    <SfDataManager.AdaptorInstance>

 

        @BeneficiaryDataAdaptor

 

    </SfDataManager.AdaptorInstance>

 

</SfDataManager>


Note: After connecting the database, we need to copy the path of the database and change the connection string with the copied path in appsettings.json file based on the NORTHWND.MDF file

Additionally, if you are looking for a custom adapter as a component, we have already documented this topic. Kindly refer to the documentation below for your reference.
https://blazor.syncfusion.com/documentation/datagrid/custom-binding#custom-adaptor-as-component



Attachment: BlazorDataGridSQLNet8_e0e3a916.zip

Loader.
Up arrow icon