Як правильно очистити об'єкти інтеропу Excel?


747

Я використовую інтероп Excel в C # ( ApplicationClass) і в остаточному пункті розмістив такий код:

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

Хоча цей вид працює, Excel.exeпроцес все ще залишається на задньому плані навіть після закриття Excel. Він виходить лише після закриття моєї програми вручну.

Що я роблю неправильно, чи є альтернатива для забезпечення належного утилізації об'єктів інтероп?


1
Ви намагаєтесь вимкнути Excel.exe, не закриваючи програму? Не впевнений, що я повністю розумію ваше запитання.
Брайант

3
Я намагаюсь переконатися, що некеровані об’єкти інтеропу розташовані належним чином. Так що не існує процесів Excel, навіть коли користувач закінчив таблицю Excel, яку ми створили з програми.
HAdes

3
Якщо ви можете спробувати це зробити, створивши
Джеремі Томпсон

Це добре перекладається в Excel?
Купи

2
Дивіться (окрім відповідей нижче) цю статтю підтримки від Microsoft, де вони спеціально дають рішення цієї проблеми: support.microsoft.com/kb/317109
Arjan

Відповіді:


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

ОНОВЛЕННЯ ПОСТАЧИ МОРТЕМ:

Я хочу, щоб кожен читач прочитав цю відповідь Ганса Пассанта дуже уважно, оскільки він пояснює пастку, в яку я натрапив і багато інших розробників. Коли я писав цю відповідь років тому, я не знав про вплив налагоджувача на сміттєзбірник і зробив неправильні висновки. Я залишаю свою відповідь незмінною заради історії, але, будь ласка, прочитайте це посилання і не йдіть шляхом "двох крапок": Розуміння збору сміття в .NET та очищення об'єктів Excel Interop з IDisposable


16
Тоді я пропоную не використовувати Excel від COM та врятувати себе від усіх проблем. Формати Excel 2007 можна використовувати без будь-якого відкриття Excel, чудового.
user7116

5
Я не зрозумів, що означають «дві крапки». Чи можете ви поясніть, будь ласка?
A9S6

22
Це означає, що вам не слід використовувати шаблон comObject.Property.PropertiesProperty (ви бачите дві точки?). Замість цього призначте comObject.Property змінній та використовуйте та розпоряджайтесь цією змінною. Більш офіційною версією вищезазначеного правила може бути що-небудь. наприклад "Призначте об'єкт com змінним, перш ніж їх використовувати. Це включає ком-об'єкти, які є властивостями іншого ком-об'єкта."
VVS

5
@Nick: Насправді, вам не потрібно будь-яке прибирання, оскільки сміттєзбірник це зробить за вас. Єдине, що вам потрібно зробити, - це призначити кожен об'єкт COM своїй змінній, щоб GC знав про це.
VVS

12
@VSS thats blalocks, GC очищує все, оскільки змінні обгортки створюються .net Framework. Просто GC може зайняти назавжди, щоб очистити його. Виклик GC.Collect після важкого інтеропу не є поганою ідеєю.
CodingBarfield

280

Ви можете фактично випустити об’єкт програми Excel, але це потрібно подбати.

Порада підтримувати названу посилання для абсолютно кожного об’єкта COM, до якого ви отримуєте доступ, а потім явно відпускати через Marshal.FinalReleaseComObject()нього, теоретично правильна, але, на жаль, дуже складно керувати на практиці. Якщо хто-небудь прослизає кудись і використовує "дві крапки", або повторює клітинки через for eachцикл або будь-яку іншу подібну команду, то ви матимете невідрегульовані об'єкти COM та ризикуєте повісити. У цьому випадку не було б можливості знайти причину в коді; вам доведеться переглянути весь код на очі і, сподіваємось, знайти причину - завдання, яке може бути майже неможливим для великого проекту.

Хороша новина полягає в тому, що вам фактично не потрібно підтримувати названу змінну посилання на кожен об’єкт COM, який ви використовуєте. Замість цього зателефонуйте GC.Collect()та GC.WaitForPendingFinalizers()випустіть усі (зазвичай другорядні) об'єкти, на які ви не маєте посилання, а потім явно відпустіть об'єкти, на які ви маєте вказану змінну посилання.

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

Наприклад, якщо припустити, що у вас була названа змінна об'єкта Range, названа змінна xlRngWorkheet, названа змінна 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()і GC.WaitForPendingFinalizers()дзвінки зроблені ДВІЧІ як в:

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

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

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

Це складна тема, але це справді все, що там є. Після встановлення цього шаблону для процедури очищення ви можете кодувати нормально, не потребуючи обгортки тощо :-)

У мене тут є підручник:

Автоматизація програм Office за допомогою 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/… )
Майк Розенблум,

Я сподіваюся, що це допомагає вирішити постійну проблему. Чи безпечно робити вивезення сміття та звільняти дзвінки в остаточному блоці?
Бретт Грін

213

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

Існують різні способи та поради щодо того, як змусити завантажувати екземпляр Excel, наприклад:

  • Вивільнення КОЖНОГО об’єкта явно з Marshal.FinalReleaseComObject () (не забуваючи про неявно створені com-об'єкти). Щоб випустити кожен створений ком-об’єкт, ви можете використовувати правило з двох крапок, згадане тут:
    Як правильно очистити об'єкти інтеропу Excel?

  • Виклик GC.Collect () та GC.WaitForPendingFinalizers (), щоб CLR випустив невикористані com-об'єкти * (Насправді це працює, детальну інформацію див. У моєму другому рішенні)

  • Перевірка, чи програма-сервер com-сервера, можливо, показує вікно повідомлень, що чекає, коли користувач відповість (хоча я не впевнений, що це може запобігти закриттю Excel, але я чув про це кілька разів)

  • Надсилання WM_CLOSE повідомлення в головне вікно Excel

  • Виконання функції, яка працює з Excel, в окремому AppDomain. Деякі люди вважають, що екземпляр Excel буде закритий, коли AppDomain буде завантажений.

  • Убивання всіх екземплярів Excel, які були створені після запуску нашого коду інтеропінгу excel.

АЛЕ! Іноді всі ці варіанти просто не допомагають або не можуть бути відповідними!

Наприклад, вчора я дізнався, що в одній із моїх функцій (яка працює з excel) Excel продовжує працювати після закінчення функції. Я все пробував! Я ретельно перевірив всю функцію 10 разів і додав Marshal.FinalReleaseComObject () на все! У мене також були GC.Collect () та GC.WaitForPendingFinalizers (). Я перевірив наявність прихованих скриньок повідомлень. Я спробував надіслати WM_CLOSE повідомлення в головне вікно Excel. Я виконував свою функцію в окремому AppDomain і вивантажував цей домен. Нічого не допомогло! Варіант із закриттям усіх екземплярів 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 закриється =)


19
сумно, що нитка вже настільки стара, що ваша чудова відповідь виявляється далеко внизу, що, на мою думку, є єдиною причиною того, що її більше не звертають до уваги ...
chiccodoro

15
Маю визнати, коли я вперше прочитав вашу відповідь, я подумав, що це гігантська хижа. Приблизно через 6 годин боротьби з цим (все випущено, у мене немає подвійних крапок тощо), я вважаю, що ваша відповідь геніальна.
Марк

3
Дякую за це Боровся з Excel, який би не закрився ні за що за пару днів до того, як я це знайшов. Відмінно.
BBlake

2
@nightcoder: приголомшлива відповідь, дійсно детальна. Ваші коментарі щодо режиму налагодження дуже правдиві і важливі, щоб ви це вказали. Один і той же код у режимі випуску часто може бути добре. Проте Process.Kill корисний лише при використанні автоматизації, а не тоді, коли ваша програма працює в Excel, наприклад, як керована надбудова COM.
Майк Розенблум

2
@DanM: Ваш підхід на 100% правильний і ніколи не підведе. Зберігаючи всі ваші змінні місцеві методу, всі посилання фактично недоступні кодом .NET. Це означає, що коли ваш нарешті розділ викликає GC.Collect (), фіналізатори для всіх ваших об’єктів COM будуть викликані з певністю. Кожен фіналізатор потім викликає Marshal.FinalReleaseComObject на об'єкт, який завершується. Тому ваш підхід простий і нерозумний. Не бійтеся, використовуючи це. (Тільки застереження: якщо ви використовуєте VSTO, який я сумніваюся, що ви є, вам потрібно буде зателефонувати GC.Collect () & GC.WaitForPendingFinalizers ДВОХ.)
Майк Розенблум

49

ОНОВЛЕННЯ : Додано код C # та посилання на завдання Windows

Я витрачав колись, намагаючись вирішити цю проблему, і на той час XtremeVBTalk був найактивнішим і чуйним. Ось посилання на мою оригінальну публікацію, чисто закриваючи процес Excel Interop, навіть якщо ваша програма виходить з ладу . Нижче наведено короткий опис публікації та код, скопійований у цю публікацію.

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

Я знайшов це чистим рішенням, оскільки ОС робить реальну роботу з очищення. Все, що вам потрібно зробити - це зареєструвати процес Excel.

Код роботи Windows

Обгортає виклики API Win32 для реєстрації процесів Interop.

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

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

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

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

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

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

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

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

    private IntPtr m_handle;
    private bool m_disposed = false;

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

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

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

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

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

    #region IDisposable Members

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

    #endregion

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

        if (disposing) {}

        Close();
        m_disposed = true;
    }

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

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

}

Примітка про код конструктора

  • У конструкторі info.LimitFlags = 0x2000;називається. 0x2000- JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSEзначення enum, і це значення визначається MSDN як:

Причиняє завершення всіх процесів, пов’язаних із завданням, коли остання ручка до завдання закрита.

Додатковий виклик API Win32, щоб отримати ідентифікатор процесу (PID)

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

Використання коду

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

2
Явне вбивство неіснуючого COM-сервера (який може обслуговувати інших клієнтів COM) - жахлива ідея. Якщо ви вдаєтеся до цього, це тому, що ваш протокол COM порушений. COM-сервери не повинні трактуватися як звичайні програми з вікнами
MickyD

38

Це працювало над проектом, над яким я працював:

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

Ми дізналися, що важливо встановити будь-які посилання на об'єкт Excel COM нульовими, коли ви закінчилися з ним. Сюди входили клітини, аркуші та все.


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];

з подальшим випуском об’єктів.


3
Як наприклад aobut xlRange.Interior.Color.
HAdes

Інтер'єр повинен бути випущений (його в просторі імен) ... Колір, з іншого боку, не викликає його (System.Drawing.Color, iirc)
MagicKat

2
насправді Color є кольором Excel, а не кольором .Net. ти просто пройдеш Довгу. Також робочі зошити деф. потрібно звільнити, робочі аркуші ... менше.
Анонімний тип

Це неправда, правило "дві точки-погано", "одна-крапка" - це нісенітниця. Крім того, вам не доведеться випускати Робочі книжки, Робочі таблиці, Діапазони, це робота 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 вважається небезпечним разом із пошуком проблеми з посиланнями, що зберігаються в живих під налагоджувачем, що бентежить моє попереднє тестування.


5
Практично ВСІ ВІДПОВІДИ НА ЦІЙ СТОРІНІ НЕБЕЗПЕЧНО ЦЕ ОДНУ. Вони працюють, але занадто багато працюють. IGNORE правила "2 DOTS". Нехай GC робить вашу роботу за вас. Додаткові дані з .Net GURU Hans Passant: stackoverflow.com/a/25135685/3852958
Алан Baljeu

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 не є частиною вашого прямого управління. Тому він залишатиметься відкритим, якщо посилання випущено!

Гарне програмування для всіх ~~


Ні, ви цього не робите, особливо якщо ви хочете дозволити взаємодію користувачів із COM-програмою (Excel у цьому випадку - OP не визначає, чи призначена взаємодія з користувачем, але не посилається на її вимкнення). Вам просто потрібно змусити абонента відпустити, щоб користувач міг вийти з Excel при закритті, скажімо, одного файлу.
downwitch

це спрацювало для мене ідеально - аварія, коли у мене був тільки objExcel.Quit (), але коли я також попередньо робив objExcel.Application.Quit (), закрито чисто.
mcmillab

13

Звичайні розробники, жодне з ваших рішень не працювало на мене, тому я вирішу реалізувати новий трюк .

Спочатку дозвольмо вказати "Яка наша мета?" => "Не бачити об'єкт excel після нашої роботи в менеджері завдань"

Добре. Не дозволяйте ні кидати викликів, а починайте руйнувати його, але врахуйте, що не руйнувати інший екземпляр os Excel, який працює паралельно.

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

<пам’ятайте, що будь-який новий процес excel під час вашої роботи в програмі excel буде виявлений як новий та знищений> <Краще рішення - захопити PID новоствореного об'єкта excel та просто знищити його>

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

.... // your job 

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

Це вирішує мою проблему, сподіваюсь і на вашу.


1
.... це трохи підхід санчака? - це схоже на те, що я також використовую :( але потрібно змінити. Проблема з використанням цього підходу полягає в тому, що часто, коли відкривається excel, на машині, на якій він працює, він має попередження в лівій панелі, кажучи щось подібне "Excel був закритий неправильно": не дуже елегантно. Я думаю, що варто віддати перевагу одній з попередніх пропозицій у цій темі.
чому theq

11

Це впевнене здається, що це було занадто складно. З мого досвіду, є лише три ключові речі, щоб Excel зачинився належним чином:

1: переконайтесь, що немає інших посилань на додаток Excel, який ви створили (все одно у вас повинен бути лише один; встановіть його null )

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

3: Excel повинен бути закритий або користувачем, який вручну закриває програму, або зателефонувавши Quitна об’єкт Excel. (Зауважте, щоQuit буде функціонувати так, як якщо б користувач намагався закрити програму, і відкриє діалогове вікно підтвердження, якщо є незбережені зміни, навіть якщо Excel не видно. Користувач може натиснути скасувати, і тоді Excel не буде закритий. )

1 має відбутися до 2, але 3 може статися будь-коли.

Один із способів реалізувати це - обернути об'єкт Excel interop власним класом, створити екземпляр interop в конструкторі та реалізувати IDisposable за допомогою Dispose, виглядаючи щось на зразок

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

Це очистить чудовий бік речей вашої програми. Після закриття Excel (вручну користувачем або телефоном Quit) ви перейдете. Якщо програма вже закрита, процес зникне під час GC.Collect()виклику.

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

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


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

І ще одна причина - повторне використання посилання, не випускаючи попереднє значення спочатку.
Грімфорт

Дякую. Я також використовував "для кожного" ваше рішення працювало на мене.
Чад Браун-Дуїн

+1 Дякую Плюс зауважте, що якщо у вас є об’єкт для діапазону Excel, і ви хочете змінити діапазон протягом життя об’єкта, я виявив, що мені довелося ReleaseComObject перед його переназначенням, що робить код трохи неохайним!
AjV Jsy

9

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

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

Деякі основні особливості:

  • Переважно незалежні від версій (і функції, що залежать від версій, документально підтверджені)
  • Ніяких залежностей
  • Ніякої PIA
  • Немає реєстрації
  • Немає ВСТО

Я жодним чином не пов'язаний з проектом; Я просто щиро ціную різке зменшення головного болю.


2
Це справді має бути позначено як відповідь. NetOffice абстрагує всю цю складність.
C. Augusto Proiete

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 буде як і раніше працює на вашому сервері.

Це вимагає великої уваги до деталей та безлічі тестових виконань під час моніторингу диспетчера завдань під час написання цього коду, але це заощаджує вам клопоту відчайдушного пошуку сторінок коду, щоб знайти один екземпляр, який ви пропустили. Це особливо важливо під час роботи в циклі, де потрібно випустити КОЖНУ ІНСТАНЦІЮ об'єкта, навіть якщо він використовує те саме ім'я змінної щоразу, коли циклічно працює.


Всередині випуску, перевірте наявність Null (відповідь Джо). Це дозволить уникнути зайвих нульових винятків. Я перевірив багато методів. Це єдиний спосіб ефективного випуску 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();
    }
}

Я виявив, що не потрібно скасовувати 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();

7

¨ ° º¤ø „¸ Shoot 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 класу програми.

Деяка теорія щодо COM-подій

COM-класи розкривають групу подій через інтерфейси зворотного дзвінка. Для того, щоб підписатися на події, клієнтський код може просто зареєструвати об’єкт, що реалізує інтерфейс зворотного виклику, і COM-клас буде викликати його методи у відповідь на конкретні події. Оскільки інтерфейс зворотного дзвінка є інтерфейсом COM, обов'язком об'єкта, що реалізує, є обов'язок зменшити кількість посилань будь-якого COM-об'єкта, який він отримує (як параметр) для будь-якого з обробників подій.

Як Excel PIA виставляє COM-події

Excel PIA виставляє COM події класу Application Excel як звичайні .NET події. Всякий раз , коли клієнтський код підписується на подію .NET (акцент на «а»), PIA створює екземпляр класу , що реалізовує інтерфейс зворотного виклику і реєструє його з Excel.

Отже, ряд об’єктів зворотного виклику реєструються в Excel у відповідь на різні запити на підписку з коду .NET. Один об'єкт зворотного дзвінка за підписку на подію.

Інтерфейс зворотного дзвінка для обробки подій означає, що PIA повинна підписатися на всі події інтерфейсу для кожного запиту на підписку .NET події. Він не може вибрати і вибрати. Після отримання зворотного виклику події об'єкт зворотного виклику перевіряє, чи пов'язаний обробник події .NET зацікавлений у поточній події чи ні, а потім або викликає обробник, або мовчки ігнорує зворотний виклик.

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

Усі ці об'єкти зворотного виклику не зменшують кількість посилань жодного із COM-об'єктів, які вони отримують (як параметри), для будь-якого з методів зворотного виклику (навіть для тих, які мовчки ігноруються). Вони покладаються виключно на сміттєзбірник CLR, щоб звільнити об'єкти COM.

Оскільки запуск GC є недетермінованим, це може призвести до затримки процесу Excel на більш тривалий час, ніж бажано, та створити враження "витоку пам'яті".

Рішення

Наразі єдине рішення - уникнути постачальника подій PIA для COM-класу та написати власного постачальника подій, який детерміновано випускає COM-об’єкти.

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


4

Як зазначали інші, вам потрібно створити чітке посилання на кожен використовуваний вами об'єкт Excel та викликати Marshal.ReleaseComObject за цією посиланням, як описано в цій статті KB . Вам також потрібно скористатися спробувати / нарешті, щоб забезпечити виклик 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 (відкрити робочу книгу, записати в діапазон, зберегти / закрити робочу книжку тощо). Клас обгортки реалізує IDisposable, ретельно реалізовує Marshal.ReleaseComObject на кожному об'єкті, який він використовує, і публічно не виставляє жодних об’єктів Excel іншій програмі.

Але такий підхід не відповідає масштабам складніших вимог.

Це великий недолік .NET COM Interop. Для складніших сценаріїв я б серйозно подумав про те, щоб написати DLL ActiveX на 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);

Варіанти разом тут .


Дуже хороший момент! Я думаю, оскільки я використовую Office 2007, від мого імені зроблено деяке очищення. Я скористався порадою далі, але не зберігав змінні, як ви запропонували тут, і EXCEL.EXE робить вихід, але я міг би просто пощастити, і якщо у мене виникнуть подальші проблеми, я обов'язково перегляну цю частину свого коду =)
Coops

3

Чудова стаття про випуск 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. Однак зауважте, що вам малоймовірно, що коли-небудь знадобиться використовувати цей метод.


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

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 нічим іншим, крім мінімальної кількості часу, але я, мабуть, просто параною на це.


1

Як деякі, напевно, вже писали, важливо не лише те, як ви закриєте Excel (об’єкт); також важливо, як ви відкриєте його, а також за типом проекту.

У додатку WPF, в основному, той самий код працює без або з дуже маленькими проблемами.

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

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

Тоді я вирішив, що перенесу код для відкриття Excel в підклас. Замість відкриття лише один раз, тепер я створюю новий об’єкт (базовий клас) і відкриваю Excel для кожного елемента та закриваю його в кінці. Існує певна ефективність покарання, але на основі декількох тестів процеси Excel закриваються без проблем (у режимі налагодження), тому також видаляються тимчасові файли. Я продовжую тестування і напишу ще трохи, якщо отримаю деякі оновлення.

Підсумок: Ви також повинні перевірити код ініціалізації, особливо якщо у вас багато класів тощо.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.