ASPX属性菜单--ASTreeView
这篇写的主要是应用系统中目录树的配置模块,提起目录结构,就会想到树组件或控件,比如.NET自带的TreeView控件,拖到WEB页面上,给她个数据源也能出来结构,对于节点不多的情况下也还是可以的。不过相比MSDN那样动态加载的的导航树,这种控件形式的目录展示还是没法做到的。下面要说的正是基于动态加载树型目录的配置,这篇文章最初构成的原型是朋友阿勇写一篇AJAX动态加载节点的树(http://doll-net.cnblogs.com/),后来根据各项目的实际应用进行了一系列扩展,独立抽出成一个可配置的树型目录模块。
首先说下该配置模块的主要功能点:
1)树根配置:树根编号为每棵树的唯一标识;树根标题可以固定文字形式也可以写带参数SQL语句,树根URL可以是固定链接地址,也可以写JS判断语句以便指向不同的链接。
2)树层配置:树层指定该层节点的名称字段,主键字段,层链接的URL(固定链接或JS链接判断),层数据源等,各层的数据源提供SQL语句书写接口,可以来自关系表或自定义表值函数。
3)每棵树(树根除外)可以有一个或多个树层,树层的第一层不允许设置递归,从第二层开始的任何一层可以设置递成归层,但是每棵树只允许有一个递归层。
4)每棵树可以设置是否显示选择框,对于一般只作为展示的目录不需要选择框,类似于授权的目录结构,节点前面还是需要选择框的。
5)每棵树下各层节点前的图标可以自定义进行设置,节点的图标根据节点字段类型读取,如部门和人员节点一般用不同的图标,默认是常见的文件夹形式的图标。
下面大致说下配置原理和应用,在此声明一下,在做这个配置模块前,我是一直觉得把SQL语句保存到数据库是很变态的做法,但是要做到灵活配置,要让其它项目组的人能自己
定义数据源,不把SQL语句保存到数据还真没其它好招。像sharepoint配置列表什么的,虽然是友好界面操作,不用开发人员写SQL语句,中间肯定也是有生成一个SQL语句的过程吧。这配置也是对开发人员来说,直接给出书写SQL语句的接口,想怎么写就怎么写,想怎么传参就怎么传参,想怎么折腾就怎么折腾。
这里我写的说明顺序不一定符合大家阅读和思考的顺序,但是我尽量说明白 ,as follows :
(1)树型结构的输出
(2)各层节点处理
(3)递归数据层处理
(4)选择框处理
(5)树型配置及应用示例
(6)功能探索——嫁接树
(7)文件下载及调用说明
(8)后记
一、树型结构的输出:
既然要动态加载目录结构,就需要通过异步方式动态输出各层数据了,后台输出的数据格式是一个JSON对象的数组,然后在前端页面进行解析,组织成TABLE和DIV格式在页面上显示,
打开目录树(树种好后)展示页面时,首先读取树根数据,同时加载第一层的数据(JSON格式),点开第一层的某个节点时,加载第二层的数据,依次类推。如一个报表目录的第一层
通过后台返回的数据如下:
[{id:0,name:"月报",fid:"M",layer:"1",url:"",type:"parent",nodetype:""},
{id:1,name:"季报",fid:"Q",layer:"1",url:"",type:"parent",nodetype:""},
{id:2,name:"半年报",fid:"H",layer:"1",url:"",type:"parent",nodetype:""},
{id:3,name:"年报",fid:"Y",layer:"1",url:"",type:"parent",nodetype:""}]
上面的数据中,id仅表示序号,实际并没有意思,fid表示节点的主键字段(同时也是其下一层节点数据的外键字段),layer表示层次,这里表示读取的第一层的数据,url表示点击节点
对应的链接地址,type表示是否叶子节点,对应与parent和child两个值,nodetype表节点类型字段,该值对应图标名称,比如对于报表类型的目录,作为叶子节点的报送单位或上层
区县可能有已报或未报的状态,那么我们可以对不同的状态加载不同的图标,不设置节点类型字段的话默认是文件夹图标,用下面两张效果图来说明:
从上面两层可以容易看出哪些基层单位报了,哪些区县也报了,当然这里只是两个状态的图标,对于有涉及流程状态的目录,可以有多个状态,只需配置对应的状态字段就可以了。
二、各层节点处理:
节点的处理相对还是有点复杂,说之前先贴一段页面呈现时加载的一个代码片段:
window.onload = function()
{
tree = new treeTask("tree");
tree.selectNodeID = tree.TagName[1].name+"0";
tree.appendHtml(getObjById("tree"),tree.getNodeHtml(-1,0,name,"",undefined,"","child")); //首先加载树根
tree.getHtmlData("&methodName=GetData&id=-1&layer=1");//然后加载第一层
if(rootUrl.substring(rootUrl.lastIndexOf('/')+1).length == 0 || rootUrl.substring(rootUrl.lastIndexOf('/')+1)=="RightBlank.aspx")
{
document.all.frm.src = "RightBlank.aspx";
}
else
{
rootUrl=rootUrl.replace(/<</g,"'");
rootUrl=rootUrl.replace(/>>/g,"'");
eval(rootUrl);
}
}
对于上面的树根链接也好及后面的要说的树层节点链接也好,URL都是JS语句形式,用eval这个函数动态执行,链接的URL书写形式类似这样的document.all.frm.src=<</Report/ShowRptForm.aspx?ReportDate=[ReportDate]>>,这里的<<>>在执行前被替换成'',在后台输出JSON数据时其实是这样的
{id:1,name:'季报',fid:'Q',layer:'1',url:'',type:'parent',nodetype:''} 里面是单引号的,IE8好像自动会把外围单引号转换输出成双引号,那当然更好没问题,但是有的浏览器就原样输出的
是单引号,如果链接地址这么写document.all.frm.src='/Report/ShowRptForm.aspx?ReportDate=[ReportDate]' 那url:里面内容的单引号会和本身外面的单引号匹配,就有
问题了,所以对单引号做了个替换。没有指定URL时,点击节点会跳转到一个空白页面RightBlank.aspx,当然也可以其它统一一个页面,也可以在URL里写return;不让跳转。
树根标题由后台输出一段脚本显示的,如下:
var name='<table border=0 cellpadding=0 cellspacing=0><tr><td style="vertical-align:top"><img src=../Images/treehead.gif></td><td style="vertical-align:bottom;line-heignt=24px">报表目录</td></tr></table>';
var rootUrl = 'RightBlank.aspx';
var showbox = 0;
</script>
① 节点图标变换处理:
每层各节点的图标有两个,一个是展开形式的,一个非展开形式的,就像展开折叠的文件夹图标那样,是一对的,图标需为gif格式的,比如上报状态的图标有report.gif和reportopen.gif一对,unreport.gif,unreportopen.gif又一对,节点类型字段的值只需要对应report和unreport这两个值,在开展时会自动去找对应的open图标。展开和折叠时
处理的JS片段如下:
if(getObjById('folder'+objNode.id).name.indexOf("open") == -1)
{
getObjById('folder'+objNode.id).src ="../images/"+getObjById('folder'+objNode.id).name+"open.gif";//显示展开图标
getObjById('folder'+objNode.id).name = getObjById('folder'+objNode.id).name+"open";//重置name属性的值
}
else
{
if(_this.isflush) { _this.isflush = false; return ;} //刷父新节点时保持节点图片为展开状态
getObjById('folder'+objNode.id).src ="../images/"+getObjById('folder'+objNode.id).name.Replace("open","")+".gif";//显示折叠图标
getObjById('folder'+objNode.id).name = getObjById('folder'+objNode.id).name.Replace("open","");//重置name属性的值
}
后台JSON数据输出后通过处理会成为table显示,代码片段如下:
{
if (url == undefined)
{url = rootUrl;}
return "<table border=0 cellpadding='1' cellspacing='1' id='"+this.TagName[0].name+nodeID+"'><tr><td style='vertical-align:top'>"
+ this.getImg(parentID,type,layer,nodeID,nodetype)
+"</td><td class='" + state + "' nowrap id='"+this.TagName[1].name+nodeID+"' onclick=\""+this.treeName+".ClickSelect(this,'"+url+"')\" onmouseover='"+this.treeName+".mouseOver(this)' onmouseout='"+this.treeName+".mouseOut(this)' style='cursor:hand'>"
+ nodeName + "</td></tr></table><div id='"+this.TagName[3].name+nodeID+"' style='display:"+(nodeID==0?"block":"none")+";margin-left:1em;'></div>";
}
其中节点名称前的图标单独调用了一个处理方法,对于非叶子节点,输出两个图片,可以点击的+-号图标和节点类型图标,对应叶子节点也是两个图标child.gif和节点类型图标,child.gif白色
空图标,起站位符图标的作用,保证父子节点有合理的缩进,看着舒服点。上面的div是作为下一层数据填充的容器,即点击某个节点开展子节点时,子节点数据填充到当前点击节点的div下。
getImg方法返回的是一个table,形如:
②添加节点处理:
对于单纯的目录展示来说,只是显示目录结构,没必要添加节点,但是对于像组织结构那样的目录,需要添加部门或人员时,还是需要添加和删除节点的功能,添加节点涉及到局部刷新和
节点图标的变化,假设A为父节点,A_1为叶子节点,选中A_1节点在其下面添加一个A_2,添加后会刷新A节点,即执行A节点的展开事件(刷新节点时强制重新读取子节点数据,否则只
在第一次开展时读取),这时A_1为父节点,图标也被变换掉,同时执行A_1节点的展开事件,显示出A_2子节点。这一连贯的处理完成了一个局部刷新,用户体验效果要好些。
③修改节点处理:
修改节点处理有点类似添加节点的处理,修改节点时如果要局部刷新有两种方式,一是把文本框节点值作为参数直接传给修改节点的方法,赋值到节点标签的innerHTML,不重新读取数据
库,二是修改当前节点后刷新当前节点的父节点,这样也就不需要传递节点名称了,这里采用第二种方法,尽量避免传参数了。因为对于统一配置平台下开发的项目也不一定好特定传某个值到外面,不过如果不用考虑特殊情况的话还是建议用第一种方法。这里添加和修改节点调用的是同一个方法,代码片段如下:
function treeAddNode()
{
if (selectNodeID == null) return;
var o = selectNodeID;
var p = tree.getParentNode(selectNodeID.replace("treeTd",""));
if (p == null) return;
tree.selectNodeID = p.id.replace("treeDiv","treeTd");
if (tree.selectNodeID == "treeTd0")
{
tree.selectNodeID = tree.TagName[1].name+"0";
tree.appendHtml(getObjById("tree"),tree.getNodeHtml(-1,0,name,"",undefined,"","child"));
tree.getHtmlData("&methodName=GetData&id=-1");
}
else
{
tree.refreshNode(p.id.replace("treeDiv",""));
}
if(getObjById("treeTbl"+o.replace("treeTd","")).innerHTML.indexOf("child.gif") ==-1) //更新叶子节点时,则不执行该叶子节点的展开事件
{
tree.tdClick(getObjById(o.replace("treeTd","treeImg")));
}
}
④删除节点处理:
删除当前选中节点时,刷新其父节点,同时执行父节点的URL点击事件,如下:
function treeDeleteNode()
{
if (selectNodeID == null) return;
var p = tree.getParentNode(selectNodeID.replace("treeTd",""));
if (p == null) return;
tree.selectNodeID = p.id.replace("treeDiv","treeTd");
tree.refreshNode(p.id.replace("treeDiv",""));
document.getElementById(tree.selectNodeID).click();//删除节点后执行父节点点击事件
}
三、递归数据层的处理:
最开始也说了,每颗树只允许有一个递归层,并且第一层不允许递归,对于绝大多数目录结构来说,通常就是给的一个递归数据源通过程序处理展现目录结构,程序处理有可能采用递归方式调用,也有可能不是的,但是数据一般是会组织成递归形式的,比如查询出来的数据一般会带有ID,ParentID等。这里的配置模块中,如果将一个树层数据源设置成递归的话,只要是没有跳出该层,展开下一层始终执行该层的SQL语句,比如部门的目录结构,第一层是一级部门,第二层部门就可能是递归了,点击第一层时加载第二层的数据,即layer+1,执行第二层的SQL语句,这时进入递归层了,同时打上一个递归标志,点击第二层部门逐级展开时layer始终是2,保证了始终执行第二层的SQL语句。贴个代码片段:
if (this.context.Session["Layer"] != null && this.context.Session["Layer"].ToString() == layer.ToString())//判断是否属于递归层
{
info = GetLayerInfo(layer);//读取层配置信息信息 此处layer>=2
nextLayer = layer + 1; //取递归层的下一层
}
else
{
info = GetLayerInfo(layer + 1);//或取下一层的节点数据
nextLayer = nextLayer + layer; //第一次取递归层的下一层
}
if (info.Recursive == "1") {......}//1表示递归,0表不递归
读取递归层数据的同时会尝试去读取递归层的下一层的SQL语句(如果还有下一层的话),如果还有第三层,部门用户层时,那么递归部门的同时,还要“递归人”,有时候人员会和子部门并列在同层次,这个在后面用个例子来说明。
四、选择框的处理:
节点前是否显示勾选框,会判断是否输出一个<input id='"+nodeID+"' name='"+parentID+"' type='checkbox' onclick='SelectAll(this)'/>标签,代码片段是这样的:
{
if(!showbox) //是否显示选择框
{
return "<table border=0 cellpadding='0' cellspacing='0'><tr><td><img class='" + type + "' layer='" + layer + "' src='../Images/child.gif' hspace='0' vspace='0' style='cursor:hand'></td><td><img id='"+ this.TagName[2].name+nodeID+"' src='../Images/"+nodetype+".gif'></td></tr></table>";
}
else
{
return "<table border=0 cellpadding='0' cellspacing='0'><tr><td><img class='" + type + "' layer='" + layer + "' src='../Images/child.gif' hspace='0' vspace='0' style='cursor:hand'></td><td><input id='"+nodeID+"' name='"+parentID+"' type='checkbox' /></td><td><img id='"+ this.TagName[2].name+nodeID+"' src='../Images/"+nodetype+".gif'></td></tr></table>";
}
}
else
{
if(!showbox)
{
return "<table border=0 cellpadding='0' cellspacing='0'><tr><td><img class='" + type + "' layer='" + layer + "' id='"+ this.TagName[2].name+nodeID+"' onclick='"+this.treeName+".tdClick(this)' src='"+ this.IMG[1].imgSrc+"' title='"+this.IMG[1].imgTitle+"' hspace='0' vspace='0' style='cursor:hand'></td><td><img id='folder"+this.TagName[2].name+nodeID+"' name='"+nodetype+"' src='../Images/"+nodetype+".gif'></td></tr></table>";
}
else
{
return "<table border=0 cellpadding='0' cellspacing='0'><tr><td><img class='" + type + "' layer='" + layer + "' id='"+ this.TagName[2].name+nodeID+"' onclick='"+this.treeName+".tdClick(this)' src='"+ this.IMG[1].imgSrc+"' title='"+this.IMG[1].imgTitle+"' hspace='0' vspace='0' style='cursor:hand'></td><td><input id='"+nodeID+"' name='"+parentID+"' type='checkbox' onclick='SelectAll(this)'/></td><td><img id='folder"+this.TagName[2].name+nodeID+"' name='"+nodetype+"' src='../Images/"+nodetype+".gif'></td></tr></table>";
}
}
由于节点是动态加载的,勾选中父节点时,如果没有展开子节点,子节点是不会被选中的,因为dom对象标签此时还没有创建,对应所有已展开的节点,勾选父节点,子节点会全部选中,这里调用了一个递归的处理方法,方法如下:
function SelectAll(box)
{
for (var i=0;i<document.form1.elements.length;i++)
{
var e = document.form1.elements[i];
if ( e.type=='checkbox')
{
if(e.name == box.id)
{
e.checked = box.checked;
SelectAll(e); //递归调用,对已展开的各层进行全选或反选
}
}
}
}
</script>
这个方法应该是可以修改得更合理些,没有深入考虑,目前的话,比如展开了节点个数是100个(即创建加载的checkbox对象有100个),如果勾选某个父节点,父节点下有5个子节点,那么一共相当于选择了6个,这样的话计算次数是6*100=600次,可以考虑从勾选节点的下游开始判断,那样计算次数就要少很多了。
五、树型配置及应用示例(种树):
种树之前,先说下配置信息在数据库的存储方式,有两张表,即树根信息表和树层信息表,如下图:
主要是树层表说下,STL_LayerIndex是层序号,动态加载各层的数据时就是通过层序号去执行对应层的SQL语句,STL_LayerName是层名称,STL_Select层对应的数据源(SQL语句), STL_KeyField是层的主键字段,该字段会传入下一层进行数据过滤,STL_NodeName是节点名称字段,目录结构的各节点显示的名称就是设置这个字段,STL_Recursive表示是否属于递归层(1是0否) ,STL_URL存储链接地址(即一段JS),STL_State这个字段应该去掉的,STL_NodeType是节点类型字段,根据该字段的值加载不同的节点图标。
①添加树根
我就以常见的人力资源结构目录为例来种一棵树,截图操作如下:
上面的树根标题也可以这样写SELECT '人力资源结构',根据需要可以带参数,这个是链接传入的参数,比如配置树的链接为TreeView.aspx?STI_Code=HR_Map&Param=ID|9527,NAME|ZXC(表示传了两个参数),标题可以写成SELECT Title FROM TABLE WHERE T_ID=[ID] AND T_NAME='[NAME]',一般种一棵树,树根标题是固定,有的情况标题是要动态变化的。
② 添加树层
简单说下上面的几个效果图 ,对应各层的数据源有三个内置的替换参数,首先是[TreeID] 这个是上一层节点的主键字段的值,[UserID]当前登录用户的ID(对应于Session["USER_ID"]),[ReportYear]控制整棵树的外围参数,对应报表目录来说一般是年度,当然也可以是其它,一般的目录这个外围参数可能用不着,一会下面再贴个实际示例给大家看下这三个参数怎么用。每层里面有个type字段,这个很重要,这个是标志是否叶子节点(0表示叶子,非0则不是叶子),会输出到前端页面做判断,对于递归层,必须动态判断是否叶子节点,上图中的下级部门是一个递归层,而且这个递归层下面还有下一层,这就是上面说的递归部门时还要“递归人” 的情况了,如果某个部门下既没有子部门也没有用户,那么就是叶子节点,所以第二层的type需要(SELECT COUNT(1) FROM Department AS d
WHERE ParentCode = d2.Code)+(SELECT COUNT(0) FROM USER WHERE DeptID=d2.Code) AS type 这样来判断,对于第三层毫无疑问是叶子层,所以type直接给0即可,在已知某个层是否叶子或非叶子层时,可以直接对type赋0或非0值。像上面那样配置后链接到TreeView.aspx?STI_Code=HR_Map看到的效果如下:
让这棵树显示选择框的情况也截个图看下:
前面也说选择框的应用对一般的展示目录没有什么意思,选择框主要应用在赋值权限的权限目录结构和选择收件人的消息通讯目录等。
删除树的时候直接做了个级联删除,把树层一起删除,如下:
这个提示消息框是不是觉得有点小酷的,用的是netman写的ymPrompt.js消息组件,比较漂亮,有很多样式可选( http://www.ajaxbbs.net/test/ymPrompt4.0/demo.html ),写这篇文章打了两家广告了,呵呵~~~~~~~~~~,顺便说下确定取消这种仿系统自带confirm提示框,一般只是界面重画了,大多都是用DIV什么的重画,实际上并没有系统自带的confirm那种功能,当然可以通过异步实现等效于confirm的功能。系统自带的confirm一个最大的特点是可以暂停后台服务器按钮的事件的执行,点击确定后继续执行服务器按钮下面的事件,重写的就不行,重写的弹出的整个框是没有返回值的,而confirm是有返回值的。不过可以借助模态窗口来实现confirm完全一样的功能,就是界面比较丑陋,因为模态窗口是有返回值的。我试了下是可以的,截个图看下
不过IE8里模态窗口的地址栏,状态栏没法屏蔽,显得比较难看,所以上面删除树的时候,我也只能是通过异步操作删除的。
到此为止已经种了一棵树,并且也看到了树型的效果,列举的这个示例其实也是比较简单的,而实际的项目应用中种的树是比较复杂的,尤其是涉及报表目录结构的树,从上层领导到基层单位,逐级根据权限查看不同类的报表并且逐级汇总数据,点击左边的汇总表,右边对应出汇总报表,点击基层表,右边出相应的基本报表数据,这样的目录就要用到前面说的内置参数,用户ID,外围参数。下面只以截图的方式来列举一个稍微复杂点的目录树,以其中一个项目中的报表逐级汇总目录为例,各层的数据源来自于自定义的表值函数。下面的截图是一棵日报树,分三级汇总,处室、区县、基层单位,处室登录后可以看所有的汇总然后上报,也可看各区县的汇总,同时可以看基层的数据,区县登录查看本区县所有基层上报的数据和每个基层单独的数据,基层就只看本单位的数据。(以下图片文字即图片内容仅为本文参考所用,在此保留最终解释权)
(处室目录)(区县目录)(基层单位目录)
简单说下上面图中URL的链接地址,点击某个节点名称右边出现相应的数据页面,肯定是要传参数的,比如 [FormCode]在URL地址中表示取该节点对应的一条数据中的编号,这里有个参数适配调整的过程,即根据列名查找对应列的值。至于不同用户登录加载不同的数据,关键关系要明确,编写自定义函数组织好数据,例如对于区县来说,读到第4层就相当于是叶子节点了,对于基层单位来说读到第3层就相当于叶子节点了,所以对type要做好控制,各层节点什么情况下是叶子节点,什么情况下是非叶子节点。
上面的实例中,实际情况不完全那样,还有另外一个要求,要求局级领导可以查看每日总的汇总数据和各个处室、各处室下各区县及各区县下的单位报表数据。对应上面的日报目录只需要做一个小小调整即可,即把第5层设置为递归层,主键设置成和第四层一样(主外键的传递要控制好),同时把第5层的type判断改为动态判断是否叶子节点就可以了,还是截个图,如下:
六、功能探索——嫁接树
虽然这个模块的功能基本能满足绝大多数项目的应用,严格来说还是有些要改善的地方,比如一棵树可以考虑允许多个递归层,另外其中一个可以考虑完善功能就是希望能把多棵树组合起来形成一棵目录树进行展现,暂把这个称之为“嫁接树”,有这个所谓的概念出现,当然是有实际项目中有这样的潜在需求,比如上面的日报目录,还有月、季、年等目录,但是日报结构比较特殊,是单独的一棵树,月报等其它报表是另外一个树,希望日、月、季、年所有报表目录用一棵树来展现,但是这两棵树的层次结构差别比较大,如果要整合成一棵树,不用嫁接的概念去体现,也不是没有办法,可以通过自定义函数来组织,这样的话函数的判断就很复杂和臃肿了,不值得。所以要考虑下多棵树的组合展现,初步的想法是再种一棵“友谊树”,这棵树负责组织不同的树,前端展现的时候展现“友谊树”就可以了。有时间再折腾吧~~~
七、文件下载及调用说
将TreeView文件夹拷贝到站点根目录,TreeView.dll复制到站点bin目录,webconfig里配置如下:
<appSettings>
<add key="ConnStr" value="数据库连接字符串"/>
</appSettings>
系统配置链接地址 : /TreeView/Tree/TreeInfoList.aspx
树型展示链接地址 : /TreeView/Tree/TreeView.aspx?STI_Code=树根编号&ReportYear=[Year](ReportYear为可选参数)
文件下载 :树型配置(点击链接下载)
补充上一篇的一个文件,上一篇实际应用也很少那么做,权当忽悠好了:) 只是一个大致参考 : download
八、后记
到此这个树型配置模块大致说完了,有些没说到,有些也没细说,上面给的下载的是发布后的文件,基本上只要数据符合数据库关系结构,利用这个配置就能组合成树型结构进行展示,要求熟悉数据库自定义函数编写,种复杂的树基本上都是通过用自定义表值函数作为数据源来实现的。还有对于这个目录选择框的扩展应用,实际应用中也是效果很好的,有空再说了,另外这个树型配置一直是在IE里跑的,其它浏览器我没试过,不行的话可以自行修改JS代码。
********************************************
本文转至:http://www.cnblogs.com/peaceli/archive/2010/07/15/plant_tree.html