Posted on Tuesday, November 5th, 2013 at 7:18 am by Pieter van der Westhuizen.

 
 

You’ll see a lot of complaints on the internet about Excel and other Microsoft Office applications not quitting properly after using the object model to perform certain actions, or showing sporadic and unpredictable behavior in COM add-ins. In the end most of these issues boil down to developers not properly disposing of COM objects.

The challenge is that despite the fact that, as .Net developers we can use the Office Interop Assemblies to access the various Office object models using managed code, the PIA’s are still essentially wrapping around COM objects. So what is the problem?

 

A general rule of thumb

Our very own Andrei Smolin wrote two great articles explaining the reasons behind why Excel does not quitand when to release COM objects in .Net. Both articles contain a lot of very useful and thorough advice and you’ll notice that it all starts with a simple rule:

’1 dot good, 2 dots bad’

I’m sure you’re staring at your screen with wide-eyed confusion at the moment, but this rule is actually very easy to explain. Consider the following code:

Excel.Application app = new Excel.Application();
Excel.Workbook book = app.Workbooks.Add();
Excel.Worksheet sheet = app.Sheets.Add();
 
sheet.Range["A1"].Value = "Lorem Ipsum";
book.SaveAs(@"C:\Temp\ExcelBook.xlsx");
book.Close();
app.Quit();

The code above will build and run without a problem, it will create a new Excel workbook, add a new sheet and set the value of the first cell in the newly created sheet. However, even after calling the Quit method of the Excel.Application object, you’ll still see the Excel.exe process in the Windows Task Managers’ list of background processes.

This strange phenomenon occurs because in the above code, we’re not releasing any COM objects and we’re also “chaining” object references by using double dots. You must ALWAYS release COM objects, even if you see no adverse effects, it might work perfectly on your PC but behave entirely different on a user’s computer.

Let’s look at how to change the code in order to safely dispose of any COM objects:

Excel.Application app = null;
Excel.Workbooks books = null;
Excel.Workbook book = null;
Excel.Sheets sheets = null;
Excel.Worksheet sheet = null;
Excel.Range range = null;
 
try
{
app = new Excel.Application();
books = app.Workbooks;
book = books.Add();
sheets = book.Sheets;
sheet = sheets.Add();
range = sheet.Range["A1"];
range.Value = "Lorem Ipsum";
book.SaveAs(@"C:\Temp\ExcelBook" + DateTime.Now.Millisecond + ".xlsx");
book.Close();
app.Quit();
}
finally
{
if (range != null) Marshal.ReleaseComObject(range);
if (sheet != null) Marshal.ReleaseComObject(sheet);
if (sheets != null) Marshal.ReleaseComObject(sheets);
if (book != null) Marshal.ReleaseComObject(book);
if (books != null) Marshal.ReleaseComObject(books);
if (app != null) Marshal.ReleaseComObject(app);
}

Pay close attention to the above code, we never used more than one dot when working with objects. We also wrapped all the code in a try-finally, so even if the code throws and exception we will still safely release the COM objects using the ReleaseComObject method on the Marshal object.

For or ForEach Loops

There is no obvious reason why you should use a for-loop rather than a ForEach loop, however it is recommended that you rather use a for loop since a for-each might cause some unexpected behavior and your code to hang. Consider the following code:

Excel.Application app = null;
Excel.Workbooks books = null;
Excel.Workbook book = null;
Excel.Sheets sheets = null;
 
try
{
app = new Excel.Application();
books = app.Workbooks;
book = books.Open(@"C:\Temp\ExcelBook.xlsx");
sheets = book.Sheets;
 
foreach (Excel.Worksheet sheet in sheets)
{
Console.WriteLine(sheet.Name);
Marshal.ReleaseComObject(sheet);
}
 
book.Close();
app.Quit();
}
finally
{
if (sheets != null) Marshal.ReleaseComObject(sheets);
if (book != null) Marshal.ReleaseComObject(book);
if (books != null) Marshal.ReleaseComObject(books);
if (app != null) Marshal.ReleaseComObject(app);
}

In the above code, everything appears to be fine. We did not use more than one dot and we safely released all COM objects using a try-finally code clock. However, using a foreach loop to loop through the Sheets collection of the Excel.Workbook object automatically generates the enumerator behind the foreach statement that uses an internal COM object, which needs to be released..

To be on the safe side, you should avoid using a foreach loop and rather use a normal for loop, and release each COM object in the collection, as illustrated below:

Excel.Application app = null;
Excel.Workbooks books = null;
Excel.Workbook book = null;
Excel.Sheets sheets = null;
 
try
{
app = new Excel.Application();
books = app.Workbooks;
book = books.Open(@"C:\Temp\ExcelBook1Sheets.xlsx");
sheets = book.Sheets;
 
for (int i = 1; i <= sheets.Count; i++)
{
Excel.Worksheet sheet = sheets.Item[i];
Console.WriteLine(sheet.Name);
if (sheet != null) Marshal.ReleaseComObject(sheet);
}
book.Close();
app.Quit();
}
finally
{
if (sheets != null) Marshal.ReleaseComObject(sheets);
if (book != null) Marshal.ReleaseComObject(book);
if (books != null) Marshal.ReleaseComObject(books);
if (app != null) Marshal.ReleaseComObject(app);
}

ReleaseComObject & FinalReleaseComObject?

When you access an Office COM object via the interop assemblies, the .Net framework automatically wraps it in a Runtime Callable Wrapper, the RCW object is also responsible for controlling the objects’ lifetime.

Keep in mind that the .Net runtime creates one RCW for each COM object. So, no matter how many references you have to a specific COM object, there will always be just one Runtime Callable Wrapper for it. As you create more references to a certain COM object the RCW’s reference count will increase and this is where the ReleaseComObject and FinalReleaseComObject come into play.

Both methods are used to release references to a RCW, ReleaseComObject simply decreases the reference count of a specific RCW, whereas FinalReleaseComObject releases ALL references to the RCW and sets the reference count to zero.

Essentially, calling FinalReleaseComObject would be similar to creating a for-loop and callingReleaseComObject until its reference count is zero. When the reference count is zero, it means the object is ready to be garbage collected.

Both methods need to be used with a relative degree of caution, if you release a COM object and try to access it afterwards an InvalidComObjectException will be shown with the following message:

“COM object that has been separated from its underlying RCW cannot be used”

As a rule we never use FinalReleaseComObject, by calling ReleaseComObject the reference counter should be decreased and if everything is correct the COM object should be properly released with a single call.FinalReleaseComObject is redundant and might cause unexpected results and a whole lot of pain.

GC.Collect & GC.WaitForPendingFinalizers

The generally accepted best practice is not to force a garbage collection in the majority of cases; however, you can release COM objects using the .Net garbage collector, as long as there are no references to the objects. In other words, the objects are set to null. Be aware that GC.Collect can be a time consuming process depending on the number of objects.

You would also need to call GC.Collect and GC.WaitForPendingFinalizers twice when working with Office COM objects since the first time you call the methods we only release objects that we are not referencing with our own variables. The second time the two methods are called is because the RCW for each COM object needs to run a finalizer that actually fully removes the COM Object from memory.

So, it is totally acceptable to see the following code in you COM add-in projects:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();

Ways to kill the Excel.exe process

WM_CLOSE

Of course, there are ways to kill the Excel process if you have to. One such way is to send a WM_CLOSE message to the Excel windows in order for it to terminate. First, you’ll need to use the DLLImportattribute to invoke the SendMessage method contained in the user32 dll. Do this by adding the following code at the top of your class:

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);

The trickiest part of this is to get the window id or hWnd of the main Excel window. Luckily, if you’ve accessed the Excel object model you can retrieve the main windows’ hWnd by checking the Hwnd property on the ActiveWindow object. The ActiveWindow object is a property on the Excel.Application object. If the Excel version you’re targeting does not have the Hwnd property on the Application object, you can use late-binding to access it, as illustrated below.

Excel.Application app = new Excel.Application();
hWnd = app.Hwnd;

After you’ve retrieved the hWnd value, call the SendMessage method as indicated below, to force the main Excel window to close:

SendMessage((IntPtr)hWnd, 0x10, IntPtr.Zero, IntPtr.Zero);

Process.Kill

An easier way kill all Excel processes, is to use the Kill method of the .Net Process object. The object can be found in the System.Diagnostics namespace. The following code will retrieve all the Excel processes and kill each one:

Process[] excelProcs = Process.GetProcessesByName("EXCEL");
foreach (Process proc in excelProcs)
{
proc.Kill();
}

Windows Job Objects

Lastly, you could also use the Windows Job Objects to properly dispose of the Excel process even if it takes a bit more work to get going. First you need to add a new class to your project:

namespace JobManagement
{
public class Job : IDisposable
{
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
static extern IntPtr CreateJobObject(IntPtr a, string lpName);
 
[DllImport("kernel32.dll")]
static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, UInt32 cbJobObjectInfoLength);
 
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
 
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CloseHandle(IntPtr hObject);
 
private IntPtr handle;
private bool disposed;
 
public Job()
{
handle = CreateJobObject(IntPtr.Zero, null);
 
var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION
{
LimitFlags = 0x2000
};
 
var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
BasicLimitInformation = info
};
 
int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length);
Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false);
 
if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length))
throw new Exception(string.Format("Unable to set information. Error: {0}", Marshal.GetLastWin32Error()));
}
 
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
 
private void Dispose(bool disposing)
{
if (disposed)
return;
 
if (disposing) { }
 
Close();
disposed = true;
}
 
public void Close()
{
CloseHandle(handle);
handle = IntPtr.Zero;
}
 
public bool AddProcess(IntPtr processHandle)
{
return AssignProcessToJobObject(handle, processHandle);
}
 
public bool AddProcess(int processId)
{
return AddProcess(Process.GetProcessById(processId).Handle);
}
 
}
 
#region Helper classes
 
[StructLayout(LayoutKind.Sequential)]
struct IO_COUNTERS
{
public UInt64 ReadOperationCount;
public UInt64 WriteOperationCount;
public UInt64 OtherOperationCount;
public UInt64 ReadTransferCount;
public UInt64 WriteTransferCount;
public UInt64 OtherTransferCount;
}
 
[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
public Int64 PerProcessUserTimeLimit;
public Int64 PerJobUserTimeLimit;
public UInt32 LimitFlags;
public UIntPtr MinimumWorkingSetSize;
public UIntPtr MaximumWorkingSetSize;
public UInt32 ActiveProcessLimit;
public UIntPtr Affinity;
public UInt32 PriorityClass;
public UInt32 SchedulingClass;
}
 
[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
public UInt32 nLength;
public IntPtr lpSecurityDescriptor;
public Int32 bInheritHandle;
}
 
[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
public IO_COUNTERS IoInfo;
public UIntPtr ProcessMemoryLimit;
public UIntPtr JobMemoryLimit;
public UIntPtr PeakProcessMemoryUsed;
public UIntPtr PeakJobMemoryUsed;
}
 
public enum JobObjectInfoType
{
AssociateCompletionPortInformation = 7,
BasicLimitInformation = 2,
BasicUIRestrictions = 4,
EndOfJobTimeInformation = 6,
ExtendedLimitInformation = 9,
SecurityLimitInformation = 5,
GroupInformation = 11
}
 
#endregion
 
}

To use the Job object, you’ll use the following code where I’ve passed the same hWnd as used with the WM_CLOSE method:

Job job = new Job();
uint pid = 0;
GetWindowThreadProcessId(new IntPtr(hWnd), out pid);
job.AddProcess(Process.GetProcessById((int)pid).Handle);

I hope you found this article useful, I certainly learned a lot whilst writing it and I hope you learn as much from it by reading it!

Thank you for reading. Until next time, keep coding!

Available downloads:

This sample Excel add-in was developed using Add-in Express for Office and .net:

Sample Excel Add-in (C#)

How to properly release Excel COM objects的更多相关文章

  1. Reference counted objects

    Reference counted objects · netty/netty Wiki https://github.com/netty/netty/wiki/Reference-counted-o ...

  2. 解决C#使用Microsoft.Office.Interop.Excel操作Excel后进程一直存在的问题

    This resolved the issue for me. Your code becomes: public Excel.Application excelApp = new Excel.App ...

  3. Test Scenarios for Excel Export functionality

    1 File should get exported in proper file extension2 File name for the exported excel file should be ...

  4. 正确释放WORD对象(COM组件) COMException: 被调用的对象已与其客户端断开连接

    本来form method=post本页面 修改为其他页面 action=save.aspx后没问题 其他问题可参考以下: 引自:http://topic.csdn.net/u/20090108/17 ...

  5. Android内存管理(4)*官方教程 含「高效内存的16条策略」 Managing Your App's Memory

    Managing Your App's Memory In this document How Android Manages Memory Sharing Memory Allocating and ...

  6. Ownership qualifiers of Objective-C: In Details

    虽然这里讲的大部分知识以前都看过,但是时不时出现某些点让我如茅塞顿开: 以前经常会忘记一些细节,这篇文章可以更好的理解细节,巩固知识体系. Ownership qualifiers In Object ...

  7. Unity 5 Game Optimization (Chris Dickinson 著)

    1. Detecting Performance Issues 2. Scripting Strategies 3. The Benefits of Batching 4. Kickstart You ...

  8. 2.5 – Garbage Collection 自动垃圾回收 Stop-the-world vs. incremental vs. concurrent 垃圾回收策略

    2.5 – Garbage Collection  自动垃圾回收 Lua 5.3 Reference Manual http://www.lua.org/manual/5.3/manual.html# ...

  9. 学习笔记 | java反序列化漏洞分析

    java反序列化漏洞是与java相关的漏洞中最常见的一种,也是网络安全工作者关注的重点.在cve中搜索关键字serialized共有174条记录,其中83条与java有关:搜索deserialized ...

随机推荐

  1. [Unity3D]Unity3D游戏开发之跑酷游戏项目解说

    大家好,我是秦元培.我參加了CSDN2014博客之星的评选,欢迎大家为我投票,同一时候希望在新的一年里大家能继续支持我的博客. 大家晚上好.我是秦元培,欢迎大家关注我的博客,我的博客地址是blog.c ...

  2. Android Eclipse Libs 的 jar 源码查看 (或者新版本ADT无法查看jar的源码)

    问题背景:在使用比较新的ADT的时候,无法导入Jar中的源码包查看源码.只好自己打开压缩包,实在恼火.在半年前,只好这样. 问题解决方案:我就以 " android-support-v4.j ...

  3. SonarQube学习入门指南

    1. 什么是SonarQube? SonarQube 官网:https://www.sonarqube.org/ SonarQube®是一种自动代码审查工具,用于检测代码中的错误,漏洞和代码异味.它可 ...

  4. studying Bitcoin

    https://github.com/bitcoinbook/bitcoinbook/blob/develop/book.asciidoc https://github.com/bitcoin/bip ...

  5. Unable to load configuration. - [unknown location]

    严重: Exception starting filter StrutsPrepareFilterUnable to load configuration. - [unknown location] ...

  6. (原创)C++11改进我们的程序之简化我们的程序(六)

    这次要讲的内容是:c++11中的lamda表达式. lamda表达式是我最喜欢的一个c++11特性之一,在我的代码中随处可见它的身影,其实在c#3.5中就引入了lamda,java中至今还没引入,要等 ...

  7. hive里的group by和distinct

    hive里的group by和distinct 前言 今天才明确知道group by实际上还是有去重读作用的,其实细想一下,按照xx分类,肯定相同的就算是一类了,也就相当于去重来,详细的看一下. gr ...

  8. mysql关联更新update

    https://blog.csdn.net/babyfish13/article/details/78082844 ****************************************** ...

  9. poj1679(判断最小生成树是否唯一)

    题意:给出n个点,m条边,要你判断最小生成树是否唯一. 思路:先做一次最小生成树操作,标记选择了的边,然后枚举已经被标记了的边,判断剩下的边组成的最小生成树是否与前面的相等,相等,则不唯一,否则唯一. ...

  10. QT-提示“database not open”

    问题现象: 要用QT开发"SQLite"时出现如下提示: QSqlQuery::exec: database not open QSqlDatabase: QSQLITE driv ...