Building a Pager control with the Atlas framework
Due to a requirement of a project I'm working on and to some threads in the ASP.NET Forums regarding client-side pagination, I've decided to implement a classic and well-known pagination logic using the Atlas framework. Please check also this post for general Atlas coding guidelines.
How it works
The Pager control exposes three properties:
- pageSize, get/set the number of records per page.
- totalRecords, get/set the number of records in the data source.
- pageIndex, get/set the current page.
and raises a
pageChanged event whenever the selected page index changes. When the event is raised, the event handler receives a PagerEventArgs instance that exposes the following methods:
- get_pageSize()
- get_totalRecords()
- get_pageIndex()
The Pager is implemented as a standalone Atlas control; this means that it doesn't need to be associated with a particular control or data source (try to declare it on a page, set its totalRecords and pageSize properties and it works).
Usage
While the purpose of a Pager control is very clear (yes, display only a number of records at a time) it can be used to navigate through an already populated data source one page at a time, but also to populate the data source on demand, i.e. with the records corresponding to the current page selected by the user. I find the second approach very interesting because it allows to combine the UI benefit brought by the Pager with a "smaller" data structure, and this usually means a gain in peformance.
Actually I wrote the Pager to implement this approach, thus my suggestion is to bind a data control (for example, a Web.UI.ListView) to a "paged" data source, i.e. a Web.Data.DataSource control populated with the records for the current page (the data source could be populated in the pageChanged event handler).
Finally, here's the code for the Pager. Obviously it can (must) be optimized (e.g. look at those string concatenations) and personalized. It's a bit longer than the preceeding examples, but I've inserted comments to make it less annoying to read.
Pager.js
Type.registerNamespace('AtlasNotes');
AtlasNotes.Pager = function(associatedElement) {
AtlasNotes.Pager.initializeBase(this, [associatedElement]);
// Private Members.
var _pageSize;
var _pageIndex;
var _totalRecords;
// Properties.
this.get_pageIndex = function() {
return _pageIndex;
}
this.set_pageIndex = function(value) {
if(value != _pageIndex) {
_pageIndex = (value < 0) ? 0 : value;
this._render();
this.raisePropertyChanged('pageIndex');
}
this._raisePageChanged();
}
this.get_pageSize = function() {
return _pageSize;
}
this.set_pageSize = function(value) {
if(value != _pageSize) {
_pageSize = (value <= 0) ? 10 : value;
this._render();
this.raisePropertyChanged('pageSize');
}
}
this.get_totalRecords = function() {
return _totalRecords;
}
this.set_totalRecords = function(value) {
if(value != _totalRecords) {
_totalRecords = (value < 0) ? 0 : value;
this._render();
this.raisePropertyChanged('totalRecords');
}
}
// Events.
// The Pager exposes the pageChanged event, which is raised
// when the page index to be displayed changes.
this.pageChanged = this.createEvent();
// Initialize / Dispose.
// These two methods are responsible for initializing/disposing
// the control. Notice how both the corresponding base class methods are
// called while performing cleanup.
this.initialize = function() {
AtlasNotes.Pager.callBaseMethod(this, 'initialize');
if(!_pageSize) {
_pageSize = 10;
}
if(!_pageIndex) {
_pageIndex = 0;
}
if(!_totalRecords) {
_totalRecords = 0;
}
}
this.dispose = function() {
AtlasNotes.Pager.callBaseMethod(this, 'dispose');
}
// Type Descriptor.
// The type descriptor allows to specify which properties, methods
// and events are exposed to declarative script usage.
this.getDescriptor = function() {
var td = AtlasNotes.Pager.callBaseMethod(this, 'getDescriptor');
td.addProperty('pageIndex', Number);
td.addProperty('pageSize', Number);
td.addProperty('totalRecords', Number);
td.addEvent('pageChanged', true);
return td;
}
// Methods.
// This method is responsible for the control rendering. It computes the
// Pager's bounds, displays statistics on pages and records and useful
// navigation links (first, last, previous, next, plus a range of page
// numbers.
this._render = function() {
this._clearPager();
var totalPages = this.getTotalPages();
var hasPages = (totalPages > 1);
if(hasPages && (_pageIndex > totalPages - 1)) {
_pageIndex = (totalPages - 1);
}
var hasFirst = (_pageIndex >= 3) && (totalPages > 5);
var hasLast = (_pageIndex + 3 < totalPages) && (totalPages > 5);
var hasPrevious = (_pageIndex > 0)
var hasNext = (_pageIndex < totalPages - 3);
var lowerBound = (totalPages <= 5) || (_pageIndex <= 1) ? 0
: (_pageIndex < (totalPages - 2)) ? (_pageIndex - 2)
: (_pageIndex == (totalPages - 2)) || (_pageIndex == (totalPages - 1)) ? (totalPages - 5)
: 0;
var upperBound = (totalPages < 5) ? totalPages
: (_pageIndex == 0) ? 5
: (_pageIndex == 1) ? (_pageIndex + 4)
: (_pageIndex < (totalPages - 2)) ? (_pageIndex + 3)
: (_pageIndex == (totalPages - 2)) ? (_pageIndex + 2)
: (_pageIndex == (totalPages - 1)) ? (_pageIndex + 1)
: 0;
if(hasPages) {
var span = document.createElement('SPAN');
span.innerHTML = 'Page ' + (_pageIndex + 1) + ' of ' + totalPages + ' ';
this.element.appendChild(span);
if(hasFirst) {
this._addNavigationLink('<<', 0, 'Go to first page');
}
if(hasPrevious) {
this._addNavigationLink('<', _pageIndex - 1, 'Go to previous page');
}
var i = (lowerBound <= 0) ? 0 : lowerBound;
for(i; i < upperBound; i++) {
this._addNavigationLink(i + 1, i, 'Go to page ' + (i + 1));
}
if(hasNext) {
this._addNavigationLink('>', _pageIndex + 1, 'Go to next page');
}
if(hasLast) {
this._addNavigationLink('>>', totalPages - 1, 'Go to last page');
}
}
}
// This method clears the Pager UI.
this._clearPager = function() {
this.element.innerHTML = '';
}
// This method allows to add a navigation link to the Pager. Each link
// can have an associated tooltip.
this._addNavigationLink = function(text, pageIndex, tooltip) {
var a;
if(pageIndex == _pageIndex) {
a = document.createElement('SPAN');
a.innerHTML = '<b>' + text + '</b>';
}
else {
a = document.createElement('A');
a.href = '#';
a.innerHTML = text;
a.onclick = Function.createDelegate(this, this._onPageClick);
a.title = tooltip;
}
a._pageIndex = pageIndex;
a.style.margin = '2px';
this.element.appendChild(a);
}
// This method returns the total pages based on the number of records
// and the page size.
this.getTotalPages = function() {
var tot = 0;
if(_totalRecords) {
if(_totalRecords == 0) {
return 0;
}
tot = Math.floor(_totalRecords / _pageSize);
if((_totalRecords % _pageSize) > 0) {
tot++;
}
}
return tot;
}
// Event Handlers.
this._onPageClick = function(e) {
_pageIndex = (e) ? e.target._pageIndex : window.event.srcElement._pageIndex;
this._render();
this._raisePageChanged();
}
this._raisePageChanged = function() {
this.pageChanged.invoke(this, new AtlasNotes.PagerEventArgs(_pageIndex, _pageSize, _totalRecords));
}
}
// Register our class with the framework.
AtlasNotes.Pager.registerSealedClass('AtlasNotes.Pager', Sys.UI.Control);
// Register our control to be used in declarative script (the Atlas markup).
Sys.TypeDescriptor.addType('script', 'pager', AtlasNotes.Pager);
// PagerEventArgs.
// When the pageChanged event is raised, the Pager passes to the event handler an instance
// of the PagerEventArgs class. This class exposes methods to retrieve the current page index,
// page size and total records.
AtlasNotes.PagerEventArgs = function(pageIndex, pageSize, totalRecords) {
var _pageIndex = pageIndex;
var _pageSize = pageSize;
var _totalRecords = totalRecords;
this.get_pageIndex = function() {
return _pageIndex;
}
this.get_pageSize = function() {
return _pageSize;
}
this.get_totalRecords = function() {
return _totalRecords;
}
// This is the Sys.ITypeDescriptorProvider implementation.
this.getDescriptor = function() {
var td = new Sys.TypeDescriptor();
return td;
}
}
AtlasNotes.PagerEventArgs.registerSealedClass('AtlasNotes.PagerEventArgs', null, Sys.ITypeDescriptorProvider);
Example
Maybe I'll post a full example in another blog entry (because this post is already too long, sorry), but here's an example of a Pager declaration:
<pager id="myPager"
pageSize="10"
totalRecords="357"
pageIndex="5"
pageChanged="onPageChanged"
/>
UPDATES
06/22/2006 - Pager.js, updated the code to the Atlas CTP and fixed some bugs in the pager logic.
03/18/2006 - Pager.js, fixed some issues with the rendering logic.