First Control
Published on 1/18/2005 11:54:00 PM by Philip

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("<a href=\"javascript:" + Page.GetPostBackClientEvent(this, i.ToString()) + "\">"); writer.Write(HttpUtility.HtmlEncode(text[i])); writer.Write("</a>"); 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 "<a href=\"#\">Crumb</a>&nbsp;&gt;&nbsp;<a href=\"#\">Crumb</a>&nbsp;&gt;&nbsp;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.

4 Comments
Tags:
Bookmark and Share
Design downloaded from Free Templates - your source for free web templates