Optimized Master/Detail DropDownList






3.93/5 (11 votes)
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.