Introduction
Although Cookie Crumb navigation may not sound familiar to you, it is something that you see almost everywhere on the internet. Without it, thousands of users would get very lost in thousands of websites. In fact, Code Project itself uses something similar. Have a look at the top of any article and you will see something like this :
All Topics, C#, .NET >> C# Control >> Beginners
The basic concept is quite simple: As a user navigates through a site, a list of previously visited pages is displayed. This allows users to easily get back to pages they have previously visited within the site.
There are many ways of implementing this idea. It seems the Code Project version uses hardcoded values and assumes that the user followed a specific path to the project. This article uses ASP.NET session data and a Web Custom Control to track the actual path a user took to the page they are viewing, although it is also possible to define a specific track.
Where It All Began
This project initially started out as an afternoon of playing around with creating an ASP.NET-based web custom control following the MSDN article here :
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/vbcon/html/vbwlkwalkthroughcreatingcustomwebcontrols.asp
Over a few afternoons, the project turned into something that would be useable in the real world, so I decided to release it as my first Code Project article.
The Demo Project
If you want to get up and running quickly and come back to the technical nitty gritty later, this is what you need to do :
- Download the demo project.
- Extract the contents of the zip file to your IIS web root.
- In IIS manager, set the SiteCompassTest directory to have an application name.
- Launch your browser to http://yourserver/SiteCompassTest/WebForm1.aspx.
- Click the links to see Site Compass do its thing.
Developing with Site Compass
Adding the control to Visual Studio
Adding the control to a web page is very simple. In VS.NET, you can just use Add/Remove Toolbox Items and select control's DLL file. The Control will appear in the toolbox, and you can add it to a page.
Control Properties
Appearance
Properties that control how the Site Compass is displayed.
CompassStyle
HorizonatalHTML - Generates a traditional horizontal style trail
VerticalHTML - Generates a vertical list trail
DHTMLMenu - Uses a DHTML menu.
If you choose the DHTMLMenu option you will have to manually add a reference to the javascript in your HEAD area, something like this :
<script src="siteCompassDHTML.js" type="text/javascript"></script>
CssClass
When rendered, the Site Compass is contained within a DIV tag. This property indicates the name of the CSS class to use for this DIV. There are 5 sample CSS classes in the project. This does not apply to the DHTML menu compass style.
CssClassLink
This indicates the name of the CSS class to use for the links in the Site Compass. This does not apply to the DHTML menu compass style.
CurrentPageIndicator
This is an indicator character(s) that is displayed against the current page. In the demo project I use *.
ItemSeperator
This is the separator between compass items.
PostFixCode
This is any html code or text you want to add immediately before the closing DIV tag. This is enclosed in anchor tags.
PreFixCode
This is any html code or text you want to add immediately after the opening DIV tag. This is enclosed in anchor tags.
Behaviour
General Behaviour properties
CaseSensitiveUrlComparison
When a Site Compass is rendered, it checks the current page item values against the items already held in the session. If a match is found, then all items after it are removed. If you set the control to compare on case, then page1.htm and Page1.htm will be considered different pages.
DivName
This is the name attribute to assign to the DIV containing the Site Compass. This can be useful if you want to refer to it using Javascript.
LinkLastItem
This indicates if the last item in the compass is a href link or not.
MaxItems
This indicates the maximum number of items to display in the Site Compass. If the total number of items exceeds this value, the first items are hidden. If the user back tracks, the old items will reappear.
Mode
Normal - Renders the normal site compass.
DisplayOnly - Doesn't add the current page to the compass, just displays what is currently in the session
OverrideTarget
This allows you to override the target attribute for all compass items with this one.
SuppressIcons
Allows you to prevent any compass item icons from being displayed.
Compass Item Properties
This is the information that is stored in the session
DisplayTitle
The text you want to show in the Site Compass in reference to this page.
IconAlt
The alternate text attribute of the compass item icon.
IconPath
A root-relative path to the compass item icon image.
LinkTarget
The target of the compass item link.
Miscellaneous Properties
SessionKey
This is a unique key to identify the session data driving the SiteCompass. Using this property, you can have multiple compasses on a single page, all tracking different things--as long as each has a different SessionKey value.
The CSS
Both the source and demo archives come with a CSS file to drive the control. There are two components to this stylesheet:
- The DIV and HREF link styles
- The DHTML Menu Styles
You can play with these styles to your hearts content, but be careful when playing with the DHTML menu style as "interesting" things can and will happen. (Refer to the credit link at the bottom of the article for more information on this menu.) If you choose not to use the DHTML menu, then this portion of the style sheet can be removed.
One other thing to consider when generating styles is that the control contains a series of links that have been visited by the user. So this portion is next to useless:
A.YourLinkStyleName:link
{
TEXT-DECORATION : none;
COLOR : aqua;
}
Just in case you need it, you can add stylesheet to your page by adding the following code to the head of your ASPX page
<link title="errata" media="screen" href="StyleSheet1.css" type="text/css" rel="stylesheet">
Public Functions
public void GenerateCompass_HTML(HtmlTextWriter output, bool designer)
public void GenerateCompass_DHTMLMenu(HtmlTextWriter output, bool designer)
The only reason these two functions are public is so that there is design time support for the control. Basically these are the two renderer functions that generate the HTML output for both design and run time.
public void ResetCompass()
This function will reset the compass and remove all items currently in the session.
public void ResetCompass(ArrayList compassData)
This function will reset the compass and add the given ArrayList of compass items as the data driving the compass.
public void ResetCompass(CompassItem item)
Resets the compass to only contain the given compass item.
public CompassItem GetCompassItemAt(int index)
Retrieves the compass item at the given index in the ArrayList.
A few things to note
When the control is rendered, the entire HTML code for it is placed within <DIV> tags. As you can see above, there are various properties that allow you to control this.
When a page is added to the control session data, it uses Page.Request.RawUrl
to get the current page path, relative to the root of the server. This will also retrieve any querystring assigned to the URL. So page1.htm and page1.htm?id=2 are different pages as far as the control is concerned.
How It Works or The Nitty Gritty
As a user views a page with a Site Compass control on it, certain information is harvested and stored in the user's session. This data is sourced from two locations:
- The
Page
parameter.
- The properties you assign to the control.
This data is placed into an object of a class (
SiteCompassItem
) and then placed into an ArrayList so that the order in which it was received can be maintained. When the ASP.NET engine requests that the control generates its HTML, it checks the current page against the pages already held in the session. If the page exists, then all items after it are removed. If it doesn't exist, then it is added to the end of the current items. This list is then used to generate HTML code which renders the compass in the browser.
The Code
I am not introducing any amazing new ideas or concepts here, so the code is relatively simple to follow and I have endeavoured to ensure that it is fully commented at every stage. That being said, there are a few points of interest that are worthy of a mention here.
Comparing Objects
The heart of the control is its ability to check if a page already exists. The first incarnation of the control simply used a few concatenated strings to store its data, but this seemed a little stone age to me, so I looked for a better solution. In the end, I created my own custom class to hold compass item data, but this resulted in an interesting problem.
As an example, look at this code:
private bool TestMe()
{
testClass tc1 = new testClass("abc", "def", "ghi");
testClass tc2 = new testClass("123", "456", "789");
testClass tc3 = new testClass("xxx", "yyy", "zzz");
testClass tc4 = new testClass("abc", "def", "ghi");
ArrayList array1 = new ArrayList();
array1.Add(tc1);
array1.Add(tc2);
array1.Add(tc3);
return array1.Contains(tc4);
}
Which uses this class
class testClass
{
public string abc;
public string def;
public string ghi;
public testClass(string abc, string def, string ghi)
{
this.abc = abc;
this.def = def;
this.ghi = ghi;
}
}
The TestMe()
function will return false. To get around this problem you need to override the classes Equals(object obj)
function as ArrayList.Contains(..)
uses this to compare objects.
In the case of this control, I use the following overrides to allow me to perform object comparisons:
public static bool _caseSensitiveUrlComparison = false;
...
...
...
public override bool Equals(object obj)
{
CompassItem compareObject = obj as CompassItem;
if (compareObject == null)
{
return false;
}
if (_caseSensitiveUrlComparison)
{
return this.ToString() == compareObject.ToString();
}
else
{
return this.ToString().ToLower() == compareObject.ToString().ToLower();
}
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public override string ToString()
{
return this.DisplayTitle + this.RawUrl + this.IconPath + this.IconAlt + this.Target;
}
The ToString()
function simply takes all the class variables strings and concatenates them into one long string. When .Equals(..)
is called by ArrayList.Contains(..)
, it uses this string to perform comparisons.
The static variable _caseSensitiveUrlComparison
allows me to set the behaviour of the ArrayList.Equals(..)
function before I call it.
For example, this portion of code handles stripping out compass items that are not required.
CompassItem.CaseSensitiveUrlComparison = this.CaseSensitiveUrlComparison;
if (compassData.Contains(newItem))
{
int index = compassData.IndexOf(newItem) + 1;
int count = compassData.Count - index;
compassData.RemoveRange(index, count);
}
Overriding Default Properties
When you create a Custom Control using the methods describe in the MSDN article linked above, you will have various default properties. In the case of this project, there were various properties that I didn't want to display as they were not relevant, used, or important to the control
Hiding these properties is simply a question of setting the Browsable(false)
attribute.
For example:
false)>
public override FontInfo Font
{
get { return base.Font; }
}
For more information on this see:
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfSystemComponentModelBrowsableAttributeClassTopic.asp
If you look at the code, the properties I have hidden all relate to styles. The reason they are hidden is that I was entirely happy (HTML validation) with the quality of HTML code generated, when used in conjunction with HtmlTextWriter
, so I decided to make the control entirely driven by style sheets.
Visual Studio Designer Support
There is nothing more annoying than developing with a control that has little or no designer support. This control has full designer support (except when you choose DHTMLMenu style) as it generates dummy entries to show the control as you would when you view it in a page.
How this is done is described in the MSDN article at the top of this page, but I thought I should cover it here as I use the run time control renderers to produce the design time control
I have created the following class that resides in the same namespace as the control class:
public class SiteCompassDesigner : System.Web.UI.Design.ControlDesigner
{
public override string GetDesignTimeHtml()
{
StringWriter sw = new StringWriter();
HtmlTextWriter tw = new HtmlTextWriter(sw);
SiteCompass scc = (SiteCompass) Component;
switch (scc.CompassStyle)
{
case SiteCompass.CompassStyles.HorizonatalHTML:
case SiteCompass.CompassStyles.VerticalHTML:
scc.GenerateCompass_HTML(tw, true);
break;
case SiteCompass.CompassStyles.DHTMLMenu:
scc.GenerateCompass_DHTMLMenu(tw, true);
break;
}
return sw.ToString();
}
}
The overridden GetDesignTimeHtml()
function is called by Visual Studio when you open an ASPX page in design view. Whatever string you return is placed where you place your control on the page. You could even do something really simple like:
public class SiteCompassDesigner : System.Web.UI.Design.ControlDesigner
{
public override string GetDesignTimeHtml()
{
return "<b>My Cool Control</b>"
}
}
But that's not that cool is it!
In the case of the MSDN article, it generates a label and returns the HTML for it all within the GetDesignTimeHtml()
function. I didn't want to have to code two variations of the compass renderers for runtime and design time support
Depending on the style of Site Compass you choose 1 of 2 renderers will be used:
public void GenerateCompass_HTML(HtmlTextWriter output, bool designer)
public void GenerateCompass_DHTMLMenu(HtmlTextWriter output, bool designer)
As you can see each takes the same parameters. When running in runtime mode the ASP.NET engine calls:
protected override void Render(HtmlTextWriter output)
The output
parameter is the current pages HtmlTextWriter
. The control then adds its own html into the writer and passes it back to the engine for further code additions and processing. My Render()
function calls the renderers described above passing designer
as false
. This means that data is retrieved from the session and used to generate the compass. In design time there is no session so when designer
is true
the code produces a dummy list of items and produces HTML code for that which is passed back to Visual Studio via the GetDesignTimeHtml()
function mentioned earlier
Possible Known Issues
- Querystrings will always be used to compare, a future version may have the ability to process querystrings
Possible Enhancements
- The ability set property driven list tags for the vertical list, at the moment the control simple adds a <BR> after each item.
- Override and expose more of the
ArrayList
functions to enhance developer control.
- The ability to track an item but not display it
- Enhance comparison options for the override
.Equals()
- Improve the readability of the output code using
.Indent
and .WriteLine()
on the HtmlTextWriter
DHTML Menu Credit
As a flight of fancy, I built in the ability to render the control as a DHTML menu. While this seemed like a good idea, my Kung Fu is weak when it comes to the dark arts of DHTML so I must give credit to the source of the tutorial that allowed me to produce the code for the Compass Menu style
http://www.brainjar.com/
Allow me to bow before your DHTML Kung Fu
History
v1.0.0.0 - This article this code
Thats all folks, thanks for tuning in.
One small thing: I unfortunately do not have anywhere to host a working sample of this project. If anybody out there can spare a little bandwidth and server space, please contact me.