What you want is quite common: you want to read your input "per page".
In other words: you have a sequence of similar items, and you want to divide this into subgroups of equal size.
If you will be using this regularly, consider to create some classes for it. This way you can use it for several problems where you need to fetch items "per page".
I regularly use it to fetch items from a database per page. Because of the generic classes, I can put an IQueryable / IEnumerable in a PageCollection class and ask for the number of pages, and for Page[3].
If you do it smart, you won't have to fetch items that you don't use, and you don't re-fetch items that you already fetched.
We hide the internals of a page. Therefore we create an interface:
interface IPage<T> : IReadOnlyCollection<T>, IEnumerable<T>
{
int PageNr {get; } // the zero based number of the page
int PageCount {get; } // the total number of available pages
int PageLength {get; } // The number of items on this page
I chose to implement IReadOnlyCollection<T> instead of IReadOnlyList<T>, because the index usually gives the incorrect impression.
For example, if you have a collection of ProductPages, then every ProductPage has zero or more Products. But what do you expect if you are on ProductPage[10], and ask for Product[3]? Some might confuse this with the Product with primary key 3.
Maybe the following methods might also be handy:
IPage<T> PreviousPage {get;}
IPage<T> NextPage {get;}
IPage<T> GetPage(int pageNr);
IReadOnlyList<T> PageCollection {get;}
}
First let's create the PageCollection. The PageCollection will create the pages:
class PageCollection<T> : IReadOnlyList<T>
{
private readonly IDictionary<int, IPage<T>> fetchedPages
= new Dictionary<int, IPage<T>();
private int pageCount = -1; // not known yet
public PageCollection<IEnumerable<T> query, pageSize)
{
// TODO: check input parameters for incorrect values
this.Query = query;
this.PageSize = pageSize;
}
public IEnumerable<T> Query {get;}
// TODO: consider IQueryable if you use databases / entity framework
public int PageSize {get;}
...
}
We need methods to fetch the number of Pages, and to fetch a page by index:
public int Count
{
get
{
if (this.pageCount < 0)
this.pageCount = this.Query.Count();
return this.pageCount;
}
}
public IPage this[int pageIndex] => this.GetPage(pageIndex);
Finally we come to the part where a page is created:
public IPage<T> GetPage(int pageIndex)
{
if (0 < pageIndex || pageIndex >= this.Count)
{
// pageIndex out of range.
// TODO: decide whether to return null or throw exception
}
if (!this.fetchedPages.TryGetValue(pageIndex, out Page<T> fetchedPage)
{
// page never fetched before, fetch it now
fetchedPage = this.FetchPage(pageIndex);
this.FetchedPages.Add(pageIndex, fetchedPage);
}
return fetchedPage;
}
I decided to save fetched pages in a Dictonary, rather than in a List. This way you can ask for Page[5] before you have fetched pages 0 to 4.
private Page<T> FetchPage(int pageIndex)
{
return new Page(this, pageIndex);
}
Well, that didn't do much: apparently it is the page that does all the work.
Time to create the Page.
You'll have to decide for yourself whether you read the complete page immediately, or only when you ask for it
class Page<T> : IPage<T>, IReadOnlyCollection<T>, IEnumerable<T>
{
public Page(PageCollection<T> pageCollection, int pageNr)
{
this.PageCollection = pageCollection;
this.PageNr = pageNr;
// if you want to fetch the data immediately:
this.PageContent = this.Query.ToList();
}
public PageCollection<T> PageCollection {get;}
public int PageNr {get;}
public int PageCount => this.PageCollection.Count;
public IReadOnlyCollection<T> PageContent {get;}
public IEnumerable<T> Query => this.PageCollection.Query
.Skip(this.PageNr * this.PageSize)
.Take(this.PageSize);
}
The implementations of IReadOnlyCollection<T> and IEnumerable<T> are fairly straight forward, the methods all call this.PageContent:
IEnumerator<T> GetEnumerator() {return this.PageContent.GetEnumerator();}
int Count => this.PageContent.Count;
etc.
"Nice to have" procedures like PreviousPage / NextPage / GetPage are one-liners, because they can be handled by asking the PageCollection:
IPage<T> PreviousPage => this.PageCollection.GetPage(this.PageNr-1);
Of course you have to decide what to do if Page gets out of range: exception or return null?
Finally usage:
const int pageSize = 25;
IEnumerable<Product> queryProducts = ...
PageCollection<Product> productPages =
new PageCollection<Product>(queryProducts, pageSize);
Page<Product> productPage = productPages.FirstOrDefault();
// this page can be used as a collection of Products
DisplayProducts(productPage);
// get the next page:
Page<Product> nextPage = productPage.NextPage;