iBatisNet实现全国省市区县三级联动
前言:通常情况下,每个公司都会有自己的基础信息库,比如存储的省市区县等等。而在实际开发中,我们可能不止一次要用到全国省市区县三级联动的效果。下面我就总结一下自己在开发中用到的三级联动代码,包括数据库脚本,C#,IBatisNet和javascript实现的前后端代码,类似实现其实也同样可以扩展到三级类别的实现上。本文贴代码为主,有兴趣的可以下载示例看一下,也许对您有帮助。
1、开发环境和组织结构介绍
环境:VS2010+SqlServer2005Express+IBatisNet
如果您已经熟悉本文作者一贯的编码风格,应该已经猜到了各项目及各个文件夹的大概作用。
2、SQL Server数据库脚本
上图AreaDAL项目下,DataBase文件夹内包含着一个Area.sql脚本文件。这个脚本我是从网上下载然后稍作部分列名和字段的改进。原文url已经不记得了,对原作者表示非常抱歉。请注意,数据库中我国的台湾省缺少对应的城市和区县。
3、javascript脚本
在客户端实现联动的效果,我们可以通过下面的两种途径实现
(1)、二维数组
一个省份对应的城市的javascript二维数组结构是这样的:
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实现中)是在服务端(页面类文件)通过下面的形式注册的:
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的填充而已:
//必须先引用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数据格式如下所示:
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数组结构发生了变化:
//必须先引用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中页面静态字段造成的问题,大家可能更加不信任这种方案。我觉得某些情况下可以使用静态变量来缓存,尤其是对于那些只读而且永远不会过期的数据。静态变量有一个非常突出的好处,就是对于开发者而言,就是定义一处静态变量,系统全局都可以调用,不用担心它“不翼而飞”,而对于所有使用的用户来说,他们所使用的都是同一份数据:
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点)定时更新静态字段:
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的时候,点击浏览器的后退按钮,发现它的国家对应省份不见了,这好像就有问题了,不知道是不是浏览器的问题:
(最下面的必填项:州/省…数据丢失了)
demo下载:demo
作者:Jeff Wong
出处:http://jeffwongishandsome.cnblogs.com/
本文版权归作者和博客园共有,欢迎围观转载。转载时请您务必在文章明显位置给出原文链接,谢谢您的合作。