以横向树方式显示Html表格
最近项目中常要画动态的Table,由于HTML表格中纵向合并单元格使用的是rowspan属性,一旦遇到纵向合并单元格的时候就会特别显得特别麻烦。其实我们项目中所画的Table大多都是些树,如果以类似TreeView添加节点的方式来构建Table对象,最后调用重写的ToString方法把整个表格呈现出来应该效果不错,避免了在代码中充斥着大量的td、tr等字符串,影响了代码的可读性及易于维护性。于是我简单得作了个类库分享给大家,希望能够对大家有。
类的结构图如下:

INode接口代码如下,其中最重要就是ToString方法了,它决定了如何呈现树。
ITree接口如下,这个接口继承了INode,特别突出了下ToString方法,其作用是呈现整个树。为了将一个纵向的树横向的呈现,我们必须使用前根遍历该树的所有子节点,并依次调用子节点的ToString方法,并在遍历到叶子节点的时候加上“回车”(</tr>)以表示该html行结束。
注意到ITree在INode的基础上加上了一个FullFill的方法,该方法用于将一个非满树用空结点补满,否则表格就会出现缺格。其中T是指用于填充树的类型,也就是用何种INode来填满这颗树。当然这应该和你用来构建这颗树的节点类型一样。
BaseNode是个模版类,其对添加和移除子节点时作了些额外控制,防止同一个INode对象添加到了2个或以上的父节点下:
BaseTree是ITree的模版实现,其中的关键在于如何计算每个节点的rowspan(只需要计算以该节点为根的子树有几个叶子节点,即表示该节点需要多少rowspan),并且这里的IEnumerator必须以前序遍历来返回所有子节点,原因之前已经提到过。
最后就是节点的具体实现了,SingletonNode是单td树节点:
而ContainIndexNode是带index的双td树节点:
其他有关INode集合以及html标签属性的接口和类的实现这里就不贴出来了,大家如果有兴趣可以下载源代码看下。
好了,赶快建个页面看下效果吧 ^_^
效果分别如下图:

第一次写blog,如果写的不好请大家见谅
TreeTable源代码
类的结构图如下:
INode接口代码如下,其中最重要就是ToString方法了,它决定了如何呈现树。
1
/// <summary>
2
/// INode [实现先序遍历]
3
/// </summary>
4
public interface INode : System.Collections.Generic.IEnumerable<INode>
5
{
6
INode Parent { get;set;} //取得父结点
7
INodeList Childs { get; } //取得下级节点
8
INodeList Leafs { get;} //取得以该节点为根的子数的叶子节点
9
bool IsLeaf { get;} //是否为叶子结点
10
11
int Tier { get;} //取得该节点在树中的所处层数(从0开始计数)
12
int Depth { get;} //取得以该节点为根的子数的深度(本层为0)
13
14
IAttributeDictionary Attributes { get;set;} //该节点的属性集合
15
16
object Content { get;set;} //节点中的内容
17
18
children operation
25
26
string ToString(); //将节点和其属性以及内容表示为网页可显示的字符串
27
28
29
}

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

25

26

27

28

29

ITree接口如下,这个接口继承了INode,特别突出了下ToString方法,其作用是呈现整个树。为了将一个纵向的树横向的呈现,我们必须使用前根遍历该树的所有子节点,并依次调用子节点的ToString方法,并在遍历到叶子节点的时候加上“回车”(</tr>)以表示该html行结束。
注意到ITree在INode的基础上加上了一个FullFill的方法,该方法用于将一个非满树用空结点补满,否则表格就会出现缺格。其中T是指用于填充树的类型,也就是用何种INode来填满这颗树。当然这应该和你用来构建这颗树的节点类型一样。
1
/// <summary>
2
/// ITree 的摘要说明
3
/// </summary>
4
public interface ITree : INode
5
{
6
//ITree FullFill(); //返回该树的"满数"
7
ITree FullFill<T>() where T : INode, new(); //泛型版本(用类型T来填充)
8
9
new string ToString(); //1循环子节点 2调用INode的[前序遍历] 3调用INode.ToString(); 4每行开始<tr>、结束(遍历到叶子节点)加上</tr> 5加上<table></table>
10
}

2

3

4

5

6

7

8

9

10

BaseNode是个模版类,其对添加和移除子节点时作了些额外控制,防止同一个INode对象添加到了2个或以上的父节点下:
1
/// <summary>
2
/// Node 的摘要说明
3
/// </summary>
4
public abstract class BaseNode : INode
5
{
6
protected INode parent; //父节点
7
protected IList<INode> childs = new List<INode>(); //子节点的"内部表现"
8
9
public BaseNode()
10
{ }
11
12
public BaseNode(INode parent)
13
{
14
parent.AddChild(this);
15
this.parent = parent;
16
}
17
18
//private void initial()
19
//{
20
21
//}
22
23
INode 成员
159
160
IEnumerable 成员
177
178
IEnumerable 成员
186
187
/// <summary>
188
/// 先序遍历,并按序入队列
189
/// </summary>
190
private void perOrderTraverse(Queue<INode> queueList, INode parentNode)
191
{
192
foreach (INode node in parentNode.Childs)
193
{
194
queueList.Enqueue(node);
195
perOrderTraverse(queueList, node);
196
}
197
}
198
199
/// <summary>
200
/// 取得深度
201
/// </summary>
202
private int getDepth(INode node)
203
{
204
if (node == null || node.Childs.Count == 0)
205
return 0;
206
207
int[] childDepthArray = new int[node.Childs.Count];
208
INodeList childList = node.Childs;
209
for (int i = 0; i < childList.Count; i++)
210
{
211
childDepthArray[i] = getDepth(childList[i]);
212
}
213
214
Array.Sort<int>(childDepthArray); //升序排序
215
return childDepthArray[childDepthArray.Length - 1] + 1; //取得最大层数子树的层数 + 1
216
}
217
218
/// <summary>
219
/// 取得层数
220
/// </summary>
221
private int getTier(INode node)
222
{
223
int tier = 0;
224
INode n = node;
225
while (n.Parent != null)
226
{
227
n = n.Parent;
228
tier++;
229
}
230
return tier;
231
}
232
}

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

159

160

177

178

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

BaseTree是ITree的模版实现,其中的关键在于如何计算每个节点的rowspan(只需要计算以该节点为根的子树有几个叶子节点,即表示该节点需要多少rowspan),并且这里的IEnumerator必须以前序遍历来返回所有子节点,原因之前已经提到过。
1
/// <summary>
2
/// Tree 的摘要说明
3
/// </summary>
4
public class BaseTree : BaseNode, ITree
5
{
6
protected IAttributeDictionary attributes = new BaseAttributeDictionary(); //属性集合
7
8
ITree 成员
51
52
IEnumerable 成员
60
61
public override IAttributeDictionary Attributes
62
{
63
get { return this.attributes; }
64
set { this.attributes = value; }
65
}
66
67
public override object Content
68
{
69
get { return this.ToString(); }
70
set { new Exception("不可更改内容"); }
71
}
72
73
/// <summary>
74
/// 1循环子节点
75
/// 2调用INode的[前序遍历]
76
/// 3调用INode.ToString();
77
/// 4每行开始<tr>、结束(遍历到叶子节点)加上<![CDATA[</tr> ]]>
78
/// 5加上<![CDATA[<table></table>]]>
79
/// </summary>
80
/// <returns></returns>
81
public override string ToString()
82
{
83
System.Text.StringBuilder builder = new System.Text.StringBuilder();
84
builder.Append("<table ").Append(this.attributes == null ? "" : this.attributes.ToString()).Append(">"); //加上table的属性
85
//多根循环
86
//foreach (INode rootNode in this.Childs)
87
{
88
builder.Append(@"<tr>");
89
foreach (INode node in this) //前序遍历
90
{
91
//加上rowspan属性
92
countRowSpan(node);
93
94
builder.Append(node.ToString());
95
if (node.IsLeaf)
96
{
97
builder.Append(@"</tr><tr>");
98
}
99
}
100
builder.Remove(builder.Length - 4, 4); //移除最后的<tr>
101
}
102
builder.Append(@"</table>");
103
return builder.ToString();
104
}
105
106
/// <summary>
107
/// 计算td的rowspan,并加上rowspan属性
108
/// </summary>
109
/// <param name="node">原结点</param>
110
protected virtual void countRowSpan(INode node)
111
{
112
int value = node.Leafs.Count; //计算叶子结点(rowspan的值)
113
if (node.Attributes == null)
114
{
115
node.Attributes = new BaseAttributeDictionary();
116
}
117
IAttri attri = new SingletonAttri("rowspan", value.ToString());
118
node.Attributes.Add(attri); //加上rowspan属性
119
}
120
121
///// <summary>
122
///// 填充固定长度线性树
123
///// </summary>
124
//private void addFixLengthNode(int length, INode parent)
125
//{
126
// if (length < 1)
127
// throw new Exception("无效长度,必须大于1");
128
129
// INode node = new SingletonNode(parent, " "); //空节点
130
// for (int i = 0; i < length - 1; i++)
131
// {
132
// INode tempNode = new SingletonNode(" "); //空节点
133
// node.AddChild(tempNode);
134
// node = tempNode; //持有下一个节点
135
// }
136
//}
137
138
/// <summary>
139
/// 填充固定长度线性树(用类型T填充)
140
/// </summary>
141
private void addFixLengthNode<T>(int length, INode parent) where T : INode, new()
142
{
143
if (length < 1)
144
throw new Exception("无效长度,必须大于1");
145
146
INode node = new T(); //空节点
147
node.Parent = parent;
148
node.Content = " ";
149
150
for (int i = 0; i < length - 1; i++)
151
{
152
INode tempNode = new T(); //空节点
153
tempNode.Content = " ";
154
155
node.AddChild(tempNode);
156
node = tempNode; //持有下一个节点
157
}
158
}
159
}

2

3

4

5

6

7

8

51

52

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

最后就是节点的具体实现了,SingletonNode是单td树节点:
1
/// <summary>
2
/// SingletonNode -- 单td树结点
3
/// </summary>
4
public class SingletonNode : BaseNode
5
{
6
private string content; //td中的内容
7
private IAttributeDictionary attributes = new BaseAttributeDictionary(); //td中的属性集合
8
9
构造器
32
33
public override IAttributeDictionary Attributes
34
{
35
get
36
{
37
return this.attributes;
38
}
39
set
40
{
41
this.attributes = value;
42
}
43
}
44
45
public override object Content
46
{
47
get
48
{
49
return this.content;
50
}
51
set
52
{
53
this.content = value.ToString();
54
}
55
}
56
57
public override string ToString()
58
{
59
System.Text.StringBuilder builder = new System.Text.StringBuilder();
60
61
string attriStr = attributes.ToString(); //属性
62
63
string showContent = this.content;
64
if (string.IsNullOrEmpty(content))
65
showContent = " "; //若该td中内容为空则需要显示一个空格,否则该td会显示不出来
66
67
builder.Append(@"<td ").Append(attriStr).Append(@">").Append(showContent).Append(@"</td>");
68
return builder.ToString();
69
}
70
71
}

2

3

4

5

6

7

8

9

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

而ContainIndexNode是带index的双td树节点:
1
/// <summary>
2
/// ContainIndexNode 包含index的结点
3
/// </summary>
4
public class ContainIndexNode : BaseNode
5
{
6
private string content; //td2中的内容
7
private IAttributeDictionary attributes = new BaseAttributeDictionary(); //td中的属性集合
8
9
构造器
31
32
public override IAttributeDictionary Attributes
33
{
34
get
35
{
36
return this.attributes;
37
}
38
set
39
{
40
this.attributes = value;
41
}
42
}
43
44
public override object Content
45
{
46
get
47
{
48
return this.content;
49
}
50
set
51
{
52
this.content = value.ToString();
53
}
54
}
55
56
public override string ToString()
57
{
58
System.Text.StringBuilder builder = new System.Text.StringBuilder();
59
60
string attriStr = attributes.ToString(); //属性
61
62
string showContent = this.content;
63
if (string.IsNullOrEmpty(content))
64
showContent = " "; //若该td中内容为空则需要显示一个空格,否则该td会显示不出来
65
66
builder.Append(@"<td ").Append(attriStr).Append(@">").Append(this.getIndex()).Append(@"</td>");
67
builder.Append(@"<td ").Append(attriStr).Append(@">").Append(showContent).Append(@"</td>");
68
return builder.ToString();
69
}
70
71
取得索引
111
}
只需要将注意力集中在单个内容对象Content的呈现方式上就可以了。
2

3

4

5

6

7

8

9

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

111

其他有关INode集合以及html标签属性的接口和类的实现这里就不贴出来了,大家如果有兴趣可以下载源代码看下。
好了,赶快建个页面看下效果吧 ^_^
1
public partial class _Default : System.Web.UI.Page
2
{
3
protected void Page_Load(object sender, EventArgs e)
4
{
5
BaseTree tree = new BaseTree();
6
7
build node
51
52
//填充树
53
tree.FullFill<SingletonNode>(); //单td的
54
//tree.FullFill<ContainIndexNode>(); //带index的双td节点表
55
56
string html = tree.ToString();
57
this.div1.InnerHtml = html;
58
}
59
60
private INode nodeFactory(string content)
61
{
62
return new SingletonNode(content); //单td的
63
//return new ContainIndexNode(content); //带index的双td节点表
64
}
65
}

2

3

4

5

6

7

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

效果分别如下图:


第一次写blog,如果写的不好请大家见谅

TreeTable源代码
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构