我真的很喜欢事情自动清理...所以我写了一些包装类来为我完成所有的清理工作!这些类在下面有更详细的文档。
最终的代码非常易读和可访问。我还没有发现任何幽灵实例在我Close()
工作簿和Quit()
应用程序之后仍在运行(除了我在进程中调试和关闭应用程序的情况)。
function void OpenCopyClose() {
var excel = new ExcelApplication();
var workbook1 = excel.OpenWorkbook("C:\Temp\file1.xslx", readOnly: true);
var readOnlysheet = workbook1.Worksheet("sheet1");
var workbook2 = excel.OpenWorkbook("C:\Temp\file2.xslx");
var writeSheet = workbook.Worksheet("sheet1");
var a1 = workbook1.Cells[1, 1];
workbook2.Cells[1, 1] = a1
workbook1.Close(false);
workbook2 .Close(true);
excel.Quit();
}
注意:您可以跳过
Close()
和
Quit()
调用,但如果您要写入Excel文档,则至少需要
Save()
。当对象超出范围(方法返回)时,类终结器将自动启动并进行任何清理。只要您在变量的作用域方面小心,例如仅在存储对COM对象的引用时将变量保持本地到当前作用域,那么来自Worksheet COM对象的任何对COM对象的引用都将自动管理和清除。如果需要,您可以轻松地将所需值复制到POCO中,或者根据下面讨论的内容创建其他包装类。
为了管理所有这些,我创建了一个名为
DisposableComObject
的类,它充当任何COM对象的包装器。它实现了
IDisposable
接口,并且还包含一个终结器,适用于那些不喜欢
using
的人。
Dispose()
方法调用
Marshal.ReleaseComObject(ComObject)
,然后将
ComObjectRef
属性设置为null。
当私有的
ComObjectRef
属性为null时,对象处于已处理状态。
如果在已释放后访问ComObject
属性,则会抛出ComObjectAccessedAfterDisposeException
异常。
Dispose()
方法可以手动调用。它也会在using
块结束时,以及在using var
变量作用域结束时由终结器调用。
Microsoft.Office.Interop.Excel
中的顶级类Application
、Workbook
和Worksheet
都有自己的包装类,每个类都是DisposableComObject
的子类。
以下是代码:
public abstract class DisposableComObject : IDisposable
{
public class ComObjectAccessedAfterDisposeException : Exception
{
public ComObjectAccessedAfterDisposeException() : base("COM object has been accessed after being disposed") { }
}
private object ComObjectRef { get; set; }
protected object ComObject => ComObjectRef ?? throw new ComObjectAccessedAfterDisposeException();
public DisposableComObject(object comObject) => ComObjectRef = comObject;
protected bool IsDisposed() => ComObjectRef is null;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposing || IsDisposed()) return;
Marshal.ReleaseComObject(ComObject);
ComObjectRef = null;
}
~DisposableComObject()
{
Dispose(true);
}
}
还有一个方便的通用子类,可以使使用稍微容易一些。
public abstract class DisposableComObject<T> : DisposableComObject
{
protected new T ComObject => (T)base.ComObject;
public DisposableComObject(T comObject) : base(comObject) { }
}
最后,我们可以使用DisposableComObject<T>
来为Excel互操作类创建包装器类。
ExcelApplication
子类具有对新Excel应用程序实例的引用,并用于打开工作簿。
OpenWorkbook()
返回一个ExcelWorkbook
,它也是DisposableComObject
的子类。
Dispose()
已被重写以在调用基本Dispose()
方法之前退出Excel应用程序。Quit()
是Dispose()
的别名。
public class ExcelApplication : DisposableComObject<Application>
{
public class OpenWorkbookActionCancelledException : Exception
{
public string Filename { get; }
public OpenWorkbookActionCancelledException(string filename, COMException ex) : base($"The workbook open action was cancelled. {ex.Message}", ex) => Filename = filename;
}
Application App => ComObject;
public ExcelApplication() : base(new Application()) { }
public ExcelWorkbook OpenWorkbook(string filename, bool readOnly = false, string password = null, string writeResPassword = null)
{
try
{
var workbook = App.Workbooks.Open(Filename: filename, UpdateLinks: (XlUpdateLinks)0, ReadOnly: readOnly, Password: password, WriteResPassword: writeResPassword, );
return new ExcelWorkbook(workbook);
}
catch (COMException ex)
{
throw new OpenWorkbookActionCancelledException(filename, ex);
}
}
public void Quit() => Dispose(true);
protected override void Dispose(bool disposing)
{
if (!disposing || IsDisposed()) return;
App.Quit();
base.Dispose(disposing);
}
}
ExcelWorkbook
还是 DisposableComObject<Workbook>
的子类,用于打开工作表。
Worksheet()
方法返回 ExcelWorksheet
,你猜对了,它也是 DisposableComObject<Workbook>
的子类。
Dispose()
方法被重写,并在调用基本的 Dispose()
之前首先关闭工作表。
注意:我添加了一些扩展方法,用于迭代 Workbook.Worksheets
。如果出现编译错误,这就是原因。我会在最后添加这些扩展方法。
public class ExcelWorkbook : DisposableComObject<Workbook>
{
public class WorksheetNotFoundException : Exception
{
public WorksheetNotFoundException(string message) : base(message) { }
}
Workbook Workbook => ComObject;
public IEnumerable<ExcelWorksheet> Worksheets => worksheets ?? (worksheets = Workbook.Worksheets.AsEnumerable<Worksheet>().Select(w => new ExcelWorksheet(w)).ToList());
private IEnumerable<ExcelWorksheet> worksheets;
public ExcelWorkbook(Workbook workbook) : base(workbook) { }
public ExcelWorksheet Worksheet(string sheetName) => Worksheet(s => s.Name == sheetName, () => $"Worksheet not found: {sheetName}");
public ExcelWorksheet Worksheet(Func<ExcelWorksheet, bool> predicate, Func<string> errorMessageAction) => Worksheets.FirstOrDefault(predicate) ?? throw new WorksheetNotFoundException(errorMessageAction.Invoke());
public bool IsReadOnly() => Workbook.ReadOnly;
public void Save()
{
Workbook.Save();
}
public void Close(bool saveChanges)
{
if (saveChanges) Save();
Dispose(true);
}
protected override void Dispose(bool disposing)
{
if (!disposing || IsDisposed()) return;
Workbook.Close();
base.Dispose(disposing);
}
}
最后是ExcelWorksheet
对象。
UsedRows()
方法只是简单地返回未包装的Microsoft.Office.Interop.Excel.Range
对象枚举。我还没有遇到过需要手动包装Microsoft.Office.Interop.Excel.Worksheet
对象属性中访问的COM对象的情况,就像使用Application
、Workbook
和Worksheet
时需要的那样。这些对象都似乎会自动清理自己。主要是我正在迭代区域并获取或设置值,所以我的特定用例不如可用功能那么高级。
在这种情况下,没有Dispose()
的覆盖版本,因为工作表不需要执行任何特殊操作。
public class ExcelWorksheet : DisposableComObject<Worksheet>
{
Worksheet Worksheet => ComObject;
public string Name => Worksheet.Name;
public Range Cells => Worksheet.Cells;
public ExcelWorksheet(Worksheet worksheet) : base(worksheet) { }
public IEnumerable<Range> UsedRows() => Worksheet.UsedRows().ToList();
}
可以添加更多的包装类。只需根据需要向ExcelWorksheet
添加其他方法,并在包装类中返回COM对象。只需复制我们在通过ExcelApplication.OpenWorkbook()
和ExcelWorkbook.WorkSheets
包装工作簿时所做的操作。
一些有用的扩展方法:
public static class EnumeratorExtensions
{
public static IEnumerable<T> AsEnumerable<T>(this IEnumerable enumerator)
{
return enumerator.GetEnumerator().AsEnumerable<T>();
}
public static IEnumerable<T> AsEnumerable<T>(this IEnumerator enumerator)
{
while (enumerator.MoveNext()) yield return (T)enumerator.Current;
}
public static IEnumerable<T> AsEnumerable<T>(this IEnumerator<T> enumerator)
{
while (enumerator.MoveNext()) yield return enumerator.Current;
}
}
public static class WorksheetExtensions
{
public static IEnumerable<Range> UsedRows(this Worksheet worksheet) =>
worksheet.UsedRange.Rows.AsEnumerable<Range>();
}