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); } } }
次はワークシートに対する操作を行う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); } } }
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); } } }
このクラスを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); } } }
- Type ElementType { get; }
- Expression Expression { get; }
- IQueryProvider Provider { get; }
の三つのプロパティを実装する必要がある。
とりあえず何を返せばいいのかわからないので、ElementTypeでXlsWorksheetの型を他の二つではnullを返しておく。
ここで一度先程のコードを実行してみるとNullReferenceExceptionが投げられた。
デバッガで追ってみると、まずProviderプロパティが呼び出され、その後にExpressionプロパティが呼び出された。これらがnullのために発生した事はわかるが役目がよくわからない。
どのように実装すればいいのかわからないので、System.Data.LinqアセンブリをReflector.NETで逆アセンブルして調べる。