Share This Using Popular Bookmarking Services


Google ads

Recommended

network monitoring software




Programs & websites tailormade for you.

A ViewState friendly DataSource pager control 

Tuesday, March 27, 2012 9:50:00 PM

Every programmer has his own style of doing things like displaying lists of databound information with some paging control.

Me, I used to favor a combination of ListView plus two DataPager controls - but getting them to work both inside an UpdatePanel and also with links for Google to follow to page 2 etc., that required the same somewhat tricky code copied to every page that needed paging. And repeating the same code is a sign that I probably better put that shared code inside a new server control!

Since I started trying to write ViewState friendly code - that is: avoiding storing the whole dataset and rendered GridView in the ViewState - I anyway needed to revise my code.

And since I had on my hands the task of upgrading a legacy project were all the data would anyway be cached in memory, I decided to make the project of writing such a standard control simpler by not requiring database paging - it is definitely not the best strategy for huge datasets, but for the use I was facing (a complex O/R mapper query that did not seem to offer database paging anyway and was already cached in memory), it was an okay solution.

The code

This is basically just one class/file that you add to your asp.net project. So I here give you the raw source code for your enjoyment - assuming that you know how to compile it and reference it so that you can use it on your .aspx page.

It might be the most heavily commented code I have ever written, so I will let the comments explain how to use it - just copy the following into Visual Studio so you can inspect it easier than here on the website:

 

using System;
using System.Collections.Generic;
using System.Text;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Collections;
using System.Collections.Specialized;
using System.Data;
 
/*
 
Copyright (C) 2012 Allan Kindberg Nielsen, Kindbergs Program Udvikling - www.kindbergs.dk
 
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
 
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.
 
*/
 
namespace Kindbergs.Controls
{
  /// <summary>
  /// The purpose of this control is to make a custom alternative for DataPager which is much more viewstate friendly and much more flexible with regards to the page life cycle
  /// - and by ViewState fiendly it means you should set ViewStateMode="disabled" for the page/control/ListView.
  /// It takes over the (automatic) databinding of an associated ListView (or Repeater etc.) by splitting up the data and only giving it the portion of the data that should 
  /// be rendered for the current page view. It automatically calls DataBind() on the ListView durring OnLoad and/or OnPreRender after updating the data to show.
  /// The parent control or page MUST MUST MUST latest in it's OnLoad event (which occurs before the OnLoad event of this control) set the DataSource - and for postbacks 
  /// it must be set to the same value as on the previous rendering (in order for events on buttons inside the ListView to correctly fire on postback clicks, 
  /// which happens after the OnLoad event).
  /// If data should be changed durring postback, set DataSource to a new list/table latest by the OnPreRender event - at which time the ListView's DataBind() will be called (again).
  /// If this pager's navigation controls has been clicked, their action will be performed durring OnPreRender - at which time the ListView's DataBind() will be called (again).
  /// If page navigation is changed by the parent page/control calling SetPageProperties() - that to will first be performed durring the OnPreRender event, with a DataBind().
  /// Data is not cached* or stored in ViewState - it is up to the parent page/control/DAL to possibly cache (huge) result sets in memory between postbacks, if that is needed.
  /// The pager can be twinned with a Literal control, in order to show the same paging controls both above and below a set of data. The Literal is updated durring OnPreRender.
  /// When UseGoogleLinks=true (default) the Literal control will contain hyperlinks instead of partial postback javascript - this is so that search engines have one place
  /// (usually at the bottom of the list) to detect and follow links to the other pages. By placing this Literal below the ListView, this will also be of benefit to the
  /// user, since the browser will jump up to the top of the new page when he clicks the link. The links use the current URL with "?page=xxx" appended to it, which is automatically
  /// caught by the pager when followed (ie: when not a postback).
  /// * exception: One piece of data is persisted in a hidden field: The current start row index which should be restored on partial postback (unless other navigation/paging
  /// overrides it), so that you can have other controls doing a partial postback without loose track of the current page to show.
  /// Limitation: By taking over the job of paging the data, using the ListView's sort command will no longer work, since it does not have access to the full data - you must pass
  /// the DataSource to this control in the right sort order.
  /// </summary>
  [ToolboxData(@"<{0}:FriendlyDataSourcePager runat=""server"" PagedControlID="""" PageSize=""10"" />")]
  public class FriendlyDataSourcePager : System.Web.UI.ControlIPostBackEventHandlerIPostBackDataHandler
  {
 
 
    // IPostBackDataHandler interface - handles remembering the current index/page between postbacks (when other actions than paging happens):
    public bool LoadPostData(string postDataKey, NameValueCollection postCollection)
    {
      if (postCollection[this.UniqueID] != null && (!_moveToDirection.HasValue || _moveToDirection.Value != MoveTo.NewStartRowIndex))
      {
        int newIndex;
        if (int.TryParse(postCollection[this.UniqueID], out newIndex))
          _startRowIndex = newIndex;
      }
      return false// always false, client should never change the value (unless you add your own javascript - unsupported)
    }
    public void RaisePostDataChangedEvent()
    {
      // not used
    }
 
 
    // IPostBackEventHandler interface that responds to page changes via javascript (using partial postbacks if inside an UpdatePanel):
    public void RaisePostBackEvent(string eventArgument)
    {
      if (eventArgument != null)
      {
        if (eventArgument == MoveTo.Next.ToString())
          _moveToDirection = MoveTo.Next;
        else if (eventArgument == MoveTo.Previous.ToString())
          _moveToDirection = MoveTo.Previous;
        else
        {
          int newPage;
          if (int.TryParse(eventArgument, out newPage))
            _goToPage = newPage;
        }
      }
    }
 
 
    // private variables that the rest of this code can freely use - note that only _startRowIndex is persisted via a HiddenField
    private int _pageSize = 10;
    private int? _startRowIndex;
    private int? _goToPage;
    private MoveTo? _moveToDirection;
    private int _buttonCount = 10;
 
    private enum MoveTo
    {
      First = 1,
      Next = 2,
      Previous = 3,
      Last = 4,
      NewDataSource = 98,
      NewStartRowIndex = 99
    }
 
 
    // --- publicly visible interfaces: -------------
 
 
    private IEnumerable __dataSource;
    /// <summary>
    /// Must be initialized with a valid DataSource (IEnumerable) latest by OnLoad on each postback.
    /// Can be reset with a new DataSource latest by OnPreRender.
    /// </summary>
    public IEnumerable DataSource
    {
      get { return __dataSource; }
      set
      {
        __dataSource = value;
        if (!_moveToDirection.HasValue)
          _moveToDirection = MoveTo.NewDataSource;
        /*
        if (pagedListView != null)
        {
          pagedListView.DataSource = _dataSource;
          BindAssociatedDataSource();
        }*/
      }
    }
 
    private string __pagedListViewID;
    /// <summary>
    /// Must be initialized in tags or latest by OnLoad with the ID of a ListView, Repeater or similar DataBoundControl
    /// </summary>
    public string ListViewID
    {
      get { return __pagedListViewID; }
      set { __pagedListViewID = value; }
    }
 
    private string __bottomPagerLiteralID;
    /// <summary>
    /// Optional: The ID of a Literal control (used to render a second set of pager controls, preferably beneath the ListView).
    /// The Literal is always updated durring OnPreRender.
    /// </summary>
    public string BottomPagerLiteralID
    {
      get { return __bottomPagerLiteralID; }
      set { __bottomPagerLiteralID = value; }
    }
 
    private bool __useGoogleLinks = true;
    /// <summary>
    /// Can optionally be set any time before Render. Set to true (default) to make one of the pagers output hyperlinks instead of JavaScript
    /// </summary>
    public bool UseGoogleLinks
    {
      get { return __useGoogleLinks; }
      set { __useGoogleLinks = value; }
    }
 
    /// <summary>
    /// Set in tags (or in code latest by OnLoad) to the number of items that should be displayed on each page - defaults to 10
    /// </summary>
    public int PageSize
    {
      get { return _pageSize; }
      set { _pageSize = value; }
    }
 
    /// <summary>
    /// Set in tags (or in code latest by OnLoad) to the number of numbers (buttons/links) that should at maximum be displayed 
    /// (will cause optional "..." links before and/or after the numbers to go to the next page of pages)
    /// </summary>
    public int ButtonCount
    {
      get { return _buttonCount; }
      set { _buttonCount = value; }
    }
 
    /// <summary>
    /// Call latest by OnPreRender to change the first visible item or the number of items on a page.
    /// Usually only used to set startIndex to 0 after changeing the DataSource.
    /// Sequence of accessing DataSource and SetPageProperties does not matter, as they are first combined durring OnLoad/OnPreRender.
    /// </summary>
    /// <param name="startIndex"></param>
    /// <param name="pageSize"></param>
    /// <param name="rebind">ignored - rebind will always happen durring OnLoad and OnPreRender (if needed)</param>
    public void SetPageProperties(int startIndex, int pageSize, bool rebind)
    {
      _moveToDirection = MoveTo.NewStartRowIndex;
      _startRowIndex = startIndex;
      _pageSize = pageSize;
      // ignore rebind ? - we always delay it until PreRender
    }
 
 
    // --- own code and event handler 'magic': -------------
 
 
    private DataBoundControl __pagedListView;
    protected DataBoundControl pagedListView
    {
      get
      {
        if (__pagedListView == null)
        {
          __pagedListView = this.NamingContainer.FindControl(ListViewID) as DataBoundControl;
          // if (_pagedListView == null)
          //   throw new Exception("FriendlyPager exception: ListViewID not found (note: it must be inherit from DataBoundControl to work)");
        }
        return __pagedListView;
      }
    }
 
    protected override void OnInit(EventArgs e)
    {
      base.OnInit(e);
 
      if (!Page.IsPostBack && !DesignMode)
      { // only use querystring on first load
        if (Page.Request.QueryString["page"] != null && (!_moveToDirection.HasValue || _moveToDirection.Value != MoveTo.NewStartRowIndex))
        {
          int newPage;
          if (int.TryParse(Page.Request.QueryString["page"], out newPage))
            _goToPage = newPage;
        }
      }
    }
 
    protected override void OnLoad(EventArgs e)
    {
      base.OnLoad(e);
 
      //ScriptManager curSM = ScriptManager.GetCurrent(Page);
      //if (curSM != null)
      //  curSM.RegisterAsyncPostBackControl(this); - not needed, an UpdatePanel will automatically discover our ID/ClientID if it has ChildrenAsTriggers=true
 
      if (_goToPage.HasValue || _moveToDirection.HasValue)
        BindAssociatedListViewWithDataSource();
    }
 
 
    /// <summary>
    /// Overridden by FriendlyDataSourcePager - call it if you really need to force databinding of the listview already during the page's OnInit event
    /// ()
    /// </summary>
    public override void DataBind()
    {
      base.DataBind();
 
      if (_goToPage.HasValue || _moveToDirection.HasValue)
        BindAssociatedListViewWithDataSource();
    }
 
    protected override void OnPreRender(EventArgs e)
    {
      base.OnPreRender(e);
 
      if (_goToPage.HasValue || _moveToDirection.HasValue) // only rebind if any changes has happened (due to postback paging or postback user code)
        BindAssociatedListViewWithDataSource();
 
      if (!string.IsNullOrEmpty(BottomPagerLiteralID))
      {
        Literal litBottomPager = this.NamingContainer.FindControl(BottomPagerLiteralID) as Literal;
        if (litBottomPager != null)
          litBottomPager.Text = ConstructPager(UseGoogleLinks);
        else
          throw new Exception("Did not find BottomPagerLiteralID as a Literal control in the NamingContainer of this control.");
      }
    }
 
 
    private int? __totalRowCount; // note: set by BindAssociatedListViewWithDataSource durring OnLoad/OnPreRender, used by ConstructPager durring OnPreRender
 
    protected void BindAssociatedListViewWithDataSource()
    {
      if (DataSource != null)
      {
        if (DataSource is ICollection)
          __totalRowCount = (DataSource as ICollection).Count;
        else if (DataSource is DataTable)
          __totalRowCount = (DataSource as DataTable).Rows.Count;
        else
          throw new Exception("Unsupported type of DataSource - sorry... Please review code to add support!");
 
        if (_goToPage.HasValue)
        { // process go to page
          _startRowIndex = _pageSize * (_goToPage.Value - 1);
          if (_startRowIndex.Value >= __totalRowCount.Value)
            _startRowIndex = 0; // catch funny robots who tries to continue numbering (but should really redirect with a warning to page 0)
        }
 
        if (!_startRowIndex.HasValue) // fail safe
          _startRowIndex = 0;
 
        if (_moveToDirection.HasValue)
        { // process MoveTo direction
          if (_moveToDirection.Value == MoveTo.Next && _startRowIndex.Value + _pageSize < __totalRowCount)
            _startRowIndex = _startRowIndex.Value + _pageSize;
          if (_moveToDirection.Value == MoveTo.Previous && _startRowIndex.Value - _pageSize >= 0)
            _startRowIndex = _startRowIndex.Value - _pageSize;
          // note that MoveTo.NewStartRowIndex and MoveTo.NewDataSource is only set to ensure a (new) DataBind if performed
        }
 
        int numToGet = _pageSize;
        if (_startRowIndex.Value + numToGet > __totalRowCount.Value)
          numToGet = __totalRowCount.Value - _startRowIndex.Value;
 
        List<object> pagedDataSource = new List<object>();
        int ix = 0;
        foreach (object obj in DataSource)
        {
          if (ix >= _startRowIndex.Value)
            pagedDataSource.Add(obj);
          ix++;
          if (ix >= __totalRowCount.Value || ix >= _startRowIndex.Value + numToGet)
            break;
        }
        // The above should work on any IEnumerable, but can it be made faster?
        // Could alternative ways like "pagedDataSource=DataSource.GetRange(_startRowIndex.Value, numToGet)" be used on some types of data? 
        // Or could indexes be used on some types of data, like  "for(int i=_startRowIndex.Value...) pagedDataSource.Add(DataSource[i]);" ?
 
        pagedListView.DataSource = pagedDataSource;
        pagedListView.DataBind();
 
        // reset so we don't rebind again unless needed:
        _moveToDirection = null;
        _goToPage = null;
      }
    }
 
    protected override void Render(HtmlTextWriter writer)
    {
      writer.Write("<div>");
 
      base.Render(writer);
 
      writer.Write(ConstructPager(UseGoogleLinks && string.IsNullOrEmpty(BottomPagerLiteralID)));
 
      if (!_startRowIndex.HasValue) // fail safe
        _startRowIndex = 0;
 
      // remember the current page between postbacks:
      // important: this control's ID/name must be output as part of the HTML so that an UpdatePanel can discover the control and enable partial postbacks for the javascript links!      
      writer.Write("<input type='hidden' ID='" + this.ClientID + "' name='" + this.UniqueID + "' value='" + _startRowIndex.Value + "'>");
      writer.Write("</div>");
    }
 
    private string ConstructPager(bool useHyperlinks)
    {
      StringBuilder sbHtml = new StringBuilder();
      if (_startRowIndex.HasValue && __totalRowCount.HasValue)
      {
        string baseUrl = "/";
        if (!DesignMode)
          baseUrl = Page.Request.Url.AbsolutePath; //.PathAndQuery; - need to remove old "page=" and recontruct the other querystrings before using PathAndQuery...
        string pageStr = "?page=";
        //if (baseUrl.Contains("?"))
        //  pageStr = "&page=";
 
        int numPages = Convert.ToInt32(Math.Ceiling(Convert.ToDouble(__totalRowCount.Value) / _pageSize));
        int curPage = _startRowIndex.Value / _pageSize + 1;
        int lastPageToShow = (numPages < _buttonCount ? numPages : _buttonCount);
 
        // render Previous
        if (curPage > 1)
          sbHtml.Append(ComposeHtmlForPage(-1, "&lt; Forrige", (useHyperlinks ? (curPage - 1).ToString() : MoveTo.Previous.ToString()), curPage, baseUrl, pageStr, useHyperlinks));
 
        int i = 1;
        // optionally add "..." before numbers
        if (curPage > _buttonCount)
        {
          i = Convert.ToInt32((curPage - 1) / _buttonCount) * _buttonCount;
          sbHtml.Append(ComposeHtmlForPage(i, "...", i.ToString(), curPage, baseUrl, pageStr, useHyperlinks));
          lastPageToShow = (numPages < i + _buttonCount ? numPages : i + _buttonCount);
          i++;
        }
 
        // render numbers
        for (; i <= lastPageToShow; i++)
          sbHtml.Append(ComposeHtmlForPage(i, i.ToString(), i.ToString(), curPage, baseUrl, pageStr, useHyperlinks));
 
        // optionally add "..." after
        if (i <= numPages)
        {
          sbHtml.Append(ComposeHtmlForPage(i, "...", i.ToString(), curPage, baseUrl, pageStr, useHyperlinks));
        }
 
        // render Next (possibly disabled if already on last page)
        if (numPages > 1)
          sbHtml.Append(ComposeHtmlForPage(-1, "Næste &gt;", (useHyperlinks ? (curPage + 1).ToString() : MoveTo.Next.ToString()), curPage, baseUrl, pageStr, useHyperlinks, (curPage >= numPages)));
 
        // Uncomment to show debug info (to ensure nothing is lost):
        //sbHtml.Append("<br/>numPages:" + numPages.ToString() + ", curPage:" + curPage.ToString() + ", __totalRowCount:" + __totalRowCount.ToString() + ", _startRowIndex:" + _startRowIndex.ToString() + ", _pageSize:" + _pageSize.ToString() + ", _buttonCount:" + _buttonCount.ToString() + ", lastPageToShow:" + lastPageToShow.ToString());
      }
      return sbHtml.ToString();
    }
 
    private string ComposeHtmlForPage(int pageNumber, string text, string argument, int curPage, string baseUrl, string pageStr, bool useHyperlinks, bool disabled = false)
    {
      if (useHyperlinks)
        return GetHtmlForLink(pageNumber, text, argument, curPage, baseUrl, pageStr, disabled);
      else
        return GetHtmlForJavaScript(pageNumber, text, argument, curPage, disabled);
    }
 
    private string GetHtmlForLink(int pageNumber, string text, string argument, int curPage, string baseUrl, string pageStr, bool disabled)
    {
      if (pageNumber == curPage)
        return "<span class='FrndPager_act'>" + text + "</span>&nbsp;";
      else if (pageNumber == 1)
        return "<a class='FrndPager' href='" + baseUrl + "' >" + text + "</a>&nbsp;";
      else if (!disabled)
        return "<a class='FrndPager' href='" + baseUrl + pageStr + argument + "' >" + text + "</a>&nbsp;";
      else
        return "<a class='FrndPager_dis' disabled='disabled'>" + text + "</a>&nbsp;";
    }
 
    private string GetHtmlForJavaScript(int pageNumber, string text, string argument, int curPage, bool disabled)
    {
      if (pageNumber == curPage)
        return "<span class='FrndPager_act'>" + text + "</span>&nbsp;";
      else if (pageNumber == 1)
        return "<a class='FrndPager' href=\"" + Page.ClientScript.GetPostBackClientHyperlink(this, argument, false) + "\" >" + text + "</a>&nbsp;";
      else if (!disabled)
        return "<a class='FrndPager' href=\"" + Page.ClientScript.GetPostBackClientHyperlink(this, argument, false) + "\" >" + text + "</a>&nbsp;";
      else
        return "<a class='FrndPager_dis' disabled='disabled'>" + text + "</a>&nbsp;";
    }
 
    /*
     * suggested stylesheet data to go with the generated HTML - works best when parent html is positioned as float:right;
     * 
span.FrndPager_act
{
  color: some color;
  cursor: default;
  font-weight: bold;
}
a.FrndPager
{
  color: some other color;
  text-decoration: none;
}
a.FrndPager:hover
{
  text-decoration: underline;
}
a.FrndPager_dis, a.FrndPager_dis:hover
{
  visibility:hidden;
}
     * 
     */
 
  }
}

 

As you have read from the code , you link this control to a ListView (or a Repeater) and in your code you must assign the datasource to this control instead of the ListView. Your page should have ViewStateMode=Disabled to avoid ViewState polution, and your code must not call DataBind() on the page or the ListView, since this control will automatically call it on the ListView at the right times during the page life cycle (during OnInit or OnLoad and again during PreRender if needed due to paging or new data loaded).

Note: "Næste" means "Next" and "Forrige" means "Previous" (it was used on a Danish website) - replace those two text pieces with proper localization.

Final words

Well, there is things in the above that I am not so proud of, such as scope of variables.

While it is a nice black box to the world around it (provided you initialize it at the right times during the page life cycle), the private variables irritate me. I need a better design philosophy for storing and accessing data that are only valid during specific stages of the processing - a class variable is no better that old time global variables, if incorrect sequence of function calls can produce unwanted results.

There is probably also bugs or special cases that the code does not cover, but in the two projects I have used it so far (one of which is a danish wine shop), it beautifully handles paging inside an UpdatePanel, and command buttons inside the GridView still fire correctly (although in one case I had to set ViewStateMode=Enabled on a child control to get it to work).

If you learned something from the code, or spotted a bug, please write a comment!

Allan K. Nielsen, Kindbergs Program Udvikling
Tweet This