个人工具

Combo 示例

Kevin

1. 问题

在我们的日常生活中,经常遇到过这样的页面:

当你对“所在省”进行操作以选择不同的省份时,“所在城市”的下拉列表将根据你选择的省份,自动列出该省份的所有城市。 这种场景我们已经司空见惯,并且,也有成套的解决方案。但将所有的解决方案都摆到桌面上,却发觉,这些解决方案并不是最优的,也不见得是完美的, 而且,其可扩展性也未必好。 举个最简单的例子,倘若再加一个下拉列表“国家”,并且需要“国家”、“省份”、“城市”三者进行联动,我们的解决方案是否能够自行适应呢? 再或者,倘若是“国家”、“省份”、“城市”、“区县”四者联动呢?甚至于当你选择某个直辖市时,要求“城市”所在的下拉列表变成readonly的时候, 我们现有的解决方案是否能够适应呢?

2. 常规解决方案

让我们首先来看看常规解决方案是怎样解决此问题的吧。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script language="JavaScript">
1function change_area() {
var cmbProvince = document.getElementById("province");
var cmbCity = document.getElementById("city");
var length = cmbCity.options.length;
for(var i=length-1; i>=0; i--) {
cmbCity.options[i] = null;
}
var province = cmbProvince.options[cmbProvince.selectedIndex].value;
if(province == "北京") {
cmbCity.options[0] = new Option("北京", "北京");
}
if(province == "上海") {
cmbCity.options[0] = new Option("上海", "上海");
}
if(province == "广东") {
cmbCity.options[0] = new Option("广州", "广州");
cmbCity.options[1] = new Option("深圳", "深圳");
cmbCity.options[2] = new Option("阳江", "阳江");
}
if(province == "安徽") {
cmbCity.options[0] = new Option("合肥", "合肥");
cmbCity.options[1] = new Option("黄山", "黄山");
cmbCity.options[2] = new Option("宿州", "宿州");
}
}
</script>
<title>选择地区</title>
</head>
<body>
<form method="post">
<%
2String province = "北京";
String city = "北京";
if (request.getParameter("province") != null) province = request.getParameter("province");
if (request.getParameter("city") != null) city = request.getParameter("city");
out.println("<h2>你现在选择的是:" + province + "," + city + "</h2>");
%>
<br> 3省:<select onchange="javascript:change_area();" id="province" name="province">
<option value="北京">北京</option>
<option value="上海">上海</option>
<option value="广东">广东</option>
<option value="安徽">安徽</option>
</select> 市: <select id="city" name="city">
<option value="北京">北京</option>
</select>
<br>
<script>
4var cmbProvince = document.getElementById("province");
var cmbCity = document.getElementById("city");
for(var i=0; i<cmbProvince.options.length; i++) {
if(cmbProvince.options[i].value == '<%=province%>') {
cmbProvince.options[i].selected = true;
change_area();
}
}
for(var i=0; i<cmbCity.options.length; i++) {
if(cmbCity.options[i].value == '<%=city%>') {
cmbCity.options[i].selected = true;
}
}
</script>
<input type="submit" value="提交">
</form>
</body>
</html>
1

Java Script函数,在此函数中根据用户选择不同的省以决定右边的下拉列表框应该列出不同的市

1

Java代码片断,根据Request中取得的参数,获得用户选择的省、市。

1

定义下拉列表框,并且在第一个下拉列表控件上设置当其值发生变化时,调用change_area函数

1

Java Script代码,根据Request中取得的参数,设置下拉列表的值为用户当前选择值

首先我们必须要指出的是,上述代码是最简易最直白的方式,我们当然可以对其进行优化,譬如:Java Script的生成可以通过后台Java Bean做到,这样可以将业务数据往后台放(无疑,省、市这些数据是业务数据);再譬如, 我们还可以通过Struts完成对Request参数的解析,以使其披上一层所谓的 MVC 的外衣。但无论怎么优化,我们始终无法解决两个根本问题:开发效率低,可扩展性差。

2.1. 开发效率低

上述常规解决方案的开发效率无疑是非常低下的,因为其本质就是通过Java代码片断拼凑HTML与Java Script。组件技术是提升软件复用度进而提升软件开发效率的有效手段,而在上述代码中,我们根本就没能发现任何组件的思想,其代码臃肿度也可见一斑。

2.2. 可扩展性差

我们不妨简单的设问一下自己,倘若我们在页面中再加一个下拉列表框“国家”,那应该怎么办呢?整个页面基本上要重新来过,其可扩展性是非常糟糕的。 再举个简单的例子,倘若你要做成中英文的,中文显示“北京”,英文显示“BeiJing”,那你又应该怎么办?最终你会发觉, 你的代码将臃肿到无法维护的程度。

3. 美丽的新世界:Opera Masks

我们来看看,如果上述程序用Opera Masks技术来进行改造,那么,程序又会变成什么样子:

<%@ page language="java" contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%>
1<%@taglib uri="http://java.sun.com/jsf/core" prefix="f"%>
<%@taglib uri="http://www.apusic.com/jsf/widget" prefix="w"%>
<%@taglib uri="http://www.apusic.com/jsf/layout" prefix="layout"%>
<%@taglib uri="http://www.apusic.com/jsf/ajax" prefix="ajax"%>
<%@taglib uri="http://java.sun.com/jsf/html" prefix="h"%>
2<f:view renderKitId="AJAX">
<w:head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </w:head>
<w:page title="选择地区">
<w:form>
3<layout:panelGrid columns="4">
<layout:cell colspan="4" rowspan="1">
<h2><h:outputText value="#{areaBean.display}"></h:outputText></h2>
</layout:cell>
<layout:cell colspan="1" rowspan="1">省</layout:cell>
<layout:cell colspan="1" rowspan="1">
4<w:combo id="province" value="#{areaBean.province}" typeAhead="true">
5<ajax:action event="onchange"></ajax:action>
<f:selectItems value="#{areaBean.provinces}" />
</w:combo>
</layout:cell>
<layout:cell colspan="1" rowspan="1">市</layout:cell>
<layout:cell colspan="1" rowspan="1">
6<w:combo id="city" binding="#{areaBean.cmbCity}" typeAhead="true">
<f:selectItems value="#{areaBean.cities}" />
</w:combo>
</layout:cell>
<layout:cell colspan="4" rowspan="1">
<w:button value="提交" />
</layout:cell>
</layout:panelGrid>
</w:form>
</w:page>
</f:view>
1

AOM(Apusic OperaMasks)所要用到的taglib的声明。如果你是用 Apusic Studio设计此页面,那么,你根本无需关心这个,Studio会自动帮你生成。

1

<f:view>代表一个视图,请注意观察后续一个参数:reanderKitId="AJAX",这意味着,整个视图的交互是基于AJAX的交互模式。

1

这里用的是一个布局组件<layout:panelGrid>,可以把它想像成一个Table的替代品。

1

下拉列表框<w:combo>,请注意,其取值来自一个后台的 Managed Bean(稍候介绍)

1

定义一个 ajax 事件响应,此处指:当combo的值发生变化时,向服务器发送一个ajax事件,并将变化的值反应到服务器端模型。

1

定义下拉列表框(城市),注意,该下拉列表框绑定在后台areaBean的一个属性:cmbCity上。

package demo;

1@ManagedBean(name="areaBean", scope=ManagedBeanScope.SESSION)
public class AreaBean {

@ManagedProperty
2private String province = "北京";

@ManagedProperty
3private UICombo cmbCity;

@ManagedProperty
4private String[] provinces = new String[]{
"北京", "上海", "广东", "安徽"
};

private Map<String, String[]> cities = new HashMap<String, String[]>();

public AreaBean() {
super();
cities.put("广东", new String[]{"广州", "深圳", "清远", "阳江"});
cities.put("安徽", new String[]{"合肥", "黄山", "宿州", "蚌埠"});
cities.put("北京", new String[]{"北京"});
cities.put("上海", new String[]{"上海"});
}

5public String[] getCities() {
return cities.get(this.province);
}

6public void setCmbCity(UICombo cmbCity) {
this.cmbCity = cmbCity;
if(cmbCity != null) cmbCity.setValue("北京");
}

public String getDisplay() {
return "你现在选择的是:" + province + "," + this.cmbCity.getValue();
}

7public void setProvince(String province) {
if(this.province.equals(province)) return;
this.province = province;
if("北京".equals(province) || "上海".equals(province)) {
this.cmbCity.setReadonly(true);
}
else {
this.cmbCity.setReadonly(false);
}
this.cmbCity.setValue(getCities()[0]);
}
}
1

此处声明一个托管Bean(Managed Bean),其生命周期是 session 级别

1

此处的 province 是一个托管属性,意味着它可以被页面(view)上的某些组件用来取值。在view中有一个<w:combo value="#{areaBean.province}">的声明,代表该组件的值取来自于此属性。

1

此属性也是一个托管属性,与 province 不一样的是,它代表着 view 中的 <w:combo binding="#{areaBean.cmbCity}">这个组件,换言之,它是该组件的服务器端对象。

1

此属性也是一个托管属性,“省”所在的 combo 组件的值便来自于此属性

1

第二个combo组件(城市)的值来自于此方法

1

cmbCity,即view中的第二个combo组件(城市)的服务器端对象,它是由容器负责初始化的。我们在此处设置其初始值。

1

当 ajax 事件发送到服务器端时,会调用 setProvince 方法,我们在此方法中,设置 cmbCity 对象的值及相关状态。

4. 点评

初一看改造后的示例,感觉代码量好像并没有少许多,但仔细分析下来,你会发觉,有如下优势:

4.1. 更好的MVC:页面(view)与业务模型(business model)的高度解耦

在常规解决方案中,UI部分与后台的业务模型是耦合在一起的,这也是造成扩展性差、维护成本高的主要原因。“MVC”已经是目前过度泛滥的词汇之一, 但事实上,很多 web 开发解决方案并没有彻底的解决此问题。以本文描述的场景为例,当需要动态生成 java script 以达到更好的客户端交互的时候,Struts 便无能为力。

4.2. 开发效率的大幅提升

我相信,绝大部分的 J2EE 开发人员看了第二种解决方案后,都不会再对第一种解决方案抱有好感,究其原因无非是第二种开发方案能够带来开发效率的大幅提升。组件模型、服务器端事件,都给开发人员带来了巨大的便利, 再辅以Apusic Studio可设化的设计,一体化的过程,相信这样一种开发方法,会使每一位 J2EE 开发人员都感到轻松与方便。

4.3. 可扩展性大大增强

如果我们需要再加一个下拉列表“国家”,并且需要“国家”、“省份”、“城市”三者进行联动,甚至加更多的下拉列表,或者其它的功能扩展,我们只需在 view 中对 UI进行重新设计,再在后台补充相应的业务逻辑与事件响应,一切尽在掌握。

4.4. 其它

事实上,第二种方案还有许多其它增强特性,譬如:我们可以设置两个下拉列表框具备“auto lookup”的功能,即,当你在下拉列表框中输入“北”, 会自动定位到“北京”并加以选中等。而这些功能,都是第一种方案所无法比拟的。

5. 更多参考

介绍了这么多,你还在等什么?赶快下载 OperaMasks,让我们立即开始体验吧!

經典

张贴人: 网上蚊子 2007-12-27 11:11

Kevin大哥能多出些文章和教程嗎? 支持您!

能给个三级菜单的例子吗?

张贴人: 邓亮 2008-01-09 18:04

怎么做感觉怎么都有问题!

能给个三级菜单的例子吗?

张贴人: 邓亮 2008-01-09 18:05

绝对不是“只需在 view 中对 UI进行重新设计,再在后台补充相应的业务逻辑与事件响应,一切尽在掌握”这么简单。

这个跟ajax4j有什么区别

张贴人: airlulu 2008-01-18 10:47

除了通过annotation声明managed-bean(这个又跟seam有什么区别) 感觉这个示例就是seam+richfaces