本文主要講述如何通過使用TreeView控制元件來實現樹結構的顯示,以及樹節點的快速查詢功能。並針對通用樹結構的資料結構儲存進行一定的分析和設計。通過文字能夠了解如何儲存層次結構的資料庫設計,如何快速使用TreeView控制元件生產樹,以及如何快速查詢樹節點。
關鍵詞:C# TreeView、樹結構儲存、樹節點查詢、層次結構
一、 概述:
樹結構(層次結構)在專案的使用中特別常見,在不同專案中使用的控制元件可能不同(如:在Extjs中使用的是TreePanel控制元件,WinForm中可能用的是TreeView,等等),即不同的框架或類庫所用的控制元件可能不同,但是資料結構儲存基本上相同,最簡單的不在乎多了一個父節點ID(ParentID)欄位。在實際開發中我們主要會考慮以下問題:
1. 樹結構顯示的程式碼重用:即如何快速得到一棵樹?傳入什麼引數就能得到樹結構
2. 基本操作:樹節點的增刪改查
3. 所選節點的資訊:如何知道該節點是否為葉子節點,該節點的深度
4. 查詢子樹:如何快速查詢某節點及其所有的子節點
5. 關鍵詞查詢:根據某個關鍵詞,查詢出一顆子樹
如果能快速解決這些問題,那麼說明設計的樹結構就基本上是可以了,也算是設計的一個檢驗標準吧!按照常例,我們還是先看一下我們要實現的功能的效果圖:
圖表 1 樹結構顯示
圖表 2 樹結構快速查詢
注:以下例子是以中國省市區層次結構來進行說明舉例。
二、 樹結構通用資料庫設計:
最簡單的樹結構只要記錄三個欄位即可(ID、Text、ParentID,其中ID為主鍵,Text為節點顯示文字,ParentID為上級節點,無上級節點則為NULL)。這種設計就能滿足樹結構的資料的儲存,非常簡單,但是這種方式設計的人是簡單了,但是給程式設計的人就苦了。比如:
1. 把所有的葉子節點的資料全部查詢出來
2. 查詢出一個列表,按深度降序排序?
3. 最常用的資料許可權,如使用者只能看到本人所屬地區的下級地區結構,如使用者屬於浙江省,那麼我看到的列表就是以浙江省為根節點的子樹,使用者屬於南昌市,那麼顯示以南昌市為根節點的子樹
當然這也能夠根據(ID、Text、ParentID)實現上面的三個要求,但是明顯開發做的工作就特別多,不敢說難,但至少我感覺沒必要。(曾經我就因為別人設計好的表,寫了一堆檢視,目的就是為了增加Leavl、Leaf等欄位,在SQL裡面寫遞迴,夠害死人的,根據特鬱悶)
後來在眾多的專案經驗中發現對於樹結構如下設計主要欄位將會使程式設計人員變的輕鬆多了,查詢也非常簡單:
通用樹結構表設計方案:
ID: (PK)主鍵,唯一識別符號,建議為GUID
Text: 節點名稱,顯示的文字
ParentID: 上級節點/父節點ID
Code: 編碼 01、0101、0102
Level: 深度 根節點為0
Leaf: 是否為葉子節點 1:是 0 :否
Sort: 排序
Remark: 備註
Value:對應TreeMapping.Value (可選,如果有該欄位,那麼可以在一個介面,維護多棵樹結構,通用樹結構設計方案)
……其他備用欄位
ID: 對應Tree.ID
Value:值(int) 如:0代表部門樹 1:代表儀器裝置類別樹
Text: 說明 如:部門、儀器裝置類別
? 設計思路:
1、 通過Tree.Value 值可以查詢某一個類別的數結構,例如要查詢儀器裝置類別樹結構資料
SELECT * FROM TTree WHERE FValue=1 -- ORDER BY FLevel,FSort
2、 排序:排序通過深度(Level)和排序欄位(Sort)綜合決定(ORDER BY FLevel,FSort),Level優先順序別更高。這樣在新增和編輯時智慧排序只需考慮同類別(Value相同)同等級(Level相同)的排序邏輯即可。
3、 查詢指定節點下的自身及其所有孩子節點:可藉助Code欄位實現,例如查詢
SELECT * FROM TTree WHERE FCode LIKE '0101%' and FValue=1。
注意:Code和排序沒有任何關係,可以是Sort(0102)>Sort(0101)
? 本例中用到的資料庫結構:
前面講述的樹結構設計方案是我個人按專案經驗設計的,靈活度高很高,適用一切我目前碰見的層次結構。但是當然也有簡化版本,像本文例子中的省市區設計就是個簡化版本,(也是應公司專案侷限,沒能按自己的方式設計),如下圖所示:
這個專案為Oracle資料庫,(注:我的個人習慣是表前加T字首,欄位前加F字首,希望不會影響大家理解,嘿嘿,個人偏愛SQL Server資料庫)。
裡面有(ID、Text、ParentID、Level、Leaf、AutoCode、Remark)欄位,AutoCode對應通用設計裡面的Code,因為有了一個Code編碼欄位了,這樣就比我通用的少了Sort、Value欄位,後面的一下欄位如(FDataServerIP)都歸屬為我通用設計裡面的備用欄位,一般沒這麼多,這裡排序交給了Code,Text組合了。
三、 通用樹結構程式:
設計好表以後,應該就是樹節點的增刪改查了,這裡我就不在講述,畢竟我寫這篇文章的標題是“如何:使用TreeView控制元件實現樹結構顯示及快速查詢”,重點是展示和查詢,否則就跑題了。介面很簡單:
圖表 3 樹結構新增/編輯介面
以後有機會我再講新增編輯刪除的後臺邏輯刪除程式碼,希望到時候會有人關注。
為了實現樹結構的顯示和查詢,我們先寫一個通用類,完整程式碼如下:
/*
* Copy Right:(C)2011 Twilight Software Development Studio
* Creat By:xuzhihong
* Create Date: 2011-08-04
* Descriptions: 獲取Department樹
*/
public class GetDepartmentTree
{
public static List<TreeNode> GetTree(DataTable dt)
{
//TreeNodeCollection nodes = null;
List<TreeNode> listNodes = new List<TreeNode>();
foreach (var type in dt.Select("FParentID is null or FParentID=''","FCode,FText ASC"))
{
var node = CreatNode(type);
listNodes.Add(node);
FillChildren(type, node.Nodes, dt);
}
return listNodes;
}
public static List<TreeNode> GetTree(DataTable dt, string keyWord)
{
if (keyWord == "" || keyWord == null)
{
return GetTree(dt);
}
else
{
DataTable dtSlt = dt.Clone();
DataColumn[] primaryKeyColumn = new DataColumn[]
{
dtSlt.Columns["FID"]
};
dtSlt.PrimaryKey = primaryKeyColumn;
DataRow[] rows = dt.Select(string.Format("FText like '%{0}%'",keyWord));
foreach (var row in rows)
{
ImportParentRow(dt, dtSlt, row);
}
return GetTree(dtSlt);
}
}
/// <summary>
/// 建立節點資訊
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
private static TreeNode CreatNode(DataRow type)
{
var entity = GetDeptEnity(type);
return new TreeNode()
{
Text = type["FCode"] + "" + type["FText"],
ToolTipText = string.Format("名稱:{0} \r\n編碼:{1}\r\n資料伺服器:{2}\r\n媒體伺服器:{3}", type["FTEXT"], type["FCode"], type["FDATASERVER"],type["FMEDIASERVER"]),
Tag = entity
};
}
/// <summary>
/// 將DataRow轉化為實體
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
private static TDepartment GetDeptEnity(DataRow type)
{
return new TDepartment()
{
FID = type["FID"] + "",
FTEXT = type["FTEXT"] + "",
FPARENTID = type["FPARENTID"] + "",
FLEVEL = Convert.ToInt32(type["FLEVEL"]),
FAUTOCODE = type["FAUTOCODE"] + "",
FCODE = type["FCODE"] + "",
FDATASERVERIP = type["FDATASERVERIP"] + "",
FDATASERVERPORT = type["FDATASERVERPORT"] + "",
FDATASERVER = type["FDATASERVER"] + "",
FMEDIASERVERIP = type["FMEDIASERVERIP"] + "",
FMEDIASERVERPORT = type["FMEDIASERVERPORT"] + "",
FMEDIASERVER = type["FMEDIASERVER"] + "",
FREMARK = type["FREMARK"] + "",
FLEAF = Convert.ToInt32((type["FLEAF"]))
};
}
/// <summary>
/// 遞迴填充子節點
/// </summary>
/// <param name="parentType"></param>
/// <param name="parentNode"></param>
/// <param name="dt"></param>
private static void FillChildren(DataRow parentType, TreeNodeCollectionparentNode, DataTable dt)
{
foreach (var type in dt.Select(string.Format("FParentID='{0}'",parentType["FID"]), "FCode,FText ASC"))
{
var node = CreatNode(type);
parentNode.Add(node);
FillChildren(type, node.Nodes, dt);
}
}
/// <summary>
/// 匯入所有父行(包括自己)
/// </summary>
/// <param name="dtSource"></param>
/// <param name="dtSlt"></param>
/// <param name="currentRow"></param>
private static void ImportParentRow(DataTable dtSource, DataTable dtSlt,DataRow currentRow)
{
if (!dtSlt.Rows.Contains(currentRow["FID"])) //不存在則匯入行
{
dtSlt.ImportRow(currentRow);
}
if (!string.IsNullOrEmpty(currentRow["FParentID"] + "")) //如果還有父項
{
DataRow row = dtSource.Select(string.Format("FID='{0}'",currentRow["FParentID"]))[0];
ImportParentRow(dtSource, dtSlt, row);
}
}
}
裡面都是靜態方法,直接呼叫即可,只要看懂遞迴函式了,我想大部分就理解了。其中:引數DataTable dt就是select * from TTree,即所有資料。
那麼我們使用TreeView控制元件顯示和查詢樹就只需要呼叫BLL層中的下面這個方法了:
/// <summary>
/// 根據條件查詢,返回查詢後的DepartmentTree
/// </summary>
/// <param name="keyWord">關鍵詞,為空表示查詢整棵樹</param>
/// <returns></returns>
public List<TreeNode> GetTree(string keyWord)
{
DataTable dt = GetList("");// dt就是select * from TTree,即表中所有資料。
return GetDepartmentTree.GetTree(dt,keyWord);
}
返回的是List<TreeNode>剛好適合TreeView用來繫結,如下所示:
/// <summary>
/// 查詢 繫結資料來源
/// </summary>
/// <param name="keyWord">關鍵詞,為空表示顯示整棵樹</param>
/// <param name="tv"></param>
public void BindTreeData(TreeView tv, string keyWord)
{
tv.Nodes.Clear();
List<TreeNode> nodes = UsingBLL.department.GetTree(keyWord);
foreach (TreeNode node in nodes)
{
tv.Nodes.Add(node);
}
}
就這些通用的程式碼,到哪需要樹,呼叫一下就有了,顯示查詢都非常方便,最後效果請見前面的概述
在TreeView查詢某一節點,通常有兩種方法,一種是遞迴的,一種不是遞迴,但都是深度優先演算法。其中,非遞迴方法效率高些,而遞迴演算法要簡潔一些。
第一種,遞迴演算法,程式碼如下:
private TreeNode FindNode( TreeNode tnParent, string strValue )
{
if( tnParent == null ) return null;
if( tnParent.Text == strValue ) return tnParent;
TreeNode tnRet = null;
foreach( TreeNode tn in tnParent.Nodes )
{
tnRet = FindNode( tn, strValue );
if( tnRet != null ) break;
}
return tnRet;
}
第二種,非遞迴演算法,程式碼如下:
private TreeNode FindNode( TreeNode tnParent, string strValue )
{
if( tnParent == null ) return null;
if( tnParent.Text == strValue ) return tnParent;
else if( tnParent.Nodes.Count == 0 ) return null;
TreeNode tnCurrent, tnCurrentPar;
//Init node
tnCurrentPar = tnParent;
tnCurrent = tnCurrentPar.FirstNode;
while( tnCurrent != null && tnCurrent != tnParent )
{
while( tnCurrent != null )
{
if( tnCurrent.Text == strValue ) return tnCurrent;
else if( tnCurrent.Nodes.Count > 0 )
{
//Go into the deepest node in current sub-path
tnCurrentPar = tnCurrent;
tnCurrent = tnCurrent.FirstNode;
}
else if( tnCurrent != tnCurrentPar.LastNode )
{
//Goto next sible node
tnCurrent = tnCurrent.NextNode;
}
else
break;
}
//Go back to parent node till its has next sible node
while( tnCurrent != tnParent && tnCurrent == tnCurrentPar.LastNode )
{
tnCurrent = tnCurrentPar;
tnCurrentPar = tnCurrentPar.Parent;
}
//Goto next sible node
if( tnCurrent != tnParent )
tnCurrent = tnCurrent.NextNode;
}
return null;
}
程式呼叫,如下:
TreeNode tnRet = null;
foreach( TreeNode tn in yourTreeView.Nodes )
{
tnRet = FindNode( tn, yourValue );
if( tnRet != null ) break;
}