RSS Feed
Download our iPhone app
Browse DevX
Sign up for e-mail newsletters from DevX


Achieving Synchronicity: A ListBox Double Feature : Page 3

See how to combine two Rendered controls to create a single control that provides the ability to move items both within and between lists.

Adding the Client-side Functionality
The first thing you're going to do is actually the most straightforward part. In order to perform the reorder using client code, you obviously have to use JavaScript and attach it to the two buttons. I suggest you take this approach: Think backwards. Write the JavaScript to a rendered HTML file just like you wrote for an old fashioned pre-ASP Web page. The best way to do this is to run the control as-is, then view the source and copy the HTML code into an editor (VS.NET will do fine) and add the JavaScript. Listing 2 shows the JavaScript you need to add to your control in its raw form. This code would then need to be built by the control using a StringBuilder/StringWriter technique (see Listing 3). The JavaScript code is made up of two functions that receive an HTML control (a <select> control in this case), takes the selected index and moves it up or down in the list—basically the same thing I showed you using server code at the beginning of this article. Now, it's very important to understand where you're adding this JavaScript code from within the Web control. In order for this JavaScript code to NOT get duplicated in the case of there being more than one EnhancedListBox control on a Web Form, you need to output it using the RegisterClientScriptBlock method from the Page.ClientScript object.

For this method to be functional, the OnInit event must call it, which you must override (see Listing 4).

The last thing you need to do to get the buttons to work right is to wire the client methods you added to them. In the code in Listing 1 you saw a reference to a method called RenderButtons. Though I'm not including that code here (it's in the downloadable code), I'll tell you that it renders buttons using the techniques I taught in my previous article. If you look back at that article, you see that I taught you that tag attributes are "stacked" using the AddAttribute method before the actual HTML tag is rendered. You'll use this same technique to wire the client-side methods to your buttons.

   string s_MoveUp = "MoveItemUp(document.all." + 
      this.ClientID + ");
Remember MoveItemUp is one of the JavaScript functions you already wrote. This code will stack this JavaScript command before rendering the button used for ordering up. You'll use the same technique for the down button. Note that I used ClientId to represent the ID of the control after rendering. In the case that this control would exist inside another composite control, this property would takes parent naming into consideration.

At this point, I want to remind you to download the finished controls as specified in the "Where to Download" sidebar, so you can see all this code properly put together into the finished product.

As the control stands now, you can use it on a Web Form successfully. You can add items to it the same way you would to a standard ListBox control. In fact, this is a complete drop-in replacement or the ASP.NET ListBox control. When you use the reorder buttons, you'll see the items in the list change order accordingly. OK, let's recall "the problem" now. If you drop a button on a Web form (no code needed for it) and you execute a postback, what do you think will happen? Well, exactly what I described earlier; any reorder changes you made with the reorder buttons will revert to whatever the control looked like before the last postback. So let's fix this.

First I'll add a little more JavaScript to the mix. Listing 5 shows this JavaScript but I'll skip the other steps and tell you that it gets added in the OnInit method override, and built using the StringBuilder/StringWriter technique. Notice the name of the JavaScript method is BuildItemList. This function builds a string representation of the entire contents of the list box and places the string into the value property of an HTML element passed into the function. Think of this as a poor man's serialization of the list contents. The output style of the serialization can vary depending on your own design, but I used something from my past as specified in the "Delimiter Origins" sidebar. The call to this JavaScript function needs to get tacked on to the other code wired to the buttons.

   string s_MoveUp = "MoveItemUp(document.all." + 
      this.ClientID + "); ";
   string s_BuildItemList = 
      "BuildItemList(document.all." + this.ClientID + 
      ",document.all.__" + this.ClientID + "); ";
      MoveUp + " " + BuildItemList);
Look at the two arguments you're sending into the BuildItemList function. The first corresponds to the ID of the rendered control (the <select> tag). The second is another ID, named the same but prefaced with a "__". This is a hidden textbox you still need to add to your Web control and it's going to act as a place holder for the "serialized" item list. I'm going to register the hidden text field in the OnPreRender event.

   protected override void OnPreRender(EventArgs e)
      if(Page != null)
             "__" + this.ID, "");
Notice that I've used the ID of our control to identify the hidden text field.

Where Are You So Far?
Ok, so far you have a fully functional Web control with client-side JavaScript wired into two buttons within it. The JavaScript successfully takes care of reordering the items in the ListBox and serializes the contents into a string; which it then stores in a hidden text field. All of this, I stress, is on the client side. If a postback occurs, none of the reorder changes will keep because the Items server property of the control has not received any of the changes you made to it when reordering; but luckily you have those changes in the form of a serialized snapshot in the hidden text field. Have you painted a mental picture yet? You now have something that you can use to synchronize with the Items property. So how do you do this?

The Synchronization
To perform the synchronization upon the first postback and on every subsequent one, ASP.NET has a LoadPostData method in the implementation of the IPostBackDataHandler interface. The LoadPostData method gets called on every postback so this is where you need to do some work.

Earlier I told you that I'd explain why I'm writing these controls using ASP.NET 2.0. Besides the fact that you should already be embracing it now, ASP.NET 2.0 fixes a small oversight left out in 1.1 that makes your job here incredibly easier. The ASP.NET ListBox control already implements the IPostBackDataHandler interface in both versions (1.1 and 2.0). But in 2.0 Microsoft made the method definitions within it virtual (Overridable in VB). This means you don't have to reimplement this interface in the EnhancedListBox control; instead you can just override the LoadPostData method. More importantly, this also means you can access the base implementation and not have to reproduce all that functionality in your extended control. What functionality you ask? Everything Microsoft put in there to handle the Items collection, the SelectedIndex, SelectedValue, and SelectedItem properties, and a whole other slew of code to perfect the functionality of the ListBox control. In ASP.NET 1.1 you would have to implement this interface in your derived control and provide your own code for the two methods defined, covering not only your own additions but also repeating everything that Microsoft already did in their control. This would be an incredible amount of work and would not only quadruple the length of this article, but would drive my hands into yet another stage of carpal tunnel syndrome.

I guess someone in Redmond saw the error in their ways and made the methods virtual so developers can have access to Microsoft's base code. So that's exactly what I'm going to show you how to do now, override the LoadPostData method (see Listing 6). In this override, you'll call the base implementation first so you can get Microsoft's code properly executed and not mess anything up that they intended for the ListBox control. You'll then add the code you need to synch up the Items collection.

Alternatively, you could leverage the ListBox control by writing this as a composite control. In that case you would need to map out every property in the ListBox to your EnhancedListBox in order to make it a drop-in replacement for the ListBox. Either this way, or in the recreation of the LoadPostData method, you would still need to write a lot of code. If I needed to write this control specifically for ASP.NET 1.1, I would most likely take the most straightforward solution, the composite control solution.

As I explained in my last article, the LoadPostData method gives you access to every field posted to the server, including your hidden text field. This is stored in the postCollection argument passed into this method. If you wondered why you needed the hidden text field instead of accessing the posted <select> element using this argument, I'll tell you why. Think back to the classic ASP days when you used the Request.Form property to access page fields. The only part of a <select> element you had access to at posting was the selected item. In the solution I'm teaching you, you need the entire contents of the list, hence the hidden text field. Listing 6 shows how to parse out the contents of the hidden text field and re-add the items into the Items collection. There's also code there to hang on to what the selected index was before you cleared out the Items collection so you can reset it later. Note how you're calling the base implementation first.

Finally you have to build the hidden text field the first time you render your control, in case the page postbacks before any attempts at reordering take place. The last line of the Render method will be:

      "<script language='javascript'>
       BuildItemList(document.all." + this.ClientID +
      ",document.all.__" + this.ClientID + 
You can see this at the end of Listing 1.

Close Icon
Thanks for your registration, follow us on our social networks to keep up-to-date