Smart ListControl in ASP.NET 1.x/2.0
 
Published: 16 Nov 2006
Abstract
In this article Bilal Haidar shows you how to make any ListControl that ships with ASP.NET 1.x/2.0 a smart control by making those controls preserve the client-side changes on their collection of items.
by Bilal Haidar
Feedback
Average Rating: 
Views (Total / Last 10 Days): 37996/ 68

Introduction

In ASP.NET when a post back to the server occurs and it happens that a control of type ListControl is present on the ASPX page, the ListControl functions as follows.

Items present in the ListControl will not be posted back to the server whether in the Forms collection or the QueryString collection depending on the Action method you are using in the Web Form.

Only the selected value is sent back to the server upon a post back.  This explains how a selected item can be detected on the server.

When the response is back to the client, the ListControl items are retrieved from the ViewState.

The above facts pop up a serious problem, especially when you are doing any changes on the client side.  Changes can be removing or adding new items to the ListControl.

Any changes on the client side will not be reflected, since the shown items on the client side of a ListControl are not posted back with the page post back.  Whatever changes you do, they will disappear once a response is sent back from the server to the client (browser), since the ListControl as mentioned above retrieves its values from the ViewState.

In some applications there is a need to add new items to a ListControl or even remove some items.  Imagine two ListControls placed side by side with some buttons in between to move items between those two ListControl.  For instance, you might have two ListBoxes to hold finished and unfinished tasks, when a task is finished you move it to the other ListBox and if a task has been moved, however, you discover later on that some fixes are needed, so you bring it back to the unfinished ListBox.

To have good performance for you web form, such movements are to be done on the client side using some JavaScript instead of posting to the server on every movement. However, as listed above, there is a problem that those changes will not be preserved by the ListBox, because of the default behavior of the ListBox or any other ListControl in ASP.NET.

This article is meant to solve this problem by making the ASP.NET ListBox in particular a smart ListControl, where it preserves its changes upon posting back to the server.  Changes can be done by either adding new items or removing items from its collection of items.

We have taken as an example the ListBox.  However, the same technique can be used to make any control in ASP.NET that implements ListControl a smart control.

The Solution

In this section we will explain in detail the solution we have implemented to solve the problem.

We have added two Hidden Fields that will be injected by the new control developed.

The first Hidden Field is named with this convention:

ListBoxID + “_ADDED”

The above field will be used to hold any item that has been added to the ListBox on the client side.  Suppose you have added the item with:

Text = “First Item”

Value = “1”

A new item will be added to the Hidden Field in the form of:

Text|Value

For instance, an item will be added as follows:

First Item|1

Any additional item will be added to the Hidden Field.

Item1|Value1,Item2|Value2

The format of how the items are added to the Hidden Field is very critical.  Since, as you will see later on, we are going to get the value of the above Hidden Field, split it based on the “,” and then each single item will be added as a new ListItem into the ListBox.

The second Hidden Field used is named with this convention:

ListBoxID + “_REMOVED”

The above Hidden Field will hold the values of all the items that have been removed from the ListBox.  Later on we will see that on the server, those values will be selected in the ListBox and removed, thus reflecting the behavior and changes done on the client side.

In addition to the above mentioned section of the solution, we will be implementing the IPostBackDataHandler, where two methods will be reworked mainly the LoadPostData and RaisePostDataChangedEvent.  More on the implementation of this interface will be discussed shortly in this article.

Classes to implement

Since we will be improving the ListBox control, it is wise first of all to implement ListBox class. The goal we are after is to upgrade the ListBox to function properly with the client side changes on its collection of items.

The code below shows how to implement the ListBox class.

Listing 1

public class xListBox : ListBox
{

All the public properties and methods of the ListBox will continue to use them as before.

One method of interest, which is the OnPreRender, will be overridden to add the two mentioned Hidden Fields in a previous section.

The OnPreRender is as follows:

Listing 2

protected override void OnPreRender(EventArgs e)
{
  base.OnPreRender(e);
 
// Force the LoadPostData to fire always
  if (Page != null)
    Page.RegisterRequiresPostBack(this);
 
// Register the hidden field to hold the added items
  Page.RegisterHiddenField(this.HFItemsAdded, "");
 
// Register the hidden field to hold the indicies of removed items
  Page.RegisterHiddenField(this.HFItemsRemoved, "");
 
// Register the JavaScript file that includes utility methods
  if (!Page.IsClientScriptBlockRegistered("UtilMethods"))
    Page.RegisterClientScriptBlock("UtilMethods", jsScript);
}

What we have done is call the base OnPreRender almost every time when developing custom controls and this is usually done to preserve any code that has been added by the base class.

Any client side code, like JavaScript or any control registering on the parent Page of the control, should be done in this stage of the control life cycle, the PreRender stage.

Notice that we have registered a JavaScript block with the key UtilityMethods.  This code provides two main client side functions, mainly the AddItemToList and RemoveItemFromList.  Those functions are used to Add and Remove items from the two Hidden Field mentioned above.  They have been implemented in such a way to keep those two Hidden Fields synchronized so to not have items present in both Hidden Fields.

The JavaScript code is shown below.

Listing 3

// Add a new ListItem to the ListBosID_ADDED HiddenField
function AddListItem(listName, text, value)
{
  var hiddenField = GetHiddenField(listName.id + '_ADDED');
 
  if (hiddenField != null)
  {
    // Add a separator
    var tmp = hiddenField.value;
    if (tmp != '')
      hiddenField.value += ',';
 
    // Add the item to the hidden field
    hiddenField.value += text + '|' + value;
 
    // if present in the REMOVED hidden field, remove it
    var removeHiddenField = GetHiddenField(listName.id + '_REMOVED');
    if (removeHiddenField != null)
    {
      var removedItems = removeHiddenField.value.split(',');
      removeHiddenField.value = '';
      for (var i = 0; i < removedItems.length; i++)
      {
        if (value != removedItems[i])
        {
          // Add a separator
          var tmp1 = removeHiddenField.value;
          if (tmp1 != '')
            removeHiddenField.value += ',';
 
          removeHiddenField.value += removedItems[i];
        }
      }
    }
  }
}
 
// Removes an item from the ListBoxID_REMOVED HiddenField
function RemoveListItem(listName, value)
{
  var hiddenField = GetHiddenField(listName.id + '_REMOVED');
 
  if (hiddenField != null)
  {
    // Add a separator
    var tmp = hiddenField.value;
    if (tmp != '')
      hiddenField.value += ',';
 
    hiddenField.value += value;
 
    // if present in the ADDED hidden field, remove it
    var addHiddenField = GetHiddenField(listName.id + '_ADDED');
    if (addHiddenField != null)
    {
      var addedItems = addHiddenField.value.split(',');
      addHiddenField.value = '';
      for (var i = 0; i < addedItems.length; i++)
      {
        if (addedItems[i].match(value) == null)
        {
          // Add a separator
          var tmp1 = addHiddenField.value;
          if (tmp1 != '')
            addHiddenField.value += ',';
 
          addHiddenField.value += addedItems[i];
        }
      }
    }
  }
}
 
// Finds a hidden field on the page
function GetHiddenField(fieldName)
{
  var hiddenField;
  hiddenField = document.getElementById(fieldName);
 
  if (hiddenField != null)
    return hiddenField;
 
  return null;
} 

The above code is being injected in the OnPreRender method.  Later on when we discuss the "How to use the new ListBox" we will show you how to call those methods properly.

There are three main methods:

AddListItem

RemoveListItem

GetHiddenField

The AddListItem starts by getting the Hidden Field titled _ADDED, where this Hidden Field contains all the items, in the form of (Text|Value) that will be added to the ListBox.  The new item to be added is constructed in the following format (Text|Value) and then appended to any item already present in the Hidden Field.  If any of those items are present in the Hidden Field titled _REMOVED they will be removed from that Hidden Field.  This ensures that items to be removed are removed and items to be added are added correctly.

The RemoveListItem starts by getting the Hidden Field titled _REMOVED, where this Hidden Field contains all the values of the items to be removed from the ListBox.  The new item’s value to be removed from the ListBox is added to any existing value in the Hidden Field and separated by a “,” sign.  Again, the item’s value to be removed is checked against the values present in the _ADDED Hidden Field to maintain consistency between items removed and added.

Finally, the GetHiddenField is a utility method that is used to find any Hidden Field on the page.

Implement IPostBackDataHandler

The IPostBackDataHandler contains two main methods:

IPostBackDataHandler.RaisePostDataChangedEvent

IPostBackDataHandler.LoadPostData

How are those two methods used when they are being implemented in a control?

Although this question is not the topic of this article, we are going to give a brief overview of what happens when the Page builds its control tree and how each control fires specific events based on the interfaces it implements.

When a post back happens, the Page recreates its tree of controls.  It starts from the main control, which is the Page itself, then for the children controls and the same continues for the sub children controls.

For each control it is being checked whether it implements some interfaces or not.  Among those interfaces is the IPostBackDataHandler.  If this interface is implemented, this means that the control has implemented the above mentioned two methods.

Those methods are usually implemented to handle any posted back data.  By default, in ASP.NET the ListBox implements the above two methods.

The LostPostData is being implemented to detect the selected index or indices in case the ListBox is configured to be in the Multiple Selection mode.

What we will do is implement the LoadPostData method.  First of all, we well keep the default behavior of this method, where we detect the selected index or indices and we will get the data stored in both Hidden Fields mentioned above.

The data stored in those two Hidden Fields will be used to remove items from the ListBox and, in this case, the _REMOVED Hidden Field is queried to get all the item indices to remove from the ListBox.  The _ADDED Hidden Field is queried to get all the items that will be added to the Items collection of the ListBox.

The code below shows the implementation of the LoadPostData.

Listing 4

bool IPostBackDataHandler.LoadPostData(string postDataKey, NameValueCollection
  postCollection)
{
// Handle the SelectedIndex or Indicies
  string[]postedItems = postCollection.GetValues(postDataKey);
  bool returnValue = false;
 
// If no selection is done on the client side
  if (postedItems == null)
  {
    if (this.SelectedIndex !=  - 1)
    {
      returnValue = true;
    }
 
// No further processing for the Selected Index/Indicies
    goto HandleClientChanges;
  }
 
// If selection is in single mode
  if (this.SelectionMode == ListSelectionMode.Single)
  {
    if (postedItems != null)
    {
// Process first item, since we have a single selection
// ListItem item = this.FindByValueInternal(postedItems[0]);
      int index = this.FindByValueInternal(postedItems[0]);
        //Items.IndexOf(item);
      if (this.SelectedIndex != index)
      {
// Change occurred
        this.SelectedIndex = index;
        returnValue = true;
      }
    }
  }
 
// Else, the selection mode is multiple
  int numberOfItemsSelected = postedItems.Length;
 
// Old selected items
  ArrayList oldSelectedItems = this.SelectedIndicesInternal;
 
// An empty arraylist of the currently selected items on the clisnde side
  ArrayList currentlySelectedItems = new ArrayList(numberOfItemsSelected);
 
// Fill in all the indicies of the posted selected items
  for (int i = 0; i < numberOfItemsSelected; i++)
  {
    currentlySelectedItems.Add(this.FindByValueInternal(postedItems[i]));
  }
 
// Get the number of currently selected indicies
  int numberOfSelectedItems = 0;
  if (oldSelectedItems != null)
  {
    numberOfSelectedItems = oldSelectedItems.Count;
  }
 
// Check if both numbers are equal
  if (numberOfItemsSelected == numberOfSelectedItems)
  {
    for (int j = 0; j < numberOfItemsSelected; j++)
    {
      int oldSelect = Convert.ToInt32(currentlySelectedItems[j]);
      int currentSelect = Convert.ToInt32(oldSelectedItems[j]);
 
      if (oldSelect != currentSelect)
      {
// Explicitly mark the item selected
        this.Items[j].Selected = true;
        returnValue = true;
      }
    }
  }
  else
  {
// The number of selected items has changed
    returnValue = true;
  }
 
// Reset all selections to the newly selected items
  if (returnValue)
  {
    this.SelectInternal(currentlySelectedItems);
  }
 
 
// Section to handle ADDED and REMOVED Items.
  HandleClientChanges:
 
// Remove the items from the Items collection
// that were marke for deletion on client side// Handle items added on Client Side
  string itemsRemoved = postCollection[this.HFItemsRemoved];
  string[]itemsRemovedCol = itemsRemoved.Split(',');
  if (itemsRemovedCol != null)
  {
    if ((itemsRemovedCol.Length > 0) && (itemsRemovedCol[0] != ""))
    {
      for (int i = 0; i < itemsRemovedCol.Length; i++)
      {
        ListItem itemToRemove = this.Items.FindByValue(itemsRemovedCol[i]);
 
// Remove from the Items Collection
        Items.Remove(itemToRemove);
      }
      returnValue = true;
    }
  }
 
// Handle items added on Client Side
  string itemsAdded = postCollection[this.HFItemsAdded];
  string[]itemsCol = itemsAdded.Split(',');
  if (itemsCol != null)
  {
    if ((itemsCol.Length > 0) && (itemsCol[0] != ""))
    {
// counter to validate returnValue
      int counter =  - 1;
      for (int i = 0; i < itemsCol.Length; i++)
      {
        string buf = itemsCol[i];
        string[]itemsTokens = buf.Split('|');
 
// Check if already added to the collection
        ListItem it = this.Items.FindByValue(itemsTokens[1]);
        if (it == null)
        {
          string text = itemsTokens[0];
          string id = itemsTokens[1];
          ListItem item = new ListItem(text, id);
          Items.Add(item);
 
// Updata counter
          counter++;
        }
      }
      returnValue = counter >  - 1 ? true : false;
    }
  }
 
  return returnValue;
}

The first set of code lines behaves as the code written for the LoadPostData of the ListBox that ships with ASP.NET.

The second part retrieves all the indices that are listed in the Hidden Field to be removed from the ListBox.

The third part retrieves all the item pairs (Text and Value) that are found in the Hidden Field used to represent the items to be added to the Items collection of the ListBox.

There is not much to be said about the RaisePostDataChangedEvent method.  The implementation is simple.

Listing 5

void IPostBackDataHandler.RaisePostDataChangedEvent()
{
  OnSelectedIndexChanged(EventArgs.Empty);
}

What happens after the Page_Load method is fired?  The result of execution of the LoadPostData, which returns a Boolean value, is checked.  If the value if true, this means that the selected indices has changed and, therefore, the need to raise the even called OnSelectedIndexChanged.

On the other hand, if LoadPostData method returned a value of false, this means that the selected indices have not been changed and then there is no need to fire the OnSelectedIndexChnaged.

With the above said, we have finished implementing the new smart ListBox control.  In the next section we will see an example on how to use this control on a web form and how to call the JavaScript utility methods mentioned in a previous section.

How to use the new Control?

In this section we will show a simple web form that contains an instance of the new ListBox that we have created above.

A button is to be added that will handle removing an item from the ListBox.

Two TextBoxes will be added to allow the user to enter a Text and a Value for the new item to be inserted into the ListBox.  In addition, a button is used to add the above values as a new item into the ListBox.

All the above buttons are client side buttons and, hence, the manipulation of the items inside the ListBox is done on the client side using JavaScript.

Finally, a server-side button is used to force a post back to the server to show you how the items inside the ListBox will be preserved.

The web form shall look something similar to the following figure.

Figure 1

You can select an item in the ListBox then press on the Remove Selected Item button.  This will remove the item from the ListBox and add its value to the _REMOVED Hidden Field.  The code for this function is shown below.

Listing 6

function RemoveItem()
{
// Get the ListBox
  var sourceListBox = document.getElementById('ListBox1');
 
// Check if the ListBox is null or not
  if (sourceListBox != null)
  {
// Get the selected item
    var selectedValue =
      sourceListBox.options[sourceListBox.options.selectedIndex].getAttribute(
      "value");
 
// Remove the selected item from the ListBoc
    sourceListBox.remove(sourceListBox.options.selectedIndex);
 
// Add the index of the item to be removed into the
// Hidden Field _REMOVED
    RemoveListItem(sourceListBox, selectedValue);
  }
}

As you can see, the code is self explanatory.  We have detected the selected item. Then we removed the item from the ListBox and, finally, we instantiated a call to the utility method that is being injected on the page by the new ListBox control created.

The RemoveListItem takes as parameters the name of the ListBox in action and the value of the item to be removed.

If you want to add a new item, you simply fill in the two TextBoxes with the Text and Value of the new Item.  Then you press on Add New Item button.  The code behind this button is shown below.

Listing 7

function AddItem()
{
  // Get the ListBox
  var sourceListBox = document.getElementById('ListBox1');
  var txt_text = document.getElementById('TextBox1');
  var txt_value = document.getElementById('TextBox2');
 
  // Check if the ListBox is null or not
  if ((sourceListBox != null) && (txt_text != null) && (txt_value != null))
  {
    // Create a new option
    var newOption = new Option();
    newOption.text = txt_text.value;
    newOption.value = txt_value.value;
 
    // Add the created item to the ListBox
    sourceListBox.options[sourceListBox.length] = newOption;
 
    // Add the new text|value to the Hidden Field _ADDED
    AddListItem(sourceListBox, newOption.text, newOption.value);
  }
}

First of all, we create a new Option, add the new Option to the ListBox and, after that, we add the new item to the _ADDED Hidden Field.

Upon posting back to the server, you would notice that the items in the ListBox are kept the same as they were before sending the request to the server and this proves that the control is functioning as well as expected!

Downloads

Conclusion

In this article we have seen together how easy it is to create a new custom server control based on the ListBox control that ships with the ASP.NET.

In addition, we saw together how to solve the problem that accompanies the ListBox and all controls that implement ListControl based class, where any manipulation of their items on the client side will not be preserved upon a post back to the server.

Hope you enjoyed this article.

Happy Dot Netting!!



User Comments

Title: Re:   
Name: Bilal Hadiar [MVP]
Date: 2007-04-04 1:51:36 AM
Comment:
Hello,
I have updated the control to work on other browsers than IE!
Check my post here:
http://bhaidar.net/cs/archive/2007/04/03/xlistbox-and-mover-control.aspx
Title: Re:   
Name: Bilal Haidar [MVP]
Date: 2006-12-09 6:40:35 AM
Comment:
Hello:
If it is not working on those two browsers, means there is something in the JavaScript that is specific for the IE only!
Let me check what I can do and come back to you!

Regards
Title: Re:   
Name: Bilal Hadiar [MVP]
Date: 2006-12-06 1:54:05 AM
Comment:
Hello, what exactly is happening? I don't have Safari here, all my work in the company is IE that is why I didn't test it there!

Regards
Title: Safari / doesnt work   
Name: Sonu Kapoor
Date: 2006-12-05 3:19:28 PM
Comment:
It doesnt work properly on safari!






Community Advice: ASP | SQL | XML | Regular Expressions | Windows


©Copyright 1998-2024 ASPAlliance.com  |  Page Processed at 2024-04-25 2:09:54 PM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search