昨天一个同学让我帮忙写个小工具,读一个Excel,只读B列的内容。我拿C#写了一个.NET4的winform程序。虽然简单,但有很多可圈可点的地方,因此撰写本文以警后人。
那个Excel是2010格式的,所以我们先要安装AccessDatabaseEngine,下载地址如下:
这样程序读取Excel的时候就不会抛出OleDb驱动未注册的异常了。
我们先看看Excel文件,格式很简单:
我们需要读的,是B列除标题以外的内容。即B2到B6的内容,观察到它是字符串,所以程序里要作为string读取。
程序界面设计如下,点击ReadExcel后,会将B列的内容显示到listBox1里:
为了通俗易懂地说明问题,我先贴出所有源代码,稍候再做分析:
using System; using System.Data; using System.Data.OleDb; using System.IO; using System.Windows.Forms; namespace ReadExcel { public partial class Form1 : Form { /// <summary> /// Response Pattern /// </summary> public class ExcelResponse { public bool IsSuccess { get; set; } public string Message { get; set; } public DataTable Item { get; set; } } public Form1() { InitializeComponent(); } private void btnReadExcel_Click(object sender, EventArgs e) { var excelFilename = @"E:\Test.xlsx"; var dtResponse = ReadData(excelFilename); if (dtResponse.IsSuccess) { lblMessage.Text = dtResponse.Message; if (null != dtResponse.Item) { // LINQ to DataTable, Reading [B] col without title var query = from item in dtResponse.Item.AsEnumerable() where item.Field<string>("F2") != "SHIT" select item.Field<string>("F2"); #region Business Logic // Example: Add into listBox foreach (var item in query) { listBox1.Items.Add(item.ToString()); } // TODO: Your Batch Operation // -- #endregion } } else { MessageBox.Show(dtResponse.Message, "Fuck!"); } } /// <summary> /// Read first sheet in Excel 2007+ File /// </summary> /// <param name="excelFilename">Path to your Excel file</param> /// <returns>ExcelResponse, containing a datatable which is result</returns> private static ExcelResponse ReadData(string excelFilename) { try { if (!File.Exists(excelFilename)) { throw new IOException(string.Format("File {0} Not Exists!", excelFilename)); } using (var conn = new OleDbConnection()) { conn.ConnectionString = @"Provider=Microsoft.ACE.OLEDB.12.0;" + @"Data Source=" + excelFilename + ";" + "Extended Properties=\"Excel 12.0 Xml;HDR=No\""; conn.Open(); OleDbDataAdapter da = new OleDbDataAdapter("select * from [Sheet1$]", conn); var ds = new DataSet(); da.Fill(ds); if (null != ds.Tables[0]) { return new ExcelResponse() { IsSuccess = true, Message = "Query Successfully Completed", Item = ds.Tables[0] }; } return new ExcelResponse { IsSuccess = true, Message = "No Data in the Excel", Item = null }; } } catch (OleDbException ex) { return new ExcelResponse { IsSuccess = false, Message = "Exception in OleDb Operation: " + ex.Message, Item = null }; } catch (Exception ex) { return new ExcelResponse { IsSuccess = false, Message = "Exception reading excel: " + ex.Message, Item = null }; } } private void Form1_Load(object sender, EventArgs e) { } } }
首先,我使用了一个叫做ExcelResponse的对象,这种设计方式是来源于Request/Response模式的。这里为了简单,只用了Response。如果大家要进行系统设计,应该先设计一个Response的抽象基类(或接口),然后不同的业务对象取名为不同的XXXResponse,继承这个基类。不统一用object类型作为Item是考虑到装箱拆箱可能引发的效率问题(毕竟在系统级别,我们往往处理的是大量对象)。
在ReadData方法中,我采用了多个catch block,以便捕获不同类型的异常。设计多个catch的原则是,小的在前,大的在后。Exception是最笼统的异常,所以放在最后。
连接Excel2010文件的时候,我们使用的驱动和Access2010数据库是一致的,即“Provider=Microsoft.ACE.OLEDB.12.0;”,记得文章开头我让大家下的一个驱动吗?这个就是在你电脑上安装一个ACE.OLEDB.12.0的驱动,不然程序走到这里肯定抛异常。
细心的同学会注意到我在判断对象null的时候,喜欢把null写在==前面。这不是为了效率考虑,而是从上个时代留下的习惯,为了防止手误把 xxx == null 写成 xxx = null,后者是直接赋值的,在C/C++的年代,后面的情况编译器未必能发现。
接下来还有一个要注意的地方,就是程序读取Excel到DataTable以后,列名是有变化的,不再叫A列、B列,而是F1和F2。在DEBUG的时候可以用VS的DataSet查看工具看到这个结构:
因此,我的LINQ语句就得这样写:
var query = from item in dtResponse.Item.AsEnumerable() where item.Field<string>("F2") != "SHIT" select item.Field<string>("F2");
这个LINQ的查询方法叫做LINQ to DataTable,但DataTable本身并不能被直接查询,所以我们需要加上.AsEnumerable()。item.Field<string>表示我们希望得到的是一个字符串类型,其实你不指定类型也是可以的。
程序最终运行结果如下:
连接字符串里设置HDR=Yes就会不会有F1,F2了,直接上列名了
例子是不错,就是表里面的单词……