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:
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<T> 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...
/// <summary>
/// Represents a page from a query
/// </summary>
/// <typeparam name="T"></typeparam>
public class PagedItemList<T> : IEnumerable<T>
where T: class, IItem
{
private IItemList<T> _items;
private int _pageNumber;
private int _pageSize;
private int _totalCount;
public PagedItemList(IItemList<T> 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 0");
if (pageSize <= 0) throw new ArgumentOutOfRangeException("pageSize", "pageSize must be greater then 0");
if (totalCount < 0) throw new ArgumentOutOfRangeException("totalCount", "totalCount must be greater then or equal to 0");
_items = items;
_pageNumber = pageNumber;
_pageSize = pageSize;
_totalCount = totalCount;
}
/// <summary>
/// The count of items on this page
/// </summary>
public int PageCount
{
get { return (int)Math.Ceiling(((double)TotalCount) / ((double)PageSize)); }
}
/// <summary>
/// Index of the first item on this page
/// </summary>
public int FirstItemIndex
{
get
{
return PageSize * (PageNumber-1);
}
}
/// <summary>
/// Index of the last item on this page
/// </summary>
public int LastItemIndex
{
get
{
return Math.Min(FirstItemIndex + (PageSize - 1), TotalCount - 1);
}
}
/// <summary>
/// 1-relative page number index
/// </summary>
public int PageNumber
{
get
{
return _pageNumber;
}
}
/// <summary>
/// The size of each page
/// </summary>
public int PageSize
{
get { return _pageSize; }
}
/// <summary>
/// Total number of results returned from the query
/// </summary>
public int TotalCount
{
get { return _totalCount; }
}
/// <summary>
/// Number of items on this page
/// </summary>
public int Count
{
get { return _items.Count; }
}
/// <summary>
/// the underlying list of items
/// </summary>
public IItemList<T> Items
{
get { return _items; }
}
public IEnumerator<T> 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<T>... here's how it looks:
public virtual PagedItemList<T> 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 0");
if (pageNumber <= 0) throw new ArgumentException("pageNumber must be greater then 0");
int count = Convert.ToInt32(_context.ExecuteScalar(query.Compile().SELECT_COUNT()));
query.Path.PageSize = pageSize;
query.Path.PageNumber = pageNumber;
return new PagedItemList<T>(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<T> 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<T> : AbstractPage
where T: class, IItem
{
private PagedItemList<T> _items;
public Base4Page(PagedItemList<T> 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<Car>(_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)...
<table cellpadding="4">
<tr>
<th>Make</th>
<th>Model</th>
<th>Year</th>
</tr>
#foreach($Car in $Cars)
<tr>
<td>$!Car.Make</td>
<td>$!Car.Model</td>
<td>$!Car.Year</td>
</tr>
#end
</table>
<div class="pagination" id="pagination">
<table width="90%" border="0">
<tr>
<td>Showing $Cars.FirstItem - $Cars.LastItem of $Cars.TotalItems</td>
<td align="right">
#if($Cars.HasFirst) $PaginationHelper.CreatePageLink( 1, "first", null, null) ) #end
#if(!$Cars.HasFirst) first #end
#if($Cars.HasPrevious) | $PaginationHelper.CreatePageLink( $Cars.PreviousIndex, "prev", null, null) ) #end
#if(!$Cars.HasPrevious) | prev #end
#if($Cars.HasNext) | $PaginationHelper.CreatePageLink( $Cars.NextIndex, "next",null, null) ) #end
#if(!$Cars.HasNext) | next #end
#if($Cars.HasLast) | $PaginationHelper.CreatePageLink( $Cars.LastIndex, "last",null, null) ) #end
#if(!$Cars.HasLast) | last #end
</td>
</tr>
</table>
</div>
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 up
{
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 /2))
{
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; i++)
{
WriteNumberedLink(builder, page, i, queryStringParams);
}
}
private void WriteLink(StringBuilder builder, int index, string text, bool disabled, IDictionary queryStringParams)
{
if (disabled)
{
builder.AppendFormat("<span class=\"disabled\">{0}</span>", 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("<span class=\"current\">{0}</span>", 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("<a href=\"{0}?{1}\" {2}>{3}</a>", 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;