首先是一个简单的接口,ITreeNode,它表示一个树的节点。取这个名字是因为类别就是“树”。还有一点约定,见代码注释
[code=C]
/// <summary>
/// 定义树结点
/// </summary>
public interface ITreeNode
{
/// <summary>
/// Id 应大于 0
/// </summary>
int Id { get; }
/// <summary>
/// 如果 ParentId 为 0,表示没有父节点
/// </summary>
int ParentId { get; }
/// <summary>
/// 节点的顺序,越小越靠前
/// </summary>
int Ordering { get; }
}
[/code]
然后是一个 Forest 类,封装一些常用操作。用它来帮助我们处理树形结构,
[code=C]
/// <summary>
/// 表示一个森林结构。
/// </summary>
public class Forest <T> : IEnumerable <T> where T : ITreeNode
{
IEnumerable <T> _allNodes;
public bool IsRoot(T node)
{
return node.ParentId == 0;
}
/// <summary>
/// 将 nodes 组织成森林结构。如果一个结点的 ParentId 等于 0,则这个结点是根结点。
/// </summary>
public Forest(IEnumerable <T> nodes)
{
// _allNodes 中的节点是对树进行前序变量的顺序
_allNodes = nodes;
// ToList 是必须的,否则堆栈溢出
IEnumerable <T> temp = _allNodes.OrderBy(x => GetPathString(x)).ToList();
_allNodes = temp;
}
public T GetNode(int id)
{
return _allNodes.SingleOrDefault(x => x.Id == id);
}
public T GetParent(T node)
{
return _allNodes.SingleOrDefault(x => x.Id == node.ParentId);
}
/// <summary>
/// 获取 node 的子结点
/// </summary>
/// <param name="node"> </param>
/// <returns> </returns>
public IEnumerable <T> GetChildNodes(T node)
{
return _allNodes
.Where(x => x.ParentId == node.Id)
.OrderBy(x => x.Ordering);
}
/// <summary>
/// 获取森林中的所有根结点
/// </summary>
/// <returns> </returns>
public IEnumerable <T> GetRootNodes()
{
return _allNodes
.Where(x => IsRoot(x))
.OrderBy(x => x.Ordering);
}
/// <summary>
/// 获取结点在树中的路径
/// </summary>
/// <returns> </returns>
public IEnumerable <T> GetPath(T node)
{
List <T> path = new List <T>();
while (node != null)
{
if (path.Contains(node))
{
string msg = string.Format("构造结点的路径时,发现结点两次出现在路径中,这说明有两个结点互为父级。结点 Id:{0}", node.Id);
throw new Exception(msg);
}
path.Add(node);
node = GetParent(node);
}
path.Reverse();
return path;
}
/// <summary>
/// 帮助排序
/// </summary>
/// <param name="node"> </param>
/// <returns> </returns>
private string GetPathString(T node)
{
IEnumerable <T> path = GetPath(node);
StringBuilder sb = new StringBuilder();
foreach (T item in path)
{
T parent = this.GetParent(item);
int parentId = parent == null ? 0 : parent.Id;
sb.AppendFormat("{0:000000}:{1:000000}:{2:000000}&", parentId, item.Ordering, item.Id);
}
return sb.ToString();
}
/// <summary>
/// 判断 n1 是否是 n2 的后代
/// </summary>
/// <param name="n1"> </param>
/// <param name="n2"> </param>
/// <returns> </returns>
public bool IsDescendant(T n1, T n2)
{
IEnumerable <T> path = GetPath(n1);
return path.Contains(n2);
}
/// <summary>
/// 获取以参数node为根的树中的所有结点
/// </summary>
/// <param name="node"> </param>
/// <returns> </returns>
public IEnumerable <T> GetNodesInTree(T node)
{
List <T> result = new List <T>();
// 对子树做层次遍历
Queue <T> queue = new Queue <T>();
queue.Enqueue(node);
while (queue.Count > 0)
{
T head = queue.Dequeue();
result.Add(head);
// 子结点入队
foreach (T child in GetChildNodes(head))
{
queue.Enqueue(child);
}
}
return result;
}
#region IEnumerable <T> 成员
public IEnumerator <T> GetEnumerator()
{
return _allNodes.GetEnumerator();
}
#endregion
#region IEnumerable 成员
IEnumerator IEnumerable.GetEnumerator()
{
return _allNodes.GetEnumerator();
}
#endregion
/// <summary>
/// 判断在 node 是否有下一个兄弟结点
/// </summary>
/// <param name="node"> </param>
/// <returns> </returns>
private bool HasNextSibling(T node)
{
T parent = GetParent(node);
if (parent == null)
{
return GetRootNodes().Last().Equals(node) == false;
}
return GetChildNodes(parent).Last().Equals(node) == false;
}
/// <summary>
/// 为每个结点的 treeLines 属性赋值。结点必须已
/// </summary>
/// <param name="categoryTable"> </param>
public string GetTreeLines(T node)
{
// 取得结点的路径
IEnumerable <T> path = GetPath(node);
// 构造树形线
StringBuilder sb = new StringBuilder();
for (int i = 0; i < path.Count(); i++)
{
T nodeInPath = path.ElementAt(i);
if (path.Count() - 1 == i) // 路径上的最后一个结点要特殊处理
{
if (HasNextSibling(nodeInPath)) // 有下一个兄弟
{
sb.Append("├");
}
else // 没有下一个兄弟
{
sb.Append("└");
}
}
else // 不是路径上最后一个结点
{
if (HasNextSibling(nodeInPath)) // 有下一个兄弟,加竖线
{
sb.Append("│");
}
else // 没有下一个兄弟,加空白
{
sb.Append(" ");
}
}
}
return sb.ToString();
}
}
[/code]
剩下的就是使用这个类了。首先需要一个实现了 ITreeNode 的类,可以让项目中的实体类实现这个接口,这里我写了一个 FakeTreeNode 类,他默然的 Ordering 值是 999。
[code=C]
public class FakeTreeNode : ITreeNode
{
public FakeTreeNode()
{
Ordering = 999;
}
#region ITreeNode 成员
public int Id
{
get;
set;
}
public int ParentId
{
get;
set;
}
public int Ordering
{
get;
set;
}
#endregion
public string Title { get; set; }
}
[/code]
测试:
[code=C]
class Program
{
public static void Main()
{
FakeTreeNode n1 = new FakeTreeNode { Id = 1, ParentId = 0, Title = "图书" };
FakeTreeNode n2 = new FakeTreeNode { Id = 2, ParentId = 1, Title = "小说" };
FakeTreeNode n3 = new FakeTreeNode { Id = 3, ParentId = 2, Title = "历史" };
FakeTreeNode n4 = new FakeTreeNode { Id = 4, ParentId = 2, Title = "武侠", Ordering = 1 };
FakeTreeNode n5 = new FakeTreeNode { Id = 5, ParentId = 2, Title = "言情" };
FakeTreeNode n6 = new FakeTreeNode { Id = 6, ParentId = 1, Title = "散文" };
FakeTreeNode n7 = new FakeTreeNode { Id = 7, ParentId = 0, Title = "电器" };
FakeTreeNode n8 = new FakeTreeNode { Id = 8, ParentId = 7, Title = "冰箱" };
FakeTreeNode n9 = new FakeTreeNode { Id = 9, ParentId = 7, Title = "电视" };
FakeTreeNode n10 = new FakeTreeNode { Id = 10, ParentId = 9, Title = "液晶电视" };
List <FakeTreeNode> nodes = new List <FakeTreeNode>();
nodes.Add(n1);
nodes.Add(n2);
nodes.Add(n3);
nodes.Add(n4);
nodes.Add(n5);
nodes.Add(n6);
nodes.Add(n7);
nodes.Add(n8);
nodes.Add(n9);
nodes.Add(n10);
Forest <FakeTreeNode> forest = new Forest <FakeTreeNode>(nodes);
output(forest);
n7.Ordering = 998; // 电器前移
n8.Ordering = 1000; // 冰箱后移
forest = new Forest <FakeTreeNode>(forest);
output(forest);
Console.ReadKey();
}
private static void output(Forest <FakeTreeNode> forest)
{
foreach (var item in forest)
{
Console.WriteLine(forest.GetTreeLines(item) + item.Title);
}
Console.WriteLine("-----------");
}
}
[/code]
使用一个 foreach (var item in forest) 循环就可以按照期望的顺序打印整个类别层次。输出:
├图书
│├小说
││├武侠
││├历史
││└言情
│└散文
└电器
├冰箱
└电视
└液晶电视
-----------
├电器
│├电视
││└液晶电视
│└冰箱
└图书
├小说
│├武侠
│├历史
│└言情
└散文
-----------
[code=C]
/// <summary>
/// 定义树结点
/// </summary>
public interface ITreeNode
{
/// <summary>
/// Id 应大于 0
/// </summary>
int Id { get; }
/// <summary>
/// 如果 ParentId 为 0,表示没有父节点
/// </summary>
int ParentId { get; }
/// <summary>
/// 节点的顺序,越小越靠前
/// </summary>
int Ordering { get; }
}
[/code]
然后是一个 Forest 类,封装一些常用操作。用它来帮助我们处理树形结构,
[code=C]
/// <summary>
/// 表示一个森林结构。
/// </summary>
public class Forest <T> : IEnumerable <T> where T : ITreeNode
{
IEnumerable <T> _allNodes;
public bool IsRoot(T node)
{
return node.ParentId == 0;
}
/// <summary>
/// 将 nodes 组织成森林结构。如果一个结点的 ParentId 等于 0,则这个结点是根结点。
/// </summary>
public Forest(IEnumerable <T> nodes)
{
// _allNodes 中的节点是对树进行前序变量的顺序
_allNodes = nodes;
// ToList 是必须的,否则堆栈溢出
IEnumerable <T> temp = _allNodes.OrderBy(x => GetPathString(x)).ToList();
_allNodes = temp;
}
public T GetNode(int id)
{
return _allNodes.SingleOrDefault(x => x.Id == id);
}
public T GetParent(T node)
{
return _allNodes.SingleOrDefault(x => x.Id == node.ParentId);
}
/// <summary>
/// 获取 node 的子结点
/// </summary>
/// <param name="node"> </param>
/// <returns> </returns>
public IEnumerable <T> GetChildNodes(T node)
{
return _allNodes
.Where(x => x.ParentId == node.Id)
.OrderBy(x => x.Ordering);
}
/// <summary>
/// 获取森林中的所有根结点
/// </summary>
/// <returns> </returns>
public IEnumerable <T> GetRootNodes()
{
return _allNodes
.Where(x => IsRoot(x))
.OrderBy(x => x.Ordering);
}
/// <summary>
/// 获取结点在树中的路径
/// </summary>
/// <returns> </returns>
public IEnumerable <T> GetPath(T node)
{
List <T> path = new List <T>();
while (node != null)
{
if (path.Contains(node))
{
string msg = string.Format("构造结点的路径时,发现结点两次出现在路径中,这说明有两个结点互为父级。结点 Id:{0}", node.Id);
throw new Exception(msg);
}
path.Add(node);
node = GetParent(node);
}
path.Reverse();
return path;
}
/// <summary>
/// 帮助排序
/// </summary>
/// <param name="node"> </param>
/// <returns> </returns>
private string GetPathString(T node)
{
IEnumerable <T> path = GetPath(node);
StringBuilder sb = new StringBuilder();
foreach (T item in path)
{
T parent = this.GetParent(item);
int parentId = parent == null ? 0 : parent.Id;
sb.AppendFormat("{0:000000}:{1:000000}:{2:000000}&", parentId, item.Ordering, item.Id);
}
return sb.ToString();
}
/// <summary>
/// 判断 n1 是否是 n2 的后代
/// </summary>
/// <param name="n1"> </param>
/// <param name="n2"> </param>
/// <returns> </returns>
public bool IsDescendant(T n1, T n2)
{
IEnumerable <T> path = GetPath(n1);
return path.Contains(n2);
}
/// <summary>
/// 获取以参数node为根的树中的所有结点
/// </summary>
/// <param name="node"> </param>
/// <returns> </returns>
public IEnumerable <T> GetNodesInTree(T node)
{
List <T> result = new List <T>();
// 对子树做层次遍历
Queue <T> queue = new Queue <T>();
queue.Enqueue(node);
while (queue.Count > 0)
{
T head = queue.Dequeue();
result.Add(head);
// 子结点入队
foreach (T child in GetChildNodes(head))
{
queue.Enqueue(child);
}
}
return result;
}
#region IEnumerable <T> 成员
public IEnumerator <T> GetEnumerator()
{
return _allNodes.GetEnumerator();
}
#endregion
#region IEnumerable 成员
IEnumerator IEnumerable.GetEnumerator()
{
return _allNodes.GetEnumerator();
}
#endregion
/// <summary>
/// 判断在 node 是否有下一个兄弟结点
/// </summary>
/// <param name="node"> </param>
/// <returns> </returns>
private bool HasNextSibling(T node)
{
T parent = GetParent(node);
if (parent == null)
{
return GetRootNodes().Last().Equals(node) == false;
}
return GetChildNodes(parent).Last().Equals(node) == false;
}
/// <summary>
/// 为每个结点的 treeLines 属性赋值。结点必须已
/// </summary>
/// <param name="categoryTable"> </param>
public string GetTreeLines(T node)
{
// 取得结点的路径
IEnumerable <T> path = GetPath(node);
// 构造树形线
StringBuilder sb = new StringBuilder();
for (int i = 0; i < path.Count(); i++)
{
T nodeInPath = path.ElementAt(i);
if (path.Count() - 1 == i) // 路径上的最后一个结点要特殊处理
{
if (HasNextSibling(nodeInPath)) // 有下一个兄弟
{
sb.Append("├");
}
else // 没有下一个兄弟
{
sb.Append("└");
}
}
else // 不是路径上最后一个结点
{
if (HasNextSibling(nodeInPath)) // 有下一个兄弟,加竖线
{
sb.Append("│");
}
else // 没有下一个兄弟,加空白
{
sb.Append(" ");
}
}
}
return sb.ToString();
}
}
[/code]
剩下的就是使用这个类了。首先需要一个实现了 ITreeNode 的类,可以让项目中的实体类实现这个接口,这里我写了一个 FakeTreeNode 类,他默然的 Ordering 值是 999。
[code=C]
public class FakeTreeNode : ITreeNode
{
public FakeTreeNode()
{
Ordering = 999;
}
#region ITreeNode 成员
public int Id
{
get;
set;
}
public int ParentId
{
get;
set;
}
public int Ordering
{
get;
set;
}
#endregion
public string Title { get; set; }
}
[/code]
测试:
[code=C]
class Program
{
public static void Main()
{
FakeTreeNode n1 = new FakeTreeNode { Id = 1, ParentId = 0, Title = "图书" };
FakeTreeNode n2 = new FakeTreeNode { Id = 2, ParentId = 1, Title = "小说" };
FakeTreeNode n3 = new FakeTreeNode { Id = 3, ParentId = 2, Title = "历史" };
FakeTreeNode n4 = new FakeTreeNode { Id = 4, ParentId = 2, Title = "武侠", Ordering = 1 };
FakeTreeNode n5 = new FakeTreeNode { Id = 5, ParentId = 2, Title = "言情" };
FakeTreeNode n6 = new FakeTreeNode { Id = 6, ParentId = 1, Title = "散文" };
FakeTreeNode n7 = new FakeTreeNode { Id = 7, ParentId = 0, Title = "电器" };
FakeTreeNode n8 = new FakeTreeNode { Id = 8, ParentId = 7, Title = "冰箱" };
FakeTreeNode n9 = new FakeTreeNode { Id = 9, ParentId = 7, Title = "电视" };
FakeTreeNode n10 = new FakeTreeNode { Id = 10, ParentId = 9, Title = "液晶电视" };
List <FakeTreeNode> nodes = new List <FakeTreeNode>();
nodes.Add(n1);
nodes.Add(n2);
nodes.Add(n3);
nodes.Add(n4);
nodes.Add(n5);
nodes.Add(n6);
nodes.Add(n7);
nodes.Add(n8);
nodes.Add(n9);
nodes.Add(n10);
Forest <FakeTreeNode> forest = new Forest <FakeTreeNode>(nodes);
output(forest);
n7.Ordering = 998; // 电器前移
n8.Ordering = 1000; // 冰箱后移
forest = new Forest <FakeTreeNode>(forest);
output(forest);
Console.ReadKey();
}
private static void output(Forest <FakeTreeNode> forest)
{
foreach (var item in forest)
{
Console.WriteLine(forest.GetTreeLines(item) + item.Title);
}
Console.WriteLine("-----------");
}
}
[/code]
使用一个 foreach (var item in forest) 循环就可以按照期望的顺序打印整个类别层次。输出:
├图书
│├小说
││├武侠
││├历史
││└言情
│└散文
└电器
├冰箱
└电视
└液晶电视
-----------
├电器
│├电视
││└液晶电视
│└冰箱
└图书
├小说
│├武侠
│├历史
│└言情
└散文
-----------