前言

之前我们曾写过一篇文章 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

如果本文对你有所启发或者帮助,请猛击“好文要顶”,支持原创,支持三石!

FineUI小技巧(7)多表头表格导出的更多相关文章

  1. FineUI小技巧(3)表格导出与文件下载

    需求描述 实际应用中,我们可能需要导出表格内容,或者在页面回发时根据用户权限下载文件(注意,这里的导出与下载,都是在后台进行的,和普通的一个链接下载文件不同). 点击按钮导出表格 由于FineUI 默 ...

  2. FineUI小技巧(1)简单的购物车页面

    起因 最初是一位 FineUI 网友对购物车功能的需求,需要根据产品单价和数量来计算所有选中商品的总价. 这个逻辑最好在前台使用JavaScript实现,如果把这个逻辑移动到后台C#实现,则会导致过多 ...

  3. FineUI小技巧(5)向子窗口传值,向父窗口传值

    前言 FineUI中经常会用到启用IFrame的Window控件,这样有助于从物理上进行代码解耦和.IFrame的引入就会涉及传值问题,如何在父窗口和子窗口之间相互传值呢? 向子窗口传值 向子窗口传值 ...

  4. FineUI小技巧(4)关闭窗体那些事

    前言 FineUI中的Window控件常用作选择.新增或编辑内容.而关闭Window控件却有很多技巧,了解这些技巧有助于项目的快速开发. 如何关闭Window控件 第一个问题就是如何关闭Window控 ...

  5. FineUI小技巧(2)将表单内全部字段禁用、只读、设置无效标识

    需求描述 对表单内的所有字段进行操作也是常见需求,这些操作有: 禁用:表单字段变灰,不响应用户动作. 只读:表单字段不变灰,但不接受用户输入(实际上是设置DOM节点的readonly属性),有触发器的 ...

  6. FineUI小技巧(6)自定义页面回发

    前言 FineUI中的绝大部分回发事件都是由控件触发了,比如按钮的点击事件,下拉列表的改变事件,表格的排序分页事件.但有时我们可能会要自己触发页面回发,这时就要知道怎么使用 JavaScript 来做 ...

  7. sql server 小技巧(7) 导出完整sql server 数据库成一个sql文件,包含表结构及数据

    1. 右健数据库 –> Tasks –> Generate Scripts   2. 选择所有的表   3. 下一步,选择Advanded, Types of data to script ...

  8. 你想的到想不到的 javascript 应用小技巧方法

    javascript 在前端应用体验小技巧继续积累. 事件源对象 event.srcElement.tagName event.srcElement.type 捕获释放 event.srcElemen ...

  9. SmartForms 小技巧

    1.添加空行,保证每一页有固定的打印的表格行数 上图,每页最多打印13行,数据只有11行,自动添加两个空行补齐 代码如下“ "定义变量: data: l_blank type i. &quo ...

随机推荐

  1. 完美 全兼容 解决 文字两端对齐 justify 中文姓名对齐

    text-align:justify; 所有浏览器都支持,text-justify之类的却只有IE支持,就不要考虑了. justify我的理解,使元素内部的子元素两端对齐,子元素当然只能是inline ...

  2. ALV TREE中双击触发PAI事件的方法

    用事件类实现双击事件,实例化后使用set handler注册到ALV对象.斜体部分为事件方法的具体实现. 代码如下 CLASS lcl_tree_event_receiver DEFINITION. ...

  3. iOS之关于 srand() 和rand()

    srand(seed)用来将随机序列的起始点设为seed srand((int)time(0))表示以当前时间对应的int值为随机序列起点,这样每次运行程序,由于起点不同才可以得到不同的随机数 tim ...

  4. 判断用户输入的银行卡号是否正确--基于Luhn算法的格式校验

    开发中,有时候,为了打造更好的用户体验,同时减轻服务器端的压力,需要对于一些如,手机号码,银行卡号,身份证号码进行格式校验 下面是判断银行卡号输入是否正确的代码(基于Luhn算法的格式校验): iOS ...

  5. iOS 根据银行卡号判断银行名称

    如何根据银行卡号判断银行名称? + (NSString *)getBankName:(NSString*) cardId{ //"发卡行.卡种名称", NSArray* bankN ...

  6. GPU大百科全书索引(有助于理解openGL工作流程)

    GPU大百科全书索引 0.GPU大百科全书 前传 看图形与装修的关系 1.GPU大百科全书 第一章:美女 方程与几何 2.GPU大百科全书 第二章 凝固生命的光栅化 3.GPU大百科全书 第三章:像素 ...

  7. 【代码笔记】iOS-获得徐家汇的天气预报

    一,代码. //获得徐家汇的天气预报 -(void)getWeatherInfo{ NSError *error; NSURLRequest *request = [NSURLRequest requ ...

  8. iOS AFNetworking内存泄漏处理方法

    iOS AFN内存泄漏处理方法 细心的你是否也发现AFN的内存泄漏的问题了呢. 在这里给大家提供一个解决AFN内存泄漏的方法. 单例解决AFN内存泄漏 + (AFHTTPSessionManager ...

  9. SSH之免密码登录

    我的虚拟机: 192.168.91.130  test1 192.168.91.131  test2 192.168.91.132  test3 1.首先在test1上生成秘钥对 之后会生成.shh/ ...

  10. DbVisualizer连接hbase

    1.添加phoneix驱动 (1).点击Tools--->Driver Manager- (2).新建一个驱动,名称为phoenix(名称随意),选择phoenix的客户端驱动,驱动类如图所示 ...