Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 103 additions & 25 deletions docs/csharp/advanced-topics/interop/snippets/OfficeInterop/program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -48,39 +49,116 @@ 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)
{
Marshal.FinalReleaseComObject(workSheet);
}
if (workbook != null)
{
workbook.Close(true); // Save changes
Marshal.FinalReleaseComObject(workbook);
}
if (excelApp != null)
{
// Note: Not calling excelApp.Quit() here since this method is
// intended to display data to the user. The Excel instance
// will remain open for the user to interact with.
Marshal.FinalReleaseComObject(excelApp);
}
}
}
//</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
Marshal.FinalReleaseComObject(document);
}
if (wordApp != null)
{
wordApp.Quit(true); // Save changes to all documents
Marshal.FinalReleaseComObject(wordApp);
}
}
}
//</Snippet9>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
//<Usings>
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Excel = Microsoft.Office.Interop.Excel;
using Word = Microsoft.Office.Interop.Word;
//</Usings>
Expand Down Expand Up @@ -43,13 +45,45 @@ private void ThisAddIn_Startup(object sender, System.EventArgs e)
//</CallDisplay>

//<PasteIntoWord>
var wordApp = new Word.Application();
wordApp.Visible = true;
wordApp.Documents.Add();
wordApp.Selection.PasteSpecial(Link: true, DisplayAsIcon: true);
CreateWordDocumentWithCleanup();
//</PasteIntoWord>
}

[MethodImpl(MethodImplOptions.NoInlining)]
private void CreateWordDocumentWithCleanup()
{
Word.Application wordApp = null;
Word.Document document = null;

try
{
wordApp = new Word.Application();
wordApp.Visible = true;
document = wordApp.Documents.Add();
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
if (document != null)
{
document.Close(true);
Marshal.FinalReleaseComObject(document);
}
if (wordApp != null)
{
wordApp.Quit(true);
Marshal.FinalReleaseComObject(wordApp);
}
}
}

//<Display>
void DisplayInExcel(IEnumerable<Account> accounts,
Action<Account, Excel.Range> DisplayFunc)
Expand All @@ -72,6 +106,108 @@ void DisplayInExcel(IEnumerable<Account> accounts,
}
//</Display>

//<ProperCleanup>
[MethodImpl(MethodImplOptions.NoInlining)]
static void CreateComObjectsAndCleanup()
{
Excel.Application excelApp = null;
Excel.Workbook workbook = null;
Excel.Worksheet worksheet = null;

try
{
excelApp = new Excel.Application();
workbook = excelApp.Workbooks.Add();
worksheet = workbook.ActiveSheet;

// Use COM objects here...
}
finally
{
// Clean up COM objects in reverse order of creation
if (worksheet != null)
{
Marshal.FinalReleaseComObject(worksheet);
}
if (workbook != null)
{
workbook.Close(true);
Marshal.FinalReleaseComObject(workbook);
}
if (excelApp != null)
{
excelApp.Quit();
Marshal.FinalReleaseComObject(excelApp);
}
}
}
//</ProperCleanup>

//<DisplayWithCleanup>
void DisplayInExcelWithCleanup(IEnumerable<Account> accounts,
Action<Account, Excel.Range> DisplayFunc)
{
DisplayInExcelCore(accounts, DisplayFunc);
}

[MethodImpl(MethodImplOptions.NoInlining)]
void DisplayInExcelCore(IEnumerable<Account> accounts,
Action<Account, Excel.Range> DisplayFunc)
{
Excel.Application excelApp = null;
Excel.Workbook workbook = null;
Excel.Worksheet worksheet = null;

try
{
excelApp = new Excel.Application();
excelApp.Visible = true;

// Add a new Excel workbook.
workbook = excelApp.Workbooks.Add();
worksheet = workbook.ActiveSheet;

worksheet.Cells[1, 1].Value = "ID";
worksheet.Cells[1, 2].Value = "Balance";

int row = 2;
foreach (var ac in accounts)
{
var cell = worksheet.Cells[row, 1];
DisplayFunc(ac, cell);
row++;
}

// Save the workbook
string fileName = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
"BankAccounts.xlsx");
workbook.SaveAs(fileName);

// Copy the results to the Clipboard.
worksheet.Range["A1:B3"].Copy();
}
finally
{
// Always clean up COM objects in reverse order of creation
if (worksheet != null)
{
Marshal.FinalReleaseComObject(worksheet);
}
if (workbook != null)
{
workbook.Close(true); // Save changes
Marshal.FinalReleaseComObject(workbook);
}
if (excelApp != null)
{
excelApp.Quit();
Marshal.FinalReleaseComObject(excelApp);
}
}
}
//</DisplayWithCleanup>


private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,48 @@ 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

The essential cleanup pattern is to use try/finally blocks and call `Marshal.FinalReleaseComObject()` on each COM object in reverse order of creation:

:::code language="csharp" source="./snippets/OfficeInterop/program.cs" id="Snippet4":::

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.
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.


> [!NOTE]
> The `DisplayInExcel` method shown above does not call `excelApp.Quit()` because it's intended to display data to the user. The Excel instance remains open for user interaction. For automation scenarios where you want to close Excel automatically, add `excelApp.Quit()` before `Marshal.FinalReleaseComObject(excelApp)`.

### 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. While the RCW does release the underlying COM object when it's garbage collected, garbage collection in .NET is non-deterministic and can be significantly delayed.

**Do I need to call GC.Collect() and GC.WaitForPendingFinalizers()?**
These calls are not usually necessary. The essential cleanup is calling `Marshal.FinalReleaseComObject()` on each COM object and proper shutdown methods like `Quit()` when appropriate. Focus on the basic cleanup pattern shown above.

**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.
Expand Down