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

前言:通常情况下,每个公司都会有自己的基础信息库,比如存储的省市区县等等。而在实际开发中,我们可能不止一次要用到全国省市区县三级联动的效果。下面我就总结一下自己在开发中用到的三级联动代码,包括数据库脚本,C#,IBatisNet和javascript实现的前后端代码,类似实现其实也同样可以扩展到三级类别的实现上。本文贴代码为主,有兴趣的可以下载示例看一下,也许对您有帮助。

 

1、开发环境和组织结构介绍

环境:VS2010+SqlServer2005Express+IBatisNet

area

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

 

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

areatest

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

infoq

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

 

demo下载:demo

你可能感兴趣的:(ibatis)