简体   繁体   中英

Create reusable Excel Styles for C# interop objects

I'm generating a couple of spreadsheets using C#, and I'm trying to create a reusable Style that can be applied to specific ranges in each spreadsheet. The issue I'm having is that the Excel process is not exiting correctly when my method finishes executing. In fact, I've been able to narrow the problem area down to the point in the code where I'm creating the Excel Style .

Workbooks books = excelApplication.Workbooks;
_Workbook wBook = books.Add("");
_Worksheet wSheet = (_Worksheet)wBook.ActiveSheet;

Styles styles = wBook.Styles;
Style columnHeader = styles.Add("ColumnHeader");
columnHeader.Font.Size = 12; // if I comment this out, excel quits correctly

Marshal.ReleaseComObject(wSheet);
Marshal.ReleaseComObject(wBook);
Marshal.ReleaseComObject(books);

The style works as expected when I apply it, but the Excel process doesn't quit when I quit my excelApplication. If I comment out the line columnHeader.Font.Size = 12; , the Excel process correctly exits. Am I missing something?

Update
I modified Govert's sample WinForms application to reflect the structure of my class, which is creating multiple spreadsheets. His application properly exited the Excel process, but my modified version does not:

using System;
using Excel = Microsoft.Office.Interop.Excel;
using System.Windows.Forms;

namespace ExcelCOMReferenceTesting
{
    public partial class Form1 : Form
    {
        private Excel.Application excelApplication;
        private Excel.Style columnHeader;

        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            DoMyExcelStuff();
            GarbageCleanup();
        }

        private void DoMyExcelStuff()
        {
            StartExcel();
            var wBook = GenerateWorksheet();

            excelApplication.Range["A1"].Value = "Name";
            excelApplication.Range["A1"].Style = columnHeader;
            wBook.SaveAs(@"c:\Test\tst" + DateTime.Now.ToString("mmss") + ".xlsx");

            // No need for Marshal.ReleaseComObject(...)
            // No need for ... = null
            StopExcel();
        }

        private void StartExcel()
        {
            excelApplication = new Excel.Application();
            excelApplication.Visible = false;
        }

        private void StopExcel()
        {
            excelApplication.UserControl = false;
            excelApplication.Quit();
        }

        private Excel._Workbook GenerateWorksheet()
        {
            Excel.Workbooks books = excelApplication.Workbooks;
            Excel.Workbook wBook = books.Add("");

            Excel.Styles styles = wBook.Styles;
            columnHeader = styles.Add("ColumnHeader");
            columnHeader.Font.Size = 12;
            columnHeader.Font.Bold = true;

            return wBook;
        }

        private void GarbageCleanup()
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }
    }
}

Probably a hanging reference to the Font / columnHeader or styles, accessing properties of properties in Excel interop can cause the Excel process to hang; try this

Workbooks books = excelApplication.Workbooks;
_Workbook wBook = books.Add("");
_Worksheet wSheet = (_Worksheet)wBook.ActiveSheet;

Styles styles = wBook.Styles;
Style columnHeader = styles.Add("ColumnHeader");
Font font = columnHeader.Font;
font.Size = 12;

Marshal.ReleaseComObject(font);
Marshal.ReleaseComObject(columnHeader);
Marshal.ReleaseComObject(styles);
Marshal.ReleaseComObject(wSheet);
Marshal.ReleaseComObject(wBook);
Marshal.ReleaseComObject(books);

font = null;
columnHeader = null;
styles = null;
wSheet = null;
wBook = null;
books = null;

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

You never need to call Marshal.ReleaseComObject in this context. The runtime is perfectly able to keep track of COM objects and release them when they are no longer referenced. Calling Marshal.ReleaseComObject is a confusing anti-pattern that sadly even some Microsoft documentation mistakenly suggests. You also don't have to set local variables to null - the references will be out of scope when the method completes.

To get Excel to close, you need to call excelApplication.Quit() , then make sure you have no live references to Excel COM objects, then call the garbage collector to clean up. You should call the garbage collector twice - you might have cases where the references form a cycle, and the first GC call will break the cycle, but the COM objects might only get properly released on the second call.

You also have to be careful with this kind of code in debug builds. References in a method are artificially kept alive until the end of the method so that they will still be accessible in the debugger. This means your local Excel COM objects might not be cleaned up by calling the GC inside that method. To avoid this issue, you might follow a pattern like this:

public void DoMyExcelStuffAndCleanup()
{
    DoMyExcelStuff();
    // Call GC twice to ensure that cleanup after cycles happens immediately
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

public void DoMyExcelStuff()
{
    Application excelApplication = ...
    // Here you access all those Excel objects ...
    // No need for Marshal.ReleaseComObject(...)
    // No need for ... = null
    excelApplication.Quit();
}

I've now tested with a new WinForms application, to which I add a single button and this code behind:

using System;
using System.Windows.Forms;
using Excel = Microsoft.Office.Interop.Excel;

namespace WinFormsComCleanup
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            DoMyExcelStuff();
            GarbageCleanup();
        }

        private void DoMyExcelStuff()
        {
            Excel.Application excelApplication = new Excel.Application();
            Excel.Workbooks books = excelApplication.Workbooks;
            Excel.Workbook wBook = books.Add("");
            Excel.Worksheet wSheet = (Excel.Worksheet)wBook.ActiveSheet;

            Excel.Styles styles = wBook.Styles;
            Excel.Style columnHeader = styles.Add("ColumnHeader");
            columnHeader.Font.Size = 12;
            columnHeader.Font.Bold = true;
            excelApplication.Range["A1"].Value = "Name";
            excelApplication.Range["A1"].Style = columnHeader;
            wBook.SaveAs(@"c:\Temp\tst" + DateTime.Now.ToString("mmss") +".xlsx");

            // No need for Marshal.ReleaseComObject(...)
            // No need for ... = null
            excelApplication.Quit();
        }

        private void GarbageCleanup()
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }
    }
}

Everything works as expected, with the Excel process stopping during the GarbageCleanup() call.


Update

After the update to the question, I suggest the following modification:

    private void StopExcel()
    {

        excelApplication.UserControl = false;
        excelApplication.Quit();

        // Set the form-level variables to null, so that no live references to Excel remain
        columnHeader = null;
        excelApplication = null;
    }

This will ensure that the object-level fields that are introduced in the updated question have Excel their references removed before the GC runs.

In my testing, with this modification Excel quits again. This is consistent with my understanding that the .NET runtime properly keeps track of COM references, and that Marshal.ReleaseComObject is never required for Excel interop.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM