Return of the PagedList

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);
        }
    }
}