2010年8月21日土曜日

ExcelのAutomationのTipsあれこれ

業務でExcelで来たデータをSQL Serevrに流し込む必要があり、ExcelのAutomation用のツールを作ったのでそのときのTipsをいくつか紹介。

Excel Automationの概要自体は下記のリンクを参考にして欲しい。
How to automate Microsoft Excel from Microsoft Visual C#.NET


必ずCOMインスタンスを開放する
Excel Automationでもっともつまづきやすいのが、アプリが終了してもいつまでもExcelのプロセスが居座ることだ。これは色々と理由があるが下記のコードのような状況が最も陥りやすい。

var application = new Excel.Application();
var workBook = application.Workbooks.Open("path", Type.Missing.... );
var sheet = workBook.Sheets[2];

Marshal.FinalReleaseComObject(sheet);
workBook.Close();
Marshal.FinalReleaseComObject(workBook);
application.Quit();
Marshal.FinalReleaseComObject(application);

問題はOpenを呼んでいるところだ。実はapplication.Workbooks.Openを呼び出した際に、内部的にapplication.WorkbooksのCOMインスタンスが生成されている。そのためCOMインスタンスを開放するためにWorkBooksもMarshal.FinalReleaseComObjectしなければならないのだが、ここではWokrBooksをハンドルしていないためにそれができない。正しくは下記のようになる。

var application = new Excel.Application();
var workBooks = application.Workbooks;
var workBook = workBooks.Open("path", Type.Missing.... );
var sheet = workBook.Sheets[2];

Marshal.FinalReleaseComObject(sheet);
workBook.Close();
Marshal.FinalReleaseComObject(workBook);
Marshal.FinalReleaseComObject(workBooks);
application.Quit();
Marshal.FinalReleaseComObject(application);


これでExcelのプロセスが居座ることは解決できたが、例外処理が起きたときや、Cellから値を取得するためにCOMインスタンスを生成したときなど、開放処理を常に行うように気を使うのはなかなか骨が折れる。そのため、開放処理を簡便に行えるようなWrapperを作ることにした。

ちなみにこのアイディアは下記のStackoverflowのGary McGillから拝借している。
c# and excel automation - ending the running instance


Wrapperクラス
class ComWrapper<T> : IDisposable
{
    public ComWrapper(T t)
    {
        ComObject = t;
    }
    public void Dispose()
    {
        Marshal.FinalReleaseComObject(ComObject);
    }
    public T ComObject { get; set; }
}

使っているところ
using (var application = new ComWrapper(new Application()))
{
 try
 {
    using (var workbooks = new ComWrapper(application.ComObject.Workbooks))
    {
        using (var workbook = new ComWrapper(workbooks.ComObject.Open(filePath, ...)))
        {
            try
            {
                using (var worksheet = new ComWrapper(workbook.ComObject.Sheets[1]))
                {
                    // なにか処理
                }
            }
            finally
            {
                workbook.ComObject.Close();
            }
        }
    }
 }
 finally
 {
    application.ComObject.Quit();
 }
}

おまけ - Cellの値を取得
for( var rowIndex = 1; rowIndex < 10; rowIndex ++)
{
 var colIndex = 0;
 using (var cell1 = new ComWrapper(worksheet.ComObject.Cells[rowIndex, ++colIndex]))
 using (var cell2 = new ComWrapper(worksheet.ComObject.Cells[rowIndex, ++colIndex]))
 {
 // 処理
 var value = cell1.ComObject.Value
 }
}


usingを使うことによりスコープ範囲外到達時に常にDisposeが呼ばれ、そのDispose内部でCOMインスタンスの開放を行っているため、COMの開放というわずらわしい処理を気にする必要がまったくなくなった。これでロジック部分に集中できるだろう。

0 件のコメント:

コメントを投稿