Rob Conery's PagedList Class (Updated)

April 8, 2008 9:33 PM

NOTE:
A new, improved version of this class is now available at:

http://www.squaredroot.com/post/2008/07/08/PagedList-Strikes-Back.aspx

Robert Muehsig has posted a great user control for the MVC framework that adds pagination links to the bottom of a paged list. In it he used a slightly customized version of Rob Conery's PagedList class that Rob was kind enough to post way back when the first CTP was released. This reminded me that I should probably post the version I have customized, as I think it makes it a bit easier to use and maintain. I've included the code below.

   1: using System;
   2: using System.Linq;
   3:  
   4: namespace System.Collections.Generic
   5: {
   6:  
   7:     public interface IPagedList
   8:     {
   9:         int TotalPages { get; }
  10:         int TotalCount { get; }
  11:         int PageIndex { get; }
  12:         int PageSize { get; }
  13:         bool HasPreviousPage { get; }
  14:         bool HasNextPage { get; }
  15:         bool IsFirstPage { get; }
  16:         bool IsLastPage { get; }
  17:     }
  18:  
  19:     public class PagedList<T> : List<T>, IPagedList
  20:     {
  21:  
  22:         public PagedList( IEnumerable<T> source, int index, int pageSize )
  23:         {
  24:  
  25:             //### set source to blank list if source is null to prevent exceptions
  26:             if( source == null )
  27:                 source = new List<T>();
  28:  
  29:             //### set properties
  30:             this.TotalCount = source.Count();
  31:             this.PageSize = pageSize;
  32:             this.PageIndex = index;
  33:             if( this.TotalCount > 0 )
  34:                 this.TotalPages = (int)Math.Ceiling( (double)this.TotalCount / (double)this.PageSize );
  35:             else
  36:                 this.TotalPages = 0;
  37:             this.HasPreviousPage = ( this.PageIndex > 1 );
  38:             this.HasNextPage = ( this.PageIndex < this.TotalPages );
  39:             this.IsFirstPage = ( this.PageIndex == 1 );
  40:             this.IsLastPage = ( this.PageIndex == this.TotalPages );
  41:  
  42:             //### argument checking
  43:             if( index < 1 || index > this.TotalPages )
  44:                 throw new ArgumentOutOfRangeException( "PageIndex out of range." );
  45:             if( pageSize < 1 )
  46:                 throw new ArgumentOutOfRangeException( "PageSize cannot be less than 1." );
  47:  
  48:             //### add items to internal list
  49:             if( this.TotalCount > 0 )
  50:                 this.AddRange( source.Skip( ( index - 1 ) * pageSize ).Take( pageSize ).ToList() );
  51:  
  52:         }
  53:  
  54:         public int TotalPages { get; private set; }
  55:         public int TotalCount { get; private set; }
  56:         public int PageIndex { get; private set; }
  57:         public int PageSize { get; private set; }
  58:         public bool HasPreviousPage { get; private set; }
  59:         public bool HasNextPage { get; private set; }
  60:         public bool IsFirstPage { get; private set; }
  61:         public bool IsLastPage { get; private set; }
  62:  
  63:     }
  64:  
  65:     public static class Pagination
  66:     {
  67:         public static PagedList<T> ToPagedList<T>( this IEnumerable<T> source, int index, int pageSize )
  68:         {
  69:             return new PagedList<T>( source, index, pageSize );
  70:         }
  71:     }
  72:  
  73: }

Changes from Rob's version:

  • Added a "TotalPages" property.
    If you're going to loop through each of the pages to display page navigation, you'll obviously need this.
  • Changed "IsPreviousPage" to "HasPreviousPage".
    It just sounds better.
  • Changed "IsNextPage" to "HasNextPage".
    See above.
  • Added a "IsFirstPage" property.
    The opposite way of using the above two properties. I prefer this way, but kept the original way for backwards compatibility (except the naming).
  • Added a "IsLastPage" property.
    See above.
  • Changed the first constructor to accept IEnumerable<T> rather than IQueryable<T>.
    I'm not exactly sure why Rob originally made it IQueryable. I'm aware that by passing an IQueryable (LINQ) object to this constructor you'll avoid retrieving the entire set (only taking the results needed), but since IQueryable inherits from IEnumerable everything should be hunky-dory. He probably had a reason and I'm going to wind up breaking all of my stuff, but IEnumerable is just so much handier. =)
  • Removed the second constructor.
    The second constructor took List<T>, which is unnecessary after changing the first constructor to accept IEnumerable.
  • Cleaned up property declarations a bit.
    Mainly to make the page a bit shorter, but also to prevent the multiple calculations that could happen in the original. Also the original allowed the changing of certain properties after an instance was created, which would put the instance into an inconsistent state.
  • Added argument checking and handled a few exception scenarios more gracefully.
    Trying to make debugging a bit friendlier.
  • Removed the second extension method that didn't specify a pageSize.
    I don't really think that baking in an extension method that sets pageSize to 10 is a good idea, I'd prefer pageSize to be explicitly set elsewhere by the calling code.
  • Moved the code to the "System.Collections.Generic" namespace.
    I'm sure a lot of you are breaking out in a cold sweat to see me putting something into a System.* namespace, but I kind of feel like this is something that the .Net team just "forgot". =) Move it wherever makes you comfortable.

Please note that I took many of these ideas from the commentary below Rob's original post. I'm sure many of you are using something similar, but I thought it would be useful to get something posted online that is a bit more fleshed out than the original example.

Thanks for the great work Rob & Robert!

Tags: , , ,
Categories: C#
Actions: E-mail | Permalink | Comments (14) RSS Feed for this post's comments.

Comments

4/9/2008 1:28 PM #

david

Nice work, and thanks for sharing. I'd built something similar but yours looks a little more robust. My guess for the IQueryable constructor would be precisely that point you make about only taking the needed results. If you have a large dataset, defering the actual database call until you've pruned it on line 50 would have a huge impact on performance.

david ca

4/12/2008 8:31 AM #

tgmdbm

Sorry to say, david, you're wrong. If it IS an IQueriable then it will be executed on line 30 when you call source.Count() because it's using the IEnumerable.Count() extension method.

Once executed, an IQueriable effecively becomes a wrapper for an IEnumerable and so on line 50 you're just iterating over (index * pageSize) items.

If it's an IQueriable, what you really want to happen is somehow execute "SELECT COUNT(*) FROM ..." and then separately execute the original IQueriable after applying Skip and Take. Only then you will only iterate over pageSize items on line 50.

(i might be wrong but i think that's how the IQueriable.Count() method works)

To accomplish this you DO need a contructor which takes an IQueriable, otherwise, as i say, you'll be calling System.Linq.Enumerable.Count(source) instead of System.Linq.Queriable.Count(source)

It is a little confusing at first but you have to think like a compiler.

tgmdbm gb

4/12/2008 8:56 AM #

tgmdbm

PS What happened to index starting at 0? I'd either rename "index" to "pageNumber" or the first page is at index = 0

snurl.com/247k6

tgmdbm gb

4/12/2008 9:01 AM #

tgmdbm

hmmm. snurl didn't work. and i your preview placed a random semi-colon after =modload&

lets see if when i post it's still there.

www.hackquest.de/modules.php

tgmdbm gb

4/12/2008 10:42 AM #

Troy Goode

Hi James,

That is a good point about IQueryable, and completely makes sense. I will update the code tomorrow.

Regarding the index starting at 1, yes it should probably be renamed to 'pageNumber'. Starting at 1 just seems to make more sense given the common use-case of this class. Agree/disagree?

Is there something specific I'm supposed to looking at on the hackquest.de page, or is it just kind of a signature thing?

Thanks for the pointers!

Troy

Troy Goode us

4/12/2008 10:55 AM #

tgmdbm

i agree, index is not very descriptive and i vote for renaming to pageNumber. It's just that if it was called index, programmers would expect 0 to be the first.

The hackquest article is just a definition of hackers, notice the paragraphs all start from 0.

Also read the last line. But read the whole thing, it's quite "funny because it's true".

tgmdbm gb

4/17/2008 5:06 PM #

tgmdbm

How are you doing?

I just noticed that AsQueriable does just what we want here.

if you call it on an IQueriable it remains unchanged, if you call it on an IEnumerable you get back a wrapper.

1 constructor, takes an IEnumerable<T> (which makes more sence), just call AsQueriable() at the top and you're done.

tgmdbm gb

4/17/2008 8:55 PM #

Troy Goode

Hmmm,

Won't the query still execute to set the TotalCount = source.Count on line 30 though, even if source was IQueryable?

Troy Goode us

4/18/2008 3:52 AM #

tgmdbm

Only 3 ways to find out.

;)

I just used Linqpad and hey presto. Two sql statements run. a SELECT Count(*) and a huge beast of a query.

The Skip and Take make the second query extremely long and ugly, but the effect is, it only returns pageSize results.

tgmdbm gb

4/18/2008 9:07 AM #

Troy Goode

Fair enough. I'll update the code today. =)

Troy Goode us

7/6/2008 8:22 PM #

Martillo

This is nice... thanks.

One issue I ran into using it with a DataPager and a ListView is that the DataPager appears to transmit a zero-based page index...

Martillo us

7/7/2008 11:32 PM #

Troy Goode

Hmmm, that is an interesting issue Martillo. I purposefully changed the PageIndex to be one-based rather zero-based to make outputting page numbers a bit easier (who has a page zero, right?), but it appears that may have created unforeseen consequences.

This code is due for an update anyway, so I'll try to get that taken care of.

Troy Goode us

7/8/2008 1:54 AM #

trackback


Trackback from: Troy Goode: SquaredRoot

7/8/2008 1:55 AM #

Troy Goode

Hi everyone. Thanks for all the feedback! I have posted a new version of this code, so please redirect all future comments to that post:

www.squaredroot.com/.../...dList-Strikes-Back.aspx

Troy Goode us

Comments are closed

Troy Goode

Troy Goode
Microsoft Certified Professional Developer
AddThis Feed Button

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in  anyway.

© Copyright 2008

Colophon

Powered by:
BlogEngine.NET 1.4
Template:
Designs by Darren
Header Font:
Stamper
Syntax Highlighting:
WLW Code Snippet Plugin