When using COM Interop with Office (usually Excel), I always carefully ensure I call Marshal.ReleaseComObject
on every reference, to avoid the problem where Excel doesn't quit as described in this KB article.
How can I ensure Excel quits when I use Interop from an OOB Silverlight application (with AutomationFactory.CreateObject
)?
Silverlight doesn't have a Marshal.ReleaseComObject
method and even calling GC.Collect
and GC.WaitForPendingFinalizers
doesn't help.
Surely Microsoft hasn't added this feature to Silverlight without a mechanism to release COM references? This seems to me to be a showstopper for automating out-of-process COM servers such as Excel.
A surprising ommission, all the more so as Pete Brown in section 5.5 of his book "Silverlight 4 in Action" goes as far as to say about AutomationFactory.CreateObject
, that:
The primary intent of this feature is to allow automation of other applications, including Microsoft Office.
UPDATE in response to Hans' comments.
I'm not convinced the "silent assassin" problem exists in typical automation of Office apps. A common use might look something like the following, which I've seen used repeatedly in WinForms applications without ever coming across the "poisoned RCW" described in the article linked by Hans:
- Create an Excel.Application instance
- Open or create a workbook
- Write data to the workbook
- Show Excel if all went well, close the workbook and call Application.Quit if not.
- Call Marshal.ReleaseComObject to release all Excel object references.
Failing to call Marshal.ReleaseComObject as recommended by Hans will leave multiple copies of Excel.exe running, as described in the KB article mentioned above - highly undesirable.
UPDATE 2
The sample I'm using to repro this is a sample from the source code for Pete Brown's book Silverlight 4 in action, there's a download link on this page. The sample solution AutomatingExcel is in Ch05.zip / 5.03. To repro:
- Make sure no instances of Excel are running
- Run AutomatingExcel sample
- An Excel workbook is opened
- Close Excel
- Observe with Task Manager that Excel is still running.
Setting all the dynamic variables to null and calling GC.Collect() seems to work as pointed out in AnthonyWJones's answer.
UPDATE 2
Otaku's answer is what I was looking for - by wrapping references in a using statement the COM references are released without the need to call GC.Collect. A bit of experimentation shows that it's more tolerant of failing to dispose every single reference, unlike the standard Marshal.ReleaseComObject
solution described in the KB article referenced above.
It would be interesting to have an authoritative take on exactly what must be disposed to ensure that all Excel references are released.
You could implement the IDisposable
interface. The best example of this I've seen is at http://csfun.blog49.fc2.com/blog-entry-79.html. The blog entry is in Japanese, but open in Chrome and let Google do the page translation for you if you don't read Japanese.
Also, if you just want the source code example of the COM wrapper directly you can download the sample app it comes in: SilverOffice.
Take look at this code:-
private void Button_Click(object sender, RoutedEventArgs e)
{
dynamic app = AutomationFactory.CreateObject("Excel.Application");
dynamic book = app.Workbooks.Add();
dynamic sheet = app.ActiveSheet();
sheet = null;
book.Close(false);
book = null;
app.Quit();
app = null;
GC.Collect();
}
The Excel process appears and then disappears. Remove the GC
and the Excel process will continue. Do you get the same if you copy this code verbatim? If so then it would suggest that somewhere in your code a reference to an excel object remains reachable from one of the thread stacks or static fields.
Do you ever hold an excel object in a field (as opposed to a local variable)?
Do you hold an excel object in what appears to be variable but is referenced from a dynamic delegate or lambda that is used as an event handler?
Are you attaching event handlers to long-lived objects from an object that has a short lifespan? If so are you ensuring you detach from those handlers properly?
Many of these things can catch devs out into leaving what they think are objects ready for GC but the GC finds them accessible and therefore not candidates for collection.
If the code above doesn't behave the same then we a looking for another issue entirely. I'm using Office 2007 on Server 2008 R2 from the latest SL 4 runtime. However if we are having a varition because of setup then we're on very shakey ground.
Edit
With some testing this appears to be effective:-
private void Button_Click(object sender, RoutedEventArgs e)
{
using (dynamic app = AutomationFactory.CreateObject("Excel.Application"))
{
using (dynamic book = app.Workbooks.Add())
{
using (dynamic sheet = app.ActiveSheet())
{
}
book.Close();
}
app.Quit();
};
GC.Collect();
}
However leave off the GC and you will end up with unwanted Excel processes left running eventually.
I would consider building the Excel file in a WCF service. You can do all the clean up there. Send to bytes to your Silverlight app and use a SaveFileDialog to send them to the user.
Silverlight has a restriction on access to the client file system. Manipulating an Excel file on the client's system, or even assuming the client has Excel installed seems like a violation of this.
by wrapping references in a using statement the COM references are released
Please note: using statements are just syntactic sugar for try/catch/finally with Dispose() in the finally clause.
Also, most applications don't allow for using statements in this case, because creation and cleanup of COM objects are distributed among various places/methods.
The critical line which is needed here reads:
((IDisposable)_excel).Dispose(); // Release COM Interface
This assumes:
dynamic _excel = AutomationFactory.CreateObject("Excel.Application");