iBatisNet实现全国省市区县三级联动

本文介绍了一种基于JavaScript和JSON的省市县三级联动效果实现方法,并探讨了使用静态字段、ASP.NET缓存等多种缓存技术来提高系统性能。
1、开发环境和组织结构介绍

环境:VS2010+SqlServer2005Express+IBatisNet

area

如果您已经熟悉本文作者一贯的编码风格,应该已经猜到了各项目及各个文件夹的大概作用。

 

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”按钮,在服务端会输出您选择的数据:

areatest

其实,在客户端脚本编程中,我们大部分精力都花在初始化(通过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的时候,点击浏览器的后退按钮,发现它的国家对应省份不见了,这好像就有问题了,不知道是不是浏览器的问题:

infoq

(最下面的必填项:州/省…数据丢失了)








本文转自JeffWong博客园博客,原文链接:http://www.cnblogs.com/jeffwongishandsome/archive/2010/11/14/1876993.html,如需转载请自行联系原作者


IBatisNet.DataMapper 1.6.1.0之简单三层 首先介绍Solution的架构,一共分四个项目: Web :前台应用; BLL :业务处理层,如果嫌麻烦,可以再进一步抽象出来,然后将代码放置Web层的aspx.cs里面; Model :这个就不多说了,地球人都知道(数据实体类) SqlMaps:资源类,把配置文件和sql mapper文件全部打包起来。 二个文件夹: Include :iBatis.Net架构的dll文件 Log :放置log文件的文件夹,在web.config里配置 1.在一些特定的环境下,一站式的解决方案未必有效 系统的部分或全部数据来自现有数据库,处于安全考虑,只对开发团队提供几条Select SQL(或存储过程)以获取所需数据,具体的表结构不予公开。 开发规范中要求,所有牵涉到业务逻辑部分的数据库操作,必须在数据库层由存储过程实现。(银行大多有这样的限制) 系统数据处理量巨大,性能要求极为苛刻,这往往意味着我们必须通过经过高度优化的SQL语句(或存储过程)才能达到系统性能设计指标。 2.iBATIS之于小型、简单系统:非常适用 iBATIS自己就很小并且简单 iBATIS不会对现存应用的设计或者数据库结构强加任何影响 iBATIS非常适合于有成长趋势的系统 3.iBATIS之于大型、企业级系统:为之而设计 iBATIS的某些特性使得它能够高效地处理大型数据集 iBATIS允许你用多种方式建立从对象到数据库的映射关系 MySpace已应用 4.使用于任何类型的关系数据库: 应用数据库 企业数据库 私有数据库 遗留数据库 简单性 性能 明确分工 可移植性:Java、.Net或者其他 开源 5.何时不该使用iBATIS 当你能永远拥有完全控制权 当你的应用需要完全动态的SQL 当你并没有使用关系数据库时 当iBATIS不起作用时
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值