Is this programming pattern the solution to Excel COM interop memory leaks?

59 views Asked by At

I don’t wish to contribute to the mysticism of this subject further than it already seems to be on internet. I’ve many articles talking about this subject, some say never Marshal.ReleaseComObject anything, there’s the “2-dot rule”, yada yada.

The fact is, my Excel VSTO addin does slow down and crash after prolonged use, and I don’t call ReleaseComObject all the time. I’ve also measured (through a timer) a drastic performance hit when summoning another COM object into existence through one’s property (e.g. hyperlink.Name returns instantly, hyperlink.Range.Address returns approx. 214 times slower) which intuitively suggest that said COM object must/should have Marshal.ReleaseComObject called upon it since .NET’s GC won’t collect it (it’d only the RCW, not the underlying COM object).

With all this out of the way, do you think interfacing with Excel interop through a set of wrapper classes of the following pattern would allow .NET’s native GC to actually take care of all memory management once again? Please do not point out the amount of coding effort necessary to make this work (I don’t care, if it makes my program not slow down and crash, I’ll do it).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Messaging;
using System.Text;
using System.Threading.Tasks;
using Excel = Microsoft.Office.Interop.Excel;
using IRibbon = Microsoft.Office.Core.IRibbonExtensibility;
using App = Microsoft.Office.Interop.Excel.Application;
using Wbs = Microsoft.Office.Interop.Excel.Workbooks;
using Wb = Microsoft.Office.Interop.Excel.Workbook;
using Shs = Microsoft.Office.Interop.Excel.Sheets;
using Ws = Microsoft.Office.Interop.Excel.Worksheet;
using Table = Microsoft.Office.Interop.Excel.ListObject;
using Row = Microsoft.Office.Interop.Excel.ListRow;
using Column = Microsoft.Office.Interop.Excel.ListColumn;
using Cells = Microsoft.Office.Interop.Excel.Range;
using Name = Microsoft.Office.Interop.Excel.Name;
using Chart = Microsoft.Office.Interop.Excel.Chart;
using Shape = Microsoft.Office.Interop.Excel.Shape;
using System.Runtime.InteropServices;

namespace Office.ExcelUtilities
{
    public class ApplicationWrapper
    {
        App _app;
        public ApplicationWrapper(App app)
        {
            _app = app;
        }
        public WorkbookWrapper AddWorkbook()
        {
            Wbs wbs = _app.Workbooks;
            Wb wb = wbs.Add();
            Marshal.ReleaseComObject(wbs);
            return new WorkbookWrapper(wb);
        }
        ~ApplicationWrapper()
        {
            Marshal.ReleaseComObject(_app);
        }
    }
    public class WorkbookWrapper
    {
        Wb _wb = null;
        internal WorkbookWrapper(Wb wb)
        {
            _wb = wb;
        }
        public WorkbookWrapper()
        {
            //just lets client hold this object
            if(_wb == null )
                throw new Exception("You didn't create a workbook right");
        }
        public WorksheetWrapper AddWorksheet(string sheetName)
        {
            Shs wss = _wb.Worksheets;
            Ws ws = wss.Add();
            ws.Name = sheetName;
            Marshal.ReleaseComObject(wss);
            return new WorksheetWrapper(ws);
        }
        ~WorkbookWrapper()
        {
            Marshal.ReleaseComObject(_wb);
        }
    }
    public class WorksheetWrapper
    {
        Ws _ws = null;
        internal WorksheetWrapper(Ws ws)
        {
            _ws = ws;
        }
        public WorksheetWrapper()
        {
            //just lets client hold this object
            if(_ws == null)
                throw new Exception("You didn't create a worksheet right");
        }
        ~WorksheetWrapper()
        {
            Marshal.ReleaseComObject(_ws);
        }
    }
}

This is obviously not complete, but you get the idea. Please excuse my renaming of base interop types. Thanks!

1

There are 1 answers

5
Dmitry Streblechenko On

since .NET’s GC won’t collect it (it’d only the RCW, not the underlying COM object).

Not true - GC will release the COM object, you just have no say over when that happens. When RCW object is disposed, it will Release() the underlying COM object, and if nothing else holds a reference to it (ref count == 0), the COM object will destroy itself.

Marshal.ReleaseComObject and multiple dot rule are only really needed in very special cases, when you want full control over the order and timing of COM object releases. E.g., if you have a loop over all items in an Outlook folder and access hundreds or thousands of items, especially if Outlook is in online mode, which limits the number of open RPC connections to the Exchange Server.

In your particular case, I doubt it will make any difference at all.