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)
{
if (m_SlaveData.Length != 0)
{
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");
}
StringBuilder s = new StringBuilder("\n<script language="JavaScript">\n");
s.Append("function doMasterClick()\n");
s.Append("{\n");
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");
s.Append("var sel = document.all['" + this.ID + "'].value;\n");
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");
s.Append("while(document.all[theSlaveDDLs[i]].length)\n");
s.Append("document.all[theSlaveDDLs[i]].options[0] = (null,null);\n");
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");
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");
s.Append("optCount = optCount+1;\n");
s.Append("}\n");
s.Append("}\n");
s.Append("}\n");
s.Append("}\n");
s.Append("<" + "/" + "script>\n");
output.Write(s.ToString());
}
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());
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.