pagination and base4

Monorail Pagination with Base4.Net

So, part of today was spent building "digg" style pagination for data I'm pulling out of base4... if you don't use digg, then what I mean is stuff like this:




border="0" />



Flickr and plenty of other sites use the same layout... and it does seem quite a convenient way to work your way through large result sets...



Fairly standard stuff, because I'm using monorail my first pit stop was the pagination helper - however there's a bit of an impedance mismatch here, as the pagination helper is geared towards paging through an IList containing all the records, where as generally speaking you're working with IItemList and wanting to make use of the inherent paging support in base4.net... hmmm... at any rate, this isn't exactly "brilliant" code - but it might prove useful if you messing around with Monorail at home - something I think you should do.

Paging and base4.net

So, first off, I built a small class to represent a "page" from a larger set of query results in my application... it looks like this...


///
/// Represents a page from a query
///

///
public class PagedItemList : IEnumerable
where T: class, IItem
{
private IItemList _items;
private int _pageNumber;
private int _pageSize;
private int _totalCount;

public PagedItemList(IItemList items, int pageSize, int pageNumber, int totalCount)
{
if (items == null) throw new ArgumentNullException("items");
if (pageNumber <= 0)="" throw="" new="" argumentoutofrangeexception("pagenumber",="" "pagenumber="" must="" be="" greater="" then="">
if (pageSize <= 0)="" throw="" new="" argumentoutofrangeexception("pagesize",="" "pagesize="" must="" be="" greater="" then="">
if (totalCount < 0)="" throw="" new="" argumentoutofrangeexception("totalcount",="" "totalcount="" must="" be="" greater="" then="" or="" equal="" to="">

_items = items;
_pageNumber = pageNumber;
_pageSize = pageSize;

_totalCount = totalCount;

}

///

/// The count of items on this page
///

public int PageCount
{
get { return (int)Math.Ceiling(((double)TotalCount) / ((double)PageSize)); }
}

///

/// Index of the first item on this page
///

public int FirstItemIndex
{
get
{
return PageSize * (PageNumber-1);
}
}

///

/// Index of the last item on this page
///

public int LastItemIndex
{
get
{
return Math.Min(FirstItemIndex + (PageSize - 1), TotalCount - 1);
}
}

///

/// 1-relative page number index
///

public int PageNumber
{
get
{
return _pageNumber;
}
}

///

/// The size of each page
///

public int PageSize
{
get { return _pageSize; }
}

///

/// Total number of results returned from the query
///

public int TotalCount
{
get { return _totalCount; }
}

///

/// Number of items on this page
///

public int Count
{
get { return _items.Count; }
}

///

/// the underlying list of items
///

public IItemList Items
{
get { return _items; }
}

public IEnumerator GetEnumerator()
{
return _items.GetEnumerator();
}

System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return ((IEnumerable)_items).GetEnumerator();
}
}



Pretty mundane, next we just extend my existing repository with a new method for returning a PagedItemList... here's how it looks:


public virtual PagedItemList Find(ObjectQuery query, int pageSize, int pageNumber)
{
if (query == null) throw new ArgumentNullException("query");
if (pageSize <= 0)="" throw="" new="" argumentexception("pagesize="" must="" be="" greater="" then="">
if (pageNumber <= 0)="" throw="" new="" argumentexception("pagenumber="" must="" be="" greater="" then="">

int count = Convert.ToInt32(_context.ExecuteScalar(query.Compile().SELECT_COUNT()));

query.Path.PageSize = pageSize;
query.Path.PageNumber = pageNumber;

return new PagedItemList(Find(query), pageSize, pageNumber, count);
}



For our query we first estabilsh the total count of results, then we just run the query again and return the results for the selected page.

From Base4.Net to Monorail

Now, our PagedItemList is useful by itself, but there's a more feature-complete interface in monorail for representing a "page" of informaton for display logic, used by the pagination helper, it's called IPaginatedPage.


public interface IPaginatedPage : IEnumerable
{
int CurrentIndex { get; }
int LastIndex { get; }
int NextIndex { get; }
int PreviousIndex { get; }
int FirstIndex { get; }
int FirstItem { get; }
int LastItem { get; }
int TotalItems { get; }
bool HasPrevious { get; }
bool HasNext { get; }
bool HasFirst { get; }
bool HasLast { get; }
}


...so, we just build a little adaptor object, using the abstract base page that implementes IPaginatedPage, called "AbstractPage" funnily enough - here I've called my adaptor Base4Page.


public class Base4Page : AbstractPage
where T: class, IItem
{
private PagedItemList _items;

public Base4Page(PagedItemList items)
{
if (items == null) throw new ArgumentNullException("items");
_items = items;

CalculatePaginationInfo(items.FirstItemIndex, items.LastItemIndex,
items.TotalCount, items.PageSize, items.PageNumber);
}

public override System.Collections.IEnumerator GetEnumerator()
{
return _items.GetEnumerator();
}
}



Soo... last of all, where's the pay off?



Well, first thing's first, now you can do this in your controller:


public void Users(int pageSize, int page)
{
ObjectQuery query = new ObjectQuery(typeof(Car));
// not enough set some further critiera...

PropertyBag.Add("Cars", new Base4Page(_carRepository.Find(query, pageSize, page)));
}



At which point, you could do some simple pagination in your views (and pagination examples for existing Monorail websites can be cut 'n' pasted in to give you a head start)...




#foreach($Car in $Cars)

#end
Make Model Year
$!Car.Make $!Car.Model $!Car.Year



And of course it's not much harder to write the logic for displaying "digg" style pagination.... I decided to write it as a helper in C#, because after messing with it for 15 minutes in brail it pissed me off too much (some things in brail are quite annoying compared to normal boo, for instance I couldn't seem to use the "range(...)" builtin, and if your view doesn't have any content after the last <% ... %> block it throws up a compile time exception... Admittedly I haven't done a get latest from the Castle site in a couple of weeks... so this might not actually be a problem any more, or maybe I just don't have the brail engine configure "just right".


public abstract class AbstractDiggPaginationHelper : AbstractHelper
{
public string CreateDiggPagination(IPaginatedPage page, int adjacents)
{
return CreateDiggPagination(page, adjacents, null);
}

public string CreateDiggPagination(IPaginatedPage page, int adjacents, IDictionary queryStringParams)
{
StringBuilder output = new StringBuilder();
WriteLink(output, page.PreviousIndex, "? prev", !page.HasPrevious, queryStringParams);

if (page.LastIndex < (4="" +="" (adjacents="" *="" 2)))="" not="" enough="" links="" to="" make="" it="" worth="" breaking="">
{
WriteNumberedLinks(output, page, 1, page.LastIndex, queryStringParams);
}
else
{
if ((page.LastIndex - (adjacents * 2) > page.CurrentIndex) && // in the middle

(page.CurrentIndex > (adjacents * 2)))
{

WriteNumberedLinks(output, page,
1, 2, queryStringParams);
WriteElipsis(output);

WriteNumberedLinks(output, page,
page.CurrentIndex - adjacents, page.CurrentIndex + adjacents,
queryStringParams);
WriteElipsis(output);

WriteNumberedLinks(output, page,
page.LastIndex - 1, page.LastIndex, queryStringParams);
}
else if (page.CurrentIndex < (page.lastindex="">
{

WriteNumberedLinks(output, page,
1, 2 + (adjacents * 2), queryStringParams);
WriteElipsis(output);

WriteNumberedLinks(output, page,
page.LastIndex - 1, page.LastIndex, queryStringParams);

}

else // at the end
{

WriteNumberedLinks(output, page,
1, 2, queryStringParams);

WriteElipsis(output);

WriteNumberedLinks(output, page,
page.LastIndex - (2 + (adjacents * 2)), page.LastIndex,
queryStringParams);
}
}

WriteLink(output, page.NextIndex, "next ?", !page.HasNext, queryStringParams);
return output.ToString();
}

private void WriteElipsis(StringBuilder builder)
{
builder.Append("...");
}

private void WriteNumberedLinks(StringBuilder builder, IPaginatedPage page, int startIndex, int endIndex, IDictionary queryStringParams)
{
for (int i=startIndex; i<= endindex;="">
{
WriteNumberedLink(builder, page, i, queryStringParams);
}
}

private void WriteLink(StringBuilder builder, int index, string text, bool disabled, IDictionary queryStringParams)
{
if (disabled)
{
builder.AppendFormat("{0}", text);
}
else
{
WritePageLink(builder, index, text, null, queryStringParams);
}
}

private void WriteNumberedLink(StringBuilder builder, IPaginatedPage page, int index, IDictionary queryStringParams)
{
if (index == page.CurrentIndex)
{
builder.AppendFormat("{0}", index);
}
else
{
WritePageLink(builder, index, index.ToString(), null, queryStringParams);
}
}

protected abstract void WritePageLink(StringBuilder builder, int page, String text, IDictionary htmlAttributes, IDictionary queryStringParams);

}



The guts of the helper is in an abstract class, simply so I could test the numbering logic without having to worry about creating a controller & http context (just implement the WritePageLink method with some simple text output ... the implementation is here:


public class DiggPaginationHelper : AbstractDiggPaginationHelper
{

protected override void WritePageLink(StringBuilder builder, int page, String text, IDictionary htmlAttributes, IDictionary queryStringParams)
{
string filePath = "";

if (CurrentContext != null)
{
filePath = CurrentContext.Request.FilePath;
}

if (queryStringParams == null)
{
queryStringParams = new Hashtable();
}

queryStringParams["page"] = page.ToString();

builder.AppendFormat("{3}", filePath, BuildQueryString(queryStringParams), GetAttributes(htmlAttributes), text);
}
}



At which point you can just register it on the controller, and call it with something like ${DiggPaginationHelper.CreateDiggPagination(tracks, 3)} and get yourself a nice little paging display.  The second parameter is the "adjacent" - which can be used to control how many pages are displayed either side of the selected page when dealing with lots of results.



Oh, and of course you might want some CSS to go with it...


/* pagination */

div.pagination {
padding: 3px;
margin: 3px;
}

div.pagination a {
color: #000099;
text-decoration: none;
padding: 2px 5px 2px 5px;
margin: 2px;
border: 1px solid #AAAFEE;
}

div.pagination a:hover, div.pagination a:active {
color: #000;
border: 1px solid #000099;
}

div.pagination span.current {
font-weight: bold;
background-color: #000099;
color: #FFF;
padding: 2px 5px 2px 5px;
margin: 2px;
border: 1px solid #000099;
}

div.pagination span.disabled {
color: #DDD;
padding: 2px 5px 2px 5px;
margin: 2px;
border: 1px solid #EEE;
}




Written on October 16, 2006