Learning SharePoint Part VII – List Pagination

A very small group of you saw this post go up and then come right back down again.  I found a bug about 30 seconds after posting this.    Here is the updated post.  NOTE: The bug had to do with not fully understanding how the pagination handled moving backwards in the list.  It appeared to be doing it correctly, but when I started to look closely at the actual items, it turned out that it was ALWAYS MOVING FORWARD.  The key is that I missed the PagedPrev query string element, which I now am including correctly.  The code has been updated below.  Beware of of voodoo programming.  :-)


It seems like everything takes just a little longer to learn than it should with SharePoint.  I am building a webpart that browses list items and allows the user to perform special actions on the list item.  After creating item styles and coming up with a CSS container to match the repeating pattern I needed, I went to add pagination to the item browser.   Pagination is usually represented in the user interface by little left-right arrows or “next page”/”previous page”.  This should be easy and it is, once you understand how SharePoint does pagination.

SPListItemCollectionPosition

The key to SharePoint list pagination is the SPListItemCollectionPosition class. The documentation’s one example didn’t help me with my specific requirements.  In particular, I need to page through the list data sorted by the “Created” date field.

The SPListItemCollectionPosition’s constructor takes one string parameter called PagingInfo.  The SDK’s description of this parameter is particularly useless: Gets or sets paging information used to generate the next page of data.   What about the string’s structure and contents?  How do you use it?Another friendly MSDN blogger, Deepak Badki, had part of the answer here.

But rather than just copy his code, I needed to understand it first. 

If you setup a list view such that it is sorted and the number of items to display is less than the number of items, you can examine the query strings resulting from paging from one page to the next and back.  Here are some examples:

First Page

AllItems.aspx

Next Page

AllItems.aspx?Paged=TRUE&p_Created=20090217%2018%3a22%3a14&p_ID=3&View=%7bB32E95A4%2d5675%2d42A5%2d8C13%2d569D13BDCAF9%7d&PageFirstRow=4

Next Page

AllItems.aspx?Paged=TRUE&p_Created=20090217%2018%3a22%3a34&p_ID=6&View=%7bB32E95A4%2d5675%2d42A5%2d8C13%2d569D13BDCAF9%7d&PageFirstRow=7

Previous Page

AllItems.aspx?Paged=TRUE&PagedPrev=TRUE&p_Created=20090217%2020%3a51%3a20&p_ID=7&View=%7bB32E95A4%2d5675%2d42A5%2d8C13%2d569D13BDCAF9%7d&PageLastRow=6

In this particular instance, the list view is configured to sort by the created date.  Let’s break down the the query string:

AllItems.aspx? – The list page

Paged=TRUE – Indicates that the list is paged.

PagedPrev=TRUE – This element only appears when the previous page is visited.

p_Created= – The first sort by parameter, which is “Created” prefixed with “p_”.  The right hand is the encoded universal date and time.

p_ID= – This is the ID of the previous page’s last item’s ID.   This is important.

View= – This is the encoded GUID for the current list. 

PageFirstRow= – The ID of the current page’s first item. 

Newbie Webpart Developer

I am definitely new to webpart development.  I have done some work in ASP.NET but not for a while.  I know that sounds strange given that so much of what we do involves web development, but I have been a back-end guy for a long, long time.  So, this is really exciting for me and a bit painful.  ASP.NET’s attempt to be like WinForms in many ways offers tremendous advantage, but it also is a bit like taking a boat on land.  I am also exploring the MVC pattern, which seems like a better fit for web development.  That is neither here nor there because to build a webpart, you have to think like a server control developer—something I haven’t done since the ASP.NET 1.0 era.

So, my challenge is to learn the SharePoint way and also learn ASP.NET, and learn how to make things pretty.  I had help on the last bit from the product group.  Here is a screenshot of the basic item browser (the browser is a browser of ideas, a topic for another post):

image 

The browser webpart is configured to browse three items at a time (this is a personalization aspect of the webpart).   At the core of the browser is one method:

    1: public static SPListItemCollection GetListPage(SPList list,
    2:                                                uint pageSize,
    3:                                                int pageIndex,                                                        
    4:                                                string pagingInfo,
    5:                                                string fields,
    6:                                                string queryCaml,
    7:                                                out bool isEndOfList)
    8: {
    9:     
   10:     SPQuery query = new SPQuery();
   11:  
   12:     query.RowLimit = pageSize;
   13:     query.ViewFields = fields;
   14:     query.Query = queryCaml;
   15:  
   16:     if (!string.IsNullOrEmpty(pagingInfo))
   17:     {
   18:         SPListItemCollectionPosition collectionPosition = new SPListItemCollectionPosition(pagingInfo);                
   19:         query.ListItemCollectionPosition = collectionPosition;
   20:     }
   21:  
   22:     SPListItemCollection returnValue = list.GetItems(query);
   23:  
   24:     isEndOfList = (((pageIndex - 1) * pageSize) + returnValue.Count) >= list.ItemCount;
   25:  
   26:     return returnValue;
   27: }

First, I really dislike methods that take this many parameters, but it takes a lot to feed this little algorithm:

SPList list – The list

uint pageSize – The page size (number of ideas per page)

int pageIndex – The current page index                                                       
string pagingInfo – The paging info (more on this later)

string fields – The fields to bring back from the list (in CAML)

string queryCaml – The CAML query to execute

out bool isEndOfList – An out parameter which signals that the results are the end of the list

Here is the method in context:

    1: SPList list = web.Lists.GetList(listGuid, false);
    2: SPView view = list.GetView(viewGuid);
    3:  
    4: List<string> fields = new List<string>();
    5:  
    6: foreach (string field in view.ViewFields)
    7: {
    8:     fields.Add(field);
    9: }
   10:  
   11: SPListItemCollection pageItems = Fx.GetListPage(list,
   12:                                                 (uint) IdeasPerPage,
   13:                                                 CurrentPage,
   14:                                                 PageInfo,                                                            
   15:                                                 Fx.GetFieldRefCaml(fields.ToArray()),
   16:                                                 caml,
   17:                                                 out _EndOfList);
   18: RenderIdeas(pageItems);

I get the list from the web and the default view.  I pack up the fields and send the whole shebang to GetListPage.  So, where does the CurrentPage and the PageInfo come from?

The key is to remember the current page’s first and last item and derive the PageInfo from that data.  Lets walk through this:

1.)  RenderIdeas – Capture the first and last ideas and send them along to AddNavigation

2.) AddNavigation – Use the first and last items, the list view GUID, and the current state to setup the navigation elements used for paging.

Okay, now for the code:

    1: private void RenderIdeas(SPListItemCollection pageItems, Guid viewGuid)
    2: {
    3:     if (CurrentPage == 1 && pageItems.Count == 0)
    4:     {
    5:         CreateNoIdeasWarning();
    6:  
    7:         return;
    8:     }
    9:  
   10:     SPListItem lastIdea = null;
   11:  
   12:     foreach (SPListItem idea in pageItems)
   13:     {
   14:         Panel ideaContainer = CreateIdeaContainer(idea);
   15:  
   16:         _ContainerPanel.Controls.Add(ideaContainer);
   17:  
   18:         lastIdea = idea;
   19:     }
   20:  
   21:     AddNavigation(pageItems[0], lastIdea, viewGuid);
   22: }

 

Pretty straightforward.  If the list is empty, show a warning; otherwise, render each idea in the list returned by GetListPage.   Now, for AddNavigation:

 

    1: private void AddNavigation(SPListItem firstIdea, SPListItem lastIdea, Guid viewGuid)
    2: {            
    3:     DateTime lastItemCreatedDate = (DateTime) lastIdea["Created"];
    4:     int lastItemId = (int) lastIdea["ID"];
    5:  
    6:     string lastItemDateData = lastItemCreatedDate.ToUniversalTime().ToString("yyyyMMdd hh:mm:ss");
    7:  
    8:     string nextPage = string.Format("Paged=TRUE~p_{0}={1}~p_ID={2}~View={3}~PageFirstRow={4}",
    9:                                     "Created",
   10:                                     SPEncode.UrlEncode(lastItemDateData),
   11:                                     lastItemId,
   12:                                     SPEncode.UrlEncode(viewGuid.ToString()),
   13:                                     firstIdea["ID"]);
   14:  
   15:     string previousPage;
   16:  
   17:     if (CurrentPage > 2)
   18:     {
   19:         previousPage = string.Format("Paged=TRUE~PagedPrev=TRUE~p_{0}={1}~p_ID={2}~View={3}~PageFirstRow={4}",
   20:                                      "Created",
   21:                                      SPEncode.UrlEncode(lastItemDateData),
   22:                                      lastItemId,
   23:                                      SPEncode.UrlEncode(viewGuid.ToString()),
   24:                                      firstIdea["ID"]);
   25:     }
   26:     else
   27:     {
   28:         previousPage = string.Format("Paged=TRUE~View={0}", SPEncode.UrlEncode(viewGuid.ToString()));
   29:     }
   30:  
   31:  
   32:     HyperLink previousPageLink = null;
   33:     if (CurrentPage > 1)
   34:     {
   35:         previousPageLink = new HyperLink();
   36:         previousPageLink.Text = "Previous Page";
   37:         previousPageLink.CssClass = "IdeaLeftArrow";
   38:         previousPageLink.NavigateUrl = string.Format("{0}?Page={1}&PageInfo={2}", Page.Request.Url.GetLeftPart(UriPartial.Path), CurrentPage - 1, previousPage);
   39:     }
   40:  
   41:     HyperLink nextPageLink = null;
   42:  
   43:     if (!_EndOfList)
   44:     {
   45:         nextPageLink = new HyperLink();
   46:         nextPageLink.Text = "Next Page";
   47:         nextPageLink.CssClass = "IdeaRightArrow";
   48:         nextPageLink.NavigateUrl = string.Format("{0}?Page={1}&PageInfo={2}", Page.Request.Url.GetLeftPart(UriPartial.Path), CurrentPage + 1, nextPage);
   49:     }
   50:  
   51:     if (nextPageLink == null && previousPageLink == null)
   52:     {
   53:         return;
   54:     }
   55:  
   56:     Panel navigationPanel = new Panel();
   57:  
   58:     navigationPanel.CssClass = "IdeaNavigationPanel";
   59:  
   60:     if (previousPageLink != null)
   61:     {
   62:         navigationPanel.Controls.Add(previousPageLink);
   63:     }
   64:     if (nextPageLink != null)
   65:     {
   66:         navigationPanel.Controls.Add(nextPageLink);
   67:     }
   68:  
   69:     _ContainerPanel.Controls.Add(navigationPanel);
   70: }

 

This should look familiar because I am constructing two query strings similar to what was discussed in the beginning of the post.  The next page URL is pretty straightforward.  The previous page URL requires a bit more explanation.  If CurrentPage is great than 2, I have to insert the PagedPrev element, otherwise it is sufficient to just add the list GUID element.

When the user clicks one of the links, this paging information is parsed out of the query string and feeds the next pagination call (from the OnLoad event):

    1: if (Page.Request.QueryString.HasKeys())
    2: {
    3:     if (Page.Request.QueryString["Page"] != null)
    4:     {
    5:         int value;
    6:         if (int.TryParse(Page.Request.QueryString["Page"], out value))
    7:         {
    8:             CurrentPage = value;
    9:         }
   10:     }
   11:  
   12:     if (Page.Request.QueryString["PageInfo"] != null)
   13:     {
   14:         _PageInfo = Page.Request.QueryString["PageInfo"];
   15:         _PageInfo = _PageInfo.Replace("~", "&");
   16:     }
   17: }
   18:  
   19: EnsureChildControls();
   20: RenderIdeaList();

That’s it folks.  I hope this helps somebody because this took me about 5 hours to figure it out.