背景介绍:
图一 图二
开发环境是:VS2008(+SP1) 及 ASP.NET 3.5(+SP1) 及 ASP.NET AJAX。
业务需求是:BI首页上会显示的CCHR的各种图表和统计数据,另外要提供一个选择组织的弹出窗口,在首页打开后,用户可以再根据自己的需要,从自己有权限的组织树弹出窗口中,只选择某些组织,查看其对应的统计数据。比如:A用户有XXX国际控股的及下面所有组织的权限,那他进入BI首页时,默认看到的是XXX国际控股这家公司全部的统计数据,如果他只想看东北的数据,就可以打开选择组织的弹出窗口(如图一),在这棵组织树上只勾选东北这个节点前的CheckBox再提交刷新首页,首页上的统计数据和图表就只有东北的数据了。
问题现象是:CCHR中XXX国际控股这个客户的组织多达3万多个,加载显示这棵3万多个节点的组织树直接让IE几乎挂掉,要等好几分钟树才能加载完,并且IE一直占用CPU百分之九十多,根本没办法用了。(网上查资料看到,如果是2000个以下的节点数选择TreeView才比较合适,多了肯定不行)
解决思路是:造一棵貌似完整的组织树,其实是一棵数据量很少的假树来欺骗用户的眼睛。比如:A用户刚进来时只加载前三层节点,用户点击某个节点时,再加载此节点的所有孙子节点。也就是刚进页面时把XXX国际控股和XXX国际控股下面的两层子节点加载进来(树默认展开前两层,所以用户只看到XXX国际控股和它的子节点(如图一)),加载三层的原因是要让XXX国际控股的所有子节点前面有"+"号(比如东北前面的加号),用户可以再点击展开,展开时,再只把此节点的所有孙子节点都加载进来,比如用户点击了东北,就加载东北的所有孙子节点(长春区、沈阳区等)进来,每次都加载孙子节点就是为了保证子节点前面有"+"号,可以继续展开,除非没有孙子节点。既然是假树,那它其实也就仅能供用户欣赏了,最后选中了哪些组织就不能从树上去取了,只能根据用户的勾选,在后台从数据源中去判断获取了。(比如:用户在上面第一个图中选了东北,如果我们从树上获取组织,只能得到东北和东北的子组织,孙子组织没有。)
解决方案:
数据源是由我们调用CCHR提供的WebService得到一个XML结构的组织树。所以我们用一个asp:XmlDataSource控件作为Treeview的数据源。
XML结构的组织树数据源示例如下:
<Success>true</Success>
<OrganizationData>
<Organization unitid='0' punitid = '' unitcode='BELLE' unitname = 'XXX国际控股' hasauthoriry = '1'>
<Organization unitid=' 12205083-c0a7-4f54-b8e5-8f0643fe3b84' punitid = '0' unitcode=' YG' unitname = '云贵'
<Organization unitid=' ab68bcb7-4e10-49b7-8923-527bafa340ec' punitid = '12205083-c0a7-4f54-b8e5-8f0643fe3b84'
<Organization unitid=' 61dd0f74-d576-4963-988e-ff9cec4b92ee' punitid = 'ab68bcb7-4e10-49b7-8923-527bafa340ec'
</Organization>
</Organization>
</Organization>
</Organization>
</OrganizationData>
</ReturnInfo>
下面是前台HTML代码:
用了UpdatePanel是为了在展开子节点时实现局部刷新。
在OnTreeNodeExpanded展开节点事件中加载此节点的孙子节点。
在OnTreeNodeCheckChanged点选CheckBox事件中,处理哪些节点被选中,定义了一个Session变量,向Session中添加或移除此组织及它的子组织。
<ContentTemplate>
<asp:TreeView ID="TreeViewOrg" runat="server" OnClick="OnTreeNodeChecked()" ShowCheckBoxes="All"
OnTreeNodeExpanded="TreeViewOrg_TreeNodeExpanded"
ExpandDepth="1" ShowLines="true">
<DataBindings>
<asp:TreeNodeBinding DataMember="Organization" SelectAction="None" ValueField="UnitID"
TextField="UnitName" TargetField="HasAuthority" />
</DataBindings>
</asp:TreeView>
<asp:XmlDataSource ID="XmlDataSource1" runat="server"></asp:XmlDataSource>
</ContentTemplate>
</asp:UpdatePanel>
下面是前台JS代码:
OnTreeNodeChecked() 方法实现了在Treeview上选中某一个节点时自动选中其所有子节点。
postBackByObject() 方法是为了在点CheckBox时,可以提交到后台去处理。
function OnTreeNodeChecked() {
var ele = event.srcElement;
if (ele.type == 'checkbox') {
var childrenDivID = ele.id.replace('CheckBox', 'Nodes');
var div = document.getElementById(childrenDivID);
if (div == null) return;
var checkBoxs = div.getElementsByTagName('INPUT');
for (var i = 0; i < checkBoxs.length; i++) {
if (checkBoxs[i].type == 'checkbox')
checkBoxs[i].checked = ele.checked;
}
postBackByObject();
}
}
//点击复选框时触发事件
function postBackByObject() {
var o = window.event.srcElement;
if (o.tagName == "INPUT" && o.type == "checkbox") {
//这里的第一个参数是UpdatePanel ID,因为我使用了MS的ASPAJAX来实现局部刷新
//如果没有使用MS的ASPAJAX,这里的两个参数都可以为空
__doPostBack("UpdatePanel1", "");
}
}
</script>
后台代码:
第一次进入页面给TreeView绑定前三层组织(注释中有详细说明):
{
//调用CCHR的Web Service取得XML结构组织树要花几秒中时间,出于性能考虑,
//对一个登陆用户,我们只取一次组织树数据放到Session中
if (Session["OrgTree"] == null)
{
Session["OrgTree"] = this.GetSecurityOrganizationTree(_loginUser, _dashboardName, _pageName);
}
string orgXml = Session["OrgTree"].ToString();
if (orgXml != null && orgXml != "")
{
//第一次打开页面时,Treeview上只加载前三层的组织,
//所以我们用下面程序从CCHR的XML组织树中只拿出前三层的XML组织树
string xmlStart = @"<ReturnInfo><Success>true</Success><OrganizationData>";
string xmlEnd = @"</OrganizationData></ReturnInfo>";
StringBuilder xmlData = new StringBuilder();
XmlDocument xmldoc = new XmlDocument();
xmldoc.LoadXml(orgXml);
XmlElement root = xmldoc.DocumentElement;
XmlElement theOrg = (XmlElement)root.SelectSingleNode("/ReturnInfo/OrganizationData");
XmlNodeList nodelist1 = theOrg.ChildNodes;
xmlData.Append(xmlStart);
if (nodelist1.Count > 0)
{
for (int i = 0; i < nodelist1.Count; i++)
{
//第一层组织
xmlData.Append(GetNewXmlNodeString(nodelist1[i]));
XmlNodeList nodelist2 = nodelist1[i].ChildNodes;
for (int j = 0; j < nodelist2.Count; j++)
{
//第二层组织
xmlData.Append(GetNewXmlNodeString(nodelist2[j]));
XmlNodeList nodelist3 = nodelist2[j].ChildNodes;
for (int k = 0; k < nodelist3.Count; k++)
{
//第三层组织
xmlData.Append(GetNewXmlNodeString(nodelist3[k]));
xmlData.Append("</Organization>");
}
xmlData.Append("</Organization>");
}
xmlData.Append("</Organization>");
}
}
xmlData.Append(xmlEnd);
//把XML字符串赋给XML DataSource控件
XmlDataSource1.Data = xmlData.ToString();
XmlDataSource1.EnableCaching = false;
XmlDataSource1.DataBind();
XmlDataSource1.XPath = "ReturnInfo/OrganizationData/Organization";
//把XML DataSource绑定到Treeview上
TreeViewOrg.DataSource = XmlDataSource1;
TreeViewOrg.DataBind();
//循环TreeView的所有节点,对HasAuthority=0,
//也就是没有某个组织权限的节点,把节点前面的CheckBox隐藏掉
HiddenNoAuthorityNodeCheckBox(this.TreeViewOrg.Nodes);
}
}
上面的方法会调用下面两个公用方法:
1、得到某个组织节点XML字符串的公用方法:
{
string xmlNodeString = "<Organization UnitID=\""
+ xmlNode.Attributes["UnitID"].Value + "\" PUnitID =\""
+ xmlNode.Attributes["PUnitID"].Value + "\" UnitCode=\""
+ xmlNode.Attributes["UnitCode"].Value + "\" UnitName = \""
+ xmlNode.Attributes["UnitName"].Value + "\" HasAuthority = \""
+ xmlNode.Attributes["HasAuthority"].Value + "\">";
return xmlNodeString;
}
2、隐藏没有权限节点前面CheckBox的公用方法:
{
foreach (TreeNode treeNode in treeNodes)
{
_treeNodeCount += 1;
if (treeNode.Target == "0")
{
treeNode.ShowCheckBox = false;
}
HiddenNoAuthorityNodeCheckBox(treeNode.ChildNodes);
}
}
在点击某个节点的事件中,给此节点添加孙子节点(添加时,如果父节点是选中的,那添加的孙子节点也要是选中状态):
{
if (e.Node.ChildNodes.Count > 0)
{
for (int i = 0; i < e.Node.ChildNodes.Count; i++)
{
XmlNodeList nodeList = GetXmlNodeChildNodeListByValue(e.Node.ChildNodes[i].Value);
if (nodeList != null)
{
e.Node.ChildNodes[i].ChildNodes.Clear();
for (int j = 0; j < nodeList.Count; j++)
{
TreeNode treeNode = new TreeNode();
treeNode.Value = nodeList[j].Attributes["UnitID"].Value;
treeNode.Text = nodeList[j].Attributes["UnitName"].Value;
treeNode.Target = nodeList[j].Attributes["HasAuthority"].Value;
treeNode.SelectAction = TreeNodeSelectAction.None;
if (e.Node.Checked)
{
treeNode.Checked = true;
}
e.Node.ChildNodes[i].ChildNodes.Add(treeNode);
}
}
}
}
}
上面的事件中会调用下面这个公用方法:
1、根据一个组织ID,去CCHR的XML组织树中找到这个XML节点子节点列表的公用方法:
{
if (Session["OrgTree"] != null)
{
string orgXml = Session["OrgTree"].ToString();
XmlDocument xmldoc = new XmlDocument();
xmldoc.LoadXml(orgXml);
XmlElement root = xmldoc.DocumentElement;
XmlElement theOrg = (XmlElement)root.SelectSingleNode(
"/ReturnInfo/OrganizationData/Organization/Organization//Organization[@UnitID='" + value + "']");
if (theOrg != null)
{
XmlNodeList nodelist = theOrg.ChildNodes;
return nodelist;
}
else
{
return null;
}
}
else
{
return null;
}
}
在每次单击某个CheckBox的事件中,获取选中的组织,有可能是添加,有可能是移除,最后都存在Session中:
如果CheckBox是选中,就先把当前点选中的组织先存到Session中,然后再递归循环它下面所有的子组织,也存入Session。反之,如果是没选中,就从Session中全部移除。
{
string checkedNodeValue = e.Node.Value;
XmlNode xmlNode = this.GetXmlNodeByValue(checkedNodeValue);
if (e.Node.Checked)
{
string xx = xmlNode.Attributes["UnitName"].Value;
if (Session["SelectedOrganizationIDs"] != null)
{
if (Session["SelectedOrganizationIDs"].ToString().IndexOf(xmlNode.Attributes["UnitID"].Value + ";") < 0)
{
Session["SelectedOrganizationIDs"] = Session["SelectedOrganizationIDs"].ToString()
+ xmlNode.Attributes["UnitID"].Value + ";";
}
}
else
{
Session["SelectedOrganizationIDs"] = xmlNode.Attributes["UnitID"].Value + ";";
}
}
else
{
string xx = xmlNode.Attributes["UnitName"].Value;
if (Session["SelectedOrganizationIDs"] != null)
{
Session["SelectedOrganizationIDs"] = Session["SelectedOrganizationIDs"].ToString().Replace(xmlNode.Attributes["UnitID"].Value + ";", "");
}
}
GetSelectedOrganization(xmlNode.ChildNodes, e.Node.Checked);
}
上面的事件中会调用下面两个公用方法:
1、根据一个组织ID,去CCHR的XML组织树中找到这个XML节点的公用方法:
{
if (Session["OrgTree"] != null)
{
string orgXml = Session["OrgTree"].ToString();
XmlDocument xmldoc = new XmlDocument();
xmldoc.LoadXml(orgXml);
XmlElement root = xmldoc.DocumentElement;
XmlNode orgNode = root.SelectSingleNode("/ReturnInfo/OrganizationData//Organization[@UnitID='" + value + "']");
return orgNode;
}
else
{
return null;
}
}
2、得到选中的组织的公用方法:
{
foreach (XmlNode xmlNode in nodeList)
{
if (selected)
{
string xx = xmlNode.Attributes["UnitName"].Value;
if (Session["SelectedOrganizationIDs"] != null)
{
if (Session["SelectedOrganizationIDs"].ToString().IndexOf(xmlNode.Attributes["UnitID"].Value + ";") < 0)
{
Session["SelectedOrganizationIDs"] = Session["SelectedOrganizationIDs"].ToString()
+ xmlNode.Attributes["UnitID"].Value + ";";
}
}
else
{
Session["SelectedOrganizationIDs"] = xmlNode.Attributes["UnitID"].Value + ";";
}
}
else
{
string xx = xmlNode.Attributes["UnitName"].Value;
if (Session["SelectedOrganizationIDs"] != null)
{
Session["SelectedOrganizationIDs"] = Session["SelectedOrganizationIDs"].ToString().Replace(xmlNode.Attributes["UnitID"].Value + ";", "");
}
}
GetSelectedOrganization(xmlNode.ChildNodes, selected);
}
}
大家如果用更好的解决方法,请不忘跟贴赐教。