一、引言
在應用系統開發中,TreeView是一種使用頻率很高的控件。它的主要特點是能夠比較清晰地實現分類、導航、瀏覽等功能。因而,它的使用方法與編程技巧也一直受到技術人員的關注。隨着應用需求的變化,在很多情況下我們需要實現數據顯示的權限控制,即用戶看到的數據是經過過濾的,或是連續值,或是一些離散的值。就TreeView而言,原先可能顯示出來的是完整的具有嚴格父子關係得節點集,而經權限過濾後所要顯示的節點可能會變得離散,不再有完整的繼承關係。本文針對這一問題,通過對已有實現方法進行分析,提出改進算法。所附示例程序進一步解釋了算法設計思想。
二、三種常見生成方式的簡單分析
如文[2,3]所述,TreeView的生成基本上有三種方式:
1. 界面設計時在TreeView設計器或者代碼中直接填充TreeView節點。
這種方式通過拖放控件的方式生成樹,應用範圍窄,是一種非編程方式;
2. 從XML文件中建立樹形結構。
這種方式通過XML文件(串)生成樹,從形式上來說,這種方式是比較直觀的。因爲XML本身就是一棵“樹”,在.NET 平臺下TreeView的自動生成代碼中,TreeView的實際內容也是由XML表示的。此外,基於XML文件生成樹對異構環境下的分佈式應用具有重要意義。事實上,利用XML作爲通用數據傳遞格式已得到普遍認可;
3. 從數據庫中得到數據(在.NET中,我們可以理解爲一個數據集),建立樹形結構。
這種方式通過父子關係遞歸生成樹,是最容易理解的一種編程實現方式。一般是自頂向下遞歸生成,得到廣泛應用。
這裏,我們不妨舉一個實際的例子來說明一下,假設我們有這樣的數據集(可以看作是一個公司的部門列表):
TagValue |
ContentValue |
ParentID |
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:數據準備,給定數據集。 l Step 1:給定待增加子節點的節點(初始時一般爲根節點),記作CurNode,以及待增加節點的ParentID值(初始時爲根節點的ParentID),記作CurParentID; l Step 2:在數據集中查找具有指定ParentID值的所有節點,得到節點集objArr[], |
最終可以得到下圖所示的TreeView:
圖1 TreeView效果圖
這種方法的缺陷在於"由父節點及子節點"的遍歷順序意味着每個子節點的父節點必須存在,否則將搜索不到,即可能出現斷層現象。在很多實際應用中,我們發現這種實現方式不能完全奏效,最典型的情況就是當需要對衆節點所表徵的實際值(比如機構列表,人員列表,資源列表等)進行權限控制時,這時往往從數據庫中篩選出來的數據集中節點會出現斷層現象。比如我們假設設定權限時給定數據如表2,即把第一行“行銷部”去掉(注:權限過濾操作已超出本文討論的範圍,這裏假定數據集已準好),則運用算法1生成的TreeView如圖2所示。
TagValue |
ContentValue |
ParentID |
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效果圖
可以看到,這裏產生了節點遺漏現象。一般來說,我們可以從兩方面入手去解決問題,一方面可以修正數據集,另一方面則可以修改生成樹算法。顯然直接修正數據集是很複雜的,且會帶來效率問題。而單方面修改生成樹算法也是不是很好(即把遺漏的節點直接插到根節點下),因爲這時會出現父輩和晚輩同級的現象。
三、通過深度編號遞歸生成樹算法
回顧到已有的一些方法(文[1~5]),其中基於節點深度生成樹的方法給我們一些啓發,我們在構造數據集時可以增加深度字段,但這裏的深度不是簡單的層級號,是一個擴展了的概念,具體地說其實是一個深度編號,它與父輩編號存在一定的對應關係。比如表1所示的數據集可以作如下編號:
TagValue |
ContentValue |
ParentID |
DepthID |
G01 |
行銷部 |
|
a001 |
G02 |
顧問部 |
|
a002 |
G03 |
研發部 |
|
a003 |
G04 |
測試部 |
|
a004 |
GS01 |
行銷一部 |
G01 |
a001001 |
GS02 |
行銷二部 |
G01 |
a001002 |
GS03 |
行銷三部 |
G01 |
a001003 |
GSL01 |
行銷一部北京辦 |
GS01 |
a001001001 |
GSL02 |
行銷一部上海辦 |
GS01 |
a001001002 |
GS04 |
顧問一部 |
G02 |
a002001 |
GS05 |
顧問二部 |
G02 |
a002002 |
GS06 |
研發一部 |
G03 |
a003001 |
GS07 |
研發二部 |
G03 |
a003002 |
GS08 |
測試一部 |
G04 |
a004001 |
GS09 |
測試二部 |
G04 |
a004002 |
GSL03 |
研發一部杭州分部 |
GS06 |
a003001001 |
GSL04 |
研發一部西安分部 |
GS06 |
a003001002 |
表3 帶深度編號的數據集
其中,DepthID即是節點的深度編號。生成深度編號的過程其實也不復雜,首先我們可以制定編號的規則,比如層級編號的前綴、編碼長度、起始值等。當給某個節點編號時,只要找到所在層級的最大編號,然後增1。具體實現過程這裏不再細述。
於是,我們很自然地想到借鑑算法1的思想設計基於深度編號的生成樹程序。這時,我們可以根據當前節點的深度編號尋找其後代節點集,但要給出一個最大跨度(可以理解爲最高級與最低級間的間隔級數),因爲不可能無限制地找下去。這種方法可以部分程度上彌補"由父節點及子節點"的遍歷的缺陷,因爲當出現斷層時會沿着編號繼續往後找。但是還是會可能漏掉,比如我們給定數據集(把“研發一部”過濾掉):
TagValue |
ContentValue |
ParentID |
DepthID |
G01 |
行銷部 |
|
a001 |
G02 |
顧問部 |
|
a002 |
G03 |
研發部 |
|
a003 |
G04 |
測試部 |
|
a004 |
GS01 |
行銷一部 |
G01 |
a001001 |
GS02 |
行銷二部 |
G01 |
a001002 |
GS03 |
行銷三部 |
G01 |
a001003 |
GSL01 |
行銷一部北京辦 |
GS01 |
a001001001 |
GSL02 |
行銷一部上海辦 |
GS01 |
a001001002 |
GS04 |
顧問一部 |
G02 |
a002001 |
GS05 |
顧問二部 |
G02 |
a002002 |
GS07 |
研發二部 |
G03 |
a003002 |
GS08 |
測試一部 |
G04 |
a004001 |
GS09 |
測試二部 |
G04 |
a004002 |
GSL03 |
研發一部杭州分部 |
GS06 |
a003001001 |
GSL04 |
研發一部西安分部 |
GS06 |
a003001002 |
表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[], 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. 查找最近的上控層級,因爲有可能父節點層級不存在。 |
此時若給定數據集(我們把“研發部”和“行銷一部”過濾掉),
TagValue |
ContentValue |
ParentID |
DepthID |
G01 |
行銷部 |
|
a001 |
G02 |
顧問部 |
|
a002 |
G04 |
測試部 |
|
a004 |
GS02 |
行銷二部 |
G01 |
a001002 |
GS03 |
行銷三部 |
G01 |
a001003 |
GSL01 |
行銷一部北京辦 |
GS01 |
a001001001 |
GSL02 |
行銷一部上海辦 |
GS01 |
a001001002 |
GS04 |
顧問一部 |
G02 |
a002001 |
GS05 |
顧問二部 |
G02 |
a002002 |
GS07 |
研發二部 |
G03 |
a003002 |
GS08 |
測試一部 |
G04 |
a004001 |
GS09 |
測試二部 |
G04 |
a004002 |
GSL03 |
研發一部杭州分部 |
GS06 |
a003001001 |
GSL04 |
研發一部西安分部 |
GS06 |
a003001002 |
表5 給定數據集
則生成樹如下圖所示,
圖4 TreeView效果圖
這正是我們需要的結果。
當然,有時爲了結構的需要,我們還會採取所謂“中立”的方法。比如對於本文所提的TreeView控件節點生成問題,如果不想再寫算法去生成深度編號,那麼我們還可以通過給數據集增加標誌位的方法,即用標誌位來標識數據是否已被篩選。在運用傳統算法生成樹後,再檢查一下是否有未被篩選的數據,若有則查找其祖輩節點,將其插入祖輩節點。不過這裏的“查找祖輩節點”是在TreeView上進行的,當節點很多時其效率肯定沒有直接在數據集上搜索高。
另外,深度編號的引入不僅會給生成樹帶來方便,還可以讓權限設置更靈活。具體到我們的示例來說,一般如果我們要把某些部門過濾掉,那麼會把這些部門一個一個挑出來,我們稱之爲“離散值設置方式”。而當系統結構龐大時,我們更希望挑選一個區間,比如把一個部門及其下控的n級過濾掉,這是一個“連續值設置方式”,這時包含層級概念的深度編號可以很好地解決這個問題。實際的系統開發中,我們也發現採用這種方式是切實可行的。
四、其他TreeView生成方式
前面提到TreeView還可以通過XML文件(串)生成。這種方式實現的關鍵是構造出一個類似於TreeView的XML文檔或字符串出來。其基本思想應該與前面討論的算法是相似的,只是在程序實現上稍微複雜一些(其中,XML節點的索引可以基於文檔對象模型(DOM)來做)。另外還要注意的是,有很多的第三方TreeView控件,他們所支持的XML文檔的格式是不盡相同的。限於篇幅,本文不詳細討論具體實現過程。
五、小結
本文主要討論了.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;
}
}
}