Jupaol
Jupaol

Reputation: 21365

Paging a GridView after data has returned asynchronously using <%@ Page Async="true"

I was reading the following blogs about ASP.Net Async pages

And a question popped on my head, please consider the following scenario:

  1. Assuming an Async page
  2. The page registers async operations to retrieve data from a database in order to release immediately the ASP.Net working thread to increase scalability
  3. The page passes paging info to these operations to paginate on the database server
  4. The operation completes and the correct delegate is called on a new thread. (Not using a thread from the ASP.Net Thread Pool)
  5. Data is returned to the page and can be bound to the GridView control on the Page_PreRendercomplete

At this point, I have the data paged on my page ready to be bound and displayed back to the user (returning only the records needed to be displayed and the number of Virtual Rows Count)

So with this information, I would like to bind it to my GridView control, but I have not figured it out how to display the paging results on my GridView

I tried using the following code:

protected override void OnPreRenderComplete(EventArgs e)
{
    if (this.shouldRefresh)
    {
        var pagedSource = new PagedDataSource
        {
            DataSource = this.Jobs, 
            AllowPaging = true,
            AllowCustomPaging = false,
            AllowServerPaging = true,
            PageSize = 3,
            CurrentPageIndex = 0,
            VirtualCount = 20
        };

        this.gv.DataSource = pagedSource;
        this.gv.DataBind();
    }

    base.OnPreRenderComplete(e);
}

But the GridView control simply ignores the VirtualCount property and the pager is never shown, this is what I get:

enter image description here

ASPX

<%@ Page Async="true" AsyncTimeout="30"  ....
...
    <asp:GridView runat="server" ID="gv" DataKeyNames="job_id" 
        AllowPaging="true" PageSize="3"
    >
        <Columns>
            <asp:CommandField ShowSelectButton="true" />
        </Columns>
        <SelectedRowStyle Font-Bold="true" />
    </asp:GridView>

ASPX Code behind

protected void Page_Load(object sender, EventArgs e)
{
    if (!this.IsPostBack)
    {
        this.shouldRefresh = true;
    }
}

public IAsyncResult BeginAsyncOperation(object sender, EventArgs e, AsyncCallback callback, object state)
{
    var operation = new MyClassResult(callback, Context, state);
    operation.StartAsync();
    return operation;
}

public void EndAsyncOperation(IAsyncResult result)
{
    var operation = result as MyClassResult;
    this.Jobs = operation.Jobs;
}

Notes:

Upvotes: 2

Views: 2293

Answers (1)

user1429080
user1429080

Reputation: 9166

I think I have something that could be at least a good starting point for further exploration. I have made a sample (furher down) to illustrate my method, which is based on a couple of ideas:

  1. In order to get paging to work, an ObjectDatasource should used. That way it's possible to tell the GridView how many rows there are in total.
  2. We need a way to let our ObjectDataSource access the data we have fetched once it is available.

The idea I came up with in order to solve 2. was to define an interface that the page where the GridView is located can implement. Then the ObjectDataSource can use a class that relays the calls for fetching data to the Page itself. When called too early, empty data will be returned, but it will be replaced by real data later on.

Let's look at some code.

Here's my aspx file:

<%@ Page Title="" Language="C#" MasterPageFile="~/Site.master" AutoEventWireup="true"
    CodeFile="GridViewTest.aspx.cs" Inherits="GridViewTest" %>

<asp:Content ID="Content1" ContentPlaceHolderID="HeadContent" runat="Server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="Server">
    <asp:GridView ID="jobsGv" runat="server" AutoGenerateColumns="false" AllowPaging="true"
        PageSize="13" OnPageIndexChanging="jobsGv_PageIndexChanging" DataSourceID="jobsDataSource">
        <Columns>
            <asp:TemplateField HeaderText="Job Id">
                <ItemTemplate>
                    <asp:Literal ID="JobId" runat="server" Text='<%# Eval("JobId") %>'></asp:Literal>
                </ItemTemplate>
            </asp:TemplateField>
            <asp:TemplateField HeaderText="Job description">
                <ItemTemplate>
                    <asp:Literal ID="Description" runat="server" Text='<%# Eval("Description") %>'></asp:Literal>
                </ItemTemplate>
            </asp:TemplateField>
            <asp:TemplateField HeaderText="Min level">
                <ItemTemplate>
                    <asp:Literal ID="MinLvl" runat="server" Text='<%# Eval("MinLvl") %>'></asp:Literal>
                </ItemTemplate>
            </asp:TemplateField>
        </Columns>
    </asp:GridView>
    <asp:ObjectDataSource ID="jobsDataSource" runat="server" TypeName="JobObjectDs" CacheDuration="0"
        SelectMethod="GetJobs" EnablePaging="True" SelectCountMethod="GetTotalJobsCount">
    </asp:ObjectDataSource>
    <asp:Button ID="button" runat="server" OnClick="button_Click" Text="Test postback" />
</asp:Content>

And the code behind:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.UI.WebControls;

public partial class GridViewTest : System.Web.UI.Page, IJobDsPage
{
    bool gridNeedsBinding = false;
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            gridNeedsBinding = true;
        }
    }
    protected void jobsGv_PageIndexChanging(object sender, GridViewPageEventArgs e)
    {
        var gv = (GridView)sender;
        newPageIndexForGv = e.NewPageIndex;
        gridNeedsBinding = true;
    }
    private int newPageIndexForGv = 0;
    protected void Page_PreRendercomplete(object sender, EventArgs e)
    {
        if (gridNeedsBinding)
        {
            // fetch data into this.jobs and this.totalJobsCount to simulate 
            // that data has just become available asynchronously
            JobDal dal = new JobDal();
            jobs = dal.GetJobs(jobsGv.PageSize, jobsGv.PageSize * newPageIndexForGv).ToList();
            totalJobsCount = dal.GetTotalJobsCount();

            //now that data is available, bind gridview
            jobsGv.DataBind();
            jobsGv.SetPageIndex(newPageIndexForGv);
        }
    }

    #region JobDsPage Members

    List<Job> jobs = new List<Job>();
    public IEnumerable<Job> GetJobs()
    {
        return jobs;
    }
    public IEnumerable<Job> GetJobs(int maximumRows, int startRowIndex)
    {
        return jobs;
    }
    int totalJobsCount;
    public int GetTotalJobsCount()
    {
        return totalJobsCount;
    }

    #endregion

    protected void button_Click(object sender, EventArgs e)
    {
    }
}

And finally some classes to tie it together. I have bunched these together in one code file in App_Code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

/// <summary>
/// Simple POCO to use as row data in GridView
/// </summary>
public class Job
{
    public int JobId { get; set; }
    public string Description { get; set; }
    public int MinLvl { get; set; }
    //etc
}

/// <summary>
/// This will simulate a DAL that fetches data
/// </summary>
public class JobDal
{
    private static int totalCount = 50; // let's pretend that db has total of 50 job records
    public IEnumerable<Job> GetJobs()
    {
        return Enumerable.Range(0, totalCount).Select(i => 
            new Job() { JobId = i, Description = "Descr " + i, MinLvl = i % 10 }); //simulate getting all records
    }
    public IEnumerable<Job> GetJobs(int maximumRows, int startRowIndex)
    {
        int count = (startRowIndex + maximumRows) > totalCount ? totalCount - startRowIndex : maximumRows;
        return Enumerable.Range(startRowIndex, count).Select(i => 
            new Job() { JobId = i, Description = "Descr " + i, MinLvl = i % 10 }); //simulate getting one page of records
    }
    public int GetTotalJobsCount()
    {
        return totalCount; // simulate counting total amount of rows
    }
}

/// <summary>
/// Interface for our page, so we can call methods in the page itself
/// </summary>
public interface IJobDsPage
{
    IEnumerable<Job> GetJobs();
    IEnumerable<Job> GetJobs(int maximumRows, int startRowIndex);
    int GetTotalJobsCount();
}

/// <summary>
/// This will be used by our ObjectDataSource
/// </summary>
public class JobObjectDs
{
    public IEnumerable<Job> GetJobs()
    {
        var currentPageAsIJobDsPage = (IJobDsPage)HttpContext.Current.CurrentHandler;
        return currentPageAsIJobDsPage.GetJobs();
    }
    public IEnumerable<Job> GetJobs(int maximumRows, int startRowIndex)
    {
        var currentPageAsIJobDsPage = (IJobDsPage)HttpContext.Current.CurrentHandler;
        return currentPageAsIJobDsPage.GetJobs(maximumRows, startRowIndex);
    }
    public int GetTotalJobsCount()
    {
        var currentPageAsIJobDsPage = (IJobDsPage)HttpContext.Current.CurrentHandler;
        return currentPageAsIJobDsPage.GetTotalJobsCount();
    }
}

So what does it all do?

Well, we have the Page that is implementing the IJobDsPage interface. On the page we have the GridView which is using the ObjectDataSource with id jobsDataSource. That is in turn using the class JobObjectDs to fetch data. And that class is in turn taking the currently executing Page from the HttpContext and casting it to the IJobDsPage interface and calling the interface methods on the Page.

So the result is that we have a GridView which uses an ObjectDataSource which calls methods in the Page to get data. If these methods are called too early, empty data is returned (new List<Job>() with a corresponding total row count of zero). But this is no problem since we are anyway manually binding the GridView when we have reached a phase in the page processing when the data IS available.

All in all my sample works although it's far from superb. As it stands, the ObjectDataSource will call its associated Select method multiple times during the request. This is not as bad as it first sounds though, because the actual fethcing of data will still happen only once. Also, the GridView will be bound to the same data twice when changing to the next page.

So there is room for improvement. But it is a least a starting point.

Upvotes: 2

Related Questions