NPOI实现Excel的导入导出,踩坑若干.

Cyan是博主【Soar360】自2014年以来开始编写整理的工具组件,用于解决现实工作中常用且与业务逻辑无关的问题。

什么是NPOI?

NPOI 是 POI 项目的 .NET 版本。POI是一个开源的Java读写Excel、WORD等微软OLE2组件文档的项目。
使用 NPOI 你就可以在没有安装 Office 或者相应环境的机器上对 WORD/EXCEL 文档进行读写。NPOI是构建在POI 3.x版本之上的,它可以在没有安装Office的情况下对Word/Excel文档进行读写操作。

来自:百度百科

关于Office格式

在老板本的Office软件中(97-2003),Excel文件的默认后缀是.xls,而新版本的Office软件(2007+)所用的默认后缀为.xlsx。这两种格式最要命的区别就是.xls后缀的文件,每个工作区中最大支持65536条数据,而.xlsx后缀的文件,每个工作区最大支持1048576行数据。

虽说65536已经足够大,但是在实际工作中确实有超过这个数值的情况,所以我们需要对两种格式都进行支持。如果数据量还是很大要怎么办?很简单,拆成多个工作区即可。

为此,我们定义了枚举“OfficeType”用来标明Excel格式:

    /// <summary>
/// Office文件格式
/// </summary>
public enum OfficeType
{
/// <summary>
/// 97-2003格式
/// </summary>
[Description("Office2003")]
Office2003,
/// <summary>
/// 2007+格式
/// </summary>
[Description("Office2007")]
Office2007
}

定义了方法“FormatFileName”来修正生成文件的文件名:

        /// <summary>
/// 格式化Excel文件名,根据Excel类型,为Excel增加后缀。
/// </summary>
/// <param name="fileName">未格式化的文件名</param>
/// <param name="officeType">Excel类型</param>
/// <returns>格式化后的Excel文件名。</returns>
public static String FormatFileName(String fileName, OfficeType officeType)
{
if (String.IsNullOrEmpty(fileName)) throw new ArgumentNullException("fileName");
var ext = officeType == OfficeType.Office2007 ? ".xlsx" : ".xls";
var name = fileName;
if (!fileName.EndsWith(ext, StringComparison.CurrentCultureIgnoreCase))
{
name += ext;
}
return name;
}

怎样的导出,才是有节操的导出?

数据导出是一个经常性的工作,这项工作在2014年5月份,占用了我2/3的工作时长。这期间遇到的问题如下:

  1. 我们要做导出的数据源格式多种多样,可能是DataSet、DataTable、List<T>甚至是 Dictionary<TKey, TValue>,如何才能兼容这些格式的数据源呢?
  2. 系统中有部分基础数据是缓存的,比如文章类型表。数据源提供的只有类型ID一列,并不包含类型名称,而导出是必须要求有类型名称的。如果为了实现导出,单独搞一个数据源或者手工再对Dto进行加工,也太过得不偿失了。
  3. 与上一条类似,如果我们的数据源返回的数据是True和Flase,而我们必须要将导出的数据显示为“是、否”或者“启用中、已停用”。
  4. 导出信息需要将数据源中的两个字段进行拼接后输出的。比如,数据源中有姓名和身份证号,但是导出数据要求输出到一个单元格中。
  5. ……

其实说白了,就两个问题:

  1. 数据源兼容
  2. 数据格式化

为了解决这两个问题,博主设计出一个接口“IExportColumn”:

/// <summary>
/// 导出列接口
/// </summary>
/// <typeparam name="T">数据行类型</typeparam>
public interface IExportColumn<in T>
{
/// <summary>
/// 列标题
/// </summary>
String Title { get; }
/// <summary>
/// 获取该列的值
/// </summary>
/// <param name="row"></param>
/// <param name="index"></param>
/// <returns></returns>
Object GetValue(T row, Int32 index);
}

只读Title属性表示导出列的标题。GetValue方法,传入数据项和该项在集合中的索引。同时增加了通用列“ExportColumn<T>”:

    /// <summary>
/// 导出列
/// </summary>
/// <typeparam name="T"></typeparam>
public class ExportColumn<T> : IExportColumn<T>
{
public ExportColumn(String title, Func<T, Int32, Object> funcGetValue)
{
if (String.IsNullOrEmpty(title)) throw new ArgumentNullException("title");
if (funcGetValue == null) throw new ArgumentNullException("funcGetValue");
this.Title = title;
this._funcGetValue = funcGetValue;
} private readonly Func<T, Int32, Object> _funcGetValue;
public string Title { get; private set; } public object GetValue(T row, int index)
{
return this._funcGetValue(row, index);
}
}

还有方便导出DataTable的“DataRowExportColumn”:

    public class DataRowExportColumn : IExportColumn<DataRow>
{
public DataRowExportColumn(String name)
: this(name, String.Empty)
{ } public DataRowExportColumn(String name, String title)
: this(name, title, null)
{ }
public DataRowExportColumn(String name, String title, Func<Object, Int32, Object> funcFormatValue)
{
if (String.IsNullOrEmpty(name)) throw new ArgumentNullException("name");
this.Name = name;
this._title = title;
this._funcFormatValue = funcFormatValue;
} public String Name { get; private set; }
private readonly String _title;
private readonly Func<Object, Int32, Object> _funcFormatValue;
public string Title
{
get { return String.IsNullOrEmpty(this._title) ? this.Name : this._title; }
} public object GetValue(DataRow row, int index)
{
var val = row[this.Name];
return this._funcFormatValue != null ? _funcFormatValue(val, index) : val;
}
}

当然,我们需要一个导出方法:

        /// <summary>
/// 导出Excel,如果Excel类型为Office2003,那么数据行数不能超过65535,如果超过,则会被拆分到多个工作区中。
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
/// <param name="dataSource">数据源</param>
/// <param name="excelType">EXCEL格式</param>
/// <param name="sheetName">工作区名称</param>
/// <param name="saveStream">保存到的文件流</param>
/// <param name="columns">导出列</param>
public static void ExportExcel<T>(IList<T> dataSource, OfficeType excelType, String sheetName, Stream saveStream, IList<IExportColumn<T>> columns)

那么,导出数据的代码看上去就像是这个样子:

            using (var fs = new FileStream(tmpFileName, FileMode.Create))
{
ExcelHelper.ExportExcel(list, OfficeType.Office2003, "保险卡", fs,
new IExportColumn<Entity.InsuranceCard>[]
{
new ExportColumn<Entity.InsuranceCard>("编号", (o, i) => o.Id),
new ExportColumn<Entity.InsuranceCard>("卡号", (o, i) => o.Number),
new ExportColumn<Entity.InsuranceCard>("类型", (o, i) => o.InsuranceCardTypeName),
new ExportColumn<Entity.InsuranceCard>("制卡时间", (o, i) => o.CreatedTime),
new ExportColumn<Entity.InsuranceCard>("是否开通", (o, i) => o.Enabled ? "已开通" : "锁定"),
new ExportColumn<Entity.InsuranceCard>("是否激活", (o, i) => o.Activated ? "已激活" : "未激活"),
new ExportColumn<Entity.InsuranceCard>("密码", (o, i) => o.Password)
});
}

什么,你说怎么兼容DataTable和Dictionary<TKey, TValue>?骚年,“dt.Rows.Cast<DataRow>().ToList()”懂不懂,“dic.Select(i => new { i.Key, i.Value }).ToList()”懂不懂?什么,你还在用.NET 2.0?LINQBridge你值得拥有。

数据导入

数据导出的数据源是来自计算机的,而数据导入的数据源是来自人的。一旦有“人”这个元素参与进来,就必须增加一系列的约束,系统才能正常理解人想要表达的操作。毕竟,计算机并不是那么智能。

如果要用Excel导入数据,我们要求,Excel的第一行必须为列标题,不能有多行标题和跨行跨列的情况。如果有任何不符合条件的,导入就会失败。没办法,机器就是机器。我们选择使用DataSet作为数据导入的返回类型,方便处理而且通用性比较强。最主要的是,可以在Visual Studio中直接查看DataSet的内容,方便排查错误。

        /// <summary>
/// 导入Excel
/// </summary>
/// <param name="fileStream"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static DataSet ImportExcel(Stream fileStream)

数据导入会自动识别Excel的格式,是97-2003还是2007+,所以,我们只需要将Excel文件的数据流传入即可。

说说那些坑

  1. 不要在ASP.NET中尝试将导出数据流直接设置为Response.OutputStream,这会导致错误。虽然Excel文件能够打开,但是有提示框。推荐的做法是输出到临时文件后让用户去下载。
  2. 如果导出手机号码、身份证等纯数字信息时,Excel会将该信息显示为科学计数法,影响使用和查看。Cyan组件中已经修复这个问题。但是如果导出格式是Csv,那么需要再数据前填充制表符"t"来纠正显示,不过这样的话在复制时,制表符也是会被读取和复制的,所以在导入时,记得要对数据进行Trim。
  3. 因为办公软件的多样性,在正式运行过程中出现了Excel软件只显示一个工作区,而NPOI读取到了多个工作区的情况。Excel不显示的工作区行数为0,所以在导入的时候,要将行数为0的工作区过滤掉。
  4. 如果Excel中包含日期,而Excel中的日期是以数字格式处理的,所以要得到正确的日期格式,需要进行额外的处理。Cyan目前已经支持。
  5. 如果Excel表格中只有10条数据,但是用户在第20行设置样式或者无意中设置了空行,那么会造成NPOI实际读取了20行的数据,但是其中的10行都是空的。不过用户可能察觉不到。所以在编写代码时要注意过滤空行。

最后,源码奉上:Cyan.Toolkit.Office

NPOI实现Excel导入导出的更多相关文章

  1. .net core 基于NPOI 的excel导入导出类,支持自定义导出哪些字段,和判断导入是否有失败的记录

    #region 从Excel导入 //用法 //var cellHeader = new Dictionary<string, string>(); //cellHeader.Add(&q ...

  2. 基于NPOI的Excel导入导出类库

    概述 支持多sheet导入导出.导出字段过滤.特性配置导入验证,非空验证,唯一验证,错误标注等 用于基础配置和普通报表的导入导出,对于复杂需求,比如合并列,公式,导出图片等暂不支持 GitHub地址: ...

  3. 分享:一个基于NPOI的excel导入导出组件(强类型)

    一.引子 新进公司被安排处理系统的数据报表任务——对学生的考试成绩进行统计并能导出到excel.虽然以前也有弄过,但感觉不是很好,所以这次狠下心,多花点时间作个让自己满意的插件. 二.适用领域 因为需 ...

  4. 基于EPPlus和NPOI实现的Excel导入导出

    基于EPPlus和NPOI实现的Excel导入导出 CollapseNav.Net.Tool.Excel(NuGet地址) 太长不看 导入 excel 文件流将会转为 ExcelTestDto 类型的 ...

  5. NPOI操作Excel导入DataTable中

    using NPOI.HSSF.UserModel; using NPOI.SS.UserModel; using System.Data; using System.IO; using NPOI.X ...

  6. Excel导入导出帮助类

    /// <summary>    /// Excel导入导出帮助类    /// 记得引入 NPOI    /// 下载地址   http://npoi.codeplex.com/rele ...

  7. ASP.NET Core 2.2 : 十六.扒一扒新的Endpoint路由方案 try.dot.net 的正确使用姿势 .Net NPOI 根据excel模板导出excel、直接生成excel .Net NPOI 上传excel文件、提交后台获取excel里的数据

    ASP.NET Core 2.2 : 十六.扒一扒新的Endpoint路由方案   ASP.NET Core 从2.2版本开始,采用了一个新的名为Endpoint的路由方案,与原来的方案在使用上差别不 ...

  8. 利用反射实现通用的excel导入导出

    如果一个项目中存在多种信息的导入导出,为了简化代码,就需要用反射实现通用的excel导入导出 实例代码如下: 1.创建一个 Book类,并编写set和get方法 package com.bean; p ...

  9. Excel导入导出的业务进化场景及组件化的设计方案(上)

    1:前言 看过我文章的网友们都知道,通常前言都是我用来打酱油扯点闲情的. 自从写了上面一篇文章之后,领导就找我谈话了,怕我有什么想不开. 所以上一篇的(下)篇,目前先不出来了,哪天我异地二次回忆的时候 ...

随机推荐

  1. SQL server 2005 PIVOT运算符的使用

    原文:SQL server 2005 PIVOT运算符的使用 PIVOT,UNPIVOT运算符是SQL server 2005支持的新功能之一,主要用来实现行到列的转换.本文主要介绍PIVOT运算符的 ...

  2. 利用Eclipse中的Maven构建Web项目(三)

    利用Eclipse中的Maven构建Web项目 1.将Maven Project转换成动态Web项目,鼠标右键项目,输入"Project Facets" 2.依据Dynamic W ...

  3. Apache Commons Math3学习笔记(2) - 多项式曲线拟合(转)

    多项式曲线拟合:org.apache.commons.math3.fitting.PolynomialCurveFitter类. 用法示例代码: // ... 创建并初始化输入数据: double[] ...

  4. ASP.NET之AdRotator实现淘宝浏览页面的商品随机推荐功能

    如今随便上个网都能够看到淘宝.京东等各大电商平台的双十一购物狂欢宣传,从2009年開始淘宝愣是把11.11这一天打造成了全民购物狂欢节.阿里巴巴的上市更是激发了阿里人的斗志,据说他们今年的目标是100 ...

  5. wpf 9张图片的连连看

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.W ...

  6. NET版微信客户端.

    微信客户端.NET版 目录 说明 功能 原理步骤 一些参考 说明 前两天比较闲,研究了一下web版微信.因为之前看过一篇博客讲微信web协议的,后来尝试分析了一下,半途中发现其实没什么意义,但又不想半 ...

  7. Swing中耗时任务需要另起新线程,这个新线程中更新GUI的操作仍需由EDT来做(转)

    最近调试程序时发现,点击某个界面时会出现卡死的情况,出现的频率还是比较频繁的. 再次出现卡死的情况后,利用jvisualvm查看线程的运行情况,dump操作之后发现线程间出现了死锁:Found one ...

  8. 2014年百度之星程序设计大赛 - 资格赛 1002 Disk Schedule(双调欧几里得旅行商问题)

    Problem Description 有非常多从磁盘读取数据的需求,包含顺序读取.随机读取.为了提高效率,须要人为安排磁盘读取.然而,在现实中,这样的做法非常复杂.我们考虑一个相对简单的场景.磁盘有 ...

  9. 与我一起extjs5(09--其定义菜单2)

    跟我一起学extjs5(09--自己定义菜单2)         这一节来定义另外三种类型的菜单类. 首先定义菜单button类.文件放于app/view/main/region文件夹以下,文件名称为 ...

  10. 中国的手写输入法iOS8.1在崩溃

    当中国的手写输入法.会导致app收起.于debug时刻.报错: 2014-10-22 14:45:10.269 App[524:170755] -[UIKBBlurredKeyView candida ...