Abstractions...

Abstractions...  mmmm... so sweet


...a post from Ayende on abstractions, and specifically a mention of the logging "abstraction" in Castle... it's an interesting thing, for the Seismic product I worked logging (via the same abstraction) into a product a year or more ago...  in the end we needed the ability to log contextual information (basically exposing features already existing in log4net at the time, but not available via the Castle ILogger interface) and so I ended up creating a new interface that was a superset of the existing abstraction, IExtendedLogger...

here's the devil... looks simple...

public interface IExtendedLogger : ILogger { void AddProperty(string key, object value); void RemoveProperty(string key); }

However... because we're being a good little abstraction "whore" we end up with a few more classes to support this new interface, and make the user experience more pleasurable:

Though I was reasonably pleased with the end result a year ago, that was probably a couple of hours work + some more time tweaking (once you include the time to code up that test fixtures) that would've been better spent building additional functionality into the product.

I have to admit the lure of a needless abstraction is ever present to me... I enjoy halving a class into an abstract and concrete implementation, and then extracting an interface is like the candy coating... prefixing the concrete implementation with "Default" makes it even seem like you've all but got people lining up to create their own versions!  Wow, isn't it powerful, flexible.. and all sorts of other words ending in "ul" or "ile".

....But in the end, needless is needless - and I haven't needed the flexibility gained from this abstraction so far... after a year... a whole year, that's pretty much like never... sure I had grand plans, but they never did come to fruition... and grand plans don't keep me fed - YAGNI strikes again.

About the only thing I find the needless abstractions do is in clarifying my thinking on what I do and don't need, or more importantly what I do and don't want people to do with my code.... it let's me define just what I want my "pit of success" behavior to be, albeit not the implementation... Perhaps some of this stuff is better reserved for throw away prototypes then production code.

I think the best book I've found for discouraging this "80's guitar solo" of abstraction..um...ism is the Framework Design Guidelines... Though these abstractions do make it easy for me to maintain and grow my code (and the orthogonality of the design is generally good) it comes at the price of other people having difficulty learning my API through experimentation, and I fail to create a progressive framework... which after reading the aforementioned book, is something we all wan't to do.

I guess the final question is, do you reverse an abstraction that isn't required after such a long time...?  Or do you just avoid making the same mistake twice and live with the abstraction, assuming it's not hurting too many people - Is it worth lumping it with the rest of the "broken windows" in a project, or is that a little too brash?

Read More

A quick observation...

Startable facility and hosting base4

Ivan's pretty keen :) last week he demonstrated hosting the base4 service in ASP.Net ... cool stuff.



Now here he has placed the implementation in the HttpApplication class, and as a quick observation this would be a good candidate for moving into it's own component... and as luck would have it, Castle has just the thing, via the Startable facility.

Startable... (and the implied, stoppable ;o)

Now the startable facility is a very simple beast, basically... you create a class, that implements this interface:


///
/// Interface for components that wish to be started by the container
///

public interface IStartable
{
void Start();
void Stop();
}


Which lives in the Castle.Core assembly... though the facility itself lives in the Castle.MicroKernel assembly... you should already have references to both, otherwise I doubt your container is working at all :P



And you then register the startable facility in the container:



type="Castle.Facilities.Startable.StartableFacility, Castle.MicroKernel" />


And thats it... now whenever you register a component in the container, the startable facility will check it, and if it's found to implement IStartable it will add it to a list of components "awaiting startup"... once any of these awaiting components has had all it's dependencies satisfied it will be started (so it may be started as soon as the component is registered, if it doesn't have any dependencies - this will be the case for our component)...



Disposing of the container will cause the components to be stopped in turn... perfect for hosting our base4 server...

Reworking the hosting code to be a startable component...

...so taking what Ivan's done, we could rework it a little... and be able to host base4 in web apps, as well as say your business logic's test harness... here's my quick 'n dirty reworked implementation as a component that I did this afternoon... my final version probably won't have any dependencies on the HttpContext as I'll just rely on explicit declaration of all the settings as parameters in the container configuration... I tend to like keeping all the config in the container, one place to look 'n all that.


public class Base4Host : IStartable
{
private const string _machineName = "localhost";
private const string _provider = "SQL2005";
private string _appName;
private int _port;
private string _root;
private ServerConfiguration _configuration;
private IServerProxy _proxy;

public ServerConfiguration Configuration
{
get { return _configuration; }
}

public IServerProxy ServerProxy
{
get { return _proxy; }
}

public Base4Host(string appName, int port)
{
if (string.IsNullOrEmpty(appName)) throw new ArgumentNullException("appName");
if (port <= 1024)="" throw="" new="" argumentoutofrangeexception("port",="" "port="" should="" be="" greater="" then="">
_appName = appName;
_port = port;
}

public Base4Host(string appName, int port, string root)
: this(appName, port)
{
if (string.IsNullOrEmpty(root)) throw new ArgumentNullException("root");
_root = root;
}

#region IStartable Members

public void Start()
{
_configuration = CreateConfiguration();
_proxy = ServerFactory.StartServer(_configuration, false);
string base4Context = string.Format("tcp://Server:@localhost:{0}/{1}", _port, _appName);
StorageContext.SetDefault(base4Context);
}

public void Stop()
{
_proxy.Stop();
_proxy = null;
_configuration = null;
}

#endregion

#region Support methods

private ServerConfiguration CreateConfiguration()
{
ServerConfiguration configuration = new ServerConfiguration();

DiscoverApplicationName();
InsertApplicationRoot(configuration);
InsertConnectionStrings(configuration);
InsertConnectivityInformation(configuration);

if (!Directory.Exists(configuration.Store.Root))
{
Directory.CreateDirectory(configuration.Store.Root);
}

return configuration;
}

private void InsertConnectivityInformation(ServerConfiguration configuration)
{
configuration.Store.Name = _appName;
configuration.Store.Provider = _provider;
configuration.Store.Port = _port;
configuration.Store.MachineName = _machineName;
}

private void DiscoverApplicationName()
{
if (string.IsNullOrEmpty(_appName))
{
if (HttpContext.Current.Application["AppName"] == null)
{
Assembly assembly = Assembly.GetCallingAssembly();
HttpContext.Current.Application["AppName"] = (AssemblyTitleAttribute.GetCustomAttribute(assembly, typeof(AssemblyTitleAttribute)) as AssemblyTitleAttribute).Title;
}

_appName = HttpContext.Current.Application["AppName"].ToString();
}
}

private void InsertApplicationRoot(ServerConfiguration configuration)
{
string root = string.IsNullOrEmpty(_root) ? HttpContext.Current.Server.MapPath("~/") : _root;
configuration.Store.Root = root.EndsWith("") ? root + "App_DataBase4" : root + "App_DataBase4";
}

private void InsertConnectionStrings(ServerConfiguration configuration)
{
string connectionStringName = ConfigurationManager.AppSettings["DefaultConnection"];

if (string.IsNullOrEmpty(connectionStringName) && ConfigurationManager.ConnectionStrings != null
&& ConfigurationManager.ConnectionStrings.Count > 0)
{
connectionStringName = ConfigurationManager.ConnectionStrings[0].Name;
}

configuration.Store.ConnectionString = string.IsNullOrEmpty(connectionStringName) ? ConfigurationManager.AppSettings["Store.ConnectionString"] :
ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString;

SqlConnectionStringBuilder connStrBuilder = new SqlConnectionStringBuilder(configuration.Store.ConnectionString);

connStrBuilder.InitialCatalog = "master";
configuration.MasterConnectionString = connStrBuilder.ToString();
}

#endregion
}



Notice we don't expose a default constructor... how can the container create it?



Well, it can't unless we supply the parameters it requires... which is just what we'll do:


<>
id="base4host.default"
type="MyProject.Base4Host, MyProject"> GoatsAndBoats 11888




Simple... a couple of useful extensions to our existing base4 facility may well be to:
  • Ensure the startable facility is installed... so we can fail-early and with a meaningful error during application
    startup.
  • Add some additional "optional" parameters for configuring the facility, so that it can take care of registering the Base4Host for us.

For the optional parameters, I'm thinking of doing something like this:


type="SomeProject.Core.Base4StorageFacility, SomeProject.Core"
host="true"
appName="GoatsAndBoats"
port="11888"
connectionString="local"
/>


At which point it's trivial to shift between accessing base4.net as a remote service vs. hosting it in your own app... unlike the remote setup you can skip providing the base4 url (context string) as it'll be implied from the app name and port.
Read More

Looking into Trac

Trac

I know it's not .Net, but that's not always a bad thing...

Well I tend to keep my eye out for things which take my fancy, or more importantly, free things that take my fancy...



Trac is one of those things, a project management and bug database web app written in Python, written by edgewall software... it's actually pretty cool... for RoR enthusiasts they're probably aware of it's existence already (considering RoR use this for manging their bugs and patches)



First off, you can run it on windows...  though a *nix is generally better, and it's not too hard to get going - took me about half an hour... second thing, it's a lot better once you install a few hacks especially:
Now, at this point what makes it good?
  • SVN Integration
  • Wiki Integration
  • Extensibility and community support

Basically I can check in code with comments containing wiki markup and references to existing "tickets" logged in the system... The wiki integration incidentally is my primary motivation for looking so closely at using this for some of the projects I've been working on... I'm a liability to Seismic Technologies... documenting build processes etc. in a wiki are one way we can minimize that... We already have a customer-focused wiki, but it doesn't encourage maintenance on a day-to-day basis like this product does.



secondary to that is the very good subversion integration, a must considering that's the only source control system I use.

Creating tickets via XML-RPC in C#

Now the last hack I suggested exposes plenty of functionality in the system via XML RPC... at which point we can start logging bugs remotely, from C#... to do this we'll need a copy of the xmlrpc.net library



I'm not conducting a tutorial here ;o) but if Trac takes your fancy you might find this code handy... first off this is our interface for logging tickets in Trac - incidentally this is only a small subset of the funcionality exposed.


[XmlRpcUrl("http://localhost/trac/login/xmlrpc")]
public interface ITicket : IXmlRpcProxy
{
[XmlRpcMethod("ticket.query")]
int[] Query(string qstr);

[XmlRpcMethod("ticket.getRecentChanges")]
int[] GetRecentChanges(DateTime since);

[XmlRpcMethod("ticket.getAvailableActions")]
string[] GetAvailableActions(int id);

[XmlRpcMethod("ticket.getTicketFields")]
TicketField[] GetTicketFields();

[XmlRpcMethod("ticket.create")]
int Create(string summary, string description, XmlRpcStruct attributes);

[XmlRpcMethod("ticket.get")]
object[] GetTicket(int id);

[XmlRpcMethod("ticket.delete")]
void Delete(int id);

[XmlRpcMethod("ticket.update")]
object[] Update(int id, string comment, XmlRpcStruct attributes);

[XmlRpcMethod("ticket.type.getAll")]
string[] GetAllTypes();

[XmlRpcMethod("ticket.resolution.getAll")]
string[] GetAllResolutions();

[XmlRpcMethod("ticket.priority.getAll")]
string[] GetAllPriorities();

[XmlRpcMethod("ticket.component.getAll")]
string[] GetAllComponents();

[XmlRpcMethod("ticket.version.getAll")]
string[] GetAllVersions();

[XmlRpcMethod("ticket.severity.getAll")]
string[] GetAllSeverities();

[XmlRpcMethod("ticket.milestone.getAll")]
string[] GetAllMilestones();
}



Now, tickets have a bunch of attributes associated with them...


public static class TicketAttributes
{
public const string Cc = "cc";
public const string Keywords = "keywords";
public const string Status = "status";
public const string Type = "type";
public const string Owner = "owner";
public const string Version = "version";
public const string Resolution = "resolution";
public const string Reporter = "reporter";
public const string Milestone = "milestone";
public const string Component = "component";
public const string Summary = "summary";
public const string Description = "description";
public const string Priority = "priority";
}


And we can make ticket information easier to get a hold of with a simple class:


///
/// represents the information for a ticket
///

public class TicketInfo
{
private int _ticketId;
private DateTime _created;
private DateTime _lastModified;
private XmlRpcStruct _attributes;

public TicketInfo()
{
}

public TicketInfo(object[] values)
{
Update(values);
}

internal void Update(object[] values)
{
if (values == null) throw new ArgumentNullException("values");
if (values.Length != 4) throw new ArgumentException("values should have 4 elements");

_ticketId = (int)values[0];
_created = DateHelper.ParseUnixTimestamp((int)values[1]);
_lastModified = DateHelper.ParseUnixTimestamp((int)values[2]);
_attributes = (XmlRpcStruct)values[3];
}

///

/// The identifier for this ticket
///

public int TicketId
{
get { return _ticketId; }
set { _ticketId = value; }
}

///

/// date and time the ticket was created
///

public DateTime Created
{
get { return _created; }
set { _created = value; }
}

///

/// date and time the ticket was last modified
///

public DateTime LastModified
{
get { return _lastModified; }
set { _lastModified = value; }
}

///

/// The attributes for this ticket, this will include any additional fields
/// that aren't defined explicitly as members of this class.
///

public XmlRpcStruct Attributes
{
get
{
if (_attributes == null) _attributes = new XmlRpcStruct();
return _attributes;
}
set { _attributes = value; }
}

public string Cc
{
get { return GetAttribute(TicketAttributes.Cc); }
set { SetAttribute(TicketAttributes.Cc, value); }
}

public string Keywords
{
get { return GetAttribute(TicketAttributes.Keywords); }
set { SetAttribute(TicketAttributes.Keywords, value); }
}

public string Status
{
get { return GetAttribute(TicketAttributes.Status); }
set { SetAttribute(TicketAttributes.Status, value); }
}

public string Type
{
get { return GetAttribute(TicketAttributes.Type); }
set { SetAttribute(TicketAttributes.Type, value); }
}

public string Owner
{
get { return GetAttribute(TicketAttributes.Owner); }
set { SetAttribute(TicketAttributes.Owner, value); }
}

public string Version
{
get { return GetAttribute(TicketAttributes.Version); }
set { SetAttribute(TicketAttributes.Version, value); }
}

public string Resolution
{
get { return GetAttribute(TicketAttributes.Resolution); }
set { SetAttribute(TicketAttributes.Resolution, value); }
}

public string Reporter
{
get { return GetAttribute(TicketAttributes.Reporter); }
set { SetAttribute(TicketAttributes.Reporter, value); }
}

public string Milestone
{
get { return GetAttribute(TicketAttributes.Milestone); }
set { SetAttribute(TicketAttributes.Milestone, value); }
}

public string Component
{
get { return GetAttribute(TicketAttributes.Component); }
set { SetAttribute(TicketAttributes.Component, value); }
}

public string Summary
{
get { return GetAttribute(TicketAttributes.Summary); }
set { SetAttribute(TicketAttributes.Summary, value); }
}

public string Description
{
get { return GetAttribute(TicketAttributes.Description); }
set { SetAttribute(TicketAttributes.Description, value); }
}

public string Priority
{
get { return GetAttribute(TicketAttributes.Priority); }
set { SetAttribute(TicketAttributes.Priority, value); }
}

#region Support methods

private string GetAttribute(string name)
{
if (Attributes.Contains(name))
{
return Convert.ToString(Attributes[name]);
}
return null;
}

private void SetAttribute(string name, string value)
{
if (Attributes.Contains(name))
{
Attributes[name] = value;
}
else
{
Attributes.Add(name, value);
}
}

#endregion
}



And last of all we have a class for managing tickets.. by the way this isn't complete, I haven't finished writing it because this is just one of my back burner projects...


public class TicketManager
{
private ITicket _ticket;

public void Connect(string url, string userName, string password)
{
_ticket = XmlRpcProxyGen.Create();
_ticket.Url = url;
_ticket.PreAuthenticate = true;
_ticket.Credentials = new NetworkCredential(userName, password);
}

public string[] GetAvailableActions(int id)
{
return _ticket.GetAvailableActions(id);
}

public string[] GetAvailableActions(TicketInfo ticket)
{
ValidateTicket(ticket);
return _ticket.GetAvailableActions(ticket.TicketId);
}

public int[] GetRecentChanges(DateTime since)
{
return _ticket.GetRecentChanges(since);
}

public void DeleteTicket(int ticketId)
{
_ticket.Delete(ticketId);
}

public void DeleteTicket(TicketInfo ticket)
{
ValidateTicket(ticket);
DeleteTicket(ticket.TicketId);
}

public void UpdateTicket(TicketInfo ticket, string comment)
{
ValidateTicket(ticket);
object[] values = _ticket.Update(ticket.TicketId, comment, ticket.Attributes);
ticket.Update(values);
}

public void CreateTicket(TicketInfo ticket)
{
if (string.IsNullOrEmpty(ticket.Summary)) throw new ArgumentNullException("ticket.Summary");
if (string.IsNullOrEmpty(ticket.Description)) throw new ArgumentNullException("ticket.Description");
if (string.IsNullOrEmpty(ticket.Type)) throw new ArgumentNullException("ticket.Type");
if (string.IsNullOrEmpty(ticket.Priority)) throw new ArgumentNullException("ticket.Priority");
if (string.IsNullOrEmpty(ticket.Component)) throw new ArgumentNullException("ticket.Component");

XmlRpcStruct tempAttributes = new XmlRpcStruct();
foreach (object key in ticket.Attributes.Keys)
{
if ((((string)key) != TicketAttributes.Description) && (((string)key) != TicketAttributes.Summary))
{
tempAttributes.Add(key, ticket.Attributes[key]);
}
}

int id = _ticket.Create(ticket.Summary, ticket.Description, ticket.Attributes);
ticket.TicketId = id;
}

private void ValidateTicket(TicketInfo ticket)
{
if (ticket == null) throw new ArgumentNullException("ticket");
if (ticket.TicketId <= 0)="" throw="" new="" argumentexception("ticketid="" must="" be="" greater="" then="">
}
}



About the only trick here is that some of the date values get returned as unix time stamps so you need to convert them...


public static DateTime ParseUnixTimestamp(double timestamp)
{
return new DateTime(1970, 1, 1, 0, 0, 0, 0).AddSeconds(timestamp);
}


Because the Syzmk Rich Media Processor product I work on is a clearing house for simple messages among other things... It becomes a small jump from here (about 30 lines of C# code, 5 for a quick and dirty "IronPython" script) to start submitting tasks and bugs via email or mobile... who knows, it could be handy if you get a call while your out of the office and need to log a bug or task.
Read More

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




Read More