Создание повторно используемых стилей Excel для объектов взаимодействия C#

Я создаю пару электронных таблиц с использованием C# и пытаюсь создать повторно используемый стиль, который можно применять к определенным диапазонам в каждой электронной таблице. У меня проблема в том, что процесс Excel не завершается правильно, когда мой метод завершает выполнение. На самом деле, я смог сузить проблемную область до точки в коде, где я создаю 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);

Стиль работает, как и ожидалось, когда я применяю его, но процесс Excel не завершается при выходе из приложения Excel. Если я закомментирую строку columnHeader.Font.Size = 12;Процесс Excel правильно завершается. Я что-то пропустил?

Обновить
Я изменил пример приложения WinForms от Govert, чтобы отразить структуру моего класса, которая создает несколько электронных таблиц. Его приложение правильно вышло из процесса Excel, но моя измененная версия не:

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();
        }
    }
}

2 ответа

Решение

Вам никогда не нужно звонить Marshal.ReleaseComObject в данном контексте. Среда выполнения прекрасно умеет отслеживать COM-объекты и освобождать их, когда на них больше нет ссылок. призвание Marshal.ReleaseComObject это запутанный анти-паттерн, который, к сожалению, даже некоторые документы Microsoft ошибочно предполагают. Вам также не нужно устанавливать локальные переменные в null - ссылки будут выходить за рамки при завершении метода.

Чтобы закрыть Excel, вам нужно позвонить excelApplication.Quit(), затем убедитесь, что у вас нет живых ссылок на объекты COM COM Excel, а затем вызовите сборщик мусора для очистки. Вы должны вызывать сборщик мусора дважды - у вас могут быть случаи, когда ссылки образуют цикл, и первый вызов GC прервет цикл, но объекты COM могут быть должным образом освобождены только при втором вызове.

Вы также должны быть осторожны с этим видом кода в отладочных сборках. Ссылки в методе искусственно поддерживаются до конца метода, так что они все еще будут доступны в отладчике. Это означает, что ваши локальные COM-объекты Excel не могут быть очищены путем вызова GC внутри этого метода. Чтобы избежать этой проблемы, вы можете использовать следующую схему:

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();
}

Сейчас я протестировал новое приложение WinForms, к которому я добавил одну кнопку и этот код:

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();
        }
    }
}

Все работает как положено, процесс Excel останавливается во время GarbageCleanup() вызов.


Обновить

После обновления вопроса я предлагаю следующую модификацию:

    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;
    }

Это обеспечит, чтобы поля уровня объекта, представленные в обновленном вопросе, удалили ссылки в Excel до запуска GC.

В моем тестировании с этой модификацией Excel снова выходит. Это согласуется с моим пониманием того, что среда выполнения.NET правильно отслеживает ссылки COM и что Marshal.ReleaseComObject никогда не требуется для взаимодействия Excel.

Вероятно, зависшая ссылка на Font / columnHeader или стили, доступ к свойствам свойств во взаимодействии Excel может привести к зависанию процесса Excel; попробуй это

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();
Другие вопросы по тегам