Как мне правильно очистить объекты взаимодействия Excel?

StackOverflow https://stackoverflow.com/questions/158706

  •  03-07-2019
  •  | 
  •  

Вопрос

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

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

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

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

Это было полезно?

Решение

Excel не завершает работу, потому что ваше приложение все еще содержит ссылки на COM-объекты.

Я предполагаю, что вы вызываете по крайней мере один элемент COM-объекта, не присваивая его переменной.

Для меня это был самый excelApp.Рабочие листы объект, который я непосредственно использовал, не присваивая его переменной:

Worksheet sheet = excelApp.Worksheets.Open(...);
...
Marshal.ReleaseComObject(sheet);

Я не знал, что внутренне C # создал оболочку для Рабочие листы COM-объект, который не был выпущен моим кодом (потому что я не знал об этом) и был причиной того, что Excel не был выгружен.

Я нашел решение своей проблемы на эта страница, который также имеет хорошее правило для использования COM - объектов в C#:

Никогда не используйте две точки с COM-объектами.


Итак, с учетом этих знаний правильным способом выполнения вышеизложенного является:

Worksheets sheets = excelApp.Worksheets; // <-- The important part
Worksheet sheet = sheets.Open(...);
...
Marshal.ReleaseComObject(sheets);
Marshal.ReleaseComObject(sheet);

ОБНОВЛЕНИЕ ПОСЛЕ ВСКРЫТИЯ:

Я хочу, чтобы каждый читатель очень внимательно прочитал этот ответ Ханса Пассанта, поскольку он объясняет ловушку, в которую попали я и многие другие разработчики.Когда я писал этот ответ много лет назад, я не знал о влиянии отладчика на сборщик мусора и сделал неправильные выводы.Я оставляю свой ответ неизменным ради истории, но, пожалуйста, прочтите эту ссылку и не надо идите путем "двух точек".: Понимание сборки мусора в .NET и Очистите объекты взаимодействия Excel с помощью IDisposable

Другие советы

На самом деле вы можете полностью освободить свой объект приложения Excel, но вам нужно быть осторожным.

Совет поддерживать именованную ссылку абсолютно для каждого COM-объекта, к которому вы обращаетесь, а затем явно освобождать его через Marshal.FinalReleaseComObject() это правильно в теории, но, к сожалению, очень трудно поддается управлению на практике.Если кто-то когда-нибудь где-нибудь проскользнет и использует "две точки", или повторяет ячейки через for each цикл или любая другая подобная команда, тогда у вас будут COM-объекты без ссылок и вы рискуете зависнуть.В этом случае не было бы никакого способа найти причину в коде;вам пришлось бы просмотреть весь свой код на глаз и, надеюсь, найти причину - задача, которая может оказаться практически невыполнимой для большого проекта.

Хорошей новостью является то, что на самом деле вам не нужно поддерживать ссылку на именованную переменную для каждого используемого вами COM-объекта.Вместо этого позвоните GC.Collect() а потом GC.WaitForPendingFinalizers() освободить все (обычно второстепенные) объекты, на которые у вас нет ссылки, а затем явно освободить объекты, на которые у вас есть ссылка на именованную переменную.

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

Например, предположим, что у вас была объектная переменная Range с именем xlRng, переменная рабочего листа с именем xlSheet, переменная рабочей книги с именем xlBook и переменная приложения Excel с именем xlApp, тогда ваш код очистки мог бы выглядеть примерно следующим образом:

// Cleanup
GC.Collect();
GC.WaitForPendingFinalizers();

Marshal.FinalReleaseComObject(xlRng);
Marshal.FinalReleaseComObject(xlSheet);

xlBook.Close(Type.Missing, Type.Missing, Type.Missing);
Marshal.FinalReleaseComObject(xlBook);

xlApp.Quit();
Marshal.FinalReleaseComObject(xlApp);

В большинстве примеров кода, которые вы увидите для очистки COM-объектов из .NET, GC.Collect() и GC.WaitForPendingFinalizers() вызовы выполняются ДВАЖДЫ, как в:

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

Однако это не должно требоваться, если только вы не используете Visual Studio Tools for Office (VSTO), который использует финализаторы, которые вызывают продвижение всего графика объектов в очереди завершения.Такие объекты не будут выпущены до тех пор, пока Далее вывоз мусора.Однако, если вы не используете VSTO, вы должны иметь возможность вызывать GC.Collect() и GC.WaitForPendingFinalizers() только один раз.

Я знаю, что явный призыв GC.Collect() это ни в коем случае (и, конечно, делать это дважды звучит очень болезненно), но, честно говоря, обойти это невозможно.При обычных операциях вы будете генерировать скрытые объекты, на которые у вас нет ссылок, и поэтому вы не сможете освободить их никакими другими способами, кроме вызова GC.Collect().

Это сложная тема, но на самом деле это все, что в ней есть.Как только вы установите этот шаблон для своей процедуры очистки, вы сможете писать обычный код, без необходимости в оболочках и т.д.:-)

У меня есть учебник по этому вопросу здесь:

Автоматизация офисных программ с помощью VB.Net / COM Interop

Он написан для VB.NET, но не пугайтесь этого, принципы точно такие же, как и при использовании C #.

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

Существуют различные способы и рекомендации по выгрузке экземпляра Excel, такие как:

  • Выпуская каждый ком явно объекта с маршалом.FinalReleaseComObject() (не забывая о неявно создал com-объектов).Чтобы освободить каждый созданный com-объект, вы можете использовать упомянутое здесь правило двух точек:
    Как мне правильно очистить объекты взаимодействия Excel?

  • Вызываем GC.Collect() и GC.WaitForPendingFinalizers(), чтобы заставить CLR освободить неиспользуемые com-объекты * (На самом деле, это работает, подробнее см. Мое второе решение)

  • Проверка, не отображается ли com-server-application возможно, окно сообщения, ожидающее ответа пользователя (хотя я не уверен, что это может предотвратить закрытие Excel , но я слышал об этом несколько раз)

  • Отправка сообщения WM_CLOSE в главное окно Excel

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

  • Уничтожаем все экземпляры Excel, которые были созданы после запуска нашего кода взаимодействия с Excel.

НО! Иногда все эти варианты просто не помогают или не могут быть подходящими!

Например, вчера я обнаружил, что в одной из моих функций (которая работает с Excel) Excel продолжает работать после завершения функции.Я перепробовал все!Я тщательно проверил всю функцию 10 раз и добавил Marshal.FinalReleaseComObject() для всего!У меня также были GC.Collect() и GC.WaitForPendingFinalizers().Я проверил, нет ли скрытых полей сообщений.Я попытался отправить сообщение WM_CLOSE в главное окно Excel.Я выполнил свою функцию в отдельном домене приложения и выгрузил этот домен.Ничего не помогало!Вариант с закрытием всех экземпляров Excel неуместен, потому что если пользователь запустит другой экземпляр Excel вручную, во время выполнения моей функции, которая также работает с Excel, то этот экземпляр также будет закрыт моей функцией.Бьюсь об заклад, пользователь не будет доволен!Так что, честно говоря, это неубедительный вариант (без обид, ребята).Так я потратил пару часов, прежде чем нашел хороший (по моему скромному мнению) решение: Завершите процесс Excel с помощью hWnd его главного окна (это первое решение).

Вот простой код:

[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

/// <summary> Tries to find and kill process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <returns>True if process was found and killed. False if process was not found by hWnd or if it could not be killed.</returns>
public static bool TryKillProcessByMainWindowHwnd(int hWnd)
{
    uint processID;
    GetWindowThreadProcessId((IntPtr)hWnd, out processID);
    if(processID == 0) return false;
    try
    {
        Process.GetProcessById((int)processID).Kill();
    }
    catch (ArgumentException)
    {
        return false;
    }
    catch (Win32Exception)
    {
        return false;
    }
    catch (NotSupportedException)
    {
        return false;
    }
    catch (InvalidOperationException)
    {
        return false;
    }
    return true;
}

/// <summary> Finds and kills process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <exception cref="ArgumentException">
/// Thrown when process is not found by the hWnd parameter (the process is not running). 
/// The identifier of the process might be expired.
/// </exception>
/// <exception cref="Win32Exception">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="NotSupportedException">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="InvalidOperationException">See Process.Kill() exceptions documentation.</exception>
public static void KillProcessByMainWindowHwnd(int hWnd)
{
    uint processID;
    GetWindowThreadProcessId((IntPtr)hWnd, out processID);
    if (processID == 0)
        throw new ArgumentException("Process has not been found by the given main window handle.", "hWnd");
    Process.GetProcessById((int)processID).Kill();
}

Как вы можете видеть, я предоставил два метода, в соответствии с шаблоном Try-Parse (я думаю, здесь это уместно).:один метод не выдает исключение, если процесс не удалось завершить (например, процесс больше не существует), а другой метод выдает исключение, если процесс не был завершен.Единственным слабым местом в этом коде являются разрешения безопасности.Теоретически, у пользователя может не быть разрешений на завершение процесса, но в 99,99% всех случаев у пользователя есть такие разрешения.Я также протестировал его с гостевой учетной записью - он работает отлично.

Итак, ваш код, работающий с Excel, может выглядеть следующим образом:

int hWnd = xl.Application.Hwnd;
// ...
// here we try to close Excel as usual, with xl.Quit(),
// Marshal.FinalReleaseComObject(xl) and so on
// ...
TryKillProcessByMainWindowHwnd(hWnd);

Вуаля!Работа Excel завершена!:)

Хорошо, давайте вернемся ко второму решению, как я и обещал в начале поста.Второе решение - вызвать GC.Collect() и GC.WaitForPendingFinalizers(). Да, они действительно работают, но здесь нужно быть осторожным!
Многие люди говорят (и я сказал), что вызов GC.Collect() не помогает.Но причина, по которой это не помогло бы, заключается в том, что все еще есть ссылки на COM-объекты!Одна из самых популярных причин, по которой GC.Бесполезность функции Collect() заключается в запуске проекта в режиме отладки.В режиме отладки объекты, на которые на самом деле больше нет ссылок, не будут собираться мусором до завершения работы метода.
Итак, если вы пробовали GC.Collect() и GC.WaitForPendingFinalizers() и это не помогло, попробуйте выполнить следующее:

1) Попробуйте запустить свой проект в режиме выпуска и проверьте, правильно ли закрылся Excel

2) Перенесите метод работы с Excel в отдельный метод.Итак, вместо чего-то подобного этому:

void GenerateWorkbook(...)
{
  ApplicationClass xl;
  Workbook xlWB;
  try
  {
    xl = ...
    xlWB = xl.Workbooks.Add(...);
    ...
  }
  finally
  {
    ...
    Marshal.ReleaseComObject(xlWB)
    ...
    GC.Collect();
    GC.WaitForPendingFinalizers();
  }
}

ты пишешь:

void GenerateWorkbook(...)
{
  try
  {
    GenerateWorkbookInternal(...);
  }
  finally
  {
    GC.Collect();
    GC.WaitForPendingFinalizers();
  }
}

private void GenerateWorkbookInternal(...)
{
  ApplicationClass xl;
  Workbook xlWB;
  try
  {
    xl = ...
    xlWB = xl.Workbooks.Add(...);
    ...
  }
  finally
  {
    ...
    Marshal.ReleaseComObject(xlWB)
    ...
  }
}

Теперь Excel закроется =)

Обновить:Добавлен код на C # и ссылка на задания Windows

Я потратил некоторое время, пытаясь разобраться в этой проблеме, и в то время XtremeVBTalk был наиболее активным и отзывчивым.Вот ссылка на мой оригинальный пост, Точное завершение процесса взаимодействия с Excel, даже если ваше приложение выходит из строя.Ниже приведено краткое содержание поста и код, скопированный в этот пост.

  • Завершение процесса взаимодействия с помощью Application.Quit() и Process.Kill() работает по большей части, но терпит неудачу, если приложения катастрофически выходят из строя.То есть.если приложение выйдет из строя, процесс Excel по-прежнему будет работать без сбоев.
  • Решение состоит в том, чтобы позволить операционной системе обрабатывать очистку ваших процессов с помощью Объекты заданий Windows использование вызовов Win32.Когда ваше основное приложение умирает, связанные процессы (т.е.Excel) также будет прекращено.

Я обнаружил, что это чистое решение, потому что операционная система выполняет реальную работу по очистке.Все, что вам нужно сделать, это Зарегистрироваться процесс Excel.

Код задания Windows

Обертывает вызовы Win32 API для регистрации процессов взаимодействия.

public enum JobObjectInfoType
{
    AssociateCompletionPortInformation = 7,
    BasicLimitInformation = 2,
    BasicUIRestrictions = 4,
    EndOfJobTimeInformation = 6,
    ExtendedLimitInformation = 9,
    SecurityLimitInformation = 5,
    GroupInformation = 11
}

[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
    public int nLength;
    public IntPtr lpSecurityDescriptor;
    public int bInheritHandle;
}

[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
    public Int64 PerProcessUserTimeLimit;
    public Int64 PerJobUserTimeLimit;
    public Int16 LimitFlags;
    public UInt32 MinimumWorkingSetSize;
    public UInt32 MaximumWorkingSetSize;
    public Int16 ActiveProcessLimit;
    public Int64 Affinity;
    public Int16 PriorityClass;
    public Int16 SchedulingClass;
}

[StructLayout(LayoutKind.Sequential)]
struct IO_COUNTERS
{
    public UInt64 ReadOperationCount;
    public UInt64 WriteOperationCount;
    public UInt64 OtherOperationCount;
    public UInt64 ReadTransferCount;
    public UInt64 WriteTransferCount;
    public UInt64 OtherTransferCount;
}

[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
    public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
    public IO_COUNTERS IoInfo;
    public UInt32 ProcessMemoryLimit;
    public UInt32 JobMemoryLimit;
    public UInt32 PeakProcessMemoryUsed;
    public UInt32 PeakJobMemoryUsed;
}

public class Job : IDisposable
{
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
    static extern IntPtr CreateJobObject(object a, string lpName);

    [DllImport("kernel32.dll")]
    static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);

    private IntPtr m_handle;
    private bool m_disposed = false;

    public Job()
    {
        m_handle = CreateJobObject(null, null);

        JOBOBJECT_BASIC_LIMIT_INFORMATION info = new JOBOBJECT_BASIC_LIMIT_INFORMATION();
        info.LimitFlags = 0x2000;

        JOBOBJECT_EXTENDED_LIMIT_INFORMATION extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
        extendedInfo.BasicLimitInformation = info;

        int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
        IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length);
        Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false);

        if (!SetInformationJobObject(m_handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length))
            throw new Exception(string.Format("Unable to set information.  Error: {0}", Marshal.GetLastWin32Error()));
    }

    #region IDisposable Members

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    #endregion

    private void Dispose(bool disposing)
    {
        if (m_disposed)
            return;

        if (disposing) {}

        Close();
        m_disposed = true;
    }

    public void Close()
    {
        Win32.CloseHandle(m_handle);
        m_handle = IntPtr.Zero;
    }

    public bool AddProcess(IntPtr handle)
    {
        return AssignProcessToJobObject(m_handle, handle);
    }

}

Примечание о коде конструктора

  • В конструкторе info.LimitFlags = 0x2000; называется. 0x2000 является ли JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE перечисляемое значение, и это значение определяется MSDN как:

Завершает все процессы, связанные с заданием, при закрытии последнего дескриптора задания.

Дополнительный вызов Win32 API для получения идентификатора процесса (PID)

    [DllImport("user32.dll", SetLastError = true)]
    public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

Использование кода

    Excel.Application app = new Excel.ApplicationClass();
    Job job = new Job();
    uint pid = 0;
    Win32.GetWindowThreadProcessId(new IntPtr(app.Hwnd), out pid);
    job.AddProcess(Process.GetProcessById((int)pid).Handle);

Это сработало для проекта, над которым я работал:

excelApp.Quit();
Marshal.ReleaseComObject (excelWB);
Marshal.ReleaseComObject (excelApp);
excelApp = null;

Мы узнали, что важно установить каждый ссылка на COM-объект Excel равна нулю, когда вы закончили с ним.Сюда входили Ячейки, Листы и все остальное.

Все, что находится в пространстве имен Excel, должно быть освобождено.Период

Ты не можешь этого делать:

Worksheet ws = excel.WorkBooks[1].WorkSheets[1];

Вы должны делать

Workbooks books = excel.WorkBooks;
Workbook book = books[1];
Sheets sheets = book.WorkSheets;
Worksheet ws = sheets[1];

за этим следует высвобождение объектов.

Сначала - ты никогда придется позвонить Marshal.ReleaseComObject(...) или Marshal.FinalReleaseComObject(...) при выполнении взаимодействия с Excel.Это сбивающий с толку антишаблон, но любая информация об этом, в том числе от Microsoft, указывающая на необходимость вручную освобождать ссылки COM из .NET неверна.Дело в том, что среда выполнения .NET и сборщик мусора корректно отслеживают и очищают ссылки COM.Для вашего кода это означает, что вы можете удалить весь цикл `while (...) вверху.

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

В-третьих, при запуске под управлением отладчика локальные ссылки будут искусственно поддерживаться в рабочем состоянии до окончания работы метода (чтобы проверка локальной переменной работала).Итак GC.Collect() вызовы неэффективны для очистки таких объектов, как rng.Cells с помощью того же метода.Вы должны разделить код, выполняющий COM-взаимодействие из GC cleanup, на отдельные методы.(Это было ключевым открытием для меня из одной части ответа, опубликованного здесь @nightcoder.)

Таким образом, общая схема была бы следующей:

Sub WrapperThatCleansUp()

    ' NOTE: Don't call Excel objects in here... 
    '       Debugger would keep alive until end, preventing GC cleanup

    ' Call a separate function that talks to Excel
    DoTheWork()

    ' Now let the GC clean up (twice, to clean up cycles too)
    GC.Collect()    
    GC.WaitForPendingFinalizers()
    GC.Collect()    
    GC.WaitForPendingFinalizers()

End Sub

Sub DoTheWork()
    Dim app As New Microsoft.Office.Interop.Excel.Application
    Dim book As Microsoft.Office.Interop.Excel.Workbook = app.Workbooks.Add()
    Dim worksheet As Microsoft.Office.Interop.Excel.Worksheet = book.Worksheets("Sheet1")
    app.Visible = True
    For i As Integer = 1 To 10
        worksheet.Cells.Range("A" & i).Value = "Hello"
    Next
    book.Save()
    book.Close()
    app.Quit()

    ' NOTE: No calls the Marshal.ReleaseComObject() are ever needed
End Sub

Существует много ложной информации и путаницы по поводу этой проблемы, включая множество сообщений в MSDN и о переполнении стека (и особенно этот вопрос!).

Что окончательно убедило меня присмотреться повнимательнее и найти правильный совет, так это сообщение в блоге Маршал.ReleaseComObject Признан опасным вместе с обнаружением проблемы со ссылками, сохраненными в отладчике, которая сбивала с толку мое предыдущее тестирование.

Я найденный полезный универсальный шаблон, который может помочь реализовать правильный шаблон удаления для COM-объектов, которым требуется маршал.ReleaseComObject вызывается, когда они выходят за пределы области видимости:

Использование:

using (AutoReleaseComObject<Application> excelApplicationWrapper = new AutoReleaseComObject<Application>(new Application()))
{
    try
    {
        using (AutoReleaseComObject<Workbook> workbookWrapper = new AutoReleaseComObject<Workbook>(excelApplicationWrapper.ComObject.Workbooks.Open(namedRangeBase.FullName, false, false, missing, missing, missing, true, missing, missing, true, missing, missing, missing, missing, missing)))
        {
           // do something with your workbook....
        }
    }
    finally
    {
         excelApplicationWrapper.ComObject.Quit();
    } 
}

Шаблон:

public class AutoReleaseComObject<T> : IDisposable
{
    private T m_comObject;
    private bool m_armed = true;
    private bool m_disposed = false;

    public AutoReleaseComObject(T comObject)
    {
        Debug.Assert(comObject != null);
        m_comObject = comObject;
    }

#if DEBUG
    ~AutoReleaseComObject()
    {
        // We should have been disposed using Dispose().
        Debug.WriteLine("Finalize being called, should have been disposed");

        if (this.ComObject != null)
        {
            Debug.WriteLine(string.Format("ComObject was not null:{0}, name:{1}.", this.ComObject, this.ComObjectName));
        }

        //Debug.Assert(false);
    }
#endif

    public T ComObject
    {
        get
        {
            Debug.Assert(!m_disposed);
            return m_comObject;
        }
    }

    private string ComObjectName
    {
        get
        {
            if(this.ComObject is Microsoft.Office.Interop.Excel.Workbook)
            {
                return ((Microsoft.Office.Interop.Excel.Workbook)this.ComObject).Name;
            }

            return null;
        }
    }

    public void Disarm()
    {
        Debug.Assert(!m_disposed);
        m_armed = false;
    }

    #region IDisposable Members

    public void Dispose()
    {
        Dispose(true);
#if DEBUG
        GC.SuppressFinalize(this);
#endif
    }

    #endregion

    protected virtual void Dispose(bool disposing)
    {
        if (!m_disposed)
        {
            if (m_armed)
            {
                int refcnt = 0;
                do
                {
                    refcnt = System.Runtime.InteropServices.Marshal.ReleaseComObject(m_comObject);
                } while (refcnt > 0);

                m_comObject = default(T);
            }

            m_disposed = true;
        }
    }
}

Ссылка:

http://www.deez.info/sengelha/2005/02/11/useful-idisposable-class-3-autoreleasecomobject/

Я не могу поверить, что эта проблема преследует мир уже 5 лет....Если вы создали приложение, вам необходимо сначала закрыть его, прежде чем удалять ссылку.

objExcel = new Excel.Application();  
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing)); 

при закрытии

objBook.Close(true, Type.Missing, Type.Missing); 
objExcel.Application.Quit();
objExcel.Quit(); 

Когда вы создаете новое приложение Excel, оно открывает программу Excel в фоновом режиме.Вам нужно дать команду этой программе Excel завершить работу, прежде чем отпустить ссылку, поскольку эта программа Excel не является частью вашего прямого управления.Следовательно, он останется открытым, если ссылка будет опубликована!

Всем хорошего программирования ~~

Обычные разработчики, ни одно из ваших решений не сработало для меня, поэтому я решаю внедрить новый трюк.

Сначала давайте уточним "Какова наша цель?" => "Не видеть объект Excel после нашей работы в диспетчере задач".

ОК.Позвольте no оспорить и начать уничтожать его, но подумайте о том, чтобы не уничтожать другие экземпляры os Excel, которые работают параллельно.

Итак, получите список текущих процессоров и извлеките PID процессов EXCEL , затем, как только ваша работа будет выполнена, у нас появится новый гость в списке процессов с уникальным PID , найдите и уничтожьте только его.

< имейте в виду, что любой новый процесс Excel во время выполнения вашего задания Excel будет обнаружен как новый и уничтожен > < Лучшим решением является захват PID нового созданного объекта Excel и просто уничтожение этого>

Process[] prs = Process.GetProcesses();
List<int> excelPID = new List<int>();
foreach (Process p in prs)
   if (p.ProcessName == "EXCEL")
       excelPID.Add(p.Id);

.... // your job 

prs = Process.GetProcesses();
foreach (Process p in prs)
   if (p.ProcessName == "EXCEL" && !excelPID.Contains(p.Id))
       p.Kill();

Это решит мою проблему, надеюсь, и вашу тоже.

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

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

2:звонить GC.Collect()

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

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

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

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

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

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

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

Чтобы добавить к причинам, по которым 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

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

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

Вот некоторые ключевые особенности:

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

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

Принятый ответ здесь правильный, но также примите к сведению, что следует избегать не только ссылок "с двумя точками", но и объектов, которые извлекаются с помощью индекса.Вам также не нужно ждать, пока вы закончите работу с программой, чтобы очистить эти объекты, лучше всего создать функции, которые будут очищать их, как только вы закончите с ними, когда это возможно.Вот созданная мной функция, которая присваивает некоторые свойства объекту стиля, называемому 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] к переменной, чтобы очистить это (не из-за двух точек, которые ссылаются на перечисление, которое не нужно освобождать, а потому, что объект, на который я ссылаюсь, на самом деле является пограничным объектом, который действительно нужно освободить).

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

Это требует большого внимания к деталям и выполнения множества тестов при мониторинге диспетчера задач при написании этого кода, но это избавляет вас от необходимости отчаянно перебирать страницы кода в поисках единственного экземпляра, который вы пропустили.Это особенно важно при работе в циклах, где вам нужно освобождать КАЖДЫЙ ЭКЗЕМПЛЯР объекта, даже если при каждом цикле он использует одно и то же имя переменной.

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

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

Я в точности следовал этому...Но я все равно сталкивался с проблемами 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();

°°¤ø „ Стреляй в 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);
}

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

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

    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 ; ru-США;Q320369

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

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

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

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

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

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

Excel PIA предоставляет COM-события класса приложения Excel как обычные .СЕТЕВЫЕ события.Всякий раз, когда клиентский код подписывается на событие .NET (ударение на "а"), ПИА создает ан экземпляр класса, реализующего интерфейс обратного вызова, и регистрирует его в Excel.

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

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

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

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

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

Решение

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

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

Как указывали другие, вам необходимо создать явную ссылку для каждого используемого вами объекта 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.Завершите работу перед выпуском объекта Application, если вы хотите, чтобы Excel закрылся.

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

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

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

Если все вышеперечисленное не сработало, попробуйте дать Excel некоторое время на закрытие своих листов:

app.workbooks.Close();
Thread.Sleep(500); // adjust, for me it works at around 300+
app.Quit();

...
FinalReleaseComObject(app);

Убедитесь, что вы освободили все объекты, связанные с Excel!

Я потратил несколько часов, перепробовав несколько способов.Все это замечательные идеи, но я наконец-то нашел свою ошибку: Если вы не освободите все объекты, ни один из вышеперечисленных способов вам не поможет как в моем случае.Убедитесь, что вы освободили все объекты, включая первый диапазон!

Excel.Range rng = (Excel.Range)worksheet.Cells[1, 1];
worksheet.Paste(rng, false);
releaseObject(rng);

Эти варианты представлены вместе здесь.

Правило двух точек у меня не сработало.В моем случае я создал метод для очистки своих ресурсов следующим образом:

private static void Clean()
{
    workBook.Close();
    Marshall.ReleaseComObject(workBook);
    excel.Quit();
    CG.Collect();
    CG.WaitForPendingFinalizers();
}

Вы должны быть очень осторожны при использовании приложений взаимодействия Word / Excel.После перепробования всех решений у нас все еще оставалось много открытых процессов "WinWord" на сервере (с более чем 2000 пользователями).

Проработав над проблемой несколько часов, я понял, что если я открою больше пары документов, используя Word.ApplicationClass.Document.Open() одновременно в разных потоках, рабочий процесс IIS (w3wp.exe) потерпела бы крах, оставляя все для winword процессов открытия!

Поэтому я предполагаю, что нет абсолютного решения этой проблемы, кроме перехода к другим методам, таким как Office Открытый XML развитие.

Отличная статья о выпуске COM-объектов - это 2.5 Освобождение COM-объектов (MSDN).

Метод, который я бы рекомендовал, - обнулить ваш Excel.Ссылки на взаимодействие, если они являются нелокальными переменными, а затем вызвать GC.Collect() и GC.WaitForPendingFinalizers() дважды.О переменных взаимодействия с локальной областью действия позаботятся автоматически.

Это устраняет необходимость сохранять именованную ссылку для каждый COM-объект.

Вот пример, взятый из статьи:

public class Test {

    // These instance variables must be nulled or Excel will not quit
    private Excel.Application xl;
    private Excel.Workbook book;

    public void DoSomething()
    {
        xl = new Excel.Application();
        xl.Visible = true;
        book = xl.Workbooks.Add(Type.Missing);

        // These variables are locally scoped, so we need not worry about them.
        // Notice I don't care about using two dots.
        Excel.Range rng = book.Worksheets[1].UsedRange;
    }

    public void CleanUp()
    {
        book = null;
        xl.Quit();
        xl = null;

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

Эти слова взяты прямо из статьи:

Почти во всех ситуациях обнуление ссылки RCW и принудительная сборка мусора будут выполнены должным образом.Если вы также вызовете GC.WaitForPendingFinalizers , сборка мусора будет настолько детерминированной, насколько вы сможете это сделать.То есть вы будете почти уверены, когда именно объект будет очищен — при возврате из второго вызова WaitForPendingFinalizers.В качестве альтернативы вы можете использовать Marshal.ReleaseComObject.Однако обратите внимание, что вам вряд ли когда-либо понадобится использовать этот метод.

Мое решение

[DllImport("user32.dll")]
static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);

private void GenerateExcel()
{
    var excel = new Microsoft.Office.Interop.Excel.Application();
    int id;
    // Find the Excel Process Id (ath the end, you kill him
    GetWindowThreadProcessId(excel.Hwnd, out id);
    Process excelProcess = Process.GetProcessById(id);

try
{
    // Your code
}
finally
{
    excel.Quit();

    // Kill him !
    excelProcess.Kill();
}

Принятый ответ меня не устроил.Следующий код в деструкторе выполнил эту работу.

if (xlApp != null)
{
    xlApp.Workbooks.Close();
    xlApp.Quit();
}

System.Diagnostics.Process[] processArray = System.Diagnostics.Process.GetProcessesByName("EXCEL");
foreach (System.Diagnostics.Process process in processArray)
{
    if (process.MainWindowTitle.Length == 0) { process.Kill(); }
}

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

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

Я запускаю приведенный ниже метод непосредственно перед завершением работы надстройки, которую я разрабатываю, поскольку она запускает событие выгрузки.Он каждый раз удаляет все зависшие экземпляры Excel.Честно говоря, я не совсем уверен, почему это работает, но у меня это работает хорошо, и его можно поместить в конец любого приложения Excel, не беспокоясь о двойных точках, маршал.ReleaseComObject, ни уничтожающие процессы.Мне было бы очень интересно услышать любые предложения относительно того, почему это эффективно.

public static void SweepExcelProcesses()
{           
            if (Process.GetProcessesByName("EXCEL").Length != 0)
            {
                Process[] processes = Process.GetProcesses();
                foreach (Process process in processes)
                {
                    if (process.ProcessName.ToString() == "excel")
                    {                           
                        string title = process.MainWindowTitle;
                    }
                }
            }
}

Я думаю, что отчасти это просто способ, которым фреймворк обрабатывает приложения Office, но я могу ошибаться.В некоторые дни некоторые приложения очищают процессы немедленно, а в другие дни кажется, что они ждут, пока приложение не закроется.В общем, я перестаю обращать внимание на детали и просто слежу за тем, чтобы в конце дня не возникало никаких дополнительных процессов.

Кроме того, и, возможно, я излишне упрощаю ситуацию, но я думаю, вы можете просто...

objExcel = new Excel.Application();
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing));
DoSomeStuff(objBook);
SaveTheBook(objBook);
objBook.Close(false, Type.Missing, Type.Missing);
objExcel.Quit();

Как я уже говорил ранее, я не склонен обращать внимание на детали появления или исчезновения процесса Excel, но у меня это обычно работает.Мне также не нравится поддерживать процессы Excel в рабочем состоянии в течение чего-либо, кроме минимального промежутка времени, но, вероятно, я просто параноик по этому поводу.

Как некоторые, вероятно, уже писали, важно не только то, как вы Закрыть excel (объект);также важно, как вы Открыть это, а также по типу проекта.

В приложении WPF в основном один и тот же код работает без проблем или с очень небольшим количеством проблем.

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

Я поместил все функции, связанные с Excel, в базовый класс, а parser - в подкласс (разные парсеры используют общие функции Excel).Я не хотел, чтобы Excel открывался и закрывался снова для каждого элемента в общем списке, поэтому я открыл его только один раз в базовом классе и закрыл в подклассе.У меня возникли проблемы при переносе кода в настольное приложение.Я перепробовал многие из вышеупомянутых решений. GC.Collect() был уже реализован ранее, в два раза больше, чем предлагалось.

Затем я решил, что перенесу код для открытия Excel в подкласс.Вместо того, чтобы открывать только один раз, теперь я создаю новый объект (базовый класс) и открываю Excel для каждого элемента и закрываю его в конце.Существует некоторое снижение производительности, но, основываясь на нескольких тестах, процессы Excel закрываются без проблем (в режиме отладки), поэтому временные файлы также удаляются.Я продолжу тестирование и напишу еще кое-что, если получу какие-то обновления.

Суть в том, что:Вы также должны проверить код инициализации, особенно если у вас много классов и т.д.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top