Как правильно очистить объекты Interop Excel?

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

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

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

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

Ответ 1

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

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

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

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

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

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

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


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

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

ПОСЛЕ MORTEM ОБНОВЛЕНИЕ:

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

Ответ 2

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

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

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

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

Например, если предположить, что у вас была переменная объекта Range с именем xlRng, переменная Worksheet с именем 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() сделаны TWICE, как в:

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

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

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

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

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

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

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

Ответ 3

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

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

  • Выпуск КАЖДОГО com-объекта явно с Marshal.FinalReleaseComObject() (не забывая о неявно созданных com-объектах). Чтобы освободить каждый созданный com-объект, вы можете использовать правило 2 точек, упомянутое здесь:
    Как правильно очистить объекты взаимодействия 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 закроется =)

Ответ 4

UPDATE: добавлен код С# и ссылка на Windows Jobs

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

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

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

Рабочий код Windows

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

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 enum, и это значение определяется MSDN как:

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

Дополнительный вызов API Win32 для получения идентификатора процесса (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);

Ответ 5

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

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

Мы узнали, что было важно установить каждую ссылку на объект COM Excel с нулевым значением, когда вы закончили с ним. Это включало ячейки, таблицы и все.

Ответ 6

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

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

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

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

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

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

Ответ 7

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

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

В-третьих, при запуске под отладчиком локальные ссылки будут искусственно поддерживаться до конца метода (так что работает локальная проверка переменных). Таким образом, вызовы GC.Collect() не эффективны для очистки объекта типа rng.Cells от того же метода. Вы должны разделить код, делающий COM-взаимодействие с очисткой GC на отдельные методы. (Это было ключевым открытием для меня, из одной части ответа, размещенного здесь by @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 и Qaru (и особенно этот вопрос!).

Что, в конце концов, убедило меня в более пристальном рассмотрении и выяснении правильного совета, было сообщение в блоге Marshal.ReleaseComObject Считается опасным вместе с поиском проблемы с ссылками, поддерживаемыми под отладчиком, которые путали мое раннее тестирование.

Ответ 8

I нашел полезный общий шаблон, который может помочь реализовать правильный шаблон удаления для COM-объектов, которым нужен Marshal.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/

Ответ 9

Я не могу поверить, что эта проблема преследовала мир в течение 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 не является частью вашего прямого управления. Поэтому он останется открытым, если ссылка будет выпущена!

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

Ответ 10

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

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

Ok. Пусть нет, чтобы бросить вызов и начать уничтожать его, но не стоит уничтожать другой экземпляр 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();

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

Ответ 11

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

1: убедитесь, что нет оставшихся ссылок на приложение excel, которое вы создали (у вас должен быть только один, установите его на null)

2: вызов GC.Collect()

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

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

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

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

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

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

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

Ответ 12

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

Ответ 13

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

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

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

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

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

Ответ 14

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

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

Это требует большого внимания деталям и множеству тестовых исполнений при мониторинге диспетчера задач при написании этого кода, но при этом вы избавляетесь от трудностей, связанных с отчаянным поиском страниц кода, чтобы найти один экземпляр, который вы пропустили. Это особенно важно при работе в цикле, где вам нужно выпустить EACH INSTANCE объекта, хотя при каждом цикле он использует одно и то же имя переменной.

Ответ 15

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

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

окончательное решение, которое работает для меня, - это перемещение одного набора

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

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

private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

Ответ 16

¨ ° º¤ø "¸ Стрелять 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);
}

Ответ 17

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

Ответ 18

Вы должны знать, что 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

Ответ 19

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

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

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

Некоторая теория событий COM

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

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

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

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

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

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

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

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

Решение

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

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

Ответ 20

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

Ответ 21

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

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

...
FinalReleaseComObject(app);

Ответ 22

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

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

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

Параметры вместе здесь.

Ответ 23

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

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

Это устраняет необходимость сохранения именованной ссылки для каждого 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. Однако обратите внимание, что вам вряд ли когда-либо понадобится использовать этот метод.

Ответ 24

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

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

Итак, я думаю, что нет абсолютного решения этой проблемы, но переключитесь на другие методы, такие как Office Open XML.

Ответ 25

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

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

Ответ 26

Я думаю, что некоторые из них - это только то, как платформа обрабатывает приложения 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 на что угодно, кроме минимального времени, но я, вероятно, просто параноик.

Ответ 27

Как некоторые из них, вероятно, уже написаны, не просто важно, как вы закрываете Excel (объект); это также важно, как вы его открываете, а также по типу проекта.

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

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

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

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

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

Ответ 28

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

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

Объявите его, добавьте код в блок finally:

finally
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
    if (excelApp != null)
    {
        excelApp.Quit();
        int hWnd = excelApp.Application.Hwnd;
        uint processID;
        GetWindowThreadProcessId((IntPtr)hWnd, out processID);
        Process[] procs = Process.GetProcessesByName("EXCEL");
        foreach (Process p in procs)
        {
            if (p.Id == processID)
                p.Kill();
        }
        Marshal.FinalReleaseComObject(excelApp);
    }
}

Ответ 29

Мое решение

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

Ответ 30

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

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