There is an updated version of the PagedList<T> code available here.
A few months ago I posted about my changes to Rob Conery's PagedList class. Since writing that article many comments have been left about how to further improve the design, which I have since incorporated into a new, further improved PagedList class. For those who aren't familiar, the PagedList class allows scenarios such as the following:
1: public void ListProducts( int pageIndex )
2: {
3: int pageSize = 10;
4: var products = productRepository.GetAllProducts()
5: .ToPagedList( pageIndex, pageSize );
6: return View( products );
7: }
So in the above scenario (an example MVC action), if you had a list of 30 products with IDs 1-30 and passed in a pageIndex of 2 you would pass the products with IDs 21-30 into your view. Best of all it uses IQueryable, so if you pass a LINQ expression or IQueryable result into ToPagedList(...) it will do the filtering on the database side! Take a look at the new improved PagedList:
Update: Per a suggestion in a post by Martijn Boland, I have tweaked the code below by converting IPagedList to IPagedList<T> and having it implement IList<T>. This will allow you to pass the IPagedList<T> around and still enumerate it in a strongly-typed manner. Also of note is Martijn's use of the PagedList class to create an MVC pager control.
1: using System.Linq;
2:
3: namespace System.Collections.Generic
4: {
5: public interface IPagedList<T> : IList<T>
6: {
7: int PageCount { get; }
8: int TotalItemCount { get; }
9: int PageIndex { get; }
10: int PageNumber { get; }
11: int PageSize { get; }
12: bool HasPreviousPage { get; }
13: bool HasNextPage { get; }
14: bool IsFirstPage { get; }
15: bool IsLastPage { get; }
16: }
17:
18: public class PagedList<T> : List<T>, IPagedList
19: {
20: public PagedList( IEnumerable<T> source, int index, int pageSize )
21: {
22: if( source is IQueryable<T> )
23: Initialize( source as IQueryable<T>, index, pageSize );
24: else
25: Initialize( source.AsQueryable(), index, pageSize );
26: }
27:
28: public PagedList( IQueryable<T> source, int index, int pageSize )
29: {
30: Initialize( source, index, pageSize );
31: }
32:
33: #region IPagedList Members
34:
35: public int PageCount { get; private set; }
36: public int TotalItemCount { get; private set; }
37: public int PageIndex { get; private set; }
38: public int PageNumber { get { return PageIndex + 1; } }
39: public int PageSize { get; private set; }
40: public bool HasPreviousPage { get; private set; }
41: public bool HasNextPage { get; private set; }
42: public bool IsFirstPage { get; private set; }
43: public bool IsLastPage { get; private set; }
44:
45: #endregion
46:
47: protected void Initialize( IQueryable<T> source, int index, int pageSize )
48: {
49: //### set source to blank list if source is null to prevent exceptions
50: if( source == null )
51: source = new List<T>().AsQueryable();
52:
53: //### set properties
54: TotalItemCount = source.Count();
55: PageSize = pageSize;
56: PageIndex = index;
57: if( TotalItemCount > 0 )
58: PageCount = (int)Math.Ceiling( TotalItemCount / (double)PageSize );
59: else
60: PageCount = 0;
61: HasPreviousPage = ( PageIndex > 0 );
62: HasNextPage = ( PageIndex < ( PageCount - 1 ) );
63: IsFirstPage = ( PageIndex <= 0 );
64: IsLastPage = ( PageIndex >= ( PageCount - 1 ) );
65:
66: //### argument checking
67: if( index < 0 )
68: throw new ArgumentOutOfRangeException( "PageIndex cannot be below 0." );
69: if( pageSize < 1 )
70: throw new ArgumentOutOfRangeException( "PageSize cannot be less than 1." );
71:
72: //### add items to internal list
73: if( TotalItemCount > 0 )
74: AddRange( source.Skip( ( index ) * pageSize ).Take( pageSize ).ToList() );
75: }
76: }
77:
78: public static class Pagination
79: {
80: public static PagedList<T> ToPagedList<T>( this IQueryable<T> source, int index, int pageSize )
81: {
82: return new PagedList<T>( source, index, pageSize );
83: }
84:
85: public static PagedList<T> ToPagedList<T>( this IEnumerable<T> source, int index, int pageSize )
86: {
87: return new PagedList<T>( source, index, pageSize );
88: }
89: }
90: }
Changes Since Previous Version:
- Changed "TotalPages" property to "PageCount".This was done to more clearly illustrate the purpose of this property (more in line with the standard List<T> .Count property).
- Changed "TotalCount" property to "TotalItemCount".This was done to differentiate between Pages and Items more clearly.
- Switched "PageIndex" back to being a zero-based index, rather than one-based.The PageIndex was set to be one-based because the most common use-case that I could think of, outputting a pager (i.e.: Previous - 1 - 2 - 3 - Next), was one-based and it would be nice to not have to do "obj.PageIndex + 1" everywhere. This was probably a poor decision. Some people found it confusing and others found that it didn't work with the standard .Net DataPager.
- Added "PageNumber" property.As suggested via comment on my previous post, PageNumber is just PageIndex + 1.
- Treat all data as IQueryable for the duration of class initialization.Tgmdbm, a frequent contributer on the ASP.Net MVC forums, pointed out some good reasons why everything should be treated as IQueryable before the .Skip(x).Take(y). I'm not 100% sold that treating it as IEnumerable would also work for IQueryable objects (since IQueryable supports IEnumerable), but to be safe I have defaulted everything to using IQueryable. You can still pass in a strict IEnumerable, but it will be converted to IQueryable for the duration of the PagedList class initialization process.
- Tested with xUnit.net.While the PagedList class isn't exactly rocket science, I want to strive to improve the quality of the code that I'm presenting to the community. I also enjoyed having an excuse to try out xUnit's data-driven testing using the [Theory] attribute (similar to MbUnit's [RowTest]) for the first time. Not much has been written about it, but I was able to find some examples from Ben Hall which got me up and testing quickly.
Here is the test class:
1: using System;
2: using System.Collections.Generic;
3: using Xunit;
4: using XunitExt;
5:
6: namespace SquaredRoot.Collections.Generic.Tests
7: {
8: public class PagedListFacts
9: {
10: [Fact]
11: public void Null_Data_Set_Doesnt_Throw_Exception()
12: {
13: //act
14: Assert.ThrowsDelegate act = () => new PagedList<object>( null, 0, 10 );
15:
16: //assert
17: Assert.DoesNotThrow( act );
18: }
19:
20: [Fact]
21: public void PageIndex_Below_Zero_Throws_ArgumentOutOfRange()
22: {
23: //arrange
24: var data = new[]{ 1, 2, 3 };
25:
26: //act
27: Assert.ThrowsDelegate act = () => data.ToPagedList( -1, 1 );
28:
29: //assert
30: Assert.Throws<ArgumentOutOfRangeException>( act );
31: }
32:
33: [Fact]
34: public void PageIndex_Above_RecordCount_Returns_Empty_List()
35: {
36: //arrange
37: var data = new[]{ 1, 2, 3 };
38:
39: //act
40: var pagedList = data.ToPagedList( 2, 3 );
41:
42: //assert
43: Assert.Equal( 0, pagedList.Count );
44: }
45:
46: [Fact]
47: public void PageSize_Below_One_Throws_ArgumentOutOfRange()
48: {
49: //arrange
50: var data = new[]{ 1, 2, 3 };
51:
52: //act
53: Assert.ThrowsDelegate act = () => data.ToPagedList( 0, 0 );
54:
55: //assert
56: Assert.Throws<ArgumentOutOfRangeException>( act );
57: }
58:
59: [Fact]
60: public void Null_Data_Set_Doesnt_Return_Null()
61: {
62: //act
63: var pagedList = new PagedList<object>( null, 0, 10 );
64:
65: //assert
66: Assert.NotNull( pagedList );
67: }
68:
69: [Fact]
70: public void Null_Data_Set_Returns_Zero_Pages()
71: {
72: //act
73: var pagedList = new PagedList<object>( null, 0, 10 );
74:
75: //assert
76: Assert.Equal( 0, pagedList.PageCount );
77: }
78:
79: [Fact]
80: public void Zero_Item_Data_Set_Returns_Zero_Pages()
81: {
82: //arrange
83: var data = new List<object>();
84:
85: //act
86: var pagedList = data.ToPagedList( 0, 10 );
87:
88: //assert
89: Assert.Equal( 0, pagedList.PageCount );
90: }
91:
92: [Fact]
93: public void DataSet_Of_One_Through_Five_PageSize_Of_Two_PageIndex_Of_One_First_Item_Is_Three()
94: {
95: //arrange
96: var data = new[]{ 1, 2, 3, 4, 5 };
97:
98: //act
99: var pagedList = data.ToPagedList( 1, 2 );
100:
101: //assert
102: Assert.Equal( 3, pagedList[0] );
103: }
104:
105: [Fact]
106: public void TotalCount_Is_Preserved()
107: {
108: //arrange
109: var data = new[]{ 1, 2, 3, 4, 5 };
110:
111: //act
112: var pagedList = data.ToPagedList( 1, 2 );
113:
114: //assert
115: Assert.Equal( 5, pagedList.TotalItemCount );
116: }
117:
118: [Fact]
119: public void PageIndex_Is_Preserved()
120: {
121: //arrange
122: var data = new[]{ 1, 2, 3, 4, 5 };
123:
124: //act
125: var pagedList = data.ToPagedList( 1, 2 );
126:
127: //assert
128: Assert.Equal( 1, pagedList.PageIndex );
129: }
130:
131: [Fact]
132: public void PageSize_Is_Preserved()
133: {
134: //arrange
135: var data = new[]{ 1, 2, 3, 4, 5 };
136:
137: //act
138: var pagedList = data.ToPagedList( 1, 2 );
139:
140: //assert
141: Assert.Equal( 2, pagedList.PageSize );
142: }
143:
144: [Fact]
145: public void Data_Is_Filtered_By_PageSize()
146: {
147: //arrange
148: var data = new[]{ 1, 2, 3, 4, 5 };
149:
150: //act
151: var pagedList = data.ToPagedList( 1, 2 );
152:
153: //assert
154: Assert.Equal( 2, pagedList.Count );
155:
156: //### related test below
157:
158: //act
159: pagedList = data.ToPagedList( 2, 2 );
160:
161: //assert
162: Assert.Equal( 1, pagedList.Count );
163: }
164:
165: [Fact]
166: public void DataSet_OneThroughThree_PageSize_One_PageIndex_Two_HasNextPage_False()
167: {
168: //arrange
169: var data = new[]{ 1, 2, 3 };
170:
171: //act
172: var pagedList = data.ToPagedList( 2, 1 );
173:
174: //assert
175: Assert.Equal( false, pagedList.HasNextPage );
176: }
177:
178: [Fact]
179: public void DataSet_OneThroughThree_PageSize_One_PageIndex_Two_IsLastPage_True()
180: {
181: //arrange
182: var data = new[]{ 1, 2, 3 };
183:
184: //act
185: var pagedList = data.ToPagedList( 2, 1 );
186:
187: //assert
188: Assert.Equal( true, pagedList.IsLastPage );
189: }
190:
191: [Theory]
192: [InlineData( new[] { 1, 2, 3 }, 0, 1 )]
193: [InlineData( new[] { 1, 2, 3 }, 1, 2 )]
194: [InlineData( new[] { 1, 2, 3 }, 2, 3 )]
195: public void Theory_PageNumber_Is_PageIndex_Plus_One( int[] integers, int pageIndex, int expectedPageNumber )
196: {
197: //arrange
198: var data = integers;
199:
200: //act
201: var pagedList = data.ToPagedList( pageIndex, 1 );
202:
203: //assert
204: Assert.Equal( expectedPageNumber, pagedList.PageNumber );
205: }
206:
207: [Theory]
208: [InlineData( new[]{ 1, 2, 3 }, 0, 1, false, true )]
209: [InlineData( new[]{ 1, 2, 3 }, 1, 1, true, true )]
210: [InlineData( new[]{ 1, 2, 3 }, 2, 1, true, false )]
211: public void Theory_HasPreviousPage_And_HasNextPage_Are_Correct( int[] integers, int pageIndex, int pageSize, bool expectedHasPrevious, bool expectedHasNext )
212: {
213: //arrange
214: var data = integers;
215:
216: //act
217: var pagedList = data.ToPagedList( pageIndex, pageSize );
218:
219: //assert
220: Assert.Equal( expectedHasPrevious, pagedList.HasPreviousPage );
221: Assert.Equal( expectedHasNext, pagedList.HasNextPage );
222: }
223:
224: [Theory]
225: [InlineData( new[]{ 1, 2, 3 }, 0, 1, true, false )]
226: [InlineData( new[]{ 1, 2, 3 }, 1, 1, false, false )]
227: [InlineData( new[]{ 1, 2, 3 }, 2, 1, false, true )]
228: public void Theory_IsFirstPage_And_IsLastPage_Are_Correct( int[] integers, int pageIndex, int pageSize, bool expectedIsFirstPage, bool expectedIsLastPage )
229: {
230: //arrange
231: var data = integers;
232:
233: //act
234: var pagedList = data.ToPagedList( pageIndex, pageSize );
235:
236: //assert
237: Assert.Equal( expectedIsFirstPage, pagedList.IsFirstPage );
238: Assert.Equal( expectedIsLastPage, pagedList.IsLastPage );
239: }
240:
241: [Theory]
242: [InlineData( new[]{ 1, 2, 3 }, 1, 3 )]
243: [InlineData( new[]{ 1, 2, 3 }, 3, 1 )]
244: [InlineData( new[]{ 1 }, 1, 1 )]
245: [InlineData( new[]{ 1, 2, 3 }, 2, 2 )]
246: [InlineData( new[]{ 1, 2, 3, 4 }, 2, 2 )]
247: [InlineData( new[]{ 1, 2, 3, 4, 5 }, 2, 3 )]
248: public void Theory_PageCount_Is_Correct( int[] integers, int pageSize, int expectedNumberOfPages )
249: {
250: //arrange
251: var data = integers;
252:
253: //act
254: var pagedList = data.ToPagedList( 0, pageSize );
255:
256: //assert
257: Assert.Equal( expectedNumberOfPages, pagedList.PageCount );
258: }
259: }
260: }
A big thanks to everyone who has thrown their two cents in to help improve this small but useful class!