A few weeks ago one of my friends asked me to make a breadcrumb button
control. The requirements were that he needed to be able to store text and an
associated command text. The control would appear as typical breadcrumbs with a
seperator in between the words. As a user moved through the application or
wizard the developer could place the text of the step the user is on with the
cooresponding command text. If the user wanted to go back to a previous step,
they could click on the control and the button would raise an event with the
command text in the EventArgs.
Simple enough. I start by creating a Web Control Library in Visual
Studio .NET 2003. I then add a Web Custom Control named
BreadcrumbButton.cs. This file will contain our breadcrumb button control
class. VS.NET will place some typical code in the class but I will delete
the text property and render method because I will write my own later.
using System;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.ComponentModel;
using System.Web.UI.Design;
using System.ComponentModel.Design;
namespace AdvancedControls
{
[
ToolboxData("<{0}:BreadcrumbButton runat=server>{0}:BreadcrumbButton>")
]
public class BreadcrumbButton : System.Web.UI.WebControls.WebControl
{
}
}
I need two collections to hold the data. One
to hold the text to display and one to hold the associated command text
that the developer can use to track which breadcrumb was clicked on. My
choices were string arrays, collections, or an arraylist to name a few. I
chose the StringCollection because it implements an arraylist under the covers
which makes it dynamically resizeable. This is better than a string
array. I also chose the StringCollection because it is strongly typed to
the string object. I thought about a hashtable because I am storing pairs
of objects but decided against it because it can't hold duplicate keys. A
developer may want to store the same text or command in the Breadcrumb Button at
some time.
To use the string collection I needed to add a using statement at the top of
the file to the namespace that contains the StringCollection. Then I added
the declarations and initialization code for these
collections.
using System.Collections.Specialized;
//...inside class
StringCollection text, command;
public BreadcrumbButton()
{
text = new StringCollection();
command = new StringCollection();
}
Now that I have two collections I can write a few methods
to manipulate those collections. Developers will want to add crumbs,
remove crumbs and know how many crumbs are in the control. When removing
crumbs a developer may know what index they would like to remove or what command
they would like to remove so I will overload the remove method. I decided
that when a remove occurs, all crumbs after that crumb will be removed because
this is expected. A developer should not need to write this loop multiple
times in a page or project.
//...inside the class
//the method to add crumbs
public void AddCrumb(string Text, string Command)
{
text.Add(Text);
command.Add(Command);
}
//the method to remove crumbs
public void RemoveToCrumb(string Command)
{
int idx = command.IndexOf(Command);
if(idx == -1)
throw new ArgumentOutOfRangeException("Command", Command, "Command was not found in the breadcrumbs");
RemoveToCrumb(idx);
}
public void RemoveToCrumb(int Index)
{
if(Index < 0 || Index > text.Count)
throw new ArgumentOutOfRangeException("Index", Index, "Index is not a valid value.");
for(int i = text.Count - 1; i > Index - 1; i--)
{
text.RemoveAt(i);
command.RemoveAt(i);
}
}
public int Count
{
get{return text.Count;}
}
An obvious property that a developer will want
to control is the text that is displayed between breadcrumbs. I
added a string property named Separator with a default value. If the
developer never changes the default value then nothing is stored in
viewstate. I used an attribute to tell VS.NET what the default value is so
it will apper bold when changed.
//...inside the class
//a property to specify a separator
[DefaultValue(" > ")]
public string Separator
{
get
{
object o = ViewState["s"];
if(o != null)
return o.ToString();
else
return " > ";
}
set{ViewState["s"] = value;}
}
Like the command button, this control will expose a
click event. I will define my own event arguments because the listener for the event may
want to know the index or the command name of the crumb that
was clicked on. I start of by creating my own click event args class and inheriting from
System.EventArgs. Then I create a delegate that defines the method signature for the event.
Third, I define the public event that developers can bind
to. Next I create a private method to fire the event from within
the control. Finally I mark the BreadcrumbButton class to implement the IPostBackEventHandler interface. This
interface requires that my class expose a method named RaisePostBackEvent. This
method will be called on the control
when it is the control responsible for posting the page to
the server. Optionally you can add an attribute to the class to set this as
the default event.
//
// Step 1
//
public class BreadcrumbClickEventArgs
{
private string command;
private int index;
public BreadcrumbClickEventArgs()
{
command = string.Empty;
index = -1;
}
public BreadcrumbClickEventArgs(string Command, int Index)
{
command = Command;
index = Index;
}
public string Command
{
get{return command;}
set{command = value;}
}
public int Index
{
get{return index;}
set{index = value;}
}
}
//
// Step 2
//
public delegate void BreadcrumbClickEventHandler(object sender, BreadcrumbClickEventArgs e);
//
// Step 3
//
//...inside the class
//the event that the developer can bind to
public event BreadcrumbClickEventHandler Click;
//
// Step 4
//
//...inside the class
//our private method for firing the event
private void OnClick(int Index, string CommandName)
{
if(Click != null)
{
//create a new event args
BreadcrumbClickEventArgs args = new BreadcrumbClickEventArgs(CommandName, Index);
Click(this, args);
}
}
//
// Step 5
//
//the complete class line
public class BreadcrumbButton : System.Web.UI.WebControls.WebControl, IPostBackEventHandler
//...inside the class
#region IPostBackEventHandler Members
public void RaisePostBackEvent(string eventArgument)
{
//eventArgument is the index
int idx = int.Parse(eventArgument);
//find the command name
string cmd = command[idx];
//fire the event
OnClick(idx, cmd);
}
#endregion
//
// Step 6
//
//Add this to the attributes of the BreadcrumbButton class
DefaultEvent("Click")
Before the control can render I have to deal with
viewstate. Because I am writing my own control I must account for posting
back and maintaining state. To do this I override two methods. The
first is SaveViewState and the second is LoadViewState. I do them in this
order because when I save the viewstate I decide how I am going to package the
state up before saving. Once that decision is made I can then un-pack the
viewstate by doing the reverse of the save method. The contents of this
control are in the two collections so those are the objects whose data needs to
be persisted. I accomplish this by using one of the objects in the System.Web.UI namespace. The Triplet object is used through out
the ViewState saving and loading process so noone will mind if I use
it here. The Triplet is designed to hold three objects. I have
two I want to store from this class and then whatever the base
class of this control returns from a call to the same method. Then I return the
triplet as this control's state. That makes loading viewstate easy. The object coming to
the LoadViewState method is a Triplet. So I simply cast the object to
a Triplet and assign all the objects back to each other. Then I call to
my base class's load method so it can load any state it saved.
//...inside class
protected override object SaveViewState()
{
Triplet t = new Triplet();
t.First = text;
t.Second = command;
t.Third = base.SaveViewState();
return t;
}
protected override void LoadViewState(object savedState)
{
Triplet t = (Triplet)savedState;
text = (StringCollection)t.First;
command = (StringCollection)t.Second;
base.LoadViewState(t.Third);
}
Finally I am ready to write the render method.
When rendering I would like to write out every crumb with a seperator. The
last crumb should not be a link because that is what the user is currently
viewing.
//...inside class
protected override void Render(HtmlTextWriter writer)
{
bool last = false;
for(int i = 0; i < text.Count; i++)
{
last = (i == text.Count - 1);
if(last)
{
writer.Write(HttpUtility.HtmlEncode(text[i]));
}
else
{
writer.Write("");
writer.Write(HttpUtility.HtmlEncode(text[i]));
writer.Write("");
writer.Write(HttpUtility.HtmlEncode(Separator));
}
}
}
At this point the class is ready to run. As a
final touch I wrote a designer class to make the control look nice in VS.NET
design view. VS.NET uses the designer class referenced from the
Designer attribute on the BreadcrumbButton class to display HTML while a
developer is designing a page instead of a gray box.
//
// Step 1 - create designer
//
public class BreadcrumbButtonDesigner : ControlDesigner
{
public override string GetDesignTimeHtml()
{
return "Crumb > Crumb > Crumb";
}
}
//
// Step 2 - add attribute
//
//add to attributes on the BreadcrumbButton class
Designer(typeof(BreadcrumbButtonDesigner), typeof(IDesigner))
The complete code can be downloaded from here.