Extending the GridView to Work With the New DataPager Control

March 19, 2008 • Dave Marini

Recently I read an article by Scott Mitchell about the new ListView and DataPager controls that are included in the .NET Framework version 3.5. This got me curious as to how to adapt other well known databound controls like the GridView so that they can work together with the DataPager control. After some light reading and rather heavy reflecting over the guts of the GridView control, I had a gameplan and was ready to go. Extending a databound control to allow communication with the DataPager control isn’t too difficult to do, but it does assume that you have an understanding of how the data is bound to the control. But why bother to do this when the GridView has its own paging control built in? Well, for one thing, being able to decouple the pager from the grid allows us to do some pretty cool things. One thing I can think of is having the GridView in the left bar and the paging control someplace in the main window. Also, we can have a grid with two pagers. Each keeps track of the current page consistently, regardless of which one we use to page through our data. Finally, the templating controls of the pager are very powerful compared to those of the GridView by itself.

Let’s start with how a databound control can communicate with the DataPager. The DataPager control can be hooked up to any control that implements the IPageableItemContainer interface located in the System.Web.Extensions assembly. Here’s a quick shot of what the interface looks like courtesy of .NET Reflector:

public interface IPageableItemContainer
{
    //Events
    event EventHandler<PageEventArgs> TotalRowCountAvailable;

    // Methods
    void SetPageProperties(int startRowIndex, int maximumRows, bool databind);

    // Properties
    int MaximumRows { get; }
    int StartRowIndex { get; }
}

The MaximumRows and StartRowIndex properties of the interface are simply there to create a way for your databound control to determine the data window to display. This effectively defines the basis for what you will consider to be a “Page” of data. The SetPageProperties method is important in the interaction with the DataPager because this is the method in your control that the DataPager will call whenever you click on any of the DataPagerField controls (The Next or Previous Buttons, for example). Finally, the interface defines an event called TotalRowCountAvailable. This event tells the DataPager how many records are in the data being bound to your control. This is obviously important as the DataPager uses the total row count to determine how man page buttons to render or whether to disable the next or previous buttons so that you don’t go to a nonexistent page index.

So let’s begin extending the GridView with the hooks for the DataPager. Thinking about the IPageableItemContainer interface in terms of the GridView, we realize that MaximumRows is equivalent to the existing PageSize property, and StartRowIndex can be calculated from the existing PageSize and PageIndex properties. We also prepare the event by declaring it and creating a corresponding event invoker. Since I want my new Paging capability to be the default for this grid, I force the Pager to hide itself every page load. You can add a toggle to this behavior should you so desire. Finally, we stub out the SetPageProperties method, but we’ll leave it blank for now as we’ll revisit it shortly. So far our new GridView looks like this:

public class PageableGridView : GridView, IPageableItemContainer
{
     public PageableGridView() : base()
     {
          PagerSettings.Visible = false;
     }

     public event EventHandler<PageEventArgs> TotalRowCountAvailable;

     public int MaximumRows
     {
          get{ return this.PageSize; }
     }

     public int StartRowIndex
     {
          get{ return (this.PageSize * this.PageIndex); }
     }

     protected virtual void OnTotalRowCountAvailable(PageEventArgs e)
     {
          if (TotalRowCountAvailable != null)
                TotalRowCountAvailable(this, e);
     }

     protected virtual void SetPageProperties(int startRowIndex, int maximumRows, bool dataBind) { }
}

The good news is that we’re more than half way there. Now is where things get a bit more complex. We still need a way to set the control with the page size and starting row values based on what the DataPager says we need to display once the user clicks on one of its buttons. This is where the SetPageProperties method comes into play. Here is a basic implementation that gets the job done:

protected virtual void SetPageProperties(int startRowIndex, int maximumRows, bool dataBind)
{
    if (databind)
    {
        PageSize = maximumRows;

        int newPageIndex = (startRowIndex / PageSize);

        if (PageIndex != newPageIndex)
        {
           OnPageIndexChanging(new GridViewPageEventArgs(newPageIndex));

           PageIndex = newPageIndex;

           OnPageIndexChanged(EventArgs.Empty);
        }
    }

    RequiresDataBinding = databind;
}

When the DataPager sends the grid its paging information, the grid needs to set any appropriate params on itself to prepare to bind to the right window of data. The 2 properties that the grid is already equipped to do this with are the PageSize and PageIndex properties. These properties can be calculated from the information sent into the method, so we set them. Of course, if the Page is being changed, we should probably fire the OnPageIndexChanging event. This means if you aren’t binding to a datasource, you’ll still need to make sure to handle this event. Finally, we instruct the grid to rebind itself should the DataPager be in the process of binding to data. This is a basic implementation so you’ll want to do any data integrity checks here as well, for example checking to ensure that the new PageIndex and StartRowIndex values are in the valid range.

There’s only one thing left to do to complete our integration of the GridView to the DataPager. For the DataPager to render the appropriate number of page buttons, or for it to know when to disable the next or previous page buttons on the pager, it needs to know how many total rows there are. This, coupled with the Page Size, which is specified declaratively on the DataPager itself, help it determine the number of pages that the data source contains. The problem is, this information isn’t known to the pager. It is known to the GridView however, and we need to get it from there and pass it on to the DataPager. But when can we be sure that the GridView has this data? The GridView control inherits from CompositeDataboundControl. This type contains a special variant of the CreateChildControls method that also takes in a property indicating whether the control is being bound to data or simply re-rendered. A little reflecting shows that the GridView uses this method to bind to its data source. Knowing this, it appears this is the place we will want to inject our trigger for the TotalRowCountAvailable event to be raised. This gets a little complicated because we need to handle the case where we’re binding to a datasource manually or using a DataSource Control like ObjectDataSource or SqlDataSource, which have the total row count specified within them. A couple of helper methods are required to ensure we get the right value:

//Gets row count from SqlDataSource and the like...
private int _GetTotalRowsFromDataSourceObject(IEnumerable dataSource)
{
    DataSourceView view = this.GetData(); if (AllowPaging && view.CanPage && view.CanRetrieveTotalRowCount)
        return base.SelectArguments.TotalRowCount;
    else
        return (PageIndex * PageSize) + _GetSourceCount(dataSource);
}

//Gets the row count from a manually bound source or from a source in viewstate
private int _GetSourceCount(IEnumerable dataSource)
{
    ICollection source = dataSource as ICollection;

    return source != null ?
         source.Count :
         (from x in dataSource.OfType<object>() select 1).Sum();
}

The _GetTotalRowsFromDataSourceObject method retrieves the total number of records from the DataSource object, if it’s available. This depends on a couple of things, such as whether the EnablePaging property was set on the DataSource Control and whether the Object has completed the query operation. Worst case, we return a single page of data and be done with it. The _GetSourceCount method is used for two particular occasions. First, it’s how you get the row count in the event that you bind the grid’s DataSource property manually and then call DataBind(). Secondly, this method will also prove useful when the grid is rebinding after a postback to data that is stored in the viewstate. In both cases, we use a little LINQ to extract the total number of resulting data items (or rows in the case of viewstate) in the datasource. Now let’s see how we use these methods to tie it all together:

protected override int CreateChildControls(IEnumerable dataSource, bool dataBinding)
   {
       int baseResult = base.CreateChildControls(dataSource, dataBinding);

       if (dataSource != null)
       {
           int dataSourceCount = (IsBoundUsingDataSourceID && dataBinding) ?
               _GetTotalRowsFromDataSource(dataSource) :
               _GetSourceCount(dataSource);

           OnTotalRowCountAvailable(new PageEventArgs(StartRowIndex, MaximumRows, dataSourceCount));
       }

       return baseResult;
   }

The base control’s CreateChildControls method is called first, since the grid uses it to actually do the databinding. if there isn’t any data to bind to, there will not be a reason to notify the pager, so we check to make sure we have data, then determine the number of rows in the source through the above mentioned process. Finally, we fire off the event with the derived data and then we return the original result so as not to affect any other operations that may rely on it. We now have a GridView that can be coupled with the new DataPager control. Here’s a sample of markup that we would use to use these controls together on a page:

<asp:DataPager ID="DataPager1" runat="server" PageSize="2" PagedControlID="grid2">
    <Fields>
        <asp:NextPreviousPagerField />
    </Fields>
</asp:DataPager>
<custom:pageablegridview id="grid2" runat="server" autogeneratecolumns="true" allowpaging="true"
        onpageindexchanging="grid2_PageIndexChanging" />

Notice that I don’t have to specify the PageSize property of the GridView because the PageSize property on the DataPager will be in control of this value. To ensure this, we could shadow the PageSize property on the grid, but this will suffice for now. So that’s really all there is to it. For newly created server controls that are bound to data, making these inroads to allow your control to work with the DataPager is pretty simple. For the existing controls, like the GridView, it’s just a matter of knowing how the binding takes place and then injecting the functionality someplace where you know the needed values will exist.

Posted in asp.net, c# and tagged with ASP.NET, C#

Dave Marini

I am a software architect with over 13 years of experience developing scalable, enterprise level applications targeted for Windows and the Web. I am specialized in the latest Microsoft Development Technologies, with recent experience using C# 4.5, the .NET Framework 4.5 and SQL Server 2005/2008/2012.

comments powered by Disqus