前言
之前我們曾寫過一篇文章 FineUI小技巧(3)表格導出與文件下載,對於在 FineUI 中導出表格數據進行了詳細描述。今天我們要更進一步,介紹下如何導出多表頭表格。
多表頭表格的標簽定義
在 ASPX 中,我們通過 GroupField 列來定義多表頭,如下所示:
<f:Grid ID="Grid1" Title="表格" EnableCollapse="true" ShowBorder="true" ShowHeader="true" Width="800px"
runat="server" DataKeyNames="Id,Name">
<Columns>
<f:TemplateField ColumnID="tfNumber" Width="60px">
<ItemTemplate>
<span id="spanNumber" runat="server"><%# Container.DataItemIndex + 1 %></span>
</ItemTemplate>
</f:TemplateField>
<f:GroupField EnableLock="true" HeaderText="分組一" TextAlign="Center">
<Columns>
<f:BoundField Width="100px" DataField="Name" DataFormatString="{0}" HeaderText="姓名" />
<f:TemplateField ColumnID="tfGender" Width="80px" HeaderText="性別" TextAlign="Center">
<ItemTemplate>
<asp:Label ID="labGender" runat="server" Text='<%# GetGender(Eval("Gender")) %>'></asp:Label>
</ItemTemplate>
</f:TemplateField>
<f:GroupField EnableLock="true" HeaderText="考試成績" TextAlign="Center">
<Columns>
<f:BoundField EnableLock="true" Width="80px" DataField="ChineseScore" SortField="ChineseScore" HeaderText="語文成績"
TextAlign="Center" />
<f:BoundField EnableLock="true" Width="80px" DataField="MathScore" SortField="MathScore" HeaderText="數學成績"
TextAlign="Center" />
<f:BoundField EnableLock="true" Width="80px" DataField="TotalScore" SortField="TotalScore" HeaderText="總成績"
TextAlign="Center" />
</Columns>
</f:GroupField>
</Columns>
</f:GroupField>
<f:BoundField ExpandUnusedSpace="True" DataField="Major" HeaderText="所學專業" />
<f:BoundField Width="100px" DataField="LogTime" DataFormatString="{0:yy-MM-dd}" HeaderText="注冊日期" />
</Columns>
</f:Grid>
這是一個樹狀的結構,通過 GroupField 的 Columns 集合來定義子列,從而實現多表頭的效果:
老方法已經不再奏效
如果照搬之前的邏輯,我們和容易寫出如下的導出代碼(處理數組很簡單,循環搞定):
protected void Button1_Click(object sender, EventArgs e)
{
Response.ClearContent();
Response.AddHeader("content-disposition", "attachment; filename=myexcel.xls");
Response.ContentType = "application/excel";
Response.ContentEncoding = System.Text.Encoding.UTF8;
Response.Write(GetGridTableHtml(Grid1));
Response.End();
}
private string GetGridTableHtml(Grid grid)
{
StringBuilder sb = new StringBuilder();
sb.Append("<meta http-equiv=\"content-type\" content=\"application/excel; charset=UTF-8\"/>");
sb.Append("<table cellspacing=\"0\" rules=\"all\" border=\"1\" style=\"border-collapse:collapse;\">");
sb.Append("<tr>");
foreach (GridColumn column in grid.Columns)
{
sb.AppendFormat("<td>{0}</td>", column.HeaderText);
}
sb.Append("</tr>");
foreach (GridRow row in grid.Rows)
{
sb.Append("<tr>");
foreach (GridColumn column in grid.Columns)
{
string html = row.Values[column.ColumnIndex].ToString();
if (column.ColumnID == "tfNumber")
{
html = (row.FindControl("spanNumber") as System.Web.UI.HtmlControls.HtmlGenericControl).InnerText;
}
else if (column.ColumnID == "tfGender")
{
html = (row.FindControl("labGender") as AspNet.Label).Text;
}
sb.AppendFormat("<td>{0}</td>", html);
}
sb.Append("</tr>");
}
sb.Append("</table>");
return sb.ToString();
}
打開導出的文件,我們會發現所有子列都不見了:
這樣很容易理解,因為在后台,FineUI 也是按照樹狀的結構存儲 Grid1.Columns 屬性的:
[{
"text": "分組一",
"columns": [{
"text": "姓名"
}, {
"text": "性別"
}, {
"text": "考試成績",
"columns": [{
"text": "語文成績"
}, {
"text": "數學成績"
}, {
"text": "總成績"
}]
}, ]
}, {
"text": "所學專業"
}, {
"text": "注冊日期"
}]
樹狀結構轉換為 table 標簽
這個還真不好辦,因為 table 標簽不像它看起來那么簡單,每個單元格都可能要設置 rowspan 和 colspan,來看下我們最終需要的結構:

最終生成的 table 標簽如下所示:
<table cellspacing="0" rules="all" border="1" style="border-collapse:collapse;">
<tr>
<th rowspan="3"></th>
<th colspan="5" style="text-align:center;">分組一</th>
<th rowspan="3">所學專業</th>
<th rowspan="3">注冊日期</th>
</tr>
<tr>
<th rowspan="2">姓名</th>
<th rowspan="2">性別</th>
<th colspan="3" style="text-align:center;">考試成績</th>
</tr>
<tr>
<th>語文成績</th>
<th>數學成績</th>
<th>總成績</th>
</tr>
</table>
最終生成了 3 行數據,每一行中 th 的個數不盡相同,每個 th 的參數也不相同,看來這個轉換要自己手工做了。
實現轉換之前,我們先來總結兩個關鍵的邏輯:
1. 如果某列有子列,則更改本列的 colspan,並且增加所有父列(向上追溯)的 colspan
2. 如果下一行有數據,則增加上一行(向上追溯)中沒有子項的列的 rowspan
每個 th 在 C# 代碼中通過 object[] 來表達,比如 [考試成績] 這一列最終的結構是:
[
1, // rowspan
3, // colspan
考試成績, // 當前列對象
分組一 // 父列對象
]
邏輯點到為止,剩下的就來看代碼了,我們把邏輯封裝到自定義類中:
/// <summary>
/// 處理多表頭的類
/// </summary>
public class MultiHeaderTable
{
// 包含 rowspan,colspan 的多表頭,方便生成 HTML 的 table 標簽
public List<List<object[]>> MultiTable = new List<List<object[]>>();
// 最終渲染的列數組
public List<GridColumn> Columns = new List<GridColumn>();
public void ResolveMultiHeaderTable(GridColumnCollection columns)
{
List<object[]> row = new List<object[]>();
foreach (GridColumn column in columns)
{
object[] cell = new object[4];
cell[0] = 1; // rowspan
cell[1] = 1; // colspan
cell[2] = column;
cell[3] = null;
row.Add(cell);
}
ResolveMultiTable(row, 0);
ResolveColumns(row);
}
private void ResolveColumns(List<object[]> row)
{
foreach (object[] cell in row)
{
GroupField groupField = cell[2] as GroupField;
if (groupField != null && groupField.Columns.Count > 0)
{
List<object[]> subrow = new List<object[]>();
foreach (GridColumn column in groupField.Columns)
{
subrow.Add(new object[]
{
1,
1,
column,
groupField
});
}
ResolveColumns(subrow);
}
else
{
Columns.Add(cell[2] as GridColumn);
}
}
}
private void ResolveMultiTable(List<object[]> row, int level)
{
List<object[]> nextrow = new List<object[]>();
foreach (object[] cell in row)
{
GroupField groupField = cell[2] as GroupField;
if (groupField != null && groupField.Columns.Count > 0)
{
// 如果當前列包含子列,則更改當前列的 colspan,以及增加父列(向上遞歸)的colspan
cell[1] = Convert.ToInt32(groupField.Columns.Count);
PlusColspan(level - 1, cell[3] as GridColumn,groupField.Columns.Count - 1);
foreach (GridColumn column in groupField.Columns)
{
nextrow.Add(new object[]
{
1,
1,
column,
groupField
});
}
}
}
MultiTable.Add(row);
// 如果當前下一行,則增加上一行(向上遞歸)中沒有子列的列的 rowspan
if (nextrow.Count > 0)
{
PlusRowspan(level);
ResolveMultiTable(nextrow, level + 1);
}
}
private void PlusRowspan(int level)
{
if (level < 0)
{
return;
}
foreach (object[] cells in MultiTable[level])
{
GroupField groupField = cells[2] as GroupField;
if (groupField != null && groupField.Columns.Count > 0)
{
// ...
}
else
{
cells[0] = Convert.ToInt32(cells[0]) + 1;
}
}
PlusRowspan(level - 1);
}
private void PlusColspan(int level, GridColumn parent, int plusCount)
{
if (level < 0)
{
return;
}
foreach (object[] cells in MultiTable[level])
{
GridColumn column = cells[2] as GridColumn;
if (column == parent)
{
cells[1] = Convert.ToInt32(cells[1]) + plusCount;
PlusColspan(level - 1, cells[3] as GridColumn, plusCount);
}
}
}
}
其實主要的邏輯就上面提到的兩點,然后需要好幾個遞歸函數來一塊完成任務。
導出的代碼調用如下:
protected void Button1_Click(object sender, EventArgs e)
{
Response.ClearContent();
Response.AddHeader("content-disposition", "attachment; filename=myexcel.xls");
Response.ContentType = "application/excel";
Response.ContentEncoding = System.Text.Encoding.UTF8;
Response.Write(GetGridTableHtml(Grid1));
Response.End();
}
private string GetGridTableHtml(Grid grid)
{
StringBuilder sb = new StringBuilder();
MultiHeaderTable mht = new MultiHeaderTable();
mht.ResolveMultiHeaderTable(Grid1.Columns);
sb.Append("<meta http-equiv=\"content-type\" content=\"application/excel; charset=UTF-8\"/>");
sb.Append("<table cellspacing=\"0\" rules=\"all\" border=\"1\" style=\"border-collapse:collapse;\">");
foreach (List<object[]> rows in mht.MultiTable)
{
sb.Append("<tr>");
foreach (object[] cell in rows)
{
int rowspan = Convert.ToInt32(cell[0]);
int colspan = Convert.ToInt32(cell[1]);
GridColumn column = cell[2] as GridColumn;
sb.AppendFormat("<th{0}{1}{2}>{3}</th>",
rowspan != 1 ? " rowspan=\"" + rowspan + "\"" : "",
colspan != 1 ? " colspan=\"" + colspan + "\"" : "",
colspan != 1 ? " style=\"text-align:center;\"" : "",
column.HeaderText);
}
sb.Append("</tr>");
}
foreach (GridRow row in grid.Rows)
{
sb.Append("<tr>");
foreach (GridColumn column in mht.Columns)
{
string html = row.Values[column.ColumnIndex].ToString();
if (column.ColumnID == "tfNumber")
{
html = (row.FindControl("spanNumber") as System.Web.UI.HtmlControls.HtmlGenericControl).InnerText;
}
else if (column.ColumnID == "tfGender")
{
html = (row.FindControl("labGender") as AspNet.Label).Text;
}
sb.AppendFormat("<td>{0}</td>", html);
}
sb.Append("</tr>");
}
sb.Append("</table>");
return sb.ToString();
}
最終導出的文件結構如下所示:

原創不易,請點贊
短短一篇文章,幾行代碼,看似輕描淡寫,實則是花了很大功夫調試。你覺得作者在整個過程中做了多少次導出文件的動作?才最終實現了這個效果!
。
。
10?
。
。
。
20?
。
。
。
。
30?
。
。
。
。
。
。
40?
。
。
。
。
。
。
。
請恕作者愚鈍,足足不下 50 次:

本章小結
本篇文章介紹了如何導出多表頭表格,重點在於樹狀結構到 table 標簽結構的轉換,雖然實現稍微復雜了點,但只要思路清晰,最終還是能否完整呈現的。
源代碼與在線示例
本系列所有文章的源代碼均可自行下載:http://fineui.codeplex.com/
在線示例:http://fineui.com/demo/#/demo/grid/grid_excel_groupfield.aspx
如果本文對你有所啟發或者幫助,請猛擊“好文要頂”,支持原創,支持三石!
