Как правильно очистить объекты взаимодействия Excel? - PullRequest
714 голосов
/ 01 октября 2008

Я использую взаимодействие Excel в C # (ApplicationClass) и поместил следующий код в мое предложение finally:

while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();

Хотя этот вид работает, процесс Excel.exe остается в фоновом режиме даже после закрытия Excel. Он открывается только после того, как мое приложение закрыто вручную.

Что я делаю не так, или есть ли альтернатива для обеспечения правильного удаления объектов взаимодействия?

Ответы [ 39 ]

10 голосов
/ 01 сентября 2011

Похоже, это было слишком сложно. По моему опыту, для правильного закрытия Excel есть только три ключевых момента:

1: убедитесь, что нет ссылок на созданное вами приложение Excel (у вас должно быть только одно; установите его на null)

2: звонок GC.Collect()

3: Excel должен быть закрыт либо пользователем, вручную закрывающим программу, либо вызовом Quit объекта Excel. (Обратите внимание, что Quit будет работать так же, как если бы пользователь пытался закрыть программу, и отобразит диалоговое окно подтверждения, если есть несохраненные изменения, даже если Excel не отображается. Пользователь может нажать «Отмена», а затем Excel не будет иметь был закрыт.)

1 должно произойти раньше, чем 2, но 3 может произойти в любое время.

Один из способов реализовать это состоит в том, чтобы обернуть объект Excel взаимодействия с вашим собственным классом, создать экземпляр взаимодействия в конструкторе и реализовать IDisposable с Dispose, похожим на

if (!mDisposed) {
   mExcel = null;
   GC.Collect();
   mDisposed = true;
}

Это очистит Excel от проблем вашей программы. Как только Excel закроется (вручную пользователем или по телефону Quit), процесс исчезнет. Если программа уже была закрыта, процесс исчезнет при вызове GC.Collect().

(Я не уверен, насколько это важно, но вам может потребоваться вызов GC.WaitForPendingFinalizers() после вызова GC.Collect(), но не обязательно избавляться от процесса Excel.)

Это работало для меня без проблем в течение многих лет. Имейте в виду, что хотя это работает, вам действительно нужно изящно закрыться, чтобы это работало. Вы по-прежнему будете накапливать процессы Excel.exe, если прервете свою программу до очистки Excel (обычно нажимая «stop» во время отладки вашей программы).

9 голосов
/ 19 ноября 2013

После попытки

  1. Отпустить COM-объекты в обратном порядке
  2. Добавьте GC.Collect() и GC.WaitForPendingFinalizers() дважды в конце
  3. Не более двух точек
  4. Закрыть рабочую книгу и выйти из приложения
  5. Запуск в режиме разблокировки

последнее решение, которое работает для меня, это переместить один набор

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

, который мы добавили в конец функции к оболочке, следующим образом:

private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}
9 голосов
/ 06 декабря 2010

Чтобы добавить причины, по которым Excel не закрывается, даже когда вы создаете прямые ссылки на каждый объект при чтении, создается цикл For.

For Each objWorkBook As WorkBook in objWorkBooks 'local ref, created from ExcelApp.WorkBooks to avoid the double-dot
   objWorkBook.Close 'or whatever
   FinalReleaseComObject(objWorkBook)
   objWorkBook = Nothing
Next 

'The above does not work, and this is the workaround:

For intCounter As Integer = 1 To mobjExcel_WorkBooks.Count
   Dim objTempWorkBook As Workbook = mobjExcel_WorkBooks.Item(intCounter)
   objTempWorkBook.Saved = True
   objTempWorkBook.Close(False, Type.Missing, Type.Missing)
   FinalReleaseComObject(objTempWorkBook)
   objTempWorkBook = Nothing
Next
9 голосов
/ 17 декабря 2009

Принятый ответ здесь правильный, но также учтите, что нужно избегать не только «двухточечных» ссылок, но и объектов, которые извлекаются через индекс. Вам также не нужно ждать, пока вы закончите с программой для очистки этих объектов, лучше всего создавать функции, которые будут очищать их, как только вы закончите с ними, когда это возможно. Вот функция, которую я создал, которая назначает некоторые свойства объекта Style с именем xlStyleHeader:

public Excel.Style xlStyleHeader = null;

private void CreateHeaderStyle()
{
    Excel.Styles xlStyles = null;
    Excel.Font xlFont = null;
    Excel.Interior xlInterior = null;
    Excel.Borders xlBorders = null;
    Excel.Border xlBorderBottom = null;

    try
    {
        xlStyles = xlWorkbook.Styles;
        xlStyleHeader = xlStyles.Add("Header", Type.Missing);

        // Text Format
        xlStyleHeader.NumberFormat = "@";

        // Bold
        xlFont = xlStyleHeader.Font;
        xlFont.Bold = true;

        // Light Gray Cell Color
        xlInterior = xlStyleHeader.Interior;
        xlInterior.Color = 12632256;

        // Medium Bottom border
        xlBorders = xlStyleHeader.Borders;
        xlBorderBottom = xlBorders[Excel.XlBordersIndex.xlEdgeBottom];
        xlBorderBottom.Weight = Excel.XlBorderWeight.xlMedium;
    }
    catch (Exception ex)
    {
        throw ex;
    }
    finally
    {
        Release(xlBorderBottom);
        Release(xlBorders);
        Release(xlInterior);
        Release(xlFont);
        Release(xlStyles);
    }
}

private void Release(object obj)
{
    // Errors are ignored per Microsoft's suggestion for this type of function:
    // http://support.microsoft.com/default.aspx/kb/317109
    try
    {
        System.Runtime.InteropServices.Marshal.ReleaseComObject(obj);
    }
    catch { } 
}

Обратите внимание, что мне пришлось установить xlBorders[Excel.XlBordersIndex.xlEdgeBottom] для переменной, чтобы очистить ее (не из-за двух точек, которые относятся к перечислению, которое не нужно освобождать, а потому, что объект, на который я ссылаюсь) на самом деле это объект Border, который должен быть освобожден).

Подобные вещи на самом деле не нужны в стандартных приложениях, которые отлично справляются с уборкой, но в приложениях ASP.NET, если вы пропустите хотя бы одно из них, независимо от того, как часто вы вызываете сборщик мусора Excel по-прежнему будет работать на вашем сервере.

Требуется много внимания к деталям и множество тестовых выполнений при мониторинге диспетчера задач при написании этого кода, но это избавляет вас от необходимости отчаянного поиска по страницам кода, чтобы найти тот экземпляр, который вы пропустили. Это особенно важно при работе в циклах, где вам нужно освобождать КАЖДУЮ ИНСТАНЦИЮ объекта, даже если он использует одно и то же имя переменной каждый раз при выполнении цикла.

9 голосов
/ 28 марта 2013

Я традиционно следовал совету, найденному в ответе ВВС . Тем не менее, пытаясь поддерживать этот ответ в курсе последних опций, я думаю, что все мои будущие проекты будут использовать библиотеку «NetOffice».

NetOffice является полной заменой Office PIA и полностью не зависит от версии. Это коллекция управляемых COM-оболочек, которые могут выполнять очистку, которая часто вызывает такие головные боли при работе с Microsoft Office в .NET.

Некоторые ключевые функции:

  • В основном не зависит от версии (а функции, зависящие от версии, задокументированы)
  • Нет зависимостей
  • Нет PIA
  • Без регистрации
  • Нет, ВСТО

Я никоим образом не связан с проектом; Я просто искренне ценю резкое уменьшение головной боли.

7 голосов
/ 14 июня 2013

„° º¤ø„ ¸ Снимайте Excel proc и жуйте жевательную резинку ¨ „¤¤º ° ¨

public class MyExcelInteropClass
{
    Excel.Application xlApp;
    Excel.Workbook xlBook;

    public void dothingswithExcel() 
    {
        try { /* Do stuff manipulating cells sheets and workbooks ... */ }
        catch {}
        finally {KillExcelProcess(xlApp);}
    }

    static void KillExcelProcess(Excel.Application xlApp)
    {
        if (xlApp != null)
        {
            int excelProcessId = 0;
            GetWindowThreadProcessId(xlApp.Hwnd, out excelProcessId);
            Process p = Process.GetProcessById(excelProcessId);
            p.Kill();
            xlApp = null;
        }
    }

    [DllImport("user32.dll")]
    static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);
}
7 голосов
/ 25 февраля 2014

Я точно следовал этому ... Но я все еще сталкивался с проблемами 1 из 1000 раз. Кто знает почему. Время вытащить молот ...

Сразу после создания экземпляра класса приложения Excel я получаю только что созданный процесс Excel.

excel = new Microsoft.Office.Interop.Excel.Application();
var process = Process.GetProcessesByName("EXCEL").OrderByDescending(p => p.StartTime).First();

Затем, когда я выполнил все вышеперечисленные действия по очистке COM, я убедился, что процесс не запущен. Если он все еще работает, убейте его!

if (!process.HasExited)
   process.Kill();
6 голосов
/ 25 июня 2012

«Никогда не используйте две точки с COM-объектами» - это хорошее правило, позволяющее избежать утечки COM-ссылок, но Excel PIA может привести к утечке больше, чем кажется на первый взгляд.

Одним из таких способов является подписка на любое событие, представляемое любым из COM-объектов объектной модели Excel.

Например, подписка на событие WorkbookOpen класса Application.

Немного теории о событиях COM

Классы COM предоставляют группу событий через интерфейсы обратного вызова. Чтобы подписаться на события, клиентский код может просто зарегистрировать объект, реализующий интерфейс обратного вызова, и класс COM будет вызывать его методы в ответ на определенные события. Поскольку интерфейс обратного вызова является интерфейсом COM, обязанность реализующего объекта - уменьшить счетчик ссылок любого COM-объекта, который он получает (в качестве параметра) для любого из обработчиков событий.

Как Excel PIA представляет события COM

Excel PIA представляет события COM класса приложения Excel как обычные события .NET. Всякий раз, когда код клиента подписывается на событие .NET (акцент на 'a'), PIA создает экземпляр класса, реализующего интерфейс обратного вызова, и регистрирует его в Excel.

Следовательно, несколько объектов обратного вызова регистрируются в Excel в ответ на различные запросы на подписку из кода .NET. Один объект обратного вызова для каждой подписки на событие.

Интерфейс обратного вызова для обработки событий означает, что PIA должна подписываться на все события интерфейса для каждого запроса подписки на событие .NET. Он не может выбирать. При получении обратного вызова события объект обратного вызова проверяет, заинтересован ли связанный обработчик события .NET в текущем событии или нет, а затем либо вызывает обработчик, либо молча игнорирует обратный вызов.

Влияние на количество ссылок на экземпляры COM

Все эти объекты обратного вызова не уменьшают счетчик ссылок ни одного из COM-объектов, которые они получают (в качестве параметров) для любого из методов обратного вызова (даже для тех, которые игнорируются без предупреждения). Они используют исключительно сборщик мусора CLR для освобождения COM-объектов.

Поскольку запуск GC является недетерминированным, это может привести к задержке процесса Excel на более длительный срок, чем требуется, и создать впечатление «утечки памяти».

Решение

На данный момент единственным решением является избежать поставщика событий PIA для класса COM и написать свой собственный поставщик событий, который детерминистически высвобождает объекты COM.

Для класса Application это можно сделать, реализовав интерфейс AppEvents, а затем зарегистрировав реализацию в Excel, используя Интерфейс IConnectionPointContainer . Класс Application (и в этом отношении все COM-объекты, отображающие события с использованием механизма обратного вызова) реализует интерфейс IConnectionPointContainer.

6 голосов
/ 06 ноября 2008

Вы должны знать, что Excel очень чувствителен к культуре, в которой вы работаете.

Вы можете обнаружить, что вам нужно установить язык в EN-US перед вызовом функций Excel. Это относится не ко всем функциям, но к некоторым из них.

    CultureInfo en_US = new System.Globalization.CultureInfo("en-US"); 
    System.Threading.Thread.CurrentThread.CurrentCulture = en_US;
    string filePathLocal = _applicationObject.ActiveWorkbook.Path;
    System.Threading.Thread.CurrentThread.CurrentCulture = orgCulture;

Это применимо, даже если вы используете VSTO.

Подробнее: http://support.microsoft.com/default.aspx?scid=kb;en-us;Q320369

5 голосов
/ 01 октября 2008

Как уже отмечали другие, вам необходимо создать явную ссылку для каждого используемого вами объекта Excel и вызвать Marshal.ReleaseComObject для этой ссылки, как описано в этой статье базы знаний . Вам также нужно использовать try / finally, чтобы гарантировать, что ReleaseComObject всегда вызывается, даже когда выдается исключение. То есть вместо:

Worksheet sheet = excelApp.Worksheets(1)
... do something with sheet

вам нужно сделать что-то вроде:

Worksheets sheets = null;
Worksheet sheet = null
try
{ 
    sheets = excelApp.Worksheets;
    sheet = sheets(1);
    ...
}
finally
{
    if (sheets != null) Marshal.ReleaseComObject(sheets);
    if (sheet != null) Marshal.ReleaseComObject(sheet);
}

Вам также необходимо вызвать Application.Quit перед освобождением объекта Application, если вы хотите закрыть Excel.

Как вы видите, это быстро становится чрезвычайно громоздким, как только вы пытаетесь сделать что-то даже умеренно сложное. Я успешно разработал .NET-приложения с помощью простого класса-оболочки, который включает несколько простых манипуляций с объектной моделью Excel (открытие рабочей книги, запись в Range, сохранение / закрытие рабочей книги и т. Д.). Класс-оболочка реализует IDisposable, тщательно реализует Marshal.ReleaseComObject для каждого объекта, который он использует, и не публикует какие-либо объекты Excel для остальной части приложения.

Но этот подход не подходит для более сложных требований.

Это большой недостаток .NET COM Interop. Для более сложных сценариев я бы серьезно подумал о написании ActiveX DLL на VB6 или другом неуправляемом языке, которому вы можете делегировать все взаимодействия с внешними COM-объектами, такими как Office. Затем вы можете ссылаться на эту ActiveX DLL из своего приложения .NET, и все будет намного проще, поскольку вам нужно будет только выпустить эту единственную ссылку.

...