Wednesday, December 12, 2007

Custom Paging Helper Class

Below is the source I wrote to encapsulate methods needed to implement custom paging. This is useful for GridView or any other time you need to page through things. The class has all functionality accessible via static methods if you prefer. All the functionality that makes sense can also be used by an instance of the class. The advantage of creating an instance of the class is you just pass your parameters once, then all the parameters that you would need to specify in the static version of the methods are taken automatically from the instance of the class. I find the later to be less error prone due to mixing up parameters. Below the class is code that I typically use to implement custom paging for a GridView using System; using System.Collections.Generic; /// <summary> /// Encapsulates the algorithm for paging a list. All indexes are 1-based. /// </summary> public class PagingHelper { int totalNonPagedRows = 0; int pageSize = 0; int totalPageCount; int currentPageIdx = 0; public PagingHelper(int currentPageIdx, int totalNonPagedRows, int pageSize) { this.totalNonPagedRows = totalNonPagedRows; this.pageSize = pageSize; this.currentPageIdx = currentPageIdx; // do some quick basic calculations based on parameters above that we will need later this.totalPageCount = GetTotalPageCount(); } public static int GetTotalPageCount(int totalNonPagedRows, int pageSize) { int totalPageCount = (int)Math.Ceiling((double)totalNonPagedRows / pageSize); return totalPageCount; } public int GetTotalPageCount() { return PagingHelper.GetTotalPageCount(totalNonPagedRows, pageSize); } public static List<int> PagesIndexes(int totalPageCount) { List<int> pageIndexes = new List<int>(); for (int i = 1; i <= totalPageCount; i++) { pageIndexes.Add(i); } return pageIndexes; } public List<int> PagesIndexes() { return PagingHelper.PagesIndexes(totalPageCount); } /// <summary> /// Returns true if there is a page before the current page. Index is 1-based /// </summary> /// <param name="currentPageIdx">1-based index</param> /// <returns></returns> public static bool HasPreviousPage(int currentPageIdx) { if (currentPageIdx == 1) return false; else return true; } /// <summary> /// Returns true if there is a page before the current page. Index is 1-based /// </summary> /// <param name="currentPageIdx">1-based index</param> /// <returns></returns> public bool HasPreviousPage() { return PagingHelper.HasPreviousPage(currentPageIdx); } /// <summary> /// Returns true if there is a page after the current page. Index is 1-based /// </summary> /// <param name="currentPageIdx">1-based index</param> /// <returns></returns> public static bool HasNextPage(int currentPageIdx, int totalPageCount) { if (currentPageIdx == totalPageCount) return false; else return true; } /// <summary> /// Returns true if there is a page after the current page. Index is 1-based /// </summary> /// <param name="currentPageIdx">1-based index</param> /// <returns></returns> public bool HasNextPage() { return PagingHelper.HasNextPage(currentPageIdx, totalPageCount); } /// <summary> /// Returns the index of the page before the current page. /// If no page exists, the current page index is returned. Index is 1-based. /// </summary> /// <param name="currentPageIdx">1-based index</param> /// <returns></returns> public static int GetPreviousPageIndex(int currentPageIdx) { if (HasPreviousPage(currentPageIdx)) { return currentPageIdx - 1; } else { return currentPageIdx; } } /// <summary> /// Returns the index of the page before the current page. /// If no page exists, the current page index is returned. Index is 1-based. /// </summary> /// <param name="currentPageIdx">1-based index</param> /// <returns></returns> public int GetPreviousPageIndex() { return PagingHelper.GetPreviousPageIndex(currentPageIdx); } /// <summary> /// Returns the index of the page after the current page. /// If no page exists, the current page index is returned. Index is 1-based. /// </summary> /// <param name="currentPageIdx">1-based index</param> /// <returns></returns> public static int GetNextPageIndex(int currentPageIdx) { return currentPageIdx + 1; } /// <summary> /// Returns the index of the page after the current page. /// If no page exists, the current page index is returned. Index is 1-based. /// </summary> /// <param name="currentPageIdx">1-based index</param> /// <returns></returns> public int GetNextPageIndex() { return PagingHelper.GetNextPageIndex(currentPageIdx); } public static bool IsValidPageIdx(int pageIdx, int totalPageCount) { if (pageIdx >= 1 && pageIdx < totalPageCount) return true; else return false; } public bool IsValidPageIdx() { return PagingHelper.IsValidPageIdx(currentPageIdx, totalPageCount); } /// <summary> /// Returns the row index (1-based) for the first row on the specifiec page /// </summary> /// <param name="pageIndex">the index of the page to get the first row index</param> /// <returns>1-based row index</returns> public static int GetStartRowIndex(int pageIndex, int pageSize) { int startRowIndex = (pageIndex - 1) * pageSize + 1; return startRowIndex; } /// <summary> /// Returns the row index (1-based) for the first row on the specifiec page /// </summary> /// <param name="pageIndex">the index of the page to get the first row index</param> /// <returns>1-based row index</returns> public int GetStartRowIndex() { return PagingHelper.GetStartRowIndex(currentPageIdx, pageSize); } public static int GetCurrentPageIndexFromUrl(string queryParameterName) { string idx = System.Web.HttpContext.Current.Request[queryParameterName]; int currentPageIdx = 1; if (!string.IsNullOrEmpty(idx)) { currentPageIdx = Convert.ToInt32(idx); } return currentPageIdx; } public static int GetCurrentPageIndexFromForm(string formParameterName) { string idx = System.Web.HttpContext.Current.Request.Form[formParameterName]; int currentPageIdx = 1; if (!string.IsNullOrEmpty(idx)) { currentPageIdx = Convert.ToInt32(idx); } return currentPageIdx; } } Here is the code I used to populate a previous, next buttons and other labels that show information about the current page, etc. I am using a ObjectDataSource and using my Custom Objects and DAL that I have referenced in some of my other blog entries. protected void ObjectDataSource1_Selecting(object sender, ObjectDataSourceSelectingEventArgs e) { int currentPageIdx = PagingHelper.GetCurrentPageIndexFromUrl("PageIdx"); lblCurrentPage.Text = Convert.ToString(currentPageIdx); int startRowIndex = PagingHelper.GetStartRowIndex(currentPageIdx, PAGE_SIZE); e.InputParameters.Clear(); e.InputParameters.Add("startRowIndex", startRowIndex); e.InputParameters.Add("maximumRows", PAGE_SIZE); e.InputParameters.Add("orderBy", e.Arguments.SortExpression); e.InputParameters.Add("totalNonPagedRowCount", e.Arguments.TotalRowCount); } protected void ObjectDataSource1_Selected(object sender, ObjectDataSourceStatusEventArgs e) { // Get Values from form and url lblTotalUnpagedRow.Text = Convert.ToString(e.OutputParameters["totalNonPagedRowCount"]); int totalUnpagedRowCount = Convert.ToInt32(lblTotalUnpagedRow.Text); int currentPageIdx = PagingHelper.GetCurrentPageIndexFromUrl("PageIdx"); // do calculations int totalPageCount = PagingHelper.GetTotalPageCount(totalUnpagedRowCount, PAGE_SIZE); // configure previous and next links linkPrevious.Enabled = PagingHelper.HasPreviousPage(currentPageIdx); linkPrevious.NavigateUrl = string.Format("GridViewPage.aspx?PageIdx={0}", PagingHelper.GetPreviousPageIndex(currentPageIdx)); linkNext.Enabled = PagingHelper.HasNextPage(currentPageIdx, totalPageCount); linkNext.NavigateUrl = string.Format("GridViewPage.aspx?PageIdx={0}", PagingHelper.GetNextPageIndex(currentPageIdx, totalPageCount)); } Here is another example, but using buttons instead of links for paging. The advantage of this is that sorting can be done without doing redirects. The above code doesn't really support sorting of custom paging without redirecting the url due to the fact that the current page index is taken from the url and to change the url we need to redirect. This can make page hit stats a little misleading. The good news is that it is Google friendly. Since it uses url instead of javascript to do the navigation (like LinkButtons or Buttons) Google can follow them. An alternative to this is use javascript and have a page that you tell Google to explicitly go to. This is a page that doesn't have to be an end user page, it just has to have your site map on it. The choice is yours. Here is the code that supports custom paging and sorting. protected void GridView1_Sorting(object sender, GridViewSortEventArgs e) { PageIndex = 1; } protected void ObjectDataSource1_Selecting(object sender, ObjectDataSourceSelectingEventArgs e) { int startRowIndex = PagingHelper.GetStartRowIndex(PageIndex, PAGE_SIZE); e.InputParameters.Clear(); e.InputParameters.Add("startRowIndex", startRowIndex); e.InputParameters.Add("maximumRows", PAGE_SIZE); e.InputParameters.Add("orderBy", e.Arguments.SortExpression); e.InputParameters.Add("totalNonPagedRowCount", e.Arguments.TotalRowCount); } protected void ObjectDataSource1_Selected(object sender, ObjectDataSourceStatusEventArgs e) { int totalNonPagedRows = Convert.ToInt32(e.OutputParameters["totalNonPagedRowCount"]); PagingHelper paging = new PagingHelper(PageIndex, totalNonPagedRows, PAGE_SIZE); // populate labels that tell paging information lblCurrentPage.Text = Convert.ToString(PageIndex); lblTotalUnpagedRow.Text = Convert.ToString(totalNonPagedRows); // enable / disable navigation buttons btnNext.Enabled = paging.HasNextPage(); btnPrevious.Enabled = paging.HasPreviousPage(); // populate drop down list of pages ddlPages.DataSource = paging.PagesIndexes(); ddlPages.DataBind(); ddlPages.SelectedValue = Convert.ToString(PageIndex); } protected void ddlPages_SelectedIndexChanged(object sender, EventArgs e) { PageIndex = Convert.ToInt32(ddlPages.SelectedValue); GridView1.DataBind(); } public int PageIndex { get { return ViewState["PageIndex"] == null ? 1 : (int)ViewState["PageIndex"]; } set { ViewState["PageIndex"] = value; } } protected void btnNext_Click(object sender, EventArgs e) { PageIndex = PagingHelper.GetNextPageIndex(PageIndex); GridView1.DataBind(); } protected void btnPrevious_Click(object sender, EventArgs e) { PageIndex = PagingHelper.GetPreviousPageIndex(PageIndex); GridView1.DataBind(); } And finally, below is how you would use similar code (I added a dropdown list for the user to select the page to go to) but doesn't use the ObjectDataSource. I find this code easier to maintain and read since it is pretty linear; unlike the ObjectDataSource that forces you to figure out the correct events to put your code it. Well, that is IF all you are doing is displaying data. If you want the other CRUD operations, or if you want to have the columns automatically created for you then I would use the ObjectDataSource. It is the new way in ASP.Net 2.0 anyway. Might as well go with the flow. :) Again, either one works, it is up to you. public partial class GridViewPage2 : System.Web.UI.Page { int PAGE_SIZE = 3; PagingHelper paging; protected void Page_Load(object sender, EventArgs e) { GridView1.EnableViewState = false; BindUI(); } protected void BindUI() { PersonDAL dal = new PersonDAL(); int startRowIndex = PagingHelper.GetStartRowIndex(PageIndex, PAGE_SIZE); int totalNonPagedRowCount; List<PersonEntity> people = dal.FetchPersonGetAllPaged(startRowIndex, PAGE_SIZE, GridView1.SortExpression, out totalNonPagedRowCount); GridView1.DataSource = people; GridView1.DataBind(); PagingHelper paging = new PagingHelper(PageIndex, totalNonPagedRowCount, PAGE_SIZE); // populate labels that tell paging information lblCurrentPage.Text = Convert.ToString(PageIndex); lblTotalUnpagedRow.Text = Convert.ToString(totalNonPagedRowCount); // enable / disable navigation buttons btnNext.Enabled = paging.HasNextPage(); btnPrevious.Enabled = paging.HasPreviousPage(); // populate drop down list of pages ddlPages.DataSource = paging.PagesIndexes(); ddlPages.DataBind(); ddlPages.SelectedValue = Convert.ToString(PageIndex); } protected void GridView1_Sorting(object sender, GridViewSortEventArgs e) { PageIndex = 1; } protected void ddlPages_SelectedIndexChanged(object sender, EventArgs e) { PageIndex = Convert.ToInt32(ddlPages.SelectedValue); GridView1.DataBind(); } public int PageIndex { get { return ViewState["PageIndex"] == null ? 1 : (int)ViewState["PageIndex"]; } set { ViewState["PageIndex"] = value; } } protected void btnNext_Click(object sender, EventArgs e) { PageIndex = PagingHelper.GetNextPageIndex(PageIndex); BindUI(); } protected void btnPrevious_Click(object sender, EventArgs e) { PageIndex = PagingHelper.GetPreviousPageIndex(PageIndex); BindUI(); } } Tips: Set GridView.AllowSorting = true. If you get "The data source 'ObjectDataSource1' does not support sorting with IEnumerable data. Automatic sorting is only supported with DataView, DataTable, and DataSet.", then you need to specify the ObjectDataSource.SortParameterName to the name of your Select parameter in your DAL.

No comments: