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

c# excel interop com-interop

286486 просмотра

30 ответа

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

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

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

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

Автор: HAdes Источник Размещён: 17.05.2019 02:35

Ответы (30)


659 плюса

Решение

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

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

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

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

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

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

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


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

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

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

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

Автор: VVS Размещён: 01.10.2008 05:30

268 плюса

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

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

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

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

Например, предполагая, что у вас есть переменная объекта Range с именем xlRng, переменная Worksheet с именемxlSheet Workbook 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 #.

Автор: Mike Rosenblum Размещён: 01.10.2008 07:54

203 плюса

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

Существуют различные способы и рекомендации по загрузке экземпляра 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 закроется =)

Автор: nightcoder Размещён: 12.12.2009 02:50

46 плюса

ОБНОВЛЕНИЕ : добавлен код C # и ссылка на рабочие места Windows

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

  • Закрытие процесса взаимодействия с 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);
Автор: joshgo Размещён: 20.08.2009 04:00

35 плюса

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

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

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

Автор: Philip Fourie Размещён: 01.10.2008 05:30

29 плюса

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

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

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

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

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

с последующим выпуском объектов.

Автор: MagicKat Размещён: 01.10.2008 05:45

18 плюса

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

Автор: Edward Wilde Размещён: 08.12.2008 11:10

17 плюса

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

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

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

What finally convinced me to have a closer look and figure out the right advice was blog post Marshal.ReleaseComObject Considered Dangerous together with finding the issue with references kept alive under the debugger that was confusing my earlier testing.

Автор: Govert Размещён: 29.06.2016 10:50

15 плюса

I cant believe this problem has haunted the world for 5 years.... If you have created an application, you need to shut it down first before removing the link.

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

when closing

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

When you new an excel application, it opens a excel program in the background. You need to command that excel program to quit before you release the link because that excel program is not part of your direct control. Therefore, it will stay open if the link is released!

Good programming everyone~~

Автор: Colin Размещён: 21.11.2010 03:41

12 плюса

Common developers, none of your solutions worked for me, so I decide to implement a new trick.

First let specify "What is our goal?" => "Not to see excel object after our job in task manager"

Ok. Let no to challenge and start destroying it, but consider not to destroy other instance os Excel which are running in parallel.

So , get the list of current processors and fetch PID of EXCEL processes , then once your job is done, we have a new guest in processes list with a unique PID ,find and destroy just that one.

< keep in mind any new excel process during your excel job will be detected as new and destroyed > < A better solution is to capture PID of new created excel object and just destroy that>

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

This resolves my issue, hope yours too.

Автор: Mohsen Afshin Размещён: 16.10.2009 11:01

10 плюса

This sure seems like it has been over-complicated. From my experience, there are just three key things to get Excel to close properly:

1: make sure there are no remaining references to the excel application you created (you should only have one anyway; set it to null)

2: call GC.Collect()

3: Excel has to be closed, either by the user manually closing the program, or by you calling Quit on the Excel object. (Note that Quit will function just as if the user tried to close the program, and will present a confirmation dialog if there are unsaved changes, even if Excel is not visible. The user could press cancel, and then Excel will not have been closed.)

1 needs to happen before 2, but 3 can happen anytime.

One way to implement this is to wrap the interop Excel object with your own class, create the interop instance in the constructor, and implement IDisposable with Dispose looking something like

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

That will clean up excel from your program's side of things. Once Excel is closed (manually by the user or by you calling Quit) the process will go away. If the program has already been closed, then the process will disappear on the GC.Collect() call.

(I'm not sure how important it is, but you may want a GC.WaitForPendingFinalizers() call after the GC.Collect() call but it is not strictly necessary to get rid of the Excel process.)

This has worked for me without issue for years. Keep in mind though that while this works, you actually have to close gracefully for it to work. You will still get accumulating excel.exe processes if you interrupt your program before Excel is cleaned up (usually by hitting "stop" while your program is being debugged).

Автор: Dave Cousineau Размещён: 31.08.2011 09:18

9 плюса

The accepted answer here is correct, but also take note that not only "two dot" references need to be avoided, but also objects that are retrieved via the index. You also do not need to wait until you are finished with the program to clean up these objects, it's best to create functions that will clean them up as soon as you're finished with them, when possible. Here is a function I created that assigns some properties of a Style object called 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 { } 
}

Notice that I had to set xlBorders[Excel.XlBordersIndex.xlEdgeBottom] to a variable in order to clean that up (Not because of the two dots, which refer to an enumeration which does not need to be released, but because the object I'm referring to is actually a Border object that does need to be released).

This sort of thing is not really necessary in standard applications, which do a great job of cleaning up after themselves, but in ASP.NET applications, if you miss even one of these, no matter how often you call the garbage collector, Excel will still be running on your server.

It requires a lot of attention to detail and many test executions while monitoring the Task Manager when writing this code, but doing so saves you the hassle of desperately searching through pages of code to find the one instance you missed. This is especially important when working in loops, where you need to release EACH INSTANCE of an object, even though it uses the same variable name each time it loops.

Автор: Chris McGrath Размещён: 17.12.2009 08:15

9 плюса

To add to reasons why Excel does not close, even when you create direct refrences to each object upon read, creation, is the 'For' loop.

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
Автор: Grimfort Размещён: 06.12.2010 01:08

9 плюса

After trying

  1. Release COM objects in reverse order
  2. Add GC.Collect() and GC.WaitForPendingFinalizers() twice at the end
  3. No more than two dots
  4. Close workbook and quit application
  5. Run in release mode

the final solution that works for me is to move one set of

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

that we added to the end of the function to a wrapper, as follows:

private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}
Автор: D.G. Размещён: 18.11.2013 10:58

8 плюса

I've traditionally followed the advice found in VVS's answer. However, in an effort to keep this answer up-to-date with the latest options, I think all my future projects will use the "NetOffice" library.

NetOffice is a complete replacement for the Office PIAs and is completely version-agnostic. It's a collection of Managed COM wrappers that can handle the cleanup that often causes such headaches when working with Microsoft Office in .NET.

Some key features are:

  • Mostly version-independent (and version-dependant features are documented)
  • No dependencies
  • No PIA
  • No registration
  • No VSTO

I am in no way affiliated with the project; I just genuinely appreciate the stark reduction in headaches.

Автор: BTownTKD Размещён: 28.03.2013 02:12

7 плюса

I followed this exactly... But I still ran into issues 1 out of 1000 times. Who knows why. Time to bring out the hammer...

Right after the Excel Application class is instantiated I get a hold of the Excel process that was just created.

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

Then once I've done all the above COM clean-up, I make sure that process isn't running. If it is still running, kill it!

if (!process.HasExited)
   process.Kill();
Автор: craigtadlock Размещён: 25.02.2014 03:35

7 плюса

¨°º¤ø„¸ Shoot Excel proc and chew bubble gum ¸„ø¤º°¨

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);
}
Автор: Antoine Meltzheim Размещён: 14.06.2013 03:16

6 плюса

You need to be aware that Excel is very sensitive to the culture you are running under as well.

You may find that you need to set the culture to EN-US before calling Excel functions. This does not apply to all functions - but some of them.

    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;

This applies even if you are using VSTO.

For details: http://support.microsoft.com/default.aspx?scid=kb;en-us;Q320369

Автор: Christian Mogensen Размещён: 06.11.2008 03:21

6 плюса

"Never use two dots with COM objects" is a great rule of thumb to avoid leakage of COM references, but Excel PIA can lead to leakage in more ways than apparent at first sight.

One of these ways is subscribing to any event exposed by any of the Excel object model's COM objects.

For example, subscribing to the Application class's WorkbookOpen event.

Some theory on COM events

COM classes expose a group of events through call-back interfaces. In order to subscribe to events, the client code can simply register an object implementing the call-back interface and the COM class will invoke its methods in response to specific events. Since the call-back interface is a COM interface, it is the duty of the implementing object to decrement the reference count of any COM object it receives (as a parameter) for any of the event handlers.

How Excel PIA expose COM Events

Excel PIA exposes COM events of Excel Application class as conventional .NET events. Whenever the client code subscribes to a .NET event (emphasis on 'a'), PIA creates an instance of a class implementing the call-back interface and registers it with Excel.

Hence, a number of call-back objects get registered with Excel in response to different subscription requests from the .NET code. One call-back object per event subscription.

A call-back interface for event handling means that, PIA has to subscribe to all interface events for every .NET event subscription request. It cannot pick and choose. On receiving an event call-back, the call-back object checks if the associated .NET event handler is interested in the current event or not and then either invokes the handler or silently ignores the call-back.

Effect on COM instance reference counts

All these call-back objects do not decrement the reference count of any of the COM objects they receive (as parameters) for any of the call-back methods (even for the ones that are silently ignored). They rely solely on the CLR garbage collector to free up the COM objects.

Since GC run is non-deterministic, this can lead to the holding off of Excel process for a longer duration than desired and create an impression of a 'memory leak'.

Solution

The only solution as of now is to avoid the PIA’s event provider for the COM class and write your own event provider which deterministically releases COM objects.

For the Application class, this can be done by implementing the AppEvents interface and then registering the implementation with Excel by using IConnectionPointContainer interface. The Application class (and for that matter all COM objects exposing events using callback mechanism) implements the IConnectionPointContainer interface.

Автор: Amit Mittal Размещён: 25.06.2012 06:52

5 плюса

As others have pointed out, you need to create an explicit reference for every Excel object you use, and call Marshal.ReleaseComObject on that reference, as described in this KB article. You also need to use try/finally to ensure ReleaseComObject is always called, even when an exception is thrown. I.e. instead of:

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

you need to do something like:

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

You also need to call Application.Quit before releasing the Application object if you want Excel to close.

As you can see, this quickly becomes extremely unwieldy as soon as you try to do anything even moderately complex. I have successfully developed .NET applications with a simple wrapper class that wraps a few simple manipulations of the Excel object model (open a workbook, write to a Range, save/close the workbook etc). The wrapper class implements IDisposable, carefully implements Marshal.ReleaseComObject on every object it uses, and does not pubicly expose any Excel objects to the rest of the app.

But this approach doesn't scale well for more complex requirements.

This is a big deficiency of .NET COM Interop. For more complex scenarios, I would seriously consider writing an ActiveX DLL in VB6 or other unmanaged language to which you can delegate all interaction with out-proc COM objects such as Office. You can then reference this ActiveX DLL from your .NET application, and things will be much easier as you will only need to release this one reference.

Автор: Joe Размещён: 01.10.2008 06:26

4 плюса

When all the stuff above didn't work, try giving Excel some time to close its sheets:

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

...
FinalReleaseComObject(app);
Автор: spiderman Размещён: 18.06.2011 12:02

3 плюса

Make sure that you release all objects related to Excel!

I spent a few hours by trying several ways. All are great ideas but I finally found my mistake: If you don't release all objects, none of the ways above can help you like in my case. Make sure you release all objects including range one!

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

The options are together here.

Автор: Ned Размещён: 08.08.2012 06:38

2 плюса

The two dots rule did not work for me. In my case I created a method to clean my resources as follows:

private static void Clean()
{
    workBook.Close();
    Marshall.ReleaseComObject(workBook);
    excel.Quit();
    CG.Collect();
    CG.WaitForPendingFinalizers();
}
Автор: Hahnemann Размещён: 12.12.2013 10:37

2 плюса

You should be very careful using Word/Excel interop applications. After trying all the solutions we still had a lot of "WinWord" process left open on server (with more than 2000 users).

After working on the problem for hours, I realized that if I open more than a couple of documents using Word.ApplicationClass.Document.Open() on different threads simultaneously, IIS worker process (w3wp.exe) would crash leaving all WinWord processes open!

So I guess there is no absolute solution to this problem, but switching to other methods such as Office Open XML development.

Автор: Arvand Размещён: 14.03.2012 03:47

2 плюса

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

Автор: Porkbutts Размещён: 11.01.2013 01:46

1 плюс

Мое решение

[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();
}
Автор: Loart Размещён: 18.06.2014 09:34

1 плюс

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

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(); }
}
Автор: Martin Размещён: 05.09.2014 11:42

1 плюс

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

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

Я запускаю описанный ниже метод непосредственно перед тем, как надстройка, которую я разрабатываю, завершает работу, так как она запускает событие выгрузки. Он удаляет все зависшие экземпляры Excel каждый раз. Честно говоря, я не совсем уверен, почему это работает, но он работает хорошо для меня и может быть помещен в конец любого приложения Excel, не беспокоясь ни о двойных точках, Marshal.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;
                    }
                }
            }
}
Автор: Tom Brearley Размещён: 02.08.2016 02:32

1 плюс

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

Автор: bill_the_loser Размещён: 01.10.2008 05:47

1 плюс

Как некоторые, возможно, уже написали, важно не только, как закрыть Excel (объект); Также важно, как вы открываете его, а также по типу проекта.

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

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

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

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

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

Автор: Blaz Brencic Размещён: 11.04.2013 02:01
Вопросы из категории :
32x32