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

747

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

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

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

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

Аид
источник
1
Вы пытаетесь закрыть Excel.exe, не закрывая приложение? Не уверен, что я полностью понимаю ваш вопрос.
Bryant
3
Я пытаюсь убедиться, что неуправляемые объекты взаимодействия удаляются должным образом. Таким образом, процессы Excel не работают, даже когда пользователь закончил работу с таблицей Excel, которую мы создали из приложения.
Гадес
3
Если вы можете попытаться сделать это, создав XML-файлы Excel, в противном случае, пожалуйста, рассмотрите VSTO без / управляемого управления памятью: jake.ginnivan.net/vsto-com-interop
Джереми Томпсон
Это переводит в Excel хорошо?
Coops
2
См. (Помимо ответов ниже) эту статью поддержки от Microsoft, где они конкретно предлагают решения этой проблемы: support.microsoft.com/kb/317109
Арджан,

Ответы:

684

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
источник
16
Тогда я предлагаю не использовать Excel из COM и избавить себя от всех проблем. Форматы Excel 2007 можно использовать, не открывая Excel, великолепно.
user7116
5
Я не понял, что означают «две точки». Не могли бы вы объяснить?
A9S6
22
Это означает, что вы не должны использовать шаблон comObject.Property.PropertiesProperty (вы видите две точки?). Вместо этого назначьте comObject.Property для переменной и используйте и утилизируйте эту переменную. Более формальной версией вышеуказанного правила может быть sth. например, «Присвойте com-объект переменным перед их использованием. Это включает в себя com-объекты, которые являются свойствами другого com-объекта».
ВВС
5
@ Ник: На самом деле, вам не нужна какая-либо очистка, так как сборщик мусора сделает это за вас. Единственное, что вам нужно сделать, - это назначить каждому COM-объекту свою собственную переменную, чтобы GC знал об этом.
ВВС
12
@VSS, вот и все, GC очищает все, так как переменные-оболочки создаются .net framework. Это может занять вечность для GC, чтобы очистить его. Вызов GC.Collect после тяжелого взаимодействия - неплохой идеал.
CodingBarfield
280

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

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

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

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

Например, при условии, что у вас есть именованная переменная объекта Range, именованная переменная xlRngWorksheet, именованная переменная xlSheetWorkbook 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()and 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 #.

Майк Розенблюм
источник
3
Связанное обсуждение можно найти на форуме ExtremeVBTalk .NET Office Automation, здесь: xtremevbtalk.com/showthread.php?t=303928 .
Майк Розенблюм
2
А если ничего не помогает, тогда можно использовать Process.Kill () (в крайнем случае), как описано здесь: stackoverflow.com/questions/51462/…
Майк Розенблюм,
1
Рад, что это сработало, Ричард. :-) А вот тонкий пример, когда простого предотвращения «двух точек» недостаточно для предотвращения проблемы: stackoverflow.com/questions/4964663/…
Майк Розенблюм
Вот мнение по этому поводу от Миши Шнеерсона из Microsoft по этой теме, в комментариях 7 октября 2010 года. ( Blogs.msdn.com/b/csharpfaq/archive/2010/09/28/… )
Майк Розенблюм
Я надеюсь, что это помогает решить постоянную проблему. Безопасно ли выполнять сборку мусора и выпускать вызовы внутри блока finally?
Бретт Грин
213

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

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

  • Освобождение КАЖДОГО com-объекта явно с помощью Marshal.FinalReleaseComObject () (не забывая о неявно созданных com-объектах). Чтобы освободить каждый созданный com-объект, вы можете использовать правило двух точек, упомянутое здесь:
    Как правильно очистить объекты взаимодействия Excel?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Вуаля! Excel прекращен! :)

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

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

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

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

ты пишешь:

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

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

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

nightcoder
источник
19
грустно, что ветка уже настолько старая, что ваш превосходный ответ появляется так далеко внизу, что, я думаю, является единственной причиной того, что за нее больше не голосуют ...
chiccodoro
15
Должен признать, когда я впервые прочитал ваш ответ, я подумал, что это гигантский клудж. После примерно 6 часов борьбы с этим (все выпущено, у меня нет двойных точек и т. Д.), Теперь я думаю, что ваш ответ гениален.
Марк
3
Спасибо за это. Я боролся с Excel, который не закрывался бы, несмотря на то, что в течение нескольких дней, прежде чем я нашел это. Отлично.
BBlake 19.10.10
2
@ nightcoder: потрясающий ответ, очень подробный. Ваши комментарии относительно режима отладки очень верны и важны, что вы указали на это. Один и тот же код в режиме выпуска часто может быть в порядке. Однако Process.Kill хорош только при использовании автоматизации, а не когда ваша программа работает в Excel, например, как управляемая надстройка COM.
Майк Розенблюм
2
@DanM: Ваш подход на 100% правильный и никогда не подведет. Сохраняя все ваши переменные локально для метода, все ссылки практически недоступны для кода .NET. Это означает, что когда ваш раздел finally вызывает GC.Collect (), финализаторы для всех ваших COM-объектов будут вызываться с уверенностью. Затем каждый финализатор вызывает Marshal.FinalReleaseComObject для финализируемого объекта. Поэтому ваш подход прост и надежен. Не бойтесь использовать это. (Только предостережение: если вы используете VSTO, в чем я сомневаюсь, вам нужно вызывать GC.Collect () & GC.WaitForPendingFinalizers ДВАЖДЫ.)
Майк Розенблюм
49

ОБНОВЛЕНИЕ : добавлен код 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
источник
2
Явное уничтожение внепроцессного COM-сервера (который может обслуживать других COM-клиентов) - ужасная идея. Если вы прибегаете к этому, это потому, что ваш протокол COM нарушен. COM-серверы не должны рассматриваться как обычное оконное приложение
MickyD
39

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

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

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

Филип Фурье
источник
30

Все, что находится в пространстве имен 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
источник
3
Как, например, xlRange.Interior.Color, например.
Гадес
Внутренняя часть должна быть освобождена (она находится в пространстве имен) ... С другой стороны, цвет - нет (вызывается из System.Drawing.Color, iirc)
MagicKat
2
на самом деле цвет - это цвет Excel, а не цвет .Net. Вы просто передаете длинную Также рабочие книги def. нужно выпустить, листы ... меньше так.
Анонимный тип
Это неправда, правило двух точек - плохо, одной - хорошо, это чепуха. Кроме того, вам не нужно выпускать рабочие книги, рабочие таблицы, диапазоны, это работа GC. Единственное, что вы никогда не должны забывать, это вызывать Quit для одного и того же объекта Excel.Application. После вызова Quit обнулите объект Excel.Application и вызовите GC.Collect (); GC.WaitForPendingFinalizers (); дважды.
Дитрих Баумгартен
29

Во-первых, вам никогда не придется звонить 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 и переполнении стека (и особенно этот вопрос!).

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

Govert
источник
5
Практически ВСЕ ОТВЕТЫ НА ЭТОЙ СТРАНИЦЕ НЕПРАВИЛЬНЫ, КРОМЕ ЭТОГО. Они работают, но слишком много работы. Игнорировать правило "2 точки" Пусть GC сделает вашу работу за вас. Дополнительные свидетельства от .Net GURU Ханса Пассанта: stackoverflow.com/a/25135685/3852958
Алан Бальжеу
1
Говерт, какое облегчение найти правильный способ сделать это. Спасибо.
Дитрих Баумгартен
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/

Эдвард Уайльд
источник
3
да это хорошо. Я думаю, что этот код может быть обновлен, хотя теперь, когда FinalReleaseCOMObject доступен.
Анонимный тип
Все еще раздражает наличие блока использования для каждого объекта. См. Здесь stackoverflow.com/questions/2191489/… для альтернативы.
Хенрик
16

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

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

Colin
источник
Нет, особенно если вы хотите разрешить взаимодействие пользователя с приложением COM (в данном случае Excel - OP не указывает, предназначено ли взаимодействие с пользователем, но не ссылается на его отключение). Вы просто должны разрешить вызывающей стороне позволить пользователю выйти из Excel при закрытии, скажем, одного файла.
downwitch
это прекрасно сработало для меня - сбой, когда у меня был только objExcel.Quit (), но когда я также сделал objExcel.Application.Quit () заранее, закрылся чисто.
mcmillab
13

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

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

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

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

Мохсен Афшин
источник
1
.... это что-то вроде санокгаммерского подхода? - это похоже на то, что я также использую :( но нужно изменить. Проблема с использованием этого подхода заключается в том, что часто при открытии Excel на машине, где он запускался, в левой панели появляется предупреждение, говорящее что-то вроде «Excel был закрыт некорректно»: не очень элегантно. Я думаю, что одно из более ранних предложений в этой теме должно быть предпочтительным.
Whytheq
11

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

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

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

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

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

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

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

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

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

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

Дейв Кузино
источник
1
Этот ответ работает и бесконечно удобнее, чем принятый ответ. Не беспокойтесь о «двух точках» с COM-объектами, не используйте Marshal.
andrew.cuthbert
9

Чтобы добавить причины, по которым 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
Grimfort
источник
И еще одна причина - повторное использование ссылки без предварительного сброса предыдущего значения.
Grimfort
Спасибо. Я также использовал «для каждого», ваше решение сработало для меня.
Чад Браун-Дуйн
+1 Спасибо. Кроме того, обратите внимание, что если у вас есть объект для диапазона Excel, и вы хотите изменить диапазон в течение срока службы объекта, я обнаружил, что мне пришлось выпустить ReleaseComObject перед его переназначением, что делает код немного неопрятным!
AjV Jsy
9

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

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

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

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

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

BTownTKD
источник
2
Это должно быть отмечено как ответ, на самом деле. NetOffice абстрагируется от всей этой сложности.
К. Аугусто Пруете
1
Я использовал NetOffice в течение значительного количества времени для написания надстройки для Excel, и она отлично работала. Единственное, на что нужно обратить внимание, это то, что, если вы не удалите использованные объекты явно, это будет сделано при выходе из приложения (потому что он все равно отслеживает их). Таким образом, практическое правило с NetOffice всегда заключается в использовании шаблона «использования» с каждым объектом Excel, таким как ячейка, диапазон или лист и т. Д.
Стас Иванов,
8

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

public Excel.Style xlStyleHeader = null;

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

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

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

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

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

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

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

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

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

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

Крис МакГрат
источник
Внутри выпуска, проверьте на Нуль (ответ Джо). Это позволит избежать ненужных нулевых исключений. Я проверил много методов. Это единственный способ эффективно выпустить Excel.
Герхард Пауэлл
8

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

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

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

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

что мы добавили в конец функции в оболочку, как показано ниже:

private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}
DG
источник
Я обнаружил, что мне не нужно обнулять ComObjects, только Quit () - Close () и FinalReleaseComObject. Я не могу поверить, что это все, чего не хватало на моем конце, чтобы заставить его работать. Большой!
Кэрол
7

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

¸ ° º¤ø „¸ Снимайте 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);
}
Антуан Мельцхайм
источник
6

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


источник
6

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

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

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

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

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

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

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

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

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

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

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

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

Решение

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

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

Амит Миттал
источник
4

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

Джо
источник
3

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

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

...
FinalReleaseComObject(app);
человек-паук
источник
2
Я ненавижу произвольные ожидания. Но +1, потому что вы правы в том, что вам нужно ждать закрытия рабочих книг. Альтернативой является опрос коллекции книг в цикле и использование произвольного ожидания в качестве времени ожидания цикла.
dFlat
3

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

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

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

Варианты вместе здесь .

Ned
источник
Очень хороший момент! Я думаю, что из-за того, что я использую Office 2007, от моего имени была проведена некоторая очистка. Я использовал совет далее, но не сохранил переменные, как вы предложили здесь, и EXCEL.EXE завершает работу, но я могу просто повезти, и если у меня возникнут дополнительные проблемы, я обязательно посмотрю эту часть моего кода =)
Coops
3

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

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

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

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

public class Test {

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

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

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

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

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

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

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

Porkbutts
источник
2

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

private static void Clean()
{
    workBook.Close();
    Marshall.ReleaseComObject(workBook);
    excel.Quit();
    CG.Collect();
    CG.WaitForPendingFinalizers();
}
Ганеман
источник
2

Мое решение

[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
источник
2

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

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

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

Арванд
источник
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(); }
}
Мартин
источник
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;
                    }
                }
            }
}
Том Брирли
источник
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
источник
1

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

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

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

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

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

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

Блаз Бренчич
источник