Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Optimized Master/Detail DropDownList

0.00/5 (No votes)
3 Feb 2003 4  
Creating a Master/Detail DropDownList that doesn't require a trip to server

Update Summary

No sooner had I used the control described in the original article when I came across another requirement where I needed �two� detail DDLs driven from a master DDL. Since I knew it would only be a matter of time when �three� would be needed I generalized the code to support any number of �slaved� DDLs. The other change I made was to provide for loading the detail DDLs when the page is loaded.

The logic and functionality is still pretty much the same except that an array of detail DDLs is used to initialize the master DDL instead of the data for a single detail DDL. And of course the client script changes in order to load an array of detail DDLs instead of just one.

Introduction

ASP is great. It allows you to do just about anything on the server. Sometimes, however, that's not the most efficient solution.

A common design requirement is to have the selection of one list determine the contents of a second list. This is normally described as a 'Master/Detail' relationship where the contents of the second list are dependent on the selection of the 'Master' or primary list. Normally you would populate the master list and by default select the first item and use that as the key to populate the second list. However, once the user selects a different item on the master list then the 'detail' list needs to be updated with the appropriate items and a trip back to the server is required.

One solution is to have the data required to populate the detail list embedded on the page. This way when the user makes a selection on the master list the data would be available on the client to populate the detail list. Typically the amount of data required to populate the detail list is fairly small and can be embedded within the page. Face it, users do not want to scroll though hundreds of items to make a selection.

The approach presented here is the 'simple and compact' solution and makes use of the browser's built-in support for XML (DOM) and the DataSet's capability to generate XML.

Building the MasterDDL control

Create a new Web Control and derive it from DropDownList. Add one property as shown below.

#region private members
private Array m_SlaveData;
#endregion
#region public properties
public Array SlaveData
{
  set{m_SlaveData = value;}
}
#endregion

The property allows the user to specify the source of the data that will be used to populate each of the DetailDDLs. An array is used to store the information and consists of the DataSet, name (identifier) of the DetailDDL, and table name (of the data). Next, override two base class methods that will allow us to create data and code on the client to populate the DetailDDLs.

protected override void AddAttributesToRender(
  HtmlTextWriter writer) 
{
  base.AddAttributesToRender(writer);
  if (m_SlaveData.Length != 0)
  {
    writer.AddAttribute("onClick", "doMasterClick()");
  }
}
protected override void Render(HtmlTextWriter output)
{
  //Write out the data for the slave ddl

  if (m_SlaveData.Length != 0)
  {
    //Write out the data for each of the detail DDLs

    for (int nCount = 0; nCount < m_SlaveData.GetLength(0); nCount++)
    {
      StringBuilder s1 = new StringBuilder("dso");
      s1.Append(m_SlaveData.GetValue(nCount,0).ToString());
      output.Write("<xml id="+s1.ToString()+">\n");
      DataSet ds = (DataSet)m_SlaveData.GetValue(nCount,1);
      ds.WriteXml(output);
      output.Write("</xml>\n");
    }

    //First the script that will be called to load the slave ddl

    StringBuilder s = new StringBuilder("\n<script language="JavaScript">\n");
    s.Append("function doMasterClick()\n");
    s.Append("{\n");
    //The array of slave ddl(s)

    s.Append("var theSlaveDDLs = new Array(");
    for (int nCount = 0; nCount < m_SlaveData.GetLength(0); nCount++)
    {
      if (nCount != 0)
        s.Append(",");
      s.Append("\""+ m_SlaveData.GetValue(nCount,0).ToString() + "\"");
      s.Append(",");
      s.Append("\""+ m_SlaveData.GetValue(nCount,2).ToString() + "\"");
    }
    s.Append(")\n");
    //Get the current selection from the master ddl

    s.Append("var sel = document.all['" + this.ID + "'].value;\n");
    //For each slave DDL...

    s.Append("for(i=0;i<theSlaveDDLs.length;i+=2)\n");
    s.Append("{\n");
    s.Append("var optCount = 0;\n");
    s.Append("var dso = new String(\"dso\"+theSlaveDDLs[i]);\n");
    s.Append("SlaveData = document.all[dso].XMLDocument;\n");
    //First erase the contents of the slave ddl, if any

    s.Append("while(document.all[theSlaveDDLs[i]].length)\n");
    s.Append("document.all[theSlaveDDLs[i]].options[0] = (null,null);\n");
    //Now add the new ones

    s.Append("for(j=0;j<SlaveData.childNodes(0).selectNodes(theSlaveDDLs[i+1])");
s.Append( ".length;j++)\n"); s.Append("{\n"); s.Append("var data = SlaveData.childNodes(0).selectNodes(");
s.Append( "theSlaveDDLs[i+1])(j);\n"); s.Append("if(sel == data.childNodes(1).text)\n"); s.Append("{\n"); s.Append("var option = new Option(data.childNodes(2).text,");
s.Append( "data.childNodes(0).text);\n"); s.Append("document.all[theSlaveDDLs[i]].options[optCount] = option;\n"); //Save the first entry as the default selection s.Append("if (optCount==0)\n"); s.Append("{\n"); s.Append("var s1 = new String(theSlaveDDLs[i] + \"_Sel\");\n"); s.Append("document.all[s1].value = data.childNodes(0).text;\n"); s.Append("var s2 = new String(theSlaveDDLs[i] + \"_Val\");\n"); s.Append("document.all[s2].value = data.childNodes(2).text;\n"); s.Append("}\n");//end if s.Append("optCount = optCount+1;\n"); s.Append("}\n");//end if s.Append("}\n");//end for(j... s.Append("}\n");//end for(i... s.Append("}\n");//end function s.Append("<" + "/" + "script>\n"); output.Write(s.ToString()); } // draw our control base.Render(output); }

The first override just provides the means of hooking in our script function. The second one is where most of the action takes place so I'll describe some of the code. The first 'for loop' takes care of writing out the detail DDLs data. The bulk of the work is done by the DataSet itself and all we have to do is wrap the data with a couple of XML tags. You can see what's going on by viewing the source code of the resulting page when you build the test app. The browser will load the data into an in-memory document object which you will be able to manipulate from code. You can get some additional information from http://www.w3.org/DOM/ on the Document Object Model and browser support.

Inside the doMasterClick function the first thing we do is create an array that holds the list of detail DDLs so that we can iterate through them. Next we locate the master DDL and get it's current selection. The rest of the function is the main loop where we load the data into each of the detail DDLs that have been specified. First we get a reference to the respective data island as 'SlaveData'. Then we find the DetailDDL and erase any contents it may have. Finally it's just a matter of iterating through the data looking for all the items that match the 'value' of the current selection of the MasterDDL. For each match that we find we create an entry in the detail DDL and set 'value' and 'data' accordingly.

The DetailDDL Control

If you just wanted to present information to the user then everything would be fine as it is and you could just use a regular DropDownList control for the detail DDL. However, most of the time we'll need to know what item the user selected on the DetailDLL so that we can perform some action. Because the DetailDLL is being populated on the client dynamically there is no mechanism to send back the selected item. Normally this would be done automatically by ASP using the ViewState mechanism when a DropDownList is populated on the server with a static list of items.

The solution I'll present here is to create a customized DropDownList for the DetailDDL. To facilitate the requirements what's needed is for the DetailDLL to persist it's selected state on the client and then send it back when the page is posted. We can do this by having the DetailDDL create two hidden input fields and then emit some script that will populate these fields when the user makes a selection. Here's the code:

public class DetailDDL: 
  System.Web.UI.WebControls.DropDownList, IPostBackDataHandler 
{
  #region private members
  private string m_strTableName;
  #endregion
  #region public properties
  public string TableName
  {
    set{m_strTableName = value;}
  }
  #endregion
  protected override void AddAttributesToRender(
    HtmlTextWriter writer) 
  {
    base.AddAttributesToRender(writer);
    writer.AddAttribute("onClick", "doSlaveClick(this.value)");
  }
  protected override void OnInit(
    EventArgs e)
  { 
    base.OnInit(e);
    this.Items.Add("");
  }
  protected override void OnPreRender(
    EventArgs e)
  {
    base.OnPreRender(e);

    if (Page != null)
    {
      Page.RegisterHiddenField(ClientID+"_Sel", "");
      Page.RegisterHiddenField(ClientID+"_Val", "");
    }
  }
  protected override void Render(
    HtmlTextWriter output)
  {
    StringBuilder s = new StringBuilder("\n<script language="JavaScript">\n");
    s.Append("function doSlaveClick(val)\n");
    s.Append("{\n");
    s.Append("var dso = new String(\"dso"+this.ID+"\");\n");
    s.Append("SlaveData = document.all[dso].XMLDocument;\n");
    s.Append("for(j=0;j<SlaveData.childNodes(0).");
s.Append( "selectNodes(\""+m_strTableName+"\").length;j++)\n"); s.Append("{\n"); s.Append("var data = SlaveData.childNodes(0).selectNodes(\""+");
s.Append( "
m_strTableName+"\")(j);\n"); s.Append("if(val == data.childNodes(0).text)\n"); s.Append("{\n"); s.Append("\ndocument.all['"+ClientID+"_Val"+"'].value = ");
s.Append( "data.childNodes(2).text;"); s.Append("\nbreak;"); s.Append("}\n"); s.Append("}\n"); s.Append("\ndocument.all['"+ClientID+"_Sel"+"'].value = val;"); s.Append("\n}\n"); s.Append("<" + "/" + "script>\n"); output.Write(s.ToString()); // draw our control base.Render(output); } bool IPostBackDataHandler.LoadPostData( string postDataKey, NameValueCollection postCollection) { string sel = postCollection[ClientID+"_Sel"]; string val = postCollection[ClientID+"_Val"]; if (sel == null || val == null) { this.SelectedIndex = -1; } else { this.Items[0].Value = sel; this.Items[0].Text = val; this.SelectedIndex = 0; } return false; } }

The only important thing to note is that we've implemented IPostBackDataHandler interface. This allows us to particiapate in the post back process. In the first override we hook in our client side script function as we did in the MasterDDL. The second override is just a little fudge. We override OnPreRender to create our two hidden fields on the client to hold our selected values. In Render() we emit the script that will be executed on the client when the user clicks on the detail DDL. And finally we implement LoadPostData() method so that we can retrieve the values from the hidden fields and setup the DetailDLLs values so the server code can use them.

In the updated version I've added a property so that the detail DDL can locate it's data island. In the previous version the selected 'value' was being returned but not the selected 'data'. In most cases this would not be a problem since what we're usually interested in is the ID of the selected item. In this updated version the detail DDL script locates it's data island, iterates to find the match for the selected item, and populates the hidden fields correctly.

Using the DropDownList

The demo project shows how to use the DLLs, not much different than regular ones. To use the Master/Detail DropDownList controls you'll need to add a reference to the MasterDetail.dll and add it to the Toolbox. The demo project makes use of the 'pubs' database but you can easily modify the code to use another database if that one is not available to you.

In the demo project you�ll also notice that I�ve hooked in the page load event to call the �doMasterClick()� method so that the detail DDLs are loaded when the page is initialized. The detail DDLs will be loaded according to the current selection in the master DDL.

One final note on the required ordering of the table for the slave DataSet. The code above expects the table to have three fields in the following order: SlaveItemID, MasterItemID, SlaveItem. I�ve left a commented section (from the original article) in the demo project that shows how you can do this through a join of two tables.

Bonus!

Did you know you can debug client side code as easily as server side? Select the Advanced tab from IE menu item Tools..InternetOptions. Make sure the 'Disable script debugging' item is cleared. Now when a page is loaded select View..ScriptDebugger..Open and you'll get a page with the source for the page. You can now set breakpoints, view variables, and single step through the code just like with the server code.

History

30 January 2003 - Original Submission.
22 February 2003 - Revised code to support any number of DetailDDLs, and revised test app accordingly.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here