昨天一个同学让我帮忙写个小工具,读一个Excel,只读B列的内容。我拿C#写了一个.NET4的winform程序。虽然简单,但有很多可圈可点的地方,因此撰写本文以警后人。

那个Excel是2010格式的,所以我们先要安装AccessDatabaseEngine,下载地址如下:

http://download.microsoft.com/download/7/0/3/703ffbcb-dc0c-4e19-b0da-1463960fdcdb/AccessDatabaseEngine.exe

这样程序读取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>表示我们希望得到的是一个字符串类型,其实你不指定类型也是可以的。

程序最终运行结果如下: