Tree Huggin!

 

   I have another server control request. This time we are looking for an ASPNET TreeView control, but we need to be able to add a button to some of the tree nodes. The button will have a different event fired than clicking on the tree node in the treeview.

   To get started we must first understand that there are two objects that we must create:

1) custom TreeNode class

2) a custom TreeView control

Let’s start with the custom TreeNode class. I am going to create a new object that inherits from the System.Web.UI.WebControls.TreeNode class:

public class TreeNodeAdam : TreeNode

{

   To add the button to the TreeView node, we override the RenderPreText method:

protected override void RenderPreText(HtmlTextWriter writer)

   Now we can add our button the StringWriter as follows:

HtmlButton btn = new HtmlButton();

btn.ID = "btn" + commandName;

btn.InnerText = buttonText;

btn.RenderControl(writer);

   We’ll leave in our call to our base class:

base.RenderPreText(writer);

   This will render the button just fine. We still need to figure out how we can get it to postback though. This is where it starts to get tricky. The TreeNode class inherits from System.Object, it’s not a control. This is very important because TreeNodes are not children of the parent control TreeView (in other words you won’t find them in the TreeView.Controls collection). They are totally autonomous. If we used an ASPNET server button control, we could wire up the Click eventhandler in the TreeNode, but it will never be called because the TreeNode is not a control, and worse the parent TreeView would have no idea that you clicked it.

   In order for the parent to know that something caused a postback that it should handle, we will call the __doPostback client side method, and pass in the ID of the TreeView control. If my TreeView control’s iD is “MyTreeView1” then we would call this:

__doPostback (‘MyTreeView1”, <something>)

   The second parameter is going to be the ID of the control that caused the postback. This way if we have many buttons, the parent treeview control will know which button caused the postback. This is exactly the same behavior as using the DataGrid control with its ItemCommand event.

   We need to add a few properties to our AdamTreeNode class so that the control is easy to use. These are the properties that I’m adding:

· ButtonText – Gets or sets the text caption displayed in the Button control.

· CommandName - Gets or sets the command name associated with the Button control that is passed to the OnItemCommand event.

· HasButton - Gets or sets a Boolean value indicating if this node has a Button control.

· TreeViewID - Gets or sets the TreeViewAdam ID associated with this node.

   I could have created this control without the TreeViewID property and just hardcoded this value, since I am going to create the TreeView as well, however this would mean that we could only have one instance of the AdamTreeViewcontrol on a page at a time, because the ID would not be unique.

   The finished overridden RenderPreText function looks like this:

protected override void RenderPreText(HtmlTextWriter writer)

        {

            if (HasButton)

            {

                HtmlButton btn = new HtmlButton();

                btn.ID = "btn" + commandName;

                btn.InnerText = buttonText;

                btn.Attributes.Add("OnClick", "javascript:__doPostBack('" + treeViewID + "','" + btn.ID + "')");

                btn.RenderControl(writer);

          base.RenderPreText(writer);

            }

            else

            {

                base.RenderPreText(writer);

            }

      }

   If you’re wondering why I used an HtmlButton instead of an ASPNET button, it’s because an ASPNET button would cause the postback to happen twice, once for its normal behaviour when you click it (form submit), and again for the code that we added to the OnClick method (__doPostback).

   Now we need to create our TreeView class. We’ll start by overiding the existing TreeView class:

public class TreeViewAdam : TreeView

{

   The only thing we really need to do our new class is add the ItemCommand event and handle that event. One very common mistake that people make when they write server controls is that they forget where the events should get handled. Remember that we don’t want to handle our events in the control, we want them handled on the page (or what good would it be to have them?). This is done by raising this event to the page. Let’s first declare our event:

private static readonly object EventItemCommand;

public event CommandEventHandler ItemCommand

{

      add

    {

          Events.AddHandler(EventItemCommand, value);

      }

      remove

      {

      Events.RemoveHandler(EventItemCommand, value);

      }

 }

    We need the EventItemCommand later, so I’ve gone ahead and declared it. I’m not going to go into too much detail about declaring events as it’s well documented here. This will Expose our ItemCommand event to our page.

   We still need to wire up our handler to our posted back event. This is easy to do for us in .NET because we have just the right event we can override, the RaisePostBackEvent. Here’s what my code looks like:

protected override void RaisePostBackEvent(string eventArgument)

{

   if (eventArgument.StartsWith("btn"))

   {

      String fixEA = eventArgument.Substring(3);

      CommandEventArgs args = new CommandEventArgs(fixEA, null);

      OnCommand(args);

   }

base.RaisePostBackEvent(eventArgument);

}

   You’ll notice I fixed up my eventArgument by checking if it starts with “btn”. A clever eye would have caught this in the _doPostback function we called earlier. I’m doing this to make sure that the control that caused the postback was one of my custom buttons that I created. In this function, I call the OnCommand function with my CommandEventArgs argument. Let’s go ahead and write the OnCommand function:

protected virtual void OnCommand(CommandEventArgs e)

{

   CommandEventHandler handler = (CommandEventHandler)base.Events[EventItemCommand];

if (handler != null)

   {

      handler(this, e);

   }

  
base.RaiseBubbleEvent(this, e);

 }

   That’s it for our custom control! We can now implement the control in our page like this:

<TVA:TVA ID="MyCustomTreeView" runat="server" OnItemCommand="TVAFunc">

   <Nodes>

       <adam:TreeNodeAdam Text="Item1" Value="Item1"

                          ButtonText="Adam" CommandName="Button1"

                          TreeViewID="MyCustomTreeView"/>

      <adam:TreeNodeAdam Text="Item2" Value="Item2"

                          HasButton="false"/>

   </Nodes>

</TVA:TVA>

 

   We handle the OnItemCommand function in the page like this:

protected void TVAFunc(object sender, CommandEventArgs e)

{

    Response.Write("You clicked: " + e.CommandName);

}

   This will work fine. But I found that there is one fatal flaw in this control. The TreeView control should use EventValidation to insure that it can't crash out parent page with invalid values. The EventValidation attribute is not passed down to inherited controls and must be explicitly added to the class decoration. Let's go ahead and add that to our TreeView control:

[SupportsEventValidation]

public class TreeViewAdam : TreeView

{

Now when you run the control and hit one of our buttons, you will see that we get an invalid call error. This is correct because the page will not allow us to call __doPostback withouth a "registerred" event (see ClientScriptManager.RegisterForEventValidation for more details). What we need to do, is register our event so we can callback with these controls and pass EventValidation.

We need to do two things:

1) Get an instance of our page (so we can use the ClientScriptManager).
2) Register the event with that instance

Here's the final RenderPreText function:

protected override void RenderPreText(HtmlTextWriter writer)

{

if (HasButton)

{

HtmlButton btn = new HtmlButton();

btn.ID = "btn" + commandName;

btn.InnerText = buttonText;

btn.Attributes.Add("OnClick", "javascript:__doPostBack('" + treeViewID + "','" + btn.ID + "')");

// Get an instance of our page

Page p = (Page)HttpContext.Current.Handler;

// Register our control with that page so we pass Event Validation

p.ClientScript.RegisterForEventValidation(treeViewID,btn.ID);

btn.RenderControl(writer);

base.RenderPreText(writer);

}

else

{

base.RenderPreText(writer);

}

}

   You can now use this control and know that we are safely implementing it following EventValidation. 

  As always enjoy! I put the source and the compiled binaries here.