I have been working on a customer's web site with a designer and we have a pretty good thing going. What with me cranking out code to make the site do nifty things and the designer making sure this doesn't look like something your kid brother threw together in 10 minutes, the site is coming along pretty well.
The site happens to be an ASP.NET 2.0 site and as such we are taking advantage of some of the ASP.NET 2.0 goodness (my general feeling on frameworks) such as master pages to aid in creating a consistent look and feel for the site. My designer had mentioned wanting to have different sub menus for each of the main menu picks across the top of the page. On the master page for each sub menu I created an ASP.NET Panel, put the sub menu content for each page in the appropriate panel, created a SiteMap and gave each page in the map an attribute called subPanel that contains the name of the relevant Panel to display. There is code in the master page's .cs file something like the following to make the correct sub menu appear:
private void ProcessSubMenu()
{
SiteMapNode sn = SiteMap.CurrentNode;
if (sn != null)
{
string panelName = sn["subPanel"];
if (!string.IsNullOrEmpty(panelName))
{
try
{
Panel currPanel = (Panel)this.FindControl(panelName);
if (currPanel != null)
{
currPanel.Visible = true;
}
}
catch { }
}
}
}
In this way the designer would be able to create as many menu items and sub menus without any more direct coding from me. Set up the panel, put in the content, enter the page name with the proper attributes in the SiteMap and everything is good to go.
After doing some more tinkering and about getting ready to wrap things up, the designer decided that it would be nice if we were to highlight the current pick in the menu. Having done something that was easy to update with the sub menus, she was wondering if we could do something similar with the menu highlighting. She decided we could use attributes in the SiteMap, create a style to assign for the selected item and could we maybe do something similar with processing in the Master Page? I said sure and came up with something like:
private void ProcessHighlighting()
{
SiteMapNode sn = SiteMap.CurrentNode;
if (sn != null)
{
string panelName = sn["subNav"];
if (!string.IsNullOrEmpty(panelName))
{
try
{
HyperLink currLink = (HyperLink)this.FindControl(panelName);
if (currLink != null)
{
currLink.CssClass = "selected";
}
}
catch { }
}
}
}
This code should look remarkably familiar. In fact, it looked so remarkably familiar I didn't like it so much. Having inherited and maintained and cursed out code like this in my past, I figured there had to be a better way do this. As for those of you that are thinking "Ctrl-C, Ctrl-V, change a few lines of code, DONE. What's the big deal?", just trust me. This is evil. For those of you that don't trust me, here is a short list of some of the traits that make this evil.
This sort of problem is easily solved in languages that treat functions as first class objects. Functional programmers work with stuff like this all the time. If you took a decent C class you should have covered, at least briefly, function pointers. The classic example, and one that shows up in many places to this day, is the passing in of a sort routine or an ordering function into a sort routine. One of the better ways I've heard of thinking about this particular code idiom refers to it as the Hole in the middle pattern. Whatever you call it, you have this mass of code that is the same and there is one little piece somewhere that needs to be different. Dynamic languages make handling these cases pretty simple since you can just pass a function around like any other variable. .NET delegates give you a pretty good way to handle these cases so that is what I used in this case to clean up this code.
Taking care of this problem doesn't require too much effort and the results are, to me, well worth the little bit of effort. The big win is that updates are much easier. Here is how I went about solving the problem.
First you need to figure out the commonalities in the two or more functions and abstract them away as much as is reasonably possible. Looking at the above code you can see both methods get the SiteMap's current node and, if it is valid, gets the value of the appropriate attribute. This value is the name of some control which, if valid, has some action performed on it. The only real differences are the name of the node's attribute and the code inside the try block. That can be expressed more generically like this:
private void ProcessMap(string attributeName, ProcessMapItem fcn)
{
SiteMapNode sn = SiteMap.CurrentNode;
if (sn != null)
{
string panelName = sn[attributeName];
if (!string.IsNullOrEmpty(panelName))
{
try
{
fcn(panelName);
}
catch { }
}
}
}
ProcessMapItem ends up being a delegate with the following definition:
delegate void ProcessMapItem(string itemName);
So at this point all you need to do is define the actions you want taken on the nodes. The functions need to have the same signature as the delegate. Here are the two that I wrote for this instance:
private void SetSubNav(string panelName)
{
Panel currPanel = (Panel)this.FindControl(panelName);
if (currPanel != null)
{
currPanel.Visible = true;
}
}
private void SetNavHighlighting(string linkName)
{
HyperLink currLink = (HyperLink)this.FindControl(linkName);
if (currLink != null)
{
currLink.CssClass = "selected";
}
}
At this point the code in the PageLoad function for the master page will contain calls to the function that processes the SiteMap like this:
ProcessMap("subNav", SetNavHighlighting);
ProcessMap("subPanel", SetSubNav);
In addition to making it easy for a designer to make site updates without having to worry about the code behind, when a designer requests updates that need to be processed like this all you need to do is decide on the name of the attribute, figure out the processing needed and code up the small function that actually does the work then call the ProcessMap function. This example only has two functions and only saves you eight lines (40 lines if you copy, paste and modify vs. 32 with passing the delegate), but that is a 20% decrease. And each update only adds another 8 or so lines (assuming the type of processing is similar) vs. 20 lines every time you copy, paste and update. If you need to do 5 different things that follow this model, you are looking at 100 lines of code vs. 56. At this point you can start to see a real difference.