Asp.net生成Excel报表
测试环境:windows2003,MSSQL2000,IIS6.0,Asp.net2.0
需求:根据用户查询条件将满足要求的信息查询出来生成xls文件供用户下载.
1.使用Excel组件生成xls文件,不采用CSV文件(逗号分割文件).
2.下载操作时采用ajax方式进行,需要提示界面.
3.需要将下载后的文件删除.
设计的过程:
1.使用Excel软件设计一个空的模版文件(temp.xls)被用.
2.每次生成时,将temp.xls拷贝一份到临时目录,文件名称随机生成(ran.xls).
3.根据用户提交的条件将数据填充到ran.xls文件中.
4.下载ran.xls文件.
5.将临时目录中的ran.xls文件删除.
分析:
A.过期文件删除处理:
针对第5点,一般可以使用定时清理程序根据文件生成时间进行清理(比方一天前的文件则删除掉),不过考虑到生成的xls文件一般在100KB以内,而且这个导出操作不很频繁(只对VIP用户提供),那么这里采用下载后马上删除的方式来实现,开始尝试了如下下载代码:
//=======注意***以下代码不能得到预期效果==============
Response.TransmitFile(filename);
//重载WriteFile(filename,true)是把整个文件读入内存再输出,跟下面的BineryWrite(byte[])表现是一样的
//Response.WriteFile(filename);//部分缓存文件
//即使加了下面两句,程序的表现也是不稳定的
//Response.Flush();
//Response.Close();
System.IO.File.Delete(filename);
//=========End==========
参考(http://forums.asp.net/t/1031479.aspx,)提到的在每次使用TransmitFile发送文件时,调用一次过期文件删除进程(按文件生成时间).考虑到TransmitFile能稳定的发送大文件,实际应用时建议按这种方式实施.
针对当前项目,这里采用一次性读入内存再逐步输出的方式进行
//===============目前采用的方式============
Response.ContentType = "application/ms-excel";
Response.AddHeader("Content-Disposition", "attachment;filename=" + HttpUtility.UrlEncode(savefilename, Encoding.GetEncoding("utf-8")));
Response.BinaryWrite(File.ReadAllBytes(filename));//直接将输出写入
Response.Flush();
System.IO.File.Delete(filename);
B.使用ajax方式下载问题:
用户点下载按钮后将下载按钮设置为不可用(防止多次点击),并且显示一个处理中的gif动画.
首先碰到的问题是使用ajax方式无法弹出文件下载对话框,后来找到使用iframe的形式来实现,相关的jquery代码大概是这样的,$("#frmDownload").attr("src","download.ashx?条件");.
起初是将生成Excel表格,发送下载文件做在一个过程里面的,使用iframe方式时,从点了下载按钮后到弹出文件下载对话框为止,对应服务器的处理时间,这段时间内客户端需要将下载按钮butOK禁用,然后显示一个gif,当弹出下载对话框时,再将按钮恢复并且隐藏gif动画,尝试了在ifrmae.onload里捕获下载框弹出事件,结果发现iframe里如果加载的是html页面时会触发,但是当是文件下载时就没有触发,估计是文件下载时IE解释器不生成DOM模型,当然也就没有JS事件了.
目前解决方法:
把Excel生成跟服务器发出下信号(指上面下载代码那一部分)分开,即首先客户端发出请求,服务器收到后生成excel文件然后返回一段json格式的信息(成功与否,以及excel文件地址),客户端收到成功信息后隐藏gif动画,恢复按钮可用,然后设置iframe.src="下载地址",这样一来就解决了上面的问题.
--另外可以考虑,照样使用iframe.onload事件来处理excel生成结束事件(隐藏动画,恢复按钮),不过服务器应该返回是一个包含<iframe src="下载地址" ></firame>的页面,即iframe里再套个iframe, 这个没测试过想来应该可性,这样就不需要将excel处理跟下载分成两步了.
参考代码如下:
///****************ajax下载过程*************************
$("#butOK").click(function(){
$("#butOK").attr("disabled",true);
$("#tip").html("<img src='/js/loading.gif'/>正在处理请求...");
$.getJSON("download.ashx?m=1",function(data){
//这里是演示,实际应用应该根据data里的信息来决定下一步操作
$("#frmDownload").attr("src","download.ashx?m=2");//附加到iframe下载
$("#butOK").attr("disabled","");
$("#tip").html("");
});
});
//********************代码结束******************
C.Excel报表生成问题:
首先这个报表包括表头(第一行是神鹰医药招商网--代理信息目录,第二行是http://www.gyzs.net/ 导出时间:******),第三行是字段名称,而且表格使用了样式以便看上去更加美观,这样一来使用OLEDB方式的Insert语句(按数据库方式处理)就会出错,Insert好象将第一行是字段,第二行就是数据行了,也不知道有没有办法解决.
另外朋友提供了一个服务端导出xml格式的数据,然后在客户端使用VBA(Js操作Excel组件)生成表格的方式,可以避免在服务器上装Excel程序,配置权限,Excel注销(回收内参)等一系列问题,不过需要对Excel进行格式化(能否下在服务器xls模版再进行操作?),用户当前电脑Excel版本是否会对Js编写有要求,在Js中使用Excel组件是否有权限以及安全方面的问题等, 多需要逐一测试.
目前解决方法:
使用Miscrosoft Excel 10.0 Object Library (Office 2002)组件,(需要在服务器上安装并配置权限)
//*****************写excel文件参考代码**********************
System.Data.DataTable dt = LoadData();//加载符合条件的数据
Object miss = System.Reflection.Missing.Value;
Excel.Application ExcelApp = new Excel.ApplicationClass();
Excel.Workbooks wbooks = (Excel.Workbooks)ExcelApp.Workbooks;
Excel.Workbook wbook = wbooks.Open(Path, miss, miss, miss, miss, miss, miss, miss, miss, miss, miss, miss, miss, miss, miss);
//获得第一个工作薄
Excel.Worksheet wsheet = (Excel.Worksheet)wbook.Worksheets.get_Item(1);
ExcelApp.Visible = false;
ExcelApp.DisplayAlerts = false;
wsheet.Cells[2, 1] = string.Format("信息来源:神鹰医药网〖http://www.gyzs.net/〗 导出时间:{0}", DateTime.Now.ToString("yyyy-MM-dd"));
//填充具体的单元格
//逐个填充
//for (int i = 0; i < dt.Rows.Count; i++)
//{
// for (int j = 1; j <= 6; j++)
// {
// wsheet.Cells[i + 4, j] = dt.Rows[i][j - 1];
// }
//}
//使用Rang填充
object[,] arr = new object[dt.Rows.Count, dt.Columns.Count];
for (int iRow = 0; iRow < dt.Rows.Count; iRow++)
{
for (int iCol = 0; iCol < dt.Columns.Count; iCol++)
{
arr[iRow, iCol] = dt.Rows[iRow][iCol];
}
}
Excel.Range range = wsheet.get_Range(wsheet.Cells[4, 1], wsheet.Cells[dt.Rows.Count + 4 - 1, dt.Columns.Count]);
range.set_Value(miss, arr);
ReleaseObject(range);
//填充结束
wbook.Close(true, miss, miss);
ExcelApp.Quit();
ReleaseObject(wsheet);
ReleaseObject(wbook);
ReleaseObject(wbooks);
ReleaseObject(ExcelApp);
GC.Collect();
GC.WaitForPendingFinalizers();
//*******************结束***********************
说明:Excel 首先是Applicate对象,代表了Excel应用程序,一个Applicate对应多个WorkBook(集合WorkBooks),一个WorkBook包含多张WorkSheet(工作表),每张表包括多个Cell(单元格式), 上面代码注释了逐个单元格填充数据的方式,目前使用的是按Range按区域(如A1-C3有9个单元格A1表示左上角的哪个,
上面使用Get_Range(Cell[LRow,LCol],Cell[RRow,RCol])获取需要填充的区域,而Object[,] arr是一个二纬数组,这些数据将被填充到这个区域(Range)里. 使用区域填充比逐个填充速度要快,3-4倍(我这里测试的信息)
附加部分:在客户端使用ActiveX方式将Table内容导入到Excel表格里
//*******需要权限,并且用户安装了合适版本的office
// 指定页面区域“单元格”内容导入Excel
function CellAreaExcel(objid) {
var oXL = null;
var oWB = null;
var oSheet = null;
try {
oXL = new ActiveXObject("Excel.Application");
} catch (e) {
alert("Excel对象无法创建");
return;
}
try {
oWB = oXL.Workbooks.Add();
oSheet = oWB.ActiveSheet;
oSheet.Cells.NumberFormatLocal = "@";
var Lenr = gridlist.rows.length;
for (var i = 0; i < Lenr; i++) {
var Lenc = $id(objid).rows(i).cells.length;
for (var j = 0; j < Lenc; j++) {
if (i == 0) {
oSheet.Cells(i + 1, j + 1).Font.Bold = true;
oSheet.Cells(i + 1, j + 1).ColumnWidth = $id(objid).rows(i).cells(j).clientWidth / 7;
}
oSheet.Cells(i + 1, j + 1).value = $id(objid).rows(i).cells(j).innerText;
}
}
oXL.Visible = true;
} catch (e) {
alert("导出到excel出错");
}
oXL.Quit();
oXL = null;
oWB = null;
oSheet = null;
idTmr = window.setInterval("Cleanup();", 1);
}