LINQ to Excel を作ってみる その2

対象とするExcelのバージョンはOffice 2003。事前にTlbimp.exeを使ってExcel.exeからRCWを作っておく。アセンブリ名はExcel.Interop名前空間Excel.Interopとしておく。

まずはシートを列挙する機能から作る。

クラス構造はだいたい↓こんな感じ

XlsWorkbookクラスをルートとして、ワークシートを表すXlsWorksheetクラスとそれを管理するXlsWorksheetsクラスからなる。
XlsWorksheetsクラスにはXlsWorkbookのプロパティからアクセスすることができる。

まずはワークブックに対する操作を行うためのXlsWorkbookクラス。

XlsWorkbook.cs

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Diagnostics;

using Excel.Interop;

namespace Excel.Linq {
    /// <summary>
    /// Excelワークブックに対する操作を提供するクラス
    /// </summary>
    public class XlsWorkbook {
        /// <summary>
        /// 省略可能引数に渡す値
        /// </summary>
        private static readonly object None = Type.Missing;

        private Application xlsApp;
        private Workbook workbook;

        /// <summary>
        /// 指定したファイル名のExcel ファイルを読み込みます。
        /// </summary>
        /// <param name="fileName">ファイル名</param>
        public XlsWorkbook(string fileName) {
            xlsApp = new ApplicationClass();
            
            workbook = xlsApp.Workbooks.Open(Path.GetFullPath(fileName),
                None, true, None, None, None, None, None, None, None, None, None, None, None, None
            );
        }
        ~XlsWorkbook() {
            Dispose();
        }
        
        protected virtual void Dispose(bool disposing) {
            if(!disposing) return;

            if(workbook != null) {
                workbook.Close(false, None, None);
                Marshal.ReleaseComObject(workbook);
                workbook = null;
            }
            if(xlsApp != null) {
                xlsApp.Quit();
                Marshal.ReleaseComObject(xlsApp);
                xlsApp = null;
            }
        }
        public void Dispose() {
            Dispose(true);

            GC.SuppressFinalize(this);
        }
    }
}

とりあえず、ここではコンストラクタで指定したファイル名のワークブックを開くだけ。生のWorkbookオブジェクトをフィールドとして保持しておき、Disposeパターンを使ってUnmanaged リソースを破棄するようにしておく。

次はワークシートに対する操作を行うXlsWorksheetクラス。

XlsWorksheet.cs

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Diagnostics;

using Excel.Interop;

namespace Excel.Linq {
    /// <summary>
    /// Excelワークシートに対する操作を提供するクラス
    /// </summary>
    public class XlsWorksheet : IDisposable {
        private Worksheet worksheet;
        
        /// <summary>
        /// ワークシート名を取得、設定します。
        /// </summary>
        public string Name {
            get { return worksheet.Name; }
            set { worksheet.Name = value; }
        }
        
        /// <summary>
        /// 生のWorksheetオブジェクトを設定するコンストラクタ
        /// </summary>
        /// <param name="worksheet">Worksheetオブジェクト</param>
        protected internal XlsWorksheet(Worksheet worksheet) {
            this.worksheet = worksheet;
        }
        ~XlsWorksheet() {
            Dispose();
        }

        protected virtual void Dispose(bool disposing) {
            if(!disposing) return;

            if(worksheet != null) {
                Marshal.ReleaseComObject(worksheet);
                worksheet = null;
            }
        }
        public void Dispose() {
            Dispose(true);

            GC.SuppressFinalize(this);
        }
    }
}

生のWorksheetオブジェクトをフィールドとして保持し、シート名を返すプロパティを持つ。

XlsWorksheetオブジェクトを列挙するXlsWorksheetsクラス。

XlsWorksheets.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Diagnostics;

using Excel.Interop;

namespace Excel.Linq {
    /// <summary>
    /// Excelワークシートのコレクションに対する操作を提供するクラス
    /// </summary>
    public class XlsWorksheets : IEnumerable<XlsWorksheet>, IDisposable {
        private Sheets worksheets;

        /// <summary>
        /// 生のWorksheetオブジェクトを設定するコンストラクタ
        /// </summary>
        /// <param name="worksheets">Worksheetオブジェクト</param>
        protected internal XlsWorksheets(Sheets worksheets) {
            this.worksheets = worksheets;
        }
        ~XlsWorksheets() {
            Dispose();
        }

        public IEnumerator<XlsWorksheet> GetEnumerator() {
            foreach(Worksheet sheet in worksheets) yield return new XlsWorksheet(sheet);
        }
        IEnumerator IEnumerable.GetEnumerator() {
            return this.GetEnumerator();
        }

        protected virtual void Dispose(bool disposing) {
            if(!disposing) return;

            if(worksheets != null) {
                Marshal.ReleaseComObject(worksheets);
                worksheets = null;
            }
        }
        public void Dispose() {
            Dispose(true);

            GC.SuppressFinalize(this);
        }
    }
}

IEnumerableインターフェースを実装し、GetEnumeratorメソッドでXlsWorksheetオブジェクトを列挙する。

このクラスをXlsWorkbookのプロパティとして追加しておく。コンストラクタにもXlsWorksheetsオブジェクトをインスタンス化するコードを追加しておく。

XlsWorkbook.cs
private XlsWorksheets worksheets;

/// <summary>
/// ワークシートのコレクションを取得します。
/// </summary>
public XlsWorksheets Worksheets {
    get { return worksheets; }
}

public XlsWorkbook(string fileName) {
    xlsApp = new ApplicationClass();
    
    workbook = xlsApp.Workbooks.Open(Path.GetFullPath(fileName),
        None, true, None, None, None, None, None, None, None, None, None, None, None, None
    );
    // ↓追加
    worksheets = new XlsWorksheets(workbook.Sheets);
}

この時点でも以下のようなクエリは成立する。

using(XlsWorkbook book = new XlsWorkbook("100.xls")) {

    var sheets = from s in book.Worksheets
                 where s.Contains("hoge")
                 select s;

    foreach(var sheet in sheets) Console.WriteLine(sheet.Name);
}

でも、これではXlsWorksheetsのGetEnumeratorメソッドで全シートがXlsWorksheetオブジェクトにラップされて列挙されるため、無駄にオブジェクトがインスタンス化されている。

そうではなくて、XlsWorksheetにラップして列挙する前の段階でwhere演算子に指定された条件でフィルタリングした結果のみをXlsWorksheetでラップして列挙してあげたい。これができればLINQの仕組みが少しは見えてくるだろう。

そのためには、まずXlsWorksheetsクラスをIQueryableインターフェースを実装するように変更する。

XlsWorksheets.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Diagnostics;

using Excel.Interop;

namespace Excel.Linq {
    /// <summary>
    /// Excelワークシートのコレクションに対する操作を提供するクラス
    /// </summary>
    public class XlsWorksheets : IQueryable<XlsWorksheet>, IDisposable {
        private Sheets worksheets;

        /// <summary>
        /// 生のWorksheetオブジェクトを設定するコンストラクタ
        /// </summary>
        /// <param name="worksheets">Worksheetオブジェクト</param>
        protected internal XlsWorksheets(Sheets worksheets) {
            this.worksheets = worksheets;
        }
        ~XlsWorksheets() {
            Dispose();
        }

        public IEnumerator<XlsWorksheet> GetEnumerator() {
            foreach(Worksheet sheet in worksheets) yield return new XlsWorksheet(sheet);
        }
        IEnumerator IEnumerable.GetEnumerator() {
            return this.GetEnumerator();
        }

        // IQueryableの実装
        Type IQueryable.ElementType {
            get { return typeof(XlsWorksheet); }
        }
        Expression IQueryable.Expression {
            get { return null; }
        }
        IQueryProvider IQueryable.Provider {
            get { return null; }
        }
        
        protected virtual void Dispose(bool disposing) {
            if(!disposing) return;

            if(worksheets != null) {
                Marshal.ReleaseComObject(worksheets);
                worksheets = null;
            }
        }
        public void Dispose() {
            Dispose(true);

            GC.SuppressFinalize(this);
        }
    }
}

IQueryableは、

  • Type ElementType { get; }
  • Expression Expression { get; }
  • IQueryProvider Provider { get; }

の三つのプロパティを実装する必要がある。

とりあえず何を返せばいいのかわからないので、ElementTypeでXlsWorksheetの型を他の二つではnullを返しておく。

ここで一度先程のコードを実行してみるとNullReferenceExceptionが投げられた。
デバッガで追ってみると、まずProviderプロパティが呼び出され、その後にExpressionプロパティが呼び出された。これらがnullのために発生した事はわかるが役目がよくわからない。

どのように実装すればいいのかわからないので、System.Data.LinqアセンブリをReflector.NETで逆アセンブルして調べる。