Как правильно очистить объекты взаимодействия Excel?
Я использую взаимодействие Excel в C# (ApplicationClass
) и поместили следующий код в мое предложение finally:
while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();
Хотя этот вид работ, Excel.exe
Процесс все еще в фоновом режиме, даже после закрытия Excel. Это только выпущено, как только мое приложение закрыто вручную.
Что я делаю не так, или есть альтернатива для обеспечения правильного удаления объектов взаимодействия?
43 ответа
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
На самом деле вы можете освободить объект приложения Excel аккуратно, но вы должны позаботиться об этом.
Совет поддерживать именованную ссылку для абсолютно каждого COM-объекта, к которому вы обращаетесь, и затем явно освобождать его через Marshal.FinalReleaseComObject()
это правильно в теории, но, к сожалению, очень сложно управлять на практике. Если кто-то когда-нибудь проскальзывает и использует "две точки", или итерирует ячейки через for each
loop, или любой другой подобный тип команды, тогда у вас будут COM-объекты без ссылок и вы рискуете зависнуть. В этом случае невозможно найти причину в коде; вам придется просмотреть весь ваш код на глаз и, надеюсь, найти причину, задачу, которая может быть почти невозможной для большого проекта.
Хорошая новость заключается в том, что вам не нужно поддерживать ссылку на именованную переменную для каждого COM-объекта, который вы используете. Вместо этого позвоните GC.Collect()
а потом GC.WaitForPendingFinalizers()
освободить все (обычно второстепенные) объекты, на которые у вас нет ссылки, а затем явно освободить объекты, на которые вы действительно ссылаетесь на именованную переменную.
Вы также должны выпустить именованные ссылки в обратном порядке важности: сначала выберите объекты диапазона, затем рабочие листы, рабочие книги и, наконец, ваш объект приложения Excel.
Например, если у вас есть переменная объекта Range с именем xlRng
переменная листа xlSheet
переменная Workbook с именем xlBook
и переменная приложения Excel с именем xlApp
тогда ваш код очистки может выглядеть примерно так:
// Cleanup
GC.Collect();
GC.WaitForPendingFinalizers();
Marshal.FinalReleaseComObject(xlRng);
Marshal.FinalReleaseComObject(xlSheet);
xlBook.Close(Type.Missing, Type.Missing, Type.Missing);
Marshal.FinalReleaseComObject(xlBook);
xlApp.Quit();
Marshal.FinalReleaseComObject(xlApp);
В большинстве примеров кода, которые вы увидите для очистки COM-объектов из.NET, GC.Collect()
а также GC.WaitForPendingFinalizers()
звонки делаются ДВАЖДЫ как в:
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
Это не должно требоваться, однако, если вы не используете Visual Studio Tools for Office (VSTO), который использует финализаторы, которые заставляют весь граф объектов продвигаться в очереди финализации. Такие объекты не будут выпущены до следующей сборки мусора. Однако, если вы не используете VSTO, вы сможете позвонить GC.Collect()
а также GC.WaitForPendingFinalizers()
только раз.
Я знаю, что явно GC.Collect()
это нет-нет (и, конечно, делать это дважды звучит очень больно), но, честно говоря, нет пути к этому. С помощью обычных операций вы будете создавать скрытые объекты, на которые у вас нет ссылок, которые вы, следовательно, не можете освободить с помощью других средств, кроме вызова GC.Collect()
,
Это сложная тема, но это все, что нужно сделать. После того, как вы установите этот шаблон для своей процедуры очистки, вы можете кодировать как обычно, без использования упаковщиков и т. Д.:-)
У меня есть учебник по этому вопросу здесь:
Автоматизация офисных программ с VB.Net / COM Interop
Он написан для VB.NET, но не стоит откладывать на это, принципы точно такие же, как при использовании C#.
Предисловие: мой ответ содержит два решения, поэтому будьте внимательны при чтении и ничего не пропустите.
Существуют различные способы и рекомендации по загрузке экземпляра Excel, такие как:
Освобождение КАЖДОГО com-объекта явным образом с помощью Marshal.FinalReleaseComObject() (не забывая о неявно созданных com-объектах). Чтобы освободить каждый созданный com-объект, вы можете использовать правило 2 точек, упомянутое здесь:
Как правильно очистить объекты взаимодействия Excel?Вызов GC.Collect() и GC.WaitForPendingFinalizers(), чтобы заставить CLR освобождать неиспользуемые com-объекты * (На самом деле, это работает, подробности см. В моем втором решении)
Проверка, может ли приложение com-server-application отображать окно сообщения, ожидающее ответа пользователя (хотя я не уверен, что это может помешать закрытию Excel, но я слышал об этом несколько раз)
Отправка сообщения WM_CLOSE в главное окно Excel
Выполнение функции, которая работает с Excel, в отдельном домене приложений. Некоторые люди считают, что экземпляр Excel будет закрыт, когда AppDomain выгружен.
Уничтожение всех экземпляров Excel, которые были созданы после запуска нашего кода взаимодействия с Excel.
НО! Иногда все эти варианты просто не помогают или не могут быть подходящими!
Например, вчера я обнаружил, что в одной из моих функций (которая работает с Excel) Excel продолжает работать после завершения функции. Я перепробовал все! Я тщательно проверил всю функцию 10 раз и добавил Marshal.FinalReleaseComObject () для всего! У меня также были GC.Collect() и GC.WaitForPendingFinalizers(). Я проверил наличие скрытых окон сообщений. Я попытался отправить сообщение WM_CLOSE в главное окно Excel. Я выполнил свою функцию в отдельном домене приложений и выгрузил этот домен. Ничего не помогло! Вариант с закрытием всех экземпляров Excel не подходит, потому что, если пользователь запускает другой экземпляр Excel вручную, во время выполнения моей функции, которая также работает с Excel, этот экземпляр также будет закрыт моей функцией. Бьюсь об заклад, пользователь не будет счастлив! Так что, если честно, это неудачный вариант (без обид, ребята). Поэтому я потратил пару часов, прежде чем нашел хорошее (по моему скромному мнению) решение: убить процесс Excel с помощью hWnd его главного окна (это первое решение).
Вот простой код:
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
/// <summary> Tries to find and kill process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <returns>True if process was found and killed. False if process was not found by hWnd or if it could not be killed.</returns>
public static bool TryKillProcessByMainWindowHwnd(int hWnd)
{
uint processID;
GetWindowThreadProcessId((IntPtr)hWnd, out processID);
if(processID == 0) return false;
try
{
Process.GetProcessById((int)processID).Kill();
}
catch (ArgumentException)
{
return false;
}
catch (Win32Exception)
{
return false;
}
catch (NotSupportedException)
{
return false;
}
catch (InvalidOperationException)
{
return false;
}
return true;
}
/// <summary> Finds and kills process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <exception cref="ArgumentException">
/// Thrown when process is not found by the hWnd parameter (the process is not running).
/// The identifier of the process might be expired.
/// </exception>
/// <exception cref="Win32Exception">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="NotSupportedException">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="InvalidOperationException">See Process.Kill() exceptions documentation.</exception>
public static void KillProcessByMainWindowHwnd(int hWnd)
{
uint processID;
GetWindowThreadProcessId((IntPtr)hWnd, out processID);
if (processID == 0)
throw new ArgumentException("Process has not been found by the given main window handle.", "hWnd");
Process.GetProcessById((int)processID).Kill();
}
Как вы можете видеть, я предоставил два метода в соответствии с шаблоном Try-Parse (я думаю, что здесь уместно): один метод не выдает исключение, если процесс не может быть уничтожен (например, процесс больше не существует) и другой метод генерирует исключение, если процесс не был уничтожен. Единственное слабое место в этом коде - это разрешения безопасности. Теоретически, у пользователя может не быть прав на уничтожение процесса, но в 99,99% случаев у пользователя есть такие права. Я также проверил это с гостевой учетной записью - он отлично работает.
Итак, ваш код, работающий с Excel, может выглядеть так:
int hWnd = xl.Application.Hwnd;
// ...
// here we try to close Excel as usual, with xl.Quit(),
// Marshal.FinalReleaseComObject(xl) and so on
// ...
TryKillProcessByMainWindowHwnd(hWnd);
Вуаля! Excel прекращается!:)
Хорошо, давайте вернемся ко второму решению, как я и обещал в начале поста.Второе решение - вызвать GC.Collect() и GC.WaitForPendingFinalizers(). Да, они действительно работают, но здесь нужно быть осторожным!
Многие говорят (и я сказал), что вызов GC.Collect() не помогает. Но причина в том, что это не поможет, если есть ссылки на COM-объекты! Одна из наиболее популярных причин, по которой GC.Collect() не помогает, - это запуск проекта в режиме отладки. В режиме отладки объекты, на которые больше нет ссылок, не будут собирать мусор до конца метода.
Итак, если вы попробовали GC.Collect() и GC.WaitForPendingFinalizers() и это не помогло, попробуйте сделать следующее:
1) Попробуйте запустить свой проект в режиме выпуска и проверьте, правильно ли закрылся Excel
2) Оберните метод работы с Excel в отдельный метод. Итак, вместо чего-то вроде этого:
void GenerateWorkbook(...)
{
ApplicationClass xl;
Workbook xlWB;
try
{
xl = ...
xlWB = xl.Workbooks.Add(...);
...
}
finally
{
...
Marshal.ReleaseComObject(xlWB)
...
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
ты пишешь:
void GenerateWorkbook(...)
{
try
{
GenerateWorkbookInternal(...);
}
finally
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
private void GenerateWorkbookInternal(...)
{
ApplicationClass xl;
Workbook xlWB;
try
{
xl = ...
xlWB = xl.Workbooks.Add(...);
...
}
finally
{
...
Marshal.ReleaseComObject(xlWB)
...
}
}
Теперь Excel закроется =)
ОБНОВЛЕНИЕ: добавлен код 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
Значение enum, и это значение определяется MSDN как:
Вызывает завершение всех процессов, связанных с заданием, когда закрывается последний дескриптор задания.
Дополнительный Win32 API Вызов для получения идентификатора процесса (PID)
[DllImport("user32.dll", SetLastError = true)]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
Используя код
Excel.Application app = new Excel.ApplicationClass();
Job job = new Job();
uint pid = 0;
Win32.GetWindowThreadProcessId(new IntPtr(app.Hwnd), out pid);
job.AddProcess(Process.GetProcessById((int)pid).Handle);
Это сработало для проекта, над которым я работал:
excelApp.Quit();
Marshal.ReleaseComObject (excelWB);
Marshal.ReleaseComObject (excelApp);
excelApp = null;
Мы узнали, что важно установить для каждой ссылки на COM-объект Excel значение null, когда вы закончили с ним. Это включало ячейки, листы и все.
Первое - вам никогда не придется звонить 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 Считается опасным, а также найти проблему со ссылками, которые остались живы в отладчике, что сбивало меня с толку в моем предыдущем тестировании.
Все, что находится в пространстве имен Excel, должно быть освобождено. период
Вы не можете делать:
Worksheet ws = excel.WorkBooks[1].WorkSheets[1];
Вы должны делать
Workbooks books = excel.WorkBooks;
Workbook book = books[1];
Sheets sheets = book.WorkSheets;
Worksheet ws = sheets[1];
с последующим выпуском объектов.
Я нашел полезный универсальный шаблон, который может помочь реализовать правильный шаблон удаления для 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/
Я не могу поверить, что эта проблема преследует мир в течение 5 лет.... Если вы создали приложение, вам необходимо закрыть его, прежде чем удалить ссылку.
objExcel = new Excel.Application();
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing));
при закрытии
objBook.Close(true, Type.Missing, Type.Missing);
objExcel.Application.Quit();
objExcel.Quit();
Когда вы создаете приложение Excel, оно открывает программу Excel в фоновом режиме. Вы должны дать команду этой программе Excel выйти, прежде чем освободить ссылку, потому что эта программа Excel не является частью вашего прямого контроля. Поэтому он останется открытым, если ссылка будет выпущена!
Хорошее программирование всем ~~
Это наверняка кажется слишком сложным. Исходя из моего опыта, для правильного закрытия Excel есть только три ключевых момента:
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" во время отладки вашей программы).
Обычные разработчики, ни одно из ваших решений не помогло мне, поэтому я решил внедрить новый прием.
Сначала позвольте уточнить "Какова наша цель?" => "Не видеть объект 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();
Это решает мою проблему, надеюсь, ваша тоже.
Я традиционно следовал совету, найденному в ответе ВВС. Тем не менее, пытаясь поддерживать этот ответ в курсе последних опций, я думаю, что все мои будущие проекты будут использовать библиотеку "NetOffice".
NetOffice является полной заменой Office PIA и полностью не зависит от версии. Это коллекция управляемых COM-оболочек, которые могут выполнять очистку, которая часто вызывает такие головные боли при работе с Microsoft Office в.NET.
Некоторые ключевые особенности:
- В основном не зависит от версии (а функции, зависящие от версии, задокументированы)
- Нет зависимостей
- Нет ПИА
- Нет регистрации
- Нет ВСТО
Я никоим образом не связан с проектом; Я просто искренне ценю резкое снижение головных болей.
Чтобы добавить причины, по которым 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
Принятый ответ здесь является правильным, но также обратите внимание, что следует избегать не только двухточечных ссылок, но и объектов, которые извлекаются через индекс. Вам также не нужно ждать, пока вы закончите с программой для очистки этих объектов, лучше всего создавать функции, которые будут очищать их, как только вы закончите с ними, когда это возможно. Вот функция, которую я создал, которая назначает некоторые свойства объекта 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 будет все еще работает на вашем сервере.
Это требует большого внимания к деталям и большого количества тестовых выполнений при мониторинге диспетчера задач при написании этого кода, но это избавляет вас от необходимости отчаянного поиска по страницам кода, чтобы найти тот экземпляр, который вы пропустили. Это особенно важно при работе в циклах, где вам нужно освобождать КАЖДУЮ ИНСТАНЦИЮ объекта, даже если он использует одно и то же имя переменной каждый раз при выполнении цикла.
После попытки
- Освободить COM-объекты в обратном порядке
- добавлять
GC.Collect()
а такжеGC.WaitForPendingFinalizers()
дважды в конце - Не более двух точек
- Закрыть книгу и выйти из приложения
- Запустить в режиме релиза
окончательное решение, которое работает для меня, это переместить один набор
GC.Collect();
GC.WaitForPendingFinalizers();
что мы добавили в конец функции в оболочку, как показано ниже:
private void FunctionWrapper(string sourcePath, string targetPath)
{
try
{
FunctionThatCallsExcel(sourcePath, targetPath);
}
finally
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
Я точно следовал этому... Но я все еще сталкивался с проблемами 1 из 1000 раз. Кто знает почему. Время вытащить молоток...
Сразу после создания экземпляра класса приложения Excel я получаю только что созданный процесс Excel.
excel = new Microsoft.Office.Interop.Excel.Application();
var process = Process.GetProcessesByName("EXCEL").OrderByDescending(p => p.StartTime).First();
Затем, когда я выполнил все вышеперечисленные действия по очистке COM, я убедился, что процесс не запущен. Если он все еще работает, убейте его!
if (!process.HasExited)
process.Kill();
¸ °º¤ø„¸ Снимайте Excel proc и жуйте жевательную резинку ¸„ ¤¤º°¨
public class MyExcelInteropClass
{
Excel.Application xlApp;
Excel.Workbook xlBook;
public void dothingswithExcel()
{
try { /* Do stuff manipulating cells sheets and workbooks ... */ }
catch {}
finally {KillExcelProcess(xlApp);}
}
static void KillExcelProcess(Excel.Application xlApp)
{
if (xlApp != null)
{
int excelProcessId = 0;
GetWindowThreadProcessId(xlApp.Hwnd, out excelProcessId);
Process p = Process.GetProcessById(excelProcessId);
p.Kill();
xlApp = null;
}
}
[DllImport("user32.dll")]
static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);
}
Вы должны знать, что Excel очень чувствителен к культуре, в которой вы работаете.
Вы можете обнаружить, что вам нужно установить язык 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
"Никогда не используйте две точки с COM-объектами" - это хорошее правило, позволяющее избежать утечки COM-ссылок, но Excel PIA может привести к утечке в большей степени, чем кажется на первый взгляд.
Одним из таких способов является подписка на любое событие, представляемое любым из объектов COM объектной модели Excel.
Например, подписка на событие WorkbookOpen класса Application.
Немного теории о событиях COM
Классы COM предоставляют группу событий через интерфейсы обратного вызова. Чтобы подписаться на события, клиентский код может просто зарегистрировать объект, реализующий интерфейс обратного вызова, и класс COM будет вызывать его методы в ответ на определенные события. Поскольку интерфейс обратного вызова является интерфейсом COM, обязанность реализующего объекта - уменьшить счетчик ссылок любого COM-объекта, который он получает (в качестве параметра) для любого из обработчиков событий.
Как Excel PIA представляет события COM
Excel PIA представляет события COM класса приложения Excel как обычные события.NET. Всякий раз, когда клиентский код подписывается на событие.NET (с акцентом на 'a'), PIA создает экземпляр класса, реализующего интерфейс обратного вызова, и регистрирует его в Excel.
Следовательно, несколько объектов обратного вызова регистрируются в Excel в ответ на различные запросы на подписку из кода.NET. Один объект обратного вызова для каждой подписки на событие.
Интерфейс обратного вызова для обработки событий означает, что PIA должна подписываться на все события интерфейса для каждого запроса подписки на событие.NET. Он не может выбирать. При получении обратного вызова события объект обратного вызова проверяет, заинтересован ли связанный обработчик события.NET в текущем событии или нет, а затем либо вызывает обработчик, либо молча игнорирует обратный вызов.
Влияние на количество ссылок на экземпляры COM
Все эти объекты обратного вызова не уменьшают счетчик ссылок ни одного из COM-объектов, которые они получают (в качестве параметров) для любого из методов обратного вызова (даже для тех, которые игнорируются без предупреждения). Они полагаются исключительно на сборщик мусора CLR для освобождения объектов COM.
Поскольку запуск GC является недетерминированным, это может привести к задержке процесса Excel на более длительный срок, чем хотелось бы, и создать впечатление "утечки памяти".
Решение
Единственное решение на данный момент - избежать поставщика событий PIA для класса COM и написать свой собственный поставщик событий, который детерминистически высвобождает объекты COM.
Для класса Application это можно сделать, реализовав интерфейс AppEvents, а затем зарегистрировав реализацию в Excel, используя интерфейс IConnectionPointContainer. Класс Application (и в этом отношении все COM-объекты, отображающие события с использованием механизма обратного вызова) реализует интерфейс IConnectionPointContainer.
Как уже отмечали другие, вам необходимо создать явную ссылку для каждого используемого вами объекта Excel и вызвать Marshal.ReleaseComObject для этой ссылки, как описано в этой статье базы знаний. Вам также нужно использовать try/finally, чтобы гарантировать, что ReleaseComObject всегда вызывается, даже когда выдается исключение. Т.е. вместо:
Worksheet sheet = excelApp.Worksheets(1)
... do something with sheet
вам нужно сделать что-то вроде:
Worksheets sheets = null;
Worksheet sheet = null
try
{
sheets = excelApp.Worksheets;
sheet = sheets(1);
...
}
finally
{
if (sheets != null) Marshal.ReleaseComObject(sheets);
if (sheet != null) Marshal.ReleaseComObject(sheet);
}
Вам также нужно вызвать Application.Quit перед освобождением объекта Application, если вы хотите закрыть Excel.
Как вы можете видеть, это быстро становится чрезвычайно громоздким, как только вы пытаетесь сделать что-то даже умеренно сложное. Я успешно разработал.NET-приложения с помощью простого класса-оболочки, который включает несколько простых манипуляций с объектной моделью Excel (открытие рабочей книги, запись в Range, сохранение / закрытие рабочей книги и т. Д.). Класс-оболочка реализует IDisposable, тщательно реализует Marshal.ReleaseComObject для каждого объекта, который он использует, и не публикует какие-либо объекты Excel для остальной части приложения.
Но этот подход не подходит для более сложных требований.
Это большой недостаток.NET COM Interop. Для более сложных сценариев я бы серьезно подумал о написании ActiveX DLL на VB6 или другом неуправляемом языке, которому вы можете делегировать все взаимодействия с внешними COM-объектами, такими как Office. Затем вы можете ссылаться на эту ActiveX DLL из вашего.NET-приложения, и все будет намного проще, поскольку вам нужно будет только выпустить эту единственную ссылку.
Отличная статья о выпуске 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. Однако учтите, что вам вряд ли когда-нибудь понадобится использовать этот метод.
Когда все вышеперечисленное не работает, попробуйте дать Excel немного времени, чтобы закрыть его листы:
app.workbooks.Close();
Thread.Sleep(500); // adjust, for me it works at around 300+
app.Quit();
...
FinalReleaseComObject(app);
Убедитесь, что вы освободили все объекты, связанные с Excel!
Я провел несколько часов, пытаясь несколькими способами. Все это отличные идеи, но я наконец нашел свою ошибку: если вы не отпустите все объекты, ни один из вышеперечисленных способов не поможет вам в моем случае. Убедитесь, что вы отпустите все объекты, включая дальний!
Excel.Range rng = (Excel.Range)worksheet.Cells[1, 1];
worksheet.Paste(rng, false);
releaseObject(rng);
Варианты здесь вместе.
Мое решение
[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();
}
Правило двух точек не сработало для меня. В моем случае я создал метод для очистки моих ресурсов следующим образом:
private static void Clean()
{
workBook.Close();
Marshall.ReleaseComObject(workBook);
excel.Quit();
CG.Collect();
CG.WaitForPendingFinalizers();
}
Вы должны быть очень осторожны, используя приложения взаимодействия Word/Excel. После того, как мы опробовали все решения, у нас все еще оставалось много процессов "WinWord", которые оставались открытыми на сервере (более 2000 пользователей).
Проработав несколько часов над проблемой, я понял, что если я открою более пары документов, используя Word.ApplicationClass.Document.Open()
в разных потоках одновременно рабочий процесс IIS (w3wp.exe) завершился сбоем, оставив все процессы WinWord открытыми!
Так что я думаю, что нет абсолютного решения этой проблемы, кроме перехода на другие методы, такие как разработка Office Open XML.
Вот действительно простой способ сделать это:
[DllImport("User32.dll")]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
...
int objExcelProcessId = 0;
Excel.Application objExcel = new Excel.Application();
GetWindowThreadProcessId(new IntPtr(objExcel.Hwnd), out objExcelProcessId);
Process.GetProcessById(objExcelProcessId).Kill();
Я думаю, что отчасти это то, как фреймворк обрабатывает приложения 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 вокруг чего-либо кроме минимального количества времени, но я, вероятно, просто параноик по этому поводу.
Использование:
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
Объявите это, добавьте код в finally
блок:
finally
{
GC.Collect();
GC.WaitForPendingFinalizers();
if (excelApp != null)
{
excelApp.Quit();
int hWnd = excelApp.Application.Hwnd;
uint processID;
GetWindowThreadProcessId((IntPtr)hWnd, out processID);
Process[] procs = Process.GetProcessesByName("EXCEL");
foreach (Process p in procs)
{
if (p.Id == processID)
p.Kill();
}
Marshal.FinalReleaseComObject(excelApp);
}
}
Принятый ответ не работал для меня. Следующий код в деструкторе сделал свою работу.
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(); }
}