1、开发环境和组织结构介绍
环境:VS2010+SqlServer2005Express+IBatisNet
如果您已经熟悉本文作者一贯的编码风格,应该已经猜到了各项目及各个文件夹的大概作用。
2、SQL Server数据库脚本
上图AreaDAL项目下,DataBase文件夹内包含着一个Area.sql脚本文件。这个脚本我是从网上下载然后稍作部分列名和字段的改进。原文url已经不记得了,对原作者表示非常抱歉。请注意,数据库中我国的台湾省缺少对应的城市和区县。
3、javascript脚本
在客户端实现联动的效果,我们可以通过下面的两种途径实现
(1)、二维数组
一个省份对应的城市的javascript二维数组结构是这样的:
1
2
3
4
|
var
provinceCityArr =
new
Array(10);
var
provinceId = 1234;
// 江苏id
var
cityArr =
new
Array(
'3456'
,
'南京'
,
'3457'
,
'苏州'
);
//数字是城市id,汉字是城市名称
provinceCityArr[0] =
new
Array(provinceId, cityArr);
//一个provinceId 对应一个city数组
|
一个城市和对应的区县类同上面的一个省份对应的城市(如果javascript可以有c#那样现成的数据结构如哈希表或者字典等等,这一方面的工作将非常简单)。
当我们选择不同的省份或者不同的城市的时候,就会触发这两个事件,这两个事件(包括下面介绍的json实现中)是在服务端(页面类文件)通过下面的形式注册的:
1
2
|
this
.ddlProvince.Attributes.Add(
"onchange"
,
"displayFirst("
+
this
.SelectFirstId +
", "
+
this
.SelectSecondId +
","
+
this
.SelectThirdId +
") "
);
this
.ddlCity.Attributes.Add(
"onchange"
,
"displaySecond("
+
this
.SelectSecondId +
", "
+
this
.SelectThirdId +
")"
);
|
而核心的两个javascript函数displayFirst和displaySecond就是对数组的遍历匹配和控件option的填充而已:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
//必须先引用selectUtil.js
/*
第一层级联
参数说明:
oSelectFirst:第一个select控件
oSelectSecond:第二个select控件
oSelectThird:第三个select控件
oArr:注册在客户端的第一个级联数组
可选参数:
oSecondValue:第一层级联的关联值 比如某一省份下对应选中的城市值
*/
function
displayFirst(oSelectFirst, oSelectSecond, oSelectThird, oArr) {
try
{
var
parentKey = oSelectFirst.options[oSelectFirst.selectedIndex].value;
if
(oArr.length > 0) {
if
(parentKey.length > 0) {
//说明选择的不是“请选择”
for
(
var
ii = 0; ii < oArr.length; ii++) {
var
item = oArr[ii];
if
(parseInt(item[0]) == parseInt(parentKey)) {
var
arrItems = item[1];
removeSelItems(oSelectSecond);
selectOptionAdd(oSelectSecond,
"请选择"
,
""
);
for
(
var
i = 0; i < arrItems.length / 2; i++) {
selectOptionAdd(oSelectSecond, arrItems[i * 2 + 1], arrItems[i * 2] +
"_"
+ arrItems[i * 2 + 1]);
// key_value
//selectOptionAdd(oSelectSecond, arrItems[i * 2 + 1], arrItems[i * 2]);//key
}
if
(arguments.length > 4) {
oSelectSecond.value = arguments[4];
//初始化选项用
}
break
;
}
}
}
else
{
removeSelItems(oSelectSecond);
selectOptionAdd(oSelectSecond,
"请选择"
,
""
);
}
removeSelItems(oSelectThird);
selectOptionAdd(oSelectThird,
"请选择"
,
""
);
}
}
catch
(e) {
alert(e.message);
}
}
/*
第二层级联
参数说明:
oSelectFirst:第一个select控件 (这里对应城市)
oSelectSecond:第二个select控件(这里对应区县)
oArr:注册在客户端的第二个级联数组
可选参数:
oThirdValue:第二层级联的关联值 比如某一城市下对应选中的区县值
*/
function
displaySecond(oSelectFirst, oSelectSecond, oArr) {
try
{
var
parentKey = oSelectFirst.options[oSelectFirst.selectedIndex].value.split(
'_'
)[0];
if
(oArr.length > 0) {
if
(parentKey.length > 0) {
//说明选择的不是“请选择”
for
(
var
ii = 0; ii < oArr.length; ii++) {
var
item = oArr[ii];
if
(parseInt(item[0]) == parseInt(parentKey)) {
var
arrItems = item[1];
removeSelItems(oSelectSecond);
selectOptionAdd(oSelectSecond,
"请选择"
,
""
);
for
(
var
i = 0; i < arrItems.length / 2; i++) {
selectOptionAdd(oSelectSecond, arrItems[i * 2 + 1], arrItems[i * 2] +
"_"
+ arrItems[i * 2 + 1]);
//key_value
//selectOptionAdd(oSelectSecond, arrItems[i * 2 + 1], arrItems[i * 2]);//key
}
if
(arguments.length > 3) {
oSelectSecond.value = arguments[3];
//初始化选项用
}
break
;
}
}
}
else
{
removeSelItems(oSelectSecond);
selectOptionAdd(oSelectSecond,
"请选择"
,
""
);
}
}
}
catch
(e) {
alert(e.message);
}
}
|
说明一下,上面注释中的selectUtil.js文件是一个对select控件操作的函数集合文件,您可以参考这一篇。
(2)、json
当我们选择省份的时候,都会借助jQuery发出一个同步ajax请求,返回省份对应的城市区县json。我们在服务端组织好的json数据格式如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
var
areaJson =
{
"ProvinceId"
: 1234,
"ProvinceName"
:
"江苏"
,
"Cities"
:
// 数组
[
{
"CityId"
: 3456,
"CityName"
:
"南京"
,
"Counties"
:
[
{
"CountyId"
: 34560,
"CountyName"
:
"玄武区"
},
{
"CountyId"
: 34561,
"CountyName"
:
"秦淮区"
}
]
},
{
"CityId"
: 3457,
"CityName"
:
"苏州"
,
"Counties"
:
[
{
"CountyId"
: 34570,
"CountyName"
:
"吴中区"
},
{
"CountyId"
: 34571,
"CountyName"
:
"昆山市"
}
]
}
]
}
|
其实,在我实现的代码里,在实现省份对应城市,城市对应区县的时候,json最终还是映射成数组,然后按照选中的option,给关联的select控件填充数据,和(1)非常类似,但是js数组结构发生了变化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
|
//必须先引用jQuery.js和selectUtil.js
var
jqRequestUrl =
"/Handler/AreaHandler.ashx"
;
var
areaJson =
null
;
//级联json
/*
第一层级联
参数说明:
oSelectFirst:第一个select控件
oSelectSecond:第二个select控件
oSelectThird:第三个select控件
可选参数:
oSecondValue:第一层级联的关联值 比如某一省份下对应选中的城市值
*/
function
displayFirst(oSelectFirst, oSelectSecond, oSelectThird) {
try
{
var
parentKey = oSelectFirst.options[oSelectFirst.selectedIndex].value;
if
(String(parentKey).length > 0) {
getAreaJson(parentKey);
}
var
oArr =
null
;
if
(areaJson !=
null
) {
oArr = areaJson.Cities;
//城市
}
if
(parentKey.length > 0) {
//说明选择的不是“请选择”
if
(oArr !=
null
) {
removeSelItems(oSelectSecond);
selectOptionAdd(oSelectSecond,
"请选择"
,
""
);
for
(
var
ii = 0; ii < oArr.length; ii++) {
var
item = oArr[ii];
var
cityId = item.CityId;
var
cityName = item.CityName;
selectOptionAdd(oSelectSecond, cityName, cityId +
"_"
+ cityName);
// key_value
//selectOptionAdd(oSelectSecond,cityName, cityId);//key
}
if
(arguments.length > 3) {
oSelectSecond.value = arguments[3];
//初始化选项用
}
}
else
{
removeSelItems(oSelectSecond);
selectOptionAdd(oSelectSecond,
"请选择"
,
""
);
}
}
else
{
removeSelItems(oSelectSecond);
selectOptionAdd(oSelectSecond,
"请选择"
,
""
);
}
removeSelItems(oSelectThird);
selectOptionAdd(oSelectThird,
"请选择"
,
""
);
}
catch
(e) {
alert(e.message);
}
}
/*
第二层级联
参数说明:
oSelectFirst:第一个select控件 (这里对应城市)
oSelectSecond:第二个select控件(这里对应区县)
可选参数:
oThirdValue:第二层级联的关联值 比如某一城市下对应选中的区县值
*/
function
displaySecond(oSelectFirst, oSelectSecond) {
try
{
var
parentKey = oSelectFirst.options[oSelectFirst.selectedIndex].value.split(
'_'
)[0];
var
oArr =
null
;
if
(areaJson !=
null
) {
var
oFirstArr = areaJson.Cities;
//城市
for
(
var
i = 0; i < oFirstArr.length; i++) {
if
(oFirstArr[i].CityId == parentKey) {
oArr = oFirstArr[i].Counties;
//区县
break
;
}
}
}
if
(parentKey.length > 0) {
//说明选择的不是“请选择”
if
(oArr !=
null
) {
removeSelItems(oSelectSecond);
selectOptionAdd(oSelectSecond,
"请选择"
,
""
);
for
(
var
ii = 0; ii < oArr.length; ii++) {
var
item = oArr[ii];
var
countyId = item.CountyId;
var
countyName = item.CountyName;
selectOptionAdd(oSelectSecond, countyName, countyId +
"_"
+ countyName);
//key_value
//selectOptionAdd(oSelectSecond, countyName, countyId);//key
}
if
(arguments.length > 2) {
oSelectSecond.value = arguments[2];
//初始化选项用
}
}
else
{
removeSelItems(oSelectSecond);
selectOptionAdd(oSelectSecond,
"请选择"
,
""
);
}
}
else
{
removeSelItems(oSelectSecond);
selectOptionAdd(oSelectSecond,
"请选择"
,
""
);
}
}
catch
(e) {
alert(e.message);
}
}
//从服务端取json数据
function
getAreaJson(provinceId) {
//第一个参数表示同步调用
$.ajax({
async:
false
,
cache:
false
,
type:
"POST"
,
url: jqRequestUrl,
data:
"action=getareajson&provinceid="
+ provinceId,
success:
function
(html) {
areaJson = eval(
'('
+ html +
')'
);
//eval转换成json
}
});
}
|
比较起来,应该比(1)更方便一些,而且效率应该比(1)高,因为遍历的次数大大减少了。
大家可以运行代码试试看,点击”Get Value”按钮,在服务端会输出您选择的数据:
其实,在客户端脚本编程中,我们大部分精力都花在初始化(通过cascadeInit函数)和数据节点匹配以及填充上。
4、C#和IBatisNet实现部分
主要是通过IBatisNet作为ORM进行数据层的操作,通过IBatis取数据等等具体细节我就不具体介绍了,这里主要来谈谈对取出数据的缓存。我们发现省市区县这类数据的一个重要特点就是它们相对稳定,变更的情况很少,而且不是敏感的数据,数据量说多也不多,说少也不少,合理利用缓存可以提升系统性能。下面就简单总结一下项目中对于不常改动的基础数据如省市县,品类等等的可以采取的几种不同的缓存方案:
(1)、静态字段缓存
用static变量进行缓存(其实我们大多时候缓存的是一个引用类型,变量存放的只是一个指针引用),大家可能会觉得不可行,尤其是知道asp.net中页面静态字段造成的问题,大家可能更加不信任这种方案。我觉得某些情况下可以使用静态变量来缓存,尤其是对于那些只读而且永远不会过期的数据。静态变量有一个非常突出的好处,就是对于开发者而言,就是定义一处静态变量,系统全局都可以调用,不用担心它“不翼而飞”,而对于所有使用的用户来说,他们所使用的都是同一份数据:
1
2
3
4
|
public
static
IList<Province> listProvinces =
null
;
//省份 对外公开
private
static
IDictionary<
int
, Province> dictProvinces =
null
;
private
static
IDictionary<
int
, City> dictCities =
null
;
private
static
IDictionary<
int
, County> dictCounties =
null
;
|
但是,静态字段缓存的一个缺点就是,它没有提供缓存过期方案,也没有线程安全机制,一旦创建后,静态变量不能被回收(但是可以将它置为null空引用,它所引用的数据对象就可以被GC回收)。还好,通过静态字段缓存大多数情况下不用考虑线程安全,因为多数情况下都是只读的数据,不会发生不一致的情况,而对于缓存过期方案,我们可以利用一个定时器代替,当然这和asp.net所提供的缓存过期方案是完全不同的,比如考虑到可能对数据进行的小部分修改,本文的程序中,我在里面加了个Timer,控制某一时刻(凌晨3点)定时更新静态字段:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
public
const
long
timerPeriodTime = 1000 * 60 * 60 * 1;
//每小时timer触发一次
private
static
System.Timers.Timer areaTimer =
null
;
/// <summary>
/// 设置timer 每天凌晨3点重新访问数据库 获取相关数据
/// </summary>
private
static
void
TimerSetUp()
{
areaTimer =
new
System.Timers.Timer();
areaTimer.Elapsed +=
new
ElapsedEventHandler(TimerArea_Elapsed);
//附加公告事件
areaTimer.Interval = timerPeriodTime;
areaTimer.Enabled =
true
;
/*当 AutoReset 设置为 false 时,Timer 只在第一个 Interval 过后引发一次 Elapsed 事件。
若要保持以 Interval 时间间隔引发 Elapsed 事件,请将 AutoReset 设置为 true。*/
areaTimer.AutoReset =
true
;
}
/// <summary>
/// 定时更新
/// </summary>
/// <param name="sender"></param>
private
static
void
TimerArea_Elapsed(
object
sender, ElapsedEventArgs e)
{
if
(DateTime.Now.Hour % 24 != 3)
//凌晨3点访问数据库
{
return
;
}
try
{
WaitCallback wcb =
new
WaitCallback(AsyncInitArea);
int
workerThreads, availabeThreads;
ThreadPool.GetAvailableThreads(
out
workerThreads,
out
availabeThreads);
if
(workerThreads > 0)
//可用线程数>0
{
ThreadPool.QueueUserWorkItem(wcb,
"定时获取地区信息"
);
//异步
}
else
{
InitArea();
}
}
catch
(Exception ex)
{
throw
ex;
}
}
private
static
void
AsyncInitArea(
object
objState)
{
InitArea();
}
|
当然,如果利用下面(2)介绍asp.net的应用程序缓存,这个Timer完全用不到,可以省去上面的代码。
(2)、asp.net缓存
大家都知道asp.net缓存有几种不同的类别。通常我们可以借助System.Web.HttpRuntime.Cache或者System.Web.Caching.Cache进行数据缓存处理。这种缓存方案也就是asp.net的应用程序数据缓存,它的优势在于它向开发人员提供了完整的依赖、过期、线程同步等的支持,也是我们最常见也最放心使用的。
(3)、IBatisNet缓存
IBatisNet自生带有缓存功能,但是需要修改配置文件,还要改进取数据的代码,而且有时候它还不稳定。虽然这是一个可供选择的解决方案,但是开发中相对用的很少,毕竟我们对缓存的控制欲常常达到了“丧心病狂”的地步,而不会过分信任这种包装过的缓存方式。
(4)、分布式缓存系统
分布式缓存,顾名思义,从表面理解,就是一台机器内存不够,将要缓存的数据分布到不同机器上存储,同时它必须确保数据不能丢失,在多台机器上都有备份。需要注意的是,缓存的数据需要序列化和反序列化,比较折腾cpu的运算能力,还有就是在网络环境下进行数据传输会占用带宽,所以对于一个大量的数据集合,通常都分割存储成多个key:value字典的形式,而不是一个key,一个大量的数据集合(我曾经在使用Memcached的过程中犯过这样的失误)。
通常,对于流量较大的大中型站点,有条件的话,几乎都会借助于分布式缓存系统如Memcached、MongoDB等等。而在本文示例中,我选择了最简单静态字段缓存。实际项目中,(2)和(4)用的相对多一些,当然通过IBatisNet的缓存也是没有问题的。
最后需要说明的是,这个联动效果我在IE9、最新的FireFox和Chrome上测试全部通过,其他浏览器(或不同版本)没有测试。
update:我刚刚注册完InfoQ的时候,点击浏览器的后退按钮,发现它的国家对应省份不见了,这好像就有问题了,不知道是不是浏览器的问题:
(最下面的必填项:州/省…数据丢失了)
本文转自JeffWong博客园博客,原文链接:http://www.cnblogs.com/jeffwongishandsome/archive/2010/11/14/1876993.html,如需转载请自行联系原作者