笔者日前涉及一个大型ASP项目的开发,其中多次遇到多维下拉菜单(对于WEB项目而言,这里专指网页中的<SELECT>元素)的问题,菜单中的数据均需要从数据库中取出,并动态的生成和变化。笔者以前曾发表过如何利用PHP和JavaScript制作二维下拉菜单的文章,目前这类文章在网络上也颇为多见,思路也有很多创新,但对于本文中谈到的多维下拉菜单,很少有人谈及。笔者无意班门弄斧,只是想把开发中的一点经验和技巧总结出来,希望能给广大的读者一点启示。
多维下拉菜单,顾名思义,也就是根据一个下拉菜单的选择,来控制其它一个或多个下拉菜单中显示的数据。举个例子来说明,在一个WEB管理系统中,用户要求通过选择单位名称,进而选择部门名称,最后选择员工。也就是说,需要提供三个下拉列表,每个下拉列表之间需要建立关联。通过第一个能选择第二个,并同时选择第三个,第四个等等。那么每一个下拉列表的显示数据之间如何建立关联,关联起来的数据又如何通过事件驱动,这正是本文所要讨论的主要内容。
熟悉VB、Delphi等RAD开发工具的朋友可能会感到疑惑。的确,在这些所谓的RAD开发工具中,我们可以利用Combo Boxes控件很容易的实现下拉菜单,进而实现他们之间的关联。但是由于WEB项目中的下拉菜单是利用HTML中的<SELECT>元素来实现的,而由于HTML的局限性,无论是对象的属性还是事件模型,都远没有RAD工具那么强大。所以,开发WEB项目,这种问题只能有一个解决途径,那就是利用JavaScript(也可能有人说可以利用java来实现,那我也只好说,您是高手)。
关于JavaScript的使用,不是本文的重点,所以不了解或不熟悉JavaScript的读者请先参考JavaScript的相关资料以获取相关信息。
好,言归正传,下面我们就一起来探讨多维下拉菜单的设计问题。为了讨论的方便,我们就以上文提到的三维下拉菜单为例,向大家一步一步的讲述设计的思路。
一、分析菜单的运作流程
首先,用户会选择单位列表,并从中选出一个单位名称,我们假定为单位A。这时,另外两个下拉列表应该做些什么?对,我们希望第二个下拉菜单能立即反映第一个下拉菜单的选择,显示并仅显示单位A中的所有部门,我们再假定菜单中第一项为部门A(默认的显示项)。那么可能有读者会问,第三个下拉菜单不是应该同时选择与部门A对应的员工数据吗?这是个很好的想法,是的,我们也应该立即改变第三个下拉菜单中的数据为部门A中的员工列表。同样,当用户选择部门时,又会改变员工列表。依次类推。
以上是一种思路,可能有一些特殊的情况,例如,在改变部门列表时,并不希望立即就选择一个部门,而是显示一个"请选择"字样的提示条目。
思路已经有了,下面就是如何实现的问题了。
二、菜单数据的容器
根据常规想法,当用户从第一个下拉菜单中选择单位名称,我们可以从数据库中选出与之相关的部门数据,并显示出来,这似乎也不无可行。但有经验的开发者就会发现,由于Web页面的无状态性,当你再次连接数据库时,Web页面必须得再次刷新。这是一个头疼的问题,一方面我们想连接数据库,可另一方面我们必须得保持用户已输入数据不被破坏。即使如此,估计用户也并不希望看到一个每次选择都刷新一次的局面。难道就没有更好的办法?
有,那就是利用JavaScript的多维数组。我们为什么不可以把需要显示的数据在第一次连接数据库时全取出来,放到数组中去?这样在每次改变菜单数据时,只要从数组中取得数据,不就可以大大的提高效率了吗?这是个令人振奋的方法,这个方法中提到的JavaScript数组,我们暂且称之为菜单数据的容器。
您的思路是不是一下子豁然开朗?可是跃跃欲试一番以后,是不是感到事情好像并不是那么简单?问题又来了,容器的结构该如何设计,数据之间的关联又如何实现呢?别急,其实这正是问题之所在。
三、数据容器结构的设计
说起容器结构的设计,我们得感谢数据结构中的链表给我们的启示--链表是通过指针联系在一起的。虽然JavaScript中没有指针的概念,但我们为什么不可以模拟一下。
为了讨论方便,我们假定数据库的结构如下:
1、 单位信息表:(unit_id, unit_name, …)
2、 部门信息表:(dept_id, unit_id, dept_name, …)
3、 员工信息表:(emp_id, dept_id, emp_name, …)
利用这个数据库结构,我们可以很容易的推导出数组的结构。您说的没错,这应该是一个多维数组。其定义方法应该象下面这样(以部门为例):
var arrDept = new Array();
arrDept[0] = new Array(unit_id0, dept_id0 dept_name0);
arrDept[1] = new Array(unit_id1, dept_id1, dept_name1);
…
arrDept[n] = new Array(unit_idn, dept_idn, dept_namen);
n的大小视实际数据量而定,例如在单位下拉菜单中,n代表单位的总数。但读者必须明白,正是由于n的不确定性,以上的代码必须通过程序动态的产生。例如对于ASP程序,我们可以在<script></script>之间嵌入这样的一段代码:
<%
Dim rs, i
'[连接数据库,取出数据]
response.write "var arrDept = new Array();" & vbNewLine
i = 0
while not rs.EOF
response.write "arrDept[" & i & "] = new Array('" & rs(unit_id) & "', '" & _
rs(dept_id) & "', '" & rs(dept_name) & "');" & vbNewLine
rs.MoveNext
i = i +1
wend
…
%>
代码拷贝框
<%
Dim rs, i
'[连接数据库,取出数据]
response.write "var arrDept = new Array();" & vbNewLine
i = 0
while not rs.EOF
response.write "arrDept[" & i & "] = new Array('" & rs(unit_id) & "', '" & _
rs(dept_id) & "', '" & rs(dept_name) & "');" & vbNewLine
rs.MoveNext
i = i +1
wend
…
%>
[Ctrl+A 全部选择 然后拷贝]
以上这段代码用来从部门表中取出数据,并产生相关的JavaScript多维数组。这只是笔者的一种演示,读者完全可以使用更灵活的方法来提取数据。
说来说去,我们还是要回到JavaScript数组的结构定义上来。聪明的读者应该已经从上述的代码中发现了数组的定义方法,但笔者还是要不厌其烦的再补充一遍:
我们把数组的第一个元素定义为指针,用来指向其"父结点"。等等,什么是父结点?父结点说明白了就是上一级结点,例如,部门的上一级是单位,员工的上一级是部门。那么第二个元素是什么?让我们来看一下下面的一段<SELECT>定义:
<SELECT NAME="s1" onChange=" SetSubMenu(this)">
<OPTION Value="1">单位1</OPTION>
<OPTION Value="2">单位2</OPTION>
….
</SELECT>
<OPTION>元素的Value属性从哪里来呢?对,就是第二个元素,依此类推,第三个元素指的就是显示在菜单中的数据喽,即上面的"单位1"、"单位2"…
读者到这里可能有些糊涂了,说这么多,这个数组到底是什么样?别急,让我们以部门为例,给出一段根据部门库中的数据动态生成的数组模拟代码:
<SCRIPT LANGUAGE="JAVASCRIPT">
<!-
…
var arrDept = new Array();
arrDept[0] = new Array('u01', 'd01', '部门1');
arrDept[1] = new Array('u01', 'd02', '部门2');
…
arrDept[8] = new Array('u06', 'd08', '部门8');
…
arrDept[15] = new Array('u08', 'd15', '部门15');
…
->
</SCRIPT>
数组终于真相大白。以"u"开头数据的代表单位的编号,即,指向单位的指针,也就是说,我们可以通过这个编号来确定该单位所属的部门;以"d"开头的数据代表部门的编号,用来供下一级选单(即员工选单)的指针使用。(注:实际使用中,数据格式根据情况而定)
有一个问题,象单位这样没有父结点的数组该如何定义?很简单,把数组的第一个元素全部置为0就行了。
下一步,是到我们编写JavaScript代码来控制菜单的显示的时候了。我们就假定您生成的三个数组分别命名为arrUnit,arrDept,arrEmp。
四、编写JavaScript代码,控制菜单的显示
其实有经验的程序员,读到这里应该知道如何进行下去。但您不妨读下去,也许,笔者的方法对您未必不是一种新的尝试。而且,据我猜测,读我这篇文章的大多数都是没有经验的程序员,呵呵,帮人帮到底吧。Come On, Let's Go.
让菜单显示出来,其实有好几种思路。利用ASP等程序直接生成<SELECT>结构、利用OPTION对象的ADD和Remove方法动态添加和改变等等,都是可以使用的方法。但,经过笔者的多次实践和摸索,有一种方法更为有效,那就是利用Script代码动态的改写整个<SELECT>框架。
好,就让我们从加载页面(document)开始,一步一步的讲解JavaScript代码到底是如何控制菜单的显示的。
既然有三个菜单,那么我们就得事先设计出这样的HTML代码(其实要不要无所谓,放在那里只是为了便于理解):
<BODY BGCOLOR="#FFFFFF" ONLOAD="body_onload()">
...
<TD>
<SELECT NAME="s0" ONCHANGE="SetSubmenu(this)"></SELECT>
</TD>
<TD>
<SELECT NAME="s1" ONCHANGE="SetSubmenu(this)"></SELECT>
</TD>
<TD>
<SELECT NAME="s2" ONCHANGE="SetSubmenu(this)"></SELECT>
</TD>
</BODY>
您有可能要问,这里怎么什么数据都没有?不要奇怪,等一下您自然就会明白。我们来看一下<BODY>对象的ONLOAD事件body_onload()做了些什么工作?
function body_onload(){
var TD = GetParent(document.all("s0"), "TD");
TD.innerHTML = MakeMenu(arrUnit, 0, 0, "s0", 1);
TD = GetParent(document.all("s1"), "TD");
TD.innerHTML = MakeMenu(arrDept, GetSelectValue(document.all("s0")), 0, "s1", 1);
TD = GetParent(document.all("s2"), "TD");
TD.innerHTML = MakeMenu(arrEmp, GetSelectValue(document.all("s1")), 0, "s2", 1);
}
让我们来研究一下。首先程序利用GetParent()函数取得s0的容器TD对象句柄,然后,利用MakMenu()函数产生菜单代码,并把代码赋值给刚才取得的TD对象;然后是s1,接着是s2.。GetParent()函数定义如下:
function GetParent(src, tag){
if (src && src.tagName!=tag){
return(GetParent(src.parentElement, tag));
}
return src;
}
这里的tag参数必须大写,例如TD、TR、TABLE,函数返回的是离src指定的元素最近的由tag标签定义的父对象。
我们要特别说明一下MakeMenu()函数,这个函数的作用不言而喻--产生菜单的HTML定义,先看看函数定义:
function MakeMenu(arrSub, pValue, cValue, name, bulSkip){
var sHTML = "<select name='" + name + "' onchange='SetSubMenu(this)' >";
if (bulSkip) sHTML += "<option value=0><未选择></option>";
for (var i=0; i < arrSub.length; i++){
if (arrSub[i][0]==pValue){
var tag = (arrSub[i][1]==cValue)?" selected>":">";
sHTML += "<option value='" + arrSub[i][1] + "'" + tag + arrSub[i][2] + "</option>";
}
}
sHTML += "</select>";
return sHTML;
}
代码拷贝框
function MakeMenu(arrSub, pValue, cValue, name, bulSkip){
var sHTML = "<select name='" + name + "' onchange='SetSubMenu(this)' >";
if (bulSkip) sHTML += "<option value=0><未选择></option>";
for (var i=0; i < arrSub.length; i++){
if (arrSub[i][0]==pValue){
var tag = (arrSub[i][1]==cValue)?" selected>":">";
sHTML += "<option value='" + arrSub[i][1] + "'" + tag + arrSub[i][2] + "</option>";
}
}
sHTML += "</select>";
return sHTML;
}
[Ctrl+A 全部选择 然后拷贝]
来看一下参数的含义。arrSub,指的是菜单数据的来源,其实就是我们上文定义的数组;pValue,指定父结点的编号,根据这个编号,我们可以找出所有的子结点数据;cValue,指定菜单的默认显示项;name,指定产生的<SELECT>菜单的