您的位置:首页 > 其它

.NET平台下带权限控制的TreeView控件节点生成算法

2004-12-05 19:01 519 查看
一、引言[/b]
在应用系统开发中,TreeView是一种使用频率很高的控件。它的主要特点是能够比较清晰地实现分类、导航、浏览等功能。因而,它的使用方法与编程技巧也一直受到技术人员的关注。随着应用需求的变化,在很多情况下我们需要实现数据显示的权限控制,即用户看到的数据是经过过滤的,或是连续值,或是一些离散的值。就TreeView而言,原先可能显示出来的是完整的具有严格父子关系得节点集,而经权限过滤后所要显示的节点可能会变得离散,不再有完整的继承关系。本文针对这一问题,通过对已有实现方法进行分析,提出改进算法。所附示例程序进一步解释了算法设计思想。

二、三种常见生成方式的简单分析[/b]
如文[2,3]所述,TreeView的生成基本上有三种方式:
1. 界面设计时在TreeView设计器或者代码中直接填充TreeView节点。
这种方式通过拖放控件的方式生成树,应用范围窄,是一种非编程方式;
2. 从XML文件中建立树形结构。
这种方式通过XML文件(串)生成树,从形式上来说,这种方式是比较直观的。因为XML本身就是一棵“树”,在.NET 平台下TreeView的自动生成代码中,TreeView的实际内容也是由XML表示的。此外,基于XML文件生成树对异构环境下的分布式应用具有重要意义。事实上,利用XML作为通用数据传递格式已得到普遍认可;
3. 从数据库中得到数据(在.NET中,我们可以理解为一个数据集),建立树形结构。
这种方式通过父子关系递归生成树,是最容易理解的一种编程实现方式。一般是自顶向下递归生成,得到广泛应用。
这里,我们不妨举一个实际的例子来说明一下,假设我们有这样的数据集(可以看作是一个公司的部门列表):

TagValueContentValueParentID
G01行销部
G02顾问部
G03研发部
G04测试部
GS01行销一部G01
GS02行销二部G01
GS03行销三部G01
GSL01行销一部北京办GS01
GSL02行销一部上海办GS01
GS04顾问一部G02
GS05顾问二部G02
GS06研发一部G03
GS07研发二部G03
GS08测试一部G04
GS09测试二部G04
GSL03研发一部杭州分部GS06
GSL04研发一部西安分部GS06
表1 示例数据集
其中,TagValue是节点的实际值,ContentValue是用户界面上节点显示的值或者说标签值,ParentID是节点的父节点的TagValue。若节点为根节点,一般设ParentID为空或等于本身的TagValue。
默认情况下,我们可以按照下面的算法把所有的节点装配成一棵树,

算法1:通过父子关系递归生成树基本算法
l Step 0:数据准备,给定数据集。
一般来说数据集遵循这样的格式,即(TagValue,ContentValue,ParentID);
l Step 1:给定待增加子节点的节点(初始时一般为根节点),记作CurNode,以及待增加节点的ParentID值(初始时为根节点的ParentID),记作CurParentID;
l Step 2:在数据集中查找具有指定ParentID值的所有节点,得到节点集objArr[],
if (objArr == null)
return;
else
{
//遍历节点集
for(int i=0; i<objArr.Length;i++)
{
将objArr[i]添加为CurNode的子节点,同时递归(即将objArr[i]作为CurNode,objArr[i]的TagValue作为CurParentID,goto Step 1);
}
}
最终可以得到下图所示的TreeView:





图1 TreeView效果图

这种方法的缺陷在于"由父节点及子节点"的遍历顺序意味着每个子节点的父节点必须存在,否则将搜索不到,即可能出现断层现象。在很多实际应用中,我们发现这种实现方式不能完全奏效,最典型的情况就是当需要对众节点所表征的实际值(比如机构列表,人员列表,资源列表等)进行权限控制时,这时往往从数据库中筛选出来的数据集中节点会出现断层现象。比如我们假设设定权限时给定数据如表2,即把第一行“行销部”去掉(注:权限过滤操作已超出本文讨论的范围,这里假定数据集已准好),则运用算法1生成的TreeView如图2所示。

TagValueContentValueParentID
G02顾问部
G03研发部
G04测试部
GS01行销一部G01
GS02行销二部G01
GS03行销三部G01
GSL01行销一部北京办GS01
GSL02行销一部上海办GS01
GS04顾问一部G02
GS05顾问二部G02
GS06研发一部G03
GS07研发二部G03
GS08测试一部G04
GS09测试二部G04
GSL03研发一部杭州分部GS06
GSL04研发一部西安分部GS06
表2 给定数据集



图2 TreeView效果图
可以看到,这里产生了节点遗漏现象。一般来说,我们可以从两方面入手去解决问题,一方面可以修正数据集,另一方面则可以修改生成树算法。显然直接修正数据集是很复杂的,且会带来效率问题。而单方面修改生成树算法也是不是很好(即把遗漏的节点直接插到根节点下),因为这时会出现父辈和晚辈同级的现象。
三、通过深度编号递归生成树算法[/b]
回顾到已有的一些方法(文[1~5]),其中基于节点深度生成树的方法给我们一些启发,我们在构造数据集时可以增加深度字段,但这里的深度不是简单的层级号,是一个扩展了的概念,具体地说其实是一个深度编号,它与父辈编号存在一定的对应关系。比如表1所示的数据集可以作如下编号:

TagValueContentValueParentIDDepthID
G01行销部a001
G02顾问部a002
G03研发部a003
G04测试部a004
GS01行销一部G01a001001
GS02行销二部G01a001002
GS03行销三部G01a001003
GSL01行销一部北京办GS01a001001001
GSL02行销一部上海办GS01a001001002
GS04顾问一部G02a002001
GS05顾问二部G02a002002
GS06研发一部G03a003001
GS07研发二部G03a003002
GS08测试一部G04a004001
GS09测试二部G04a004002
GSL03研发一部杭州分部GS06a003001001
GSL04研发一部西安分部GS06a003001002
表3 带深度编号的数据集
其中,DepthID即是节点的深度编号。生成深度编号的过程其实也不复杂,首先我们可以制定编号的规则,比如层级编号的前缀、编码长度、起始值等。当给某个节点编号时,只要找到所在层级的最大编号,然后增1。具体实现过程这里不再细述。
于是,我们很自然地想到借鉴算法1的思想设计基于深度编号的生成树程序。这时,我们可以根据当前节点的深度编号寻找其后代节点集,但要给出一个最大跨度(可以理解为最高级与最低级间的间隔级数),因为不可能无限制地找下去。这种方法可以部分程度上弥补"由父节点及子节点"的遍历的缺陷,因为当出现断层时会沿着编号继续往后找。但是还是会可能漏掉,比如我们给定数据集(把“研发一部”过滤掉):

TagValueContentValueParentIDDepthID
G01行销部a001
G02顾问部a002
G03研发部a003
G04测试部a004
GS01行销一部G01a001001
GS02行销二部G01a001002
GS03行销三部G01a001003
GSL01行销一部北京办GS01a001001001
GSL02行销一部上海办GS01a001001002
GS04顾问一部G02a002001
GS05顾问二部G02a002002
GS07研发二部G03a003002
GS08测试一部G04a004001
GS09测试二部G04a004002
GSL03研发一部杭州分部GS06a003001001
GSL04研发一部西安分部GS06a003001002
表4 给定数据集
在生成树过程中,当从“研发部”(a003)往下找子节点时,找到的应该是“研发二部”(a003002),因为它是最近的节点。而下面的顺序就是沿着“研发二部”再往下找,显然不可能找到“研发一部杭州分部”和“研发一部西安分部”,因为编号规则不一样,这样生成的树同样会漏掉节点。
我们提出一种新的算法,即打破传统的遍历顺序,采用由底向上的遍历顺序。形象地说,传统的方法是通过一个既有根节点或父节点来不断衍生新的子节点(如图3(a)所示),而新的算法是通过不断聚集节点,形成子树集,最后汇成一棵树(如图3(b)所示)。



图3 TreeView节点生成流程示意图

算法2:由底向上按层次(深度)遍历法生成树算法
l Step 0:数据准备,给定数据集(TagValue,ContentValue,DepthID), TagValue是节点的实际值,ContentValue是节点显示的值或者说标签值,DepthID是节点的深度编号。若节点为根节点,一般其DepthID长度为最短。给定最大深度iMaxDepLen和最小深度iMinDepLen。给定用于存储当前子树的Hashtable;
l Step 1:给定当前遍历的层级长度iCurDepLen,初始设为iMaxDepLen;
l Step 2:在数据集中根据给定iCurDepLen查找满足条件的层级,得到该层级的节点集objArr[],
if (objArr == null)
return;
else
{
//遍历节点集
for(int i=0; i<objArr.Length;i++)
{
Step 2.1 查找objArr[i]的父节点,若无父节点,直接加入,goto Step 2.2;若有父节点,先查找父节点是否已在Hashtable中。若有,将其从Hashtable中移出并记为tempParNode;否则生成新节点tempParNode;goto Step 2.3
Step 2.2 若当前节点objArr[i]不在Hashtable中,在Hashtable中添加objArr[i];continue;
Step 2.3 若当前节点objArr[i]不在Hashtable中,根据objArr[i]生成节点tempNode;否则,将其从Hashtable中移出,并记为tempNode;将tempNode插到tempParNode中,并将存入Hashtable。
}
}
l Step 3:若当前层级iCurDepLen大于最小层级iMinDepLen,则继续回溯,将iCurDepLen减1并作为当前iCurDepLen,goto Step 2;否则goto Step 4.
l Step 4:在得到用Hashtable存储的节点表后(实际上是一子树表),遍历Hashtable,将各棵子树插入TreeView.
在该算法中,我们一开始便计算好数据集中节点深度编号的最小长度和最大长度,目的是为了不盲目搜索。但如果数据集中每一层级的深度编号是固定长的,则可以更简化搜索过程。存放临时子树的Hashtable的键值是当前子树根节点的Tag值,这样的好处是查找相当方便,不需要在TreeView中遍历一个个节点。所以,每次处理上一层级的节点,只需看其父节点在不在Hashtable中,若在将其插入子树,否则增加Hashtable项。
附录示例程序实现了这一算法,这里介绍一下关键的几个函数。

函数形式及其参数解释功能
PopulateCompleteTree(ref System.Windows.Forms.TreeView objTreeView,DataSet dsSource,string strTreeCaption,int iTagIndex,int iContentIndex,int iDepthIndex)
1. objTreeView是最终要生成的TreeView;
2. dsSource是给定数据集;
3. strTreeCaption指定TreeView根节点的名称;
4. iTagIndex是数据集中TagValue字段的列号;
5. iContentIndex是数据集中ContentValue字段的列号;
6. iDepthIndex是数据集中DepthID字段的列号;
1. 采用层次(深度)遍历法生成树主调函数;
2. 调用CollectNodes(DataSet dsSource,int iTagIndex,int iContentIndex,int iDepthIndex,int iCurDepLen,int iMinDepLen,ref Hashtable objArrNode)
CollectNodes(DataSet dsSource,int iTagIndex,int iContentIndex,int iDepthIndex,int iCurDepLen,int iMinDepLen,ref Hashtable objArrNode)
1. dsSource,iTagIndex,iContentIndex,iDepthIndex同上;
2. iCurDepLen指当前层级深度编号长度;
3. iMinDepLen指最小深度即最顶层深度编号长度;
4. objArrNode指用于存放中间子树的Hashtable
1. 从底往上聚集节点;
2. 调用 LookupParentNode(DataSet dsSource,int iDepthIndex,string strSubDepth,int iTagIndex,int iContentIndex)
LookupParentNode(DataSet dsSource,int iDepthIndex,string strSubDepth,int iTagIndex,int iContentIndex)
1. dsSource,iTagIndex,iContentIndex,iDepthIndex同上;
2. strSubDepth指当前节点的深度编号(因为是递归查找)
1. 查找最近的上控层级,因为有可能父节点层级不存在。
此时若给定数据集(我们把“研发部”和“行销一部”过滤掉),

TagValueContentValueParentIDDepthID
G01行销部a001
G02顾问部a002
G04测试部a004
GS02行销二部G01a001002
GS03行销三部G01a001003
GSL01行销一部北京办GS01a001001001
GSL02行销一部上海办GS01a001001002
GS04顾问一部G02a002001
GS05顾问二部G02a002002
GS07研发二部G03a003002
GS08测试一部G04a004001
GS09测试二部G04a004002
GSL03研发一部杭州分部GS06a003001001
GSL04研发一部西安分部GS06a003001002
表5 给定数据集
则生成树如下图所示,



图4 TreeView效果图
这正是我们需要的结果。
当然,有时为了结构的需要,我们还会采取所谓“中立”的方法。比如对于本文所提的TreeView控件节点生成问题,如果不想再写算法去生成深度编号,那么我们还可以通过给数据集增加标志位的方法,即用标志位来标识数据是否已被筛选。在运用传统算法生成树后,再检查一下是否有未被筛选的数据,若有则查找其祖辈节点,将其插入祖辈节点。不过这里的“查找祖辈节点”是在TreeView上进行的,当节点很多时其效率肯定没有直接在数据集上搜索高。
另外,深度编号的引入不仅会给生成树带来方便,还可以让权限设置更灵活。具体到我们的示例来说,一般如果我们要把某些部门过滤掉,那么会把这些部门一个一个挑出来,我们称之为“离散值设置方式”。而当系统结构庞大时,我们更希望挑选一个区间,比如把一个部门及其下控的n级过滤掉,这是一个“连续值设置方式”,这时包含层级概念的深度编号可以很好地解决这个问题。实际的系统开发中,我们也发现采用这种方式是切实可行的。

四、其他TreeView[/b]生成方式[/b]
前面提到TreeView还可以通过XML文件(串)生成。这种方式实现的关键是构造出一个类似于TreeView的XML文档或字符串出来。其基本思想应该与前面讨论的算法是相似的,只是在程序实现上稍微复杂一些(其中,XML节点的索引可以基于文档对象模型(DOM)来做)。另外还要注意的是,有很多的第三方TreeView控件,他们所支持的XML文档的格式是不尽相同的。限于篇幅,本文不详细讨论具体实现过程。
五、小结[/b]
本文主要讨论了.NET平台下TreeView控件节点生成程序设计,结合已有方法和实际需求,对设计方法进行了研究,给出了比较完整的解决方法。
在树的具体应用中,除了生成树之外,节点的增、删、改、查甚至节点的升级和降级都是很常见的。本质上说,这些操作所涉及的是与业务相关的数据库操作,所以在采用“由底向上按层次(深度)遍历法”生成的TreeView中,这些操作的实现与传统方法是一致的,额外的操作无非是添加或修改深度编号。当然,实际需求是变化多端的,相应算法的设计与分析也是无止境的。

参考文献(Reference):
[1] Zane Thomas. DataViewTree for Windows Forms,http://www.abderaware.com/WhitePapers/ datatreeview.htm
[2] 李洪根. 树形结构在开发中的应用, http://www.microsoft.com/china/community/Column/ 21.mspx
[3] 李洪根. .NET平台下Web树形结构程序设计, http://www.microsoft.com/china/community/ Column/30.mspx
[4] Don Schlichting. Populating the TreeView Control from a Database, http://www.15seconds. com/issue/030827.htm
[5] HOW TO: Populate a TreeView Control from a Dataset in Visual Basic .NET, http://support. microsoft.com/?kbid=320755
[6] Scott Mitchell. Displaying XML Data in the Internet Explorer TreeView Control,http://aspnet. 4guysfromrolla.com/articles/051403-1.aspx

-------------
source code:

using System;
using System.Data;
using System.Windows.Forms;
using System.Collections;

namespace PopTreeView
{
/// <summary>
/// TreeOperator 的摘要说明。
/// </summary>
public class TreeOperator
{
public TreeOperator()
{
//
// TODO: 在此处添加构造函数逻辑
//
}

/// <summary>
/// 采用层次(深度)遍历法生成树
/// </summary>
/// <param name="objTreeView">目标树</param>
/// <param name="dsSource">数据集</param>
/// <param name="strTreeCaption">树显示名</param>
/// <param name="iTagIndex">值索引</param>
/// <param name="iContentIndex">内容索引</param>
/// <param name="iDepthIndex">层次索引</param>
public static void PopulateCompleteTree(ref System.Windows.Forms.TreeView objTreeView,DataSet dsSource,string strTreeCaption,int iTagIndex,int iContentIndex,int iDepthIndex)
{
//从底层开始遍历,开辟一个HashTable(以Tag值为关键字),存放当前计算的节点
objTreeView.Nodes.Clear();
int iMaxLen = GetMaxDepthLen(dsSource,iDepthIndex);
int iMinLen = GetTopDepthLen(dsSource,iDepthIndex);
Hashtable objArrNode = new Hashtable();
CollectNodes(dsSource,iTagIndex,iContentIndex,iDepthIndex,iMaxLen,iMinLen,ref objArrNode);
TreeNode objRootNode = new TreeNode(strTreeCaption);

//在得到节点表后,插入树
foreach(object objNode in objArrNode.Values)
{
TreeNode objNewNode = new TreeNode();
objNewNode = (TreeNode)objNode;
objRootNode.Nodes.Add(objNewNode);
}
objTreeView.Nodes.Add(objRootNode);
}

/// <summary>
/// 从底往上聚集节点
/// </summary>
/// <param name="dsSource"></param>
/// <param name="iTagIndex"></param>
/// <param name="iContentIndex"></param>
/// <param name="iDepthIndex"></param>
/// <param name="iCurDepLen"></param>
/// <param name="iMinDepLen"></param>
/// <param name="objArrNode"></param>
private static void CollectNodes(DataSet dsSource,int iTagIndex,int iContentIndex,int iDepthIndex,int iCurDepLen,int iMinDepLen,ref Hashtable objArrNode)
{
//收集节点
System.Data.DataView dv;
System.Windows.Forms.TreeNode tempNode;

//查找给定层节点
int i=iCurDepLen;
do
{
dv = new DataView(dsSource.Tables[0]);
string strExpr = "LEN(TRIM("+dsSource.Tables[0].Columns[iDepthIndex].ColumnName+"))="+Convert.ToString(i);
dv.RowFilter = strExpr;
i--;
}while(i>=iMinDepLen && dv.Count<=0);
iCurDepLen = i+1;
#region 逐层回溯,收集节点
foreach(System.Data.DataRowView drow in dv)
{
//查找父节点
string[] strArrParentInfo = LookupParentNode(dsSource,iDepthIndex,drow[iDepthIndex].ToString().Trim(),iTagIndex,iContentIndex);
string strTagValue = drow[iTagIndex].ToString().Trim();
string strContentValue = drow[iContentIndex].ToString();
//若无父节点,直接加入
if (strArrParentInfo == null)
{
//当前节点不在Hashtable中
if (objArrNode[strTagValue]==null)
{
//添加当前节点
tempNode = new TreeNode(strContentValue);
tempNode.Tag = strTagValue;
objArrNode.Add(strTagValue,tempNode);
}
}
else //有父节点,此时先查找父节点是否已在Hashtable中
{
string strParTagValue = strArrParentInfo[0].Trim();
string strParContentValue = strArrParentInfo[1].Trim();
//父节点已在Hashtable中
if (objArrNode[strParTagValue]!= null)
{
//当前节点不在Hashtable中
if (objArrNode[strTagValue]==null)
{
tempNode = new TreeNode(strContentValue);
tempNode.Tag = strTagValue;
}
else
{
//取出并移除该节点,然后插入父节点
tempNode = new TreeNode();
tempNode =(TreeNode)objArrNode[strTagValue];
objArrNode.Remove(strTagValue);
}
//插入到父节点中
TreeNode tempParNode = new TreeNode();
tempParNode = (TreeNode)objArrNode[strParTagValue];
tempParNode.Nodes.Add(tempNode);
objArrNode[strParTagValue] = tempParNode;
}
else //父节点不在Hashtable中
{
//当前节点不在Hashtable中
if (objArrNode[strTagValue]==null)
{
tempNode = new TreeNode(strContentValue);
tempNode.Tag = strTagValue;
}
else
{
//取出并移除该节点,然后插入父节点
tempNode = new TreeNode();
tempNode = (TreeNode)objArrNode[strTagValue];
objArrNode.Remove(strTagValue);
}
//创建父节点并将当前节点插入到父节点中
TreeNode tempParNode = new TreeNode(strParContentValue);
tempParNode.Tag = strParTagValue;
tempParNode.Nodes.Add(tempNode);
objArrNode.Add(strParTagValue,tempParNode);
}
}
}
#endregion

//还有未遍历的层
if (iCurDepLen>iMinDepLen)
{
CollectNodes(dsSource,iTagIndex,iContentIndex,iDepthIndex,iCurDepLen-1,iMinDepLen,ref objArrNode);
}

}

/// <summary>
/// 查找父亲节点
/// </summary>
/// <param name="dsSource"></param>
/// <param name="iDepthIndex"></param>
/// <param name="strSubDepth"></param>
/// <param name="iTagIndex"></param>
/// <param name="iContentIndex"></param>
/// <returns>找到返回由Tag值,内容值和深度值组成的字符串数组,否则返回null</returns>
private static string[] LookupParentNode(DataSet dsSource,int iDepthIndex,string strSubDepth,int iTagIndex,int iContentIndex)
{
System.Data.DataView dv;
int iSubLen = strSubDepth.Length;

if (iSubLen<=1)
{
return null;
}

int i=1;
do
{
dv = new DataView(dsSource.Tables[0]);
string strExpr ="TRIM("+dsSource.Tables[0].Columns[iDepthIndex].ColumnName+") = '"+strSubDepth.Substring(0,iSubLen-i)+"'";
dv.RowFilter = strExpr;
i++;
}while(i<iSubLen && dv.Count<=0);
if (dv.Count<=0)
{
return null;
}
else
{
string[] strArr = {dv[0][iTagIndex].ToString(),dv[0][iContentIndex].ToString(),dv[0][iDepthIndex].ToString()};
return strArr;
}
}

/// <summary>
/// 得到最大深度值(深度的长度)
/// </summary>
/// <param name="dsSource">数据集</param>
/// <param name="iDepthIndex">深度索引(列号)</param>
/// <returns>最大深度值</returns>
private static int GetMaxDepthLen(DataSet dsSource,int iDepthIndex)
{
DataRowCollection objRowCol = dsSource.Tables[0].Rows;
int iMax = objRowCol[0][iDepthIndex].ToString().Trim().Length;

foreach(DataRow objRow in objRowCol)
{
int iCurlen = objRow[iDepthIndex].ToString().Trim().Length;
if (iMax<iCurlen)
{
iMax = iCurlen;
}
}
return iMax;
}

/// <summary>
/// 得到最小深度值(深度的长度)
/// </summary>
/// <param name="dsSource">数据集</param>
/// <param name="iDepthIndex">深度索引(列号)</param>
/// <returns>最小深度值</returns>
private static int GetTopDepthLen(DataSet dsSource,int iDepthIndex)
{
DataRowCollection objRowCol = dsSource.Tables[0].Rows;
int iMin = objRowCol[0][iDepthIndex].ToString().Trim().Length;

foreach(DataRow objRow in objRowCol)
{
int iCurlen = objRow[iDepthIndex].ToString().Trim().Length;
if (iMin>iCurlen)
{
iMin = iCurlen;
}
}
return iMin;
}

}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐