I really like when things clean up after them selves... So I made some wrapper classes that do all the cleanup for me! These are documented further down.
The end code is quite readable and accessible. I haven't yet found any phantom instances of Excel running after I Close()
the workbooks and Quit()
the application (besides where I debug and close the app mid process).
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");
// do all the excel manipulation
// read from the first workbook, write to the second workbook.
var a1 = workbook1.Cells[1, 1];
workbook2.Cells[1, 1] = a1
// explicit clean-up
workbook1.Close(false);
workbook2 .Close(true);
excel.Quit();
}
Note: You can skip the Close()
and Quit()
calls but if you are writing to an Excel document you will at least want to Save()
. When the objects go out of scope (the method returns) the class finalizers will automatically kick in and do any cleanup. Any references to COM objects from the Worksheet COM object will automatically be managed and cleaned up as long as you are careful with the scope of your variables, eg keep variables local to the current scope only when storing references to COM objects. You can easily copy values you need to POCOs if you need, or create additional wrapper classes as discussed below.
To manage all this, I have created a class, DisposableComObject
, that acts as a wrapper for any COM object. It implements the IDisposable
interface and also contains a finalizer for those that don't like using
.
The Dispose()
method calls Marshal.ReleaseComObject(ComObject)
and then sets the ComObjectRef
property to null.
The object is in a disposed state when the private ComObjectRef
property is null.
If the ComObject
property is accessed after being disposed, a ComObjectAccessedAfterDisposeException
exception is thrown.
The Dispose()
method can be called manually. It is also called by the finalizer, at the conclusion of a using
block, and for using var
at the conclusion of the scope of that variable.
The top level classes from Microsoft.Office.Interop.Excel
, Application
, Workbook
, and Worksheet
, get their own wrapper classes where each are subclasses of DisposableComObject
Here is the code:
/// <summary>
/// References to COM objects must be explicitly released when done.
/// Failure to do so can result in odd behavior and processes remaining running after the application has stopped.
/// This class helps to automate the process of disposing the references to COM objects.
/// </summary>
public abstract class DisposableComObject : IDisposable
{
public class ComObjectAccessedAfterDisposeException : Exception
{
public ComObjectAccessedAfterDisposeException() : base("COM object has been accessed after being disposed") { }
}
/// <summary>The actual COM object</summary>
private object ComObjectRef { get; set; }
/// <summary>The COM object to be used by subclasses</summary>
/// <exception cref="ComObjectAccessedAfterDisposeException">When the COM object has been disposed</exception>
protected object ComObject => ComObjectRef ?? throw new ComObjectAccessedAfterDisposeException();
public DisposableComObject(object comObject) => ComObjectRef = comObject;
/// <summary>
/// True, if the COM object has been disposed.
/// </summary>
protected bool IsDisposed() => ComObjectRef is null;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // in case a subclass implements a finalizer
}
/// <summary>
/// This method releases the COM object and removes the reference.
/// This allows the garbage collector to clean up any remaining instance.
/// </summary>
/// <param name="disposing">Set to true</param>
protected virtual void Dispose(bool disposing)
{
if (!disposing || IsDisposed()) return;
Marshal.ReleaseComObject(ComObject);
ComObjectRef = null;
}
~DisposableComObject()
{
Dispose(true);
}
}
There is also a handy generic subclass which makes usage slightly easier.
public abstract class DisposableComObject<T> : DisposableComObject
{
protected new T ComObject => (T)base.ComObject;
public DisposableComObject(T comObject) : base(comObject) { }
}
Finally, we can use DisposableComObject<T>
to create our wrapper classes for the Excel interop classes.
The ExcelApplication
subclass has a reference to a new Excel application instance and is used to open workbooks.
OpenWorkbook()
returns an ExcelWorkbook
which is also a subclass of DisposableComObject.
Dispose()
has been overridden to quit the Excel application before calling the base Dispose()
method. Quit()
is an alias of 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;
}
/// <summary>The actual Application from Interop.Excel</summary>
Application App => ComObject;
public ExcelApplication() : base(new Application()) { }
/// <summary>Open a workbook.</summary>
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)
{
// If the workbook is already open and the request mode is not read-only, the user will be presented
// with a prompt from the Excel application asking if the workbook should be opened in read-only mode.
// This exception is raised when when the user clicks the Cancel button in that prompt.
throw new OpenWorkbookActionCancelledException(filename, ex);
}
}
/// <summary>Quit the running application.</summary>
public void Quit() => Dispose(true);
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (!disposing || IsDisposed()) return;
App.Quit();
base.Dispose(disposing);
}
}
ExcelWorkbook
also subclasses DisposableComObject<Workbook>
and is used to open worksheets.
The Worksheet()
methods returns ExcelWorksheet
which, you guessed it, is also an subclass of DisposableComObject<Workbook>
.
The Dispose()
method is overridden and fist closes the worksheet before calling the base Dispose()
.
NOTE: I've added some extension methods which is uses to iterate over Workbook.Worksheets
. If you get compile errors, this is why. Ill add the extension methods at the end.
public class ExcelWorkbook : DisposableComObject<Workbook>
{
public class WorksheetNotFoundException : Exception
{
public WorksheetNotFoundException(string message) : base(message) { }
}
/// <summary>The actual Workbook from Interop.Excel</summary>
Workbook Workbook => ComObject;
/// <summary>The worksheets within the workbook</summary>
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) { }
/// <summary>
/// Get the worksheet matching the <paramref name="sheetName"/>
/// </summary>
/// <param name="sheetName">The name of the Worksheet</param>
public ExcelWorksheet Worksheet(string sheetName) => Worksheet(s => s.Name == sheetName, () => $"Worksheet not found: {sheetName}");
/// <summary>
/// Get the worksheet matching the <paramref name="predicate"/>
/// </summary>
/// <param name="predicate">A function to test each Worksheet for a macth</param>
public ExcelWorksheet Worksheet(Func<ExcelWorksheet, bool> predicate, Func<string> errorMessageAction) => Worksheets.FirstOrDefault(predicate) ?? throw new WorksheetNotFoundException(errorMessageAction.Invoke());
/// <summary>
/// Returns true of the workbook is read-only
/// </summary>
public bool IsReadOnly() => Workbook.ReadOnly;
/// <summary>
/// Save changes made to the workbook
/// </summary>
public void Save()
{
Workbook.Save();
}
/// <summary>
/// Close the workbook and optionally save changes
/// </summary>
/// <param name="saveChanges">True is save before close</param>
public void Close(bool saveChanges)
{
if (saveChanges) Save();
Dispose(true);
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (!disposing || IsDisposed()) return;
Workbook.Close();
base.Dispose(disposing);
}
}
Finally, the ExcelWorksheet
.
UsedRows()
simply returns an enumerable of unwrapped Microsoft.Office.Interop.Excel.Range
objects. I haven't yet encountered a situation where COM objects accessed from properties of the Microsoft.Office.Interop.Excel.Worksheet
object need to manually wrapped like was needed with Application
, Workbook
, and Worksheet
. These all seem to clean them selves up automatically. Mostly, I was just iterating over Ranges and getting or setting values, so my particular use-case isn't as advanced as the available functionality.
There is no override of Dispose()
in this case as no special action needs to take place for worksheets.
public class ExcelWorksheet : DisposableComObject<Worksheet>
{
/// <summary>The actual Worksheet from Interop.Excel</summary>
Worksheet Worksheet => ComObject;
/// <summary>The worksheet name</summary>
public string Name => Worksheet.Name;
// <summary>The worksheets cells (Unwrapped COM object)</summary>
public Range Cells => Worksheet.Cells;
public ExcelWorksheet(Worksheet worksheet) : base(worksheet) { }
/// <inheritdoc cref="WorksheetExtensions.UsedRows(Worksheet)"/>
public IEnumerable<Range> UsedRows() => Worksheet.UsedRows().ToList();
}
It is possible to add even more wrapper classes. Just add additional methods to ExcelWorksheet
as needed and return the COM object in a wrapper class. Just copy what we did when wrapping the workbook via ExcelApplication.OpenWorkbook()
and ExcelWorkbook.WorkSheets
.
Some useful extension methods:
public static class EnumeratorExtensions
{
/// <summary>
/// Converts the <paramref name="enumerator"/> to an IEnumerable of type <typeparamref name="T"/>
/// </summary>
public static IEnumerable<T> AsEnumerable<T>(this IEnumerable enumerator)
{
return enumerator.GetEnumerator().AsEnumerable<T>();
}
/// <summary>
/// Converts the <paramref name="enumerator"/> to an IEnumerable of type <typeparamref name="T"/>
/// </summary>
public static IEnumerable<T> AsEnumerable<T>(this IEnumerator enumerator)
{
while (enumerator.MoveNext()) yield return (T)enumerator.Current;
}
/// <summary>
/// Converts the <paramref name="enumerator"/> to an IEnumerable of type <typeparamref name="T"/>
/// </summary>
public static IEnumerable<T> AsEnumerable<T>(this IEnumerator<T> enumerator)
{
while (enumerator.MoveNext()) yield return enumerator.Current;
}
}
public static class WorksheetExtensions
{
/// <summary>
/// Returns the rows within the used range of this <paramref name="worksheet"/>
/// </summary>
/// <param name="worksheet">The worksheet</param>
public static IEnumerable<Range> UsedRows(this Worksheet worksheet) =>
worksheet.UsedRange.Rows.AsEnumerable<Range>();
}