-
Notifications
You must be signed in to change notification settings - Fork 6k
Add comprehensive COM object cleanup documentation and examples for Excel Interop #47088
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
ce0f127
7befd6a
69cdbfc
94b85e8
5d03bf1
bcfba59
59e3252
5411805
ce5d34f
c097dc9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
|
||
using System; | ||
using System.Collections.Generic; | ||
using System.Runtime.InteropServices; | ||
//<Snippet1> | ||
using Excel = Microsoft.Office.Interop.Excel; | ||
using Word = Microsoft.Office.Interop.Word; | ||
|
@@ -48,39 +49,128 @@ static void Main(string[] args) | |
//<Snippet4> | ||
static void DisplayInExcel(IEnumerable<Account> accounts) | ||
{ | ||
var excelApp = new Excel.Application(); | ||
// Make the object visible. | ||
excelApp.Visible = true; | ||
Excel.Application excelApp = null; | ||
Excel.Workbook workbook = null; | ||
Excel.Worksheet workSheet = null; | ||
|
||
try | ||
{ | ||
excelApp = new Excel.Application(); | ||
// Make the object visible. | ||
excelApp.Visible = true; | ||
|
||
// Create a new, empty workbook and add it to the collection returned | ||
// by property Workbooks. The new workbook becomes the active workbook. | ||
// Add has an optional parameter for specifying a particular template. | ||
// Because no argument is sent in this example, Add creates a new workbook. | ||
excelApp.Workbooks.Add(); | ||
// Create a new, empty workbook and add it to the collection returned | ||
// by property Workbooks. The new workbook becomes the active workbook. | ||
// Add has an optional parameter for specifying a particular template. | ||
// Because no argument is sent in this example, Add creates a new workbook. | ||
workbook = excelApp.Workbooks.Add(); | ||
|
||
// This example uses a single workSheet. The explicit type casting is | ||
// removed in a later procedure. | ||
Excel._Worksheet workSheet = (Excel.Worksheet)excelApp.ActiveSheet; | ||
// This example uses a single workSheet. The explicit type casting is | ||
// removed in a later procedure. | ||
workSheet = (Excel.Worksheet)excelApp.ActiveSheet; | ||
|
||
// Establish column headings in cells A1 and B1. | ||
workSheet.Cells[1, "A"] = "ID Number"; | ||
workSheet.Cells[1, "B"] = "Current Balance"; | ||
|
||
var row = 1; | ||
foreach (var acct in accounts) | ||
{ | ||
row++; | ||
workSheet.Cells[row, "A"] = acct.ID; | ||
workSheet.Cells[row, "B"] = acct.Balance; | ||
} | ||
|
||
workSheet.Columns[1].AutoFit(); | ||
workSheet.Columns[2].AutoFit(); | ||
|
||
// Put the spreadsheet contents on the clipboard. | ||
workSheet.Range["A1:B3"].Copy(); | ||
|
||
// Save the workbook before closing | ||
string fileName = System.IO.Path.Combine( | ||
Environment.GetFolderPath(Environment.SpecialFolder.Desktop), | ||
"BankAccounts.xlsx"); | ||
workbook.SaveAs(fileName); | ||
} | ||
finally | ||
{ | ||
// Clean up COM objects in reverse order of creation | ||
if (workSheet != null) | ||
{ | ||
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(workSheet); | ||
workSheet = null; | ||
} | ||
if (workbook != null) | ||
{ | ||
workbook.Close(true); // Save changes | ||
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(workbook); | ||
workbook = null; | ||
} | ||
if (excelApp != null) | ||
{ | ||
excelApp.DisplayAlerts = true; | ||
excelApp.Quit(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This sample is called "DisplayInExcel". The name suggests that the Excel is expected to say open, displaying the content when the sample finishes executing. Should this be dropped in this sample? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point about the semantic mismatch. I've removed the |
||
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(excelApp); | ||
excelApp = null; | ||
} | ||
|
||
// Force garbage collection | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. .NET runtime does not guarantee that objects above gets collected. The JIT is free to extend lifetimes till end of the method. Assigning null to local variables like The only reliable way to get objects collected is by factoring out the code where it is not desirable to extend lifetimes into its own non-inlineable method. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're absolutely right. I've updated the COM cleanup pattern to use separate non-inlineable methods that guarantee object references go out of scope. The new pattern removes the unreliable null assignments and GC.Collect() calls, using |
||
GC.Collect(); | ||
GC.WaitForPendingFinalizers(); | ||
} | ||
} | ||
//</Snippet4> | ||
|
||
//<Snippet9> | ||
static void CreateIconInWordDoc() | ||
{ | ||
var wordApp = new Word.Application(); | ||
wordApp.Visible = true; | ||
|
||
// The Add method has four reference parameters, all of which are | ||
// optional. Visual C# allows you to omit arguments for them if | ||
// the default values are what you want. | ||
wordApp.Documents.Add(); | ||
|
||
// PasteSpecial has seven reference parameters, all of which are | ||
// optional. This example uses named arguments to specify values | ||
// for two of the parameters. Although these are reference | ||
// parameters, you do not need to use the ref keyword, or to create | ||
// variables to send in as arguments. You can send the values directly. | ||
wordApp.Selection.PasteSpecial( Link: true, DisplayAsIcon: true); | ||
Word.Application wordApp = null; | ||
Word.Document document = null; | ||
|
||
try | ||
{ | ||
wordApp = new Word.Application(); | ||
wordApp.Visible = true; | ||
|
||
// The Add method has four reference parameters, all of which are | ||
// optional. Visual C# allows you to omit arguments for them if | ||
// the default values are what you want. | ||
document = wordApp.Documents.Add(); | ||
|
||
// PasteSpecial has seven reference parameters, all of which are | ||
// optional. This example uses named arguments to specify values | ||
// for two of the parameters. Although these are reference | ||
// parameters, you do not need to use the ref keyword, or to create | ||
// variables to send in as arguments. You can send the values directly. | ||
wordApp.Selection.PasteSpecial( Link: true, DisplayAsIcon: true); | ||
|
||
// Save the document | ||
string fileName = System.IO.Path.Combine( | ||
Environment.GetFolderPath(Environment.SpecialFolder.Desktop), | ||
"BankAccountsLink.docx"); | ||
document.SaveAs(fileName); | ||
} | ||
finally | ||
{ | ||
// Clean up COM objects in reverse order of creation | ||
if (document != null) | ||
{ | ||
document.Close(true); // Save changes | ||
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(document); | ||
document = null; | ||
} | ||
if (wordApp != null) | ||
{ | ||
wordApp.Quit(true); // Save changes to all documents | ||
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(wordApp); | ||
wordApp = null; | ||
} | ||
|
||
// Force garbage collection | ||
GC.Collect(); | ||
GC.WaitForPendingFinalizers(); | ||
} | ||
} | ||
//</Snippet9> | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -96,6 +96,52 @@ This code demonstrates several of the features in C#: the ability to omit the `r | |
|
||
Press F5 to run the application. Excel starts and displays a table that contains the information from the two accounts in `bankAccounts`. Then a Word document appears that contains a link to the Excel table. | ||
|
||
## Important: COM object cleanup and resource management | ||
|
||
The examples shown above demonstrate basic Office Interop functionality, but they don't include proper cleanup of COM objects. This is a critical issue in production applications because failing to properly release COM objects can result in orphaned Office processes that remain in memory even after your application closes. | ||
|
||
### Why COM object cleanup is necessary | ||
|
||
COM objects in Office Interop require explicit cleanup because: | ||
|
||
- The .NET garbage collector doesn't automatically release COM objects | ||
- Each Excel or Word object you create holds resources that must be manually released | ||
- Without proper cleanup, Office applications remain running in the background | ||
- This applies to all COM objects: Application, Workbooks, Worksheets, Ranges, and more | ||
|
||
### Proper cleanup pattern | ||
|
||
Use the following pattern to ensure all COM objects are properly released: | ||
|
||
:::code language="csharp" source="./snippets/OfficeWalkthrough/ThisAddIn.cs" id="ProperCleanup"::: | ||
|
||
Add the following enhanced version of the `DisplayInExcel` method that includes proper COM object cleanup: | ||
|
||
:::code language="csharp" source="./snippets/OfficeWalkthrough/ThisAddIn.cs" id="DisplayWithCleanup"::: | ||
|
||
This pattern ensures that: | ||
|
||
- COM objects are released even if an exception occurs | ||
- Excel processes don't remain orphaned in Task Manager | ||
- Memory is properly freed | ||
- The application behaves reliably in production environments | ||
|
||
For production applications, always implement this cleanup pattern for every COM object you create, including Application, Workbooks, Worksheets, Ranges, and other Office objects. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not think that this pattern should be followed by most production applications out there. The delayed release of the COM objects that is a side-effect of garbage collection is not a problem for most production applications. Vast majority of developers love that .NET garbage collection takes care of it for them and they are perfectly fine paying a small price for it. Stepping back, this is introductory sample for Office Interop. it is appropriate to complicate it by manual lifetime management. I think it may be better to leave this sample alone, and move the advanced topic of manual lifetime management to a new advanced sample. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're absolutely right. I've simplified the walkthrough to remove the complex manual cleanup patterns and restored the original simple examples that rely on .NET garbage collection. The introductory sample now maintains the appropriate level of complexity for beginners, and the delayed COM object release is acceptable for most applications. Fixed in commit c097dc9. |
||
|
||
### Common questions about COM object cleanup | ||
|
||
**Why can't garbage collection handle this automatically?** | ||
COM objects use reference counting for memory management, which is different from .NET's garbage collection. The .NET runtime creates a Runtime Callable Wrapper (RCW) around each COM object, but the RCW doesn't automatically release the underlying COM object when it's garbage collected. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is invalid statement. RCW does release the underlying COM object when it's garbage collected. The problem is the delayed nature of garbage collection. not that the RCW is not released. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're correct. Fixed the inaccurate statement to reflect that RCWs do release underlying COM objects when garbage collected - the issue is the non-deterministic timing of garbage collection, not that RCWs don't release objects. Fixed in commit ce5d34f. |
||
|
||
**Why must I set objects to null after calling Marshal.FinalReleaseComObject?** | ||
Setting references to null ensures that your code can't accidentally use a released COM object, which would throw an exception. It also helps the garbage collector by removing any remaining managed references. | ||
|
||
**Why call GC.Collect() and GC.WaitForPendingFinalizers()?** | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The sample do not call GC.Collect and GC.WaitForPendingFinalizers anymore. Should they? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated the documentation to clarify that |
||
These calls force immediate garbage collection, which helps ensure that any remaining RCWs are cleaned up promptly. While not always strictly necessary, they provide additional safety in COM interop scenarios. | ||
|
||
**What happens if I don't follow this pattern?** | ||
Without proper cleanup, Office applications remain running in the background even after your application exits. You can verify this by checking Task Manager - you'll see excel.exe or winword.exe processes that weren't properly terminated. These orphaned processes consume memory and can cause issues with future Office automation. | ||
|
||
## Clean up the completed project | ||
|
||
In Visual Studio, select **Clean Solution** on the **Build** menu. Otherwise, the add-in runs every time that you open Excel on your computer. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These assignments of null have no impact. The JIT compiler is likely to optimize them out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, I've removed all the null assignments since the JIT compiler optimizes them out anyway. The cleanup now focuses only on the effective
Marshal.FinalReleaseComObject()
calls. Fixed in commit 59e3252.