Return of the PagedList

// June 15th, 2009 // C#, LINQ, MVC

It has been nearly a year since I posted an updated version of the PagedList<T> functionality originally created by Scott Guthrie and posted by Rob Conery. Since then I have used the class in a number of projects and find it indispensable.

A few days ago, Craig Stuntz reported an interesting observation: when the first page is returned, the class performs a Skip(0). Suprisingly, this is not free. With that in mind, I set out to correct that issue as well as incorporate a few changes I’ve made over the past year. The result is nearly identical to the last posted version, just a bit more readable. Additionally…

  • The source is now available on CodePlex: http://pagedlist.codeplex.com. This should make finding and downloading the code easier than finding the correct blog entry on some dude’s blog.
  • I have posted a release-compiled, XML commented, signed assembly on CodePlex. I got tired of having to copy the source into multiple projects and finding a place to put it in that project’s taxonomy.
  • Further incremental changes can be found in the Change Log on the CodePlex project site.

Download from CodePlex

IPagedList<T>.cs

using System.Collections.Generic;

namespace PagedList
{
	public interface IPagedList<T> : IList<T>
	{
		int PageCount { get; }
		int TotalItemCount { get; }
		int PageIndex { get; }
		int PageNumber { get; }
		int PageSize { get; }
		bool HasPreviousPage { get; }
		bool HasNextPage { get; }
		bool IsFirstPage { get; }
		bool IsLastPage { get; }
	}
}

PagedList<T>.cs

using System;
using System.Collections.Generic;
using System.Linq;

namespace PagedList
{
	public class PagedList<T> : List<T>, IPagedList<T>
	{
		public PagedList(IEnumerable<T> superset, int index, int pageSize)
		{
			// set source to blank list if superset is null to prevent exceptions
			var source = superset == null
			                      	? new List<T>().AsQueryable()
									: superset.AsQueryable();

			TotalItemCount = source.Count();
			PageSize = pageSize;
			PageIndex = index;
			if (TotalItemCount > 0)
				PageCount = (int) Math.Ceiling(TotalItemCount/(double) PageSize);
			else
				PageCount = 0;

			if (index < 0)
				throw new ArgumentOutOfRangeException("index", index, "PageIndex cannot be below 0.");
			if (pageSize < 1)
				throw new ArgumentOutOfRangeException("pageSize", pageSize, "PageSize cannot be less than 1.");

			// add items to internal list
			if (TotalItemCount > 0)
				if (index == 0)
					AddRange(source.Take(pageSize).ToList());
				else
					AddRange(source.Skip((index) * pageSize).Take(pageSize).ToList());
		}

		public int PageCount { get; private set; }
		public int TotalItemCount { get; private set; }
		public int PageIndex { get; private set; }
		public int PageSize { get; private set; }

		public int PageNumber
		{
			get { return PageIndex + 1; }
		}

		public bool HasPreviousPage
		{
			get { return PageIndex > 0; }
		}

		public bool HasNextPage
		{
			get { return PageIndex < (PageCount - 1); }
		}

		public bool IsFirstPage
		{
			get { return PageIndex <= 0; }
		}

		public bool IsLastPage
		{
			get { return PageIndex >= (PageCount - 1); }
		}
	}
}

PagedListExtensions.cs

using System.Collections.Generic;
using System.Linq;

namespace PagedList
{
	public static class PagedListExtensions
	{
		public static IPagedList<T> ToPagedList<T>(this IEnumerable<T> superset, int index, int pageSize)
		{
			return new PagedList<T>(superset, index, pageSize);
		}
	}
}

PagedListFacts.cs

using System;
using System.Collections.Generic;
using Xunit;
using Xunit.Extensions;

namespace PagedList.Tests
{
	public class PagedListFacts
	{
		[Fact]
		public void Null_Data_Set_Doesnt_Throw_Exception()
		{
			//act
			Assert.ThrowsDelegate act = () => new PagedList<object>(null, 0, 10);

			//assert
			Assert.DoesNotThrow(act);
		}

		[Fact]
		public void PageIndex_Below_Zero_Throws_ArgumentOutOfRange()
		{
			//arrange
			var data = new[] {1, 2, 3};

			//act
			Assert.ThrowsDelegate act = () => data.ToPagedList(-1, 1);

			//assert
			Assert.Throws<ArgumentOutOfRangeException>(act);
		}

		[Fact]
		public void PageIndex_Above_RecordCount_Returns_Empty_List()
		{
			//arrange
			var data = new[] {1, 2, 3};

			//act
			var pagedList = data.ToPagedList(2, 3);

			//assert
			Assert.Equal(0, pagedList.Count);
		}

		[Fact]
		public void PageSize_Below_One_Throws_ArgumentOutOfRange()
		{
			//arrange
			var data = new[] {1, 2, 3};

			//act
			Assert.ThrowsDelegate act = () => data.ToPagedList(0, 0);

			//assert
			Assert.Throws<ArgumentOutOfRangeException>(act);
		}

		[Fact]
		public void Null_Data_Set_Doesnt_Return_Null()
		{
			//act
			var pagedList = new PagedList<object>(null, 0, 10);

			//assert
			Assert.NotNull(pagedList);
		}

		[Fact]
		public void Null_Data_Set_Returns_Zero_Pages()
		{
			//act
			var pagedList = new PagedList<object>(null, 0, 10);

			//assert
			Assert.Equal(0, pagedList.PageCount);
		}

		[Fact]
		public void Zero_Item_Data_Set_Returns_Zero_Pages()
		{
			//arrange
			var data = new List<object>();

			//act
			var pagedList = data.ToPagedList(0, 10);

			//assert
			Assert.Equal(0, pagedList.PageCount);
		}

		[Fact]
		public void DataSet_Of_One_Through_Five_PageSize_Of_Two_PageIndex_Of_One_First_Item_Is_Three()
		{
			//arrange
			var data = new[] {1, 2, 3, 4, 5};

			//act
			var pagedList = data.ToPagedList(1, 2);

			//assert
			Assert.Equal(3, pagedList[0]);
		}

		[Fact]
		public void TotalCount_Is_Preserved()
		{
			//arrange
			var data = new[] {1, 2, 3, 4, 5};

			//act
			var pagedList = data.ToPagedList(1, 2);

			//assert
			Assert.Equal(5, pagedList.TotalItemCount);
		}

		[Fact]
		public void PageIndex_Is_Preserved()
		{
			//arrange
			var data = new[] {1, 2, 3, 4, 5};

			//act
			var pagedList = data.ToPagedList(1, 2);

			//assert
			Assert.Equal(1, pagedList.PageIndex);
		}

		[Fact]
		public void PageSize_Is_Preserved()
		{
			//arrange
			var data = new[] {1, 2, 3, 4, 5};

			//act
			var pagedList = data.ToPagedList(1, 2);

			//assert
			Assert.Equal(2, pagedList.PageSize);
		}

		[Fact]
		public void Data_Is_Filtered_By_PageSize()
		{
			//arrange
			var data = new[] {1, 2, 3, 4, 5};

			//act
			var pagedList = data.ToPagedList(1, 2);

			//assert
			Assert.Equal(2, pagedList.Count);

			//### related test below

			//act
			pagedList = data.ToPagedList(2, 2);

			//assert
			Assert.Equal(1, pagedList.Count);
		}

		[Fact]
		public void DataSet_OneThroughSix_PageSize_Three_PageIndex_Zero_FirstValue_Is_One()
		{
			//arrange
			var data = new[] { 1, 2, 3, 4, 5, 6 };

			//act
			var pagedList = data.ToPagedList(0, 3);

			//assert
			Assert.Equal(1, pagedList[0]);
		}

		[Fact]
		public void DataSet_OneThroughThree_PageSize_One_PageIndex_Two_HasNextPage_False()
		{
			//arrange
			var data = new[] {1, 2, 3};

			//act
			var pagedList = data.ToPagedList(2, 1);

			//assert
			Assert.Equal(false, pagedList.HasNextPage);
		}

		[Fact]
		public void DataSet_OneThroughThree_PageSize_One_PageIndex_Two_IsLastPage_True()
		{
			//arrange
			var data = new[] {1, 2, 3};

			//act
			var pagedList = data.ToPagedList(2, 1);

			//assert
			Assert.Equal(true, pagedList.IsLastPage);
		}

		[Fact]
		public void DataSet_OneAndTwo_PageSize_One_PageIndex_One_FirstValue_Is_Two()
		{
			//arrange
			var data = new[] { 1, 2 };

			//act
			var pagedList = data.ToPagedList(1, 1);

			//assert
			Assert.Equal(2, pagedList[0]);
		}

		[Theory]
		[InlineData(new[] {1, 2, 3}, 0, 1)]
		[InlineData(new[] {1, 2, 3}, 1, 2)]
		[InlineData(new[] {1, 2, 3}, 2, 3)]
		public void Theory_PageNumber_Is_PageIndex_Plus_One(int[] integers, int pageIndex, int expectedPageNumber)
		{
			//arrange
			var data = integers;

			//act
			var pagedList = data.ToPagedList(pageIndex, 1);

			//assert
			Assert.Equal(expectedPageNumber, pagedList.PageNumber);
		}

		[Theory]
		[InlineData(new[] {1, 2, 3}, 0, 1, false, true)]
		[InlineData(new[] {1, 2, 3}, 1, 1, true, true)]
		[InlineData(new[] {1, 2, 3}, 2, 1, true, false)]
		public void Theory_HasPreviousPage_And_HasNextPage_Are_Correct(int[] integers, int pageIndex, int pageSize,
		                                                               bool expectedHasPrevious, bool expectedHasNext)
		{
			//arrange
			var data = integers;

			//act
			var pagedList = data.ToPagedList(pageIndex, pageSize);

			//assert
			Assert.Equal(expectedHasPrevious, pagedList.HasPreviousPage);
			Assert.Equal(expectedHasNext, pagedList.HasNextPage);
		}

		[Theory]
		[InlineData(new[] {1, 2, 3}, 0, 1, true, false)]
		[InlineData(new[] {1, 2, 3}, 1, 1, false, false)]
		[InlineData(new[] {1, 2, 3}, 2, 1, false, true)]
		public void Theory_IsFirstPage_And_IsLastPage_Are_Correct(int[] integers, int pageIndex, int pageSize,
		                                                          bool expectedIsFirstPage, bool expectedIsLastPage)
		{
			//arrange
			var data = integers;

			//act
			var pagedList = data.ToPagedList(pageIndex, pageSize);

			//assert
			Assert.Equal(expectedIsFirstPage, pagedList.IsFirstPage);
			Assert.Equal(expectedIsLastPage, pagedList.IsLastPage);
		}

		[Theory]
		[InlineData(new[] {1, 2, 3}, 1, 3)]
		[InlineData(new[] {1, 2, 3}, 3, 1)]
		[InlineData(new[] {1}, 1, 1)]
		[InlineData(new[] {1, 2, 3}, 2, 2)]
		[InlineData(new[] {1, 2, 3, 4}, 2, 2)]
		[InlineData(new[] {1, 2, 3, 4, 5}, 2, 3)]
		public void Theory_PageCount_Is_Correct(int[] integers, int pageSize, int expectedNumberOfPages)
		{
			//arrange
			var data = integers;

			//act
			var pagedList = data.ToPagedList(0, pageSize);

			//assert
			Assert.Equal(expectedNumberOfPages, pagedList.PageCount);
		}
	}
}
Kick It on DotNetKicks.comShout It on DotNetShoutOuts.com

12 Responses to “Return of the PagedList”

  1. Damien Guard says:

    Skip(0) will also not be a no-op in LINQ to SQL in 4.0 – special casing it caused all sorts of subtle odd behavior not least of all people thinking queries were valid because they worked on the first page only to find they broke when they hit the second.

    [)amien

  2. Troy Goode says:

    Ah, interesting. Thanks for the background info Damien.

  3. [...] Return of the PagedList | SquaredRoot [...]

  4. Return of the PagedList | SquaredRoot…

    Thank you for submitting this cool story – Trackback from DotNetShoutout…

  5. Pure Krome says:

    Will there be a PagedList baked into ASP.NET MVC 2 ? If so, I hope they grab this project….

  6. magz says:

    Hey Troy! this is really brilliant, well done!! I know this is MVC related, but I should still be able to use this in a Winforms app, right? I know its a silly question to ask, but I have a VERy huge result set, that has to be paged.

    Also, is it possibe to implement sorting?

  7. Kim Cu Hoang says:

    Hi,

    How can we implement this with Ajax? Can you show me?

    Thanks.

  8. coolgu97 says:

    Not able To Use this Using Strongly-Type Helpers in MVC 2 RTM

  9. Troy Goode says:

    @Pure Krome: that would’ve been nice, but as we can see – no, it wasn’t added into MVC2 :-(

  10. Troy Goode says:

    @magz: yes, it’ll work just fine in WinForms

  11. Troy Goode says:

    @Kim Cu Hoang: assuming you’re using MVC, you can return the PagedList wrapped by a JsonResult. that should get you down the right path…

  12. Troy Goode says:

    @coolgu97: can you elaborate?

Leave a Reply