c# - tipo - microsoft.office.interop.excel download
¿Cómo limpio adecuadamente los objetos de interoperabilidad de Excel? (30)
Desarrolladores comunes, ninguna de sus soluciones funcionó para mí, así que decido implementar un nuevo truco .
Primero dejemos especificar "¿Cuál es nuestro objetivo?" => "No ver objeto de Excel después de nuestro trabajo en el administrador de tareas"
De acuerdo. Deje que no lo desafíe y comience a destruirlo, pero considere no destruir otras instancias de Excel que se ejecutan en paralelo.
Entonces, obtenga la lista de procesadores actuales y obtenga PID de procesos EXCEL, luego, una vez que haya terminado su trabajo, tenemos un nuevo invitado en la lista de procesos con un PID único, encuentre y destruya solo ese.
<tenga en cuenta que cualquier nuevo proceso de Excel durante su trabajo de Excel se detectará como nuevo y se destruirá> <Una mejor solución es capturar el PID del nuevo objeto de Excel creado y simplemente destruirlo>
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();
Esto resuelve mi problema, espero el tuyo también.
Estoy usando la interoperabilidad de Excel en C # ( ApplicationClass
) y he colocado el siguiente código en mi cláusula finally:
while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();
Aunque este tipo de trabajo, el proceso Excel.exe
todavía está en segundo plano incluso después de que cierre Excel. Solo se libera una vez que mi aplicación se cierra manualmente.
¿Qué estoy haciendo mal, o hay una alternativa para garantizar que los objetos de interoperabilidad se eliminen correctamente?
Esto funcionó para un proyecto en el que estaba trabajando:
excelApp.Quit();
Marshal.ReleaseComObject (excelWB);
Marshal.ReleaseComObject (excelApp);
excelApp = null;
Aprendimos que era importante establecer nulas todas las referencias a un objeto COM de Excel cuando terminaba con él. Esto incluía las células, las hojas, y todo.
Esto seguro que parece que ha sido demasiado complicado. Desde mi experiencia, solo hay tres cosas clave para que Excel se cierre correctamente:
1: asegúrese de que no haya referencias restantes a la aplicación de Excel que creó (solo debería tener una de todas formas; ajústela a null
)
2: llamada GC.Collect()
3: Excel debe ser cerrado, ya sea por el usuario que cierra manualmente el programa o llamando a Quit
en el objeto de Excel.(Tenga en cuenta que Quit
funcionará como si el usuario intentara cerrar el programa y presentará un cuadro de diálogo de confirmación si hay cambios no guardados, incluso si Excel no está visible. El usuario podría presionar Cancelar y luego Excel no se habrá cerrado. )
1 debe suceder antes de 2, pero 3 puede suceder en cualquier momento.
Una forma de implementar esto es envolver el objeto Excel de interoperabilidad con su propia clase, crear la instancia de interoperabilidad en el constructor e implementar IDisposable con Dispose buscando algo así como
if (!mDisposed) {
mExcel = null;
GC.Collect();
mDisposed = true;
}
Eso limpiará sobresalir del lado de tu programa. Una vez que Excel se cierre (manualmente por el usuario o por usted llamando Quit
), el proceso desaparecerá. Si el programa ya se ha cerrado, el proceso desaparecerá en la GC.Collect()
llamada.
(No estoy seguro de lo importante que es, pero es posible que desee una GC.WaitForPendingFinalizers()
llamada después de la GC.Collect()
llamada, pero no es estrictamente necesario deshacerse del proceso de Excel).
Esto me ha funcionado sin problemas durante años. Tenga en cuenta que, si bien esto funciona, realmente tiene que cerrar con gracia para que funcione. Seguirá obteniendo procesos de excel.exe acumulados si interrumpe su programa antes de que Excel se limpie (generalmente presionando "detener" mientras su programa se está depurando).
Excel no se cierra porque su aplicación aún contiene referencias a objetos COM.
Supongo que está invocando al menos un miembro de un objeto COM sin asignarlo a una variable.
Para mi fue el objeto excelApp.Worksheets que usé directamente sin asignarlo a una variable:
Worksheet sheet = excelApp.Worksheets.Open(...);
...
Marshal.ReleaseComObject(sheet);
No sabía que internamente C # creó un envoltorio para el objeto COM de las hojas de trabajo que no fue liberado por mi código (porque no estaba al tanto) y fue la causa por la que Excel no se descargó.
Encontré la solución a mi problema en esta página , que también tiene una buena regla para el uso de objetos COM en C #:
Nunca use dos puntos con objetos COM.
Entonces, con este conocimiento, la forma correcta de hacer lo anterior es:
Worksheets sheets = excelApp.Worksheets; // <-- The important part
Worksheet sheet = sheets.Open(...);
...
Marshal.ReleaseComObject(sheets);
Marshal.ReleaseComObject(sheet);
ACTUALIZACIÓN POST MORTEM:
Quiero que todos los lectores lean esta respuesta de Hans Passant con mucho cuidado, ya que explica la trampa en la que me he encontrado y muchos otros desarrolladores. Cuando escribí esta respuesta hace años, no sabía el efecto que el depurador tiene en el recolector de basura y extrajo conclusiones erróneas. Mantengo mi respuesta inalterada por el bien de la historia, pero lea este enlace y no siga el camino de "los dos puntos": comprender la recolección de basura en .NET y limpiar objetos de interoperabilidad de Excel con IDisposable
Mi solución
[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();
}
No puedo creer que este problema haya obsesionado al mundo durante 5 años ... Si ha creado una aplicación, debe apagarla antes de eliminar el enlace.
objExcel = new Excel.Application();
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing));
cuando cierre
objBook.Close(true, Type.Missing, Type.Missing);
objExcel.Application.Quit();
objExcel.Quit();
Cuando se abre una aplicación de Excel, se abre un programa de Excel en segundo plano. Debe ordenar a ese programa de Excel que salga antes de liberar el enlace porque ese programa de Excel no forma parte de su control directo. Por lo tanto, permanecerá abierto si el enlace es publicado!
Buena programación a todos ~~
Primero, nunca tendrá que llamar a Marshal.ReleaseComObject(...)
o Marshal.FinalReleaseComObject(...)
cuando haga interoperabilidad de Excel. Es un anti-patrón confuso, pero cualquier información sobre esto, incluida la de Microsoft, que indique que debe liberar manualmente las referencias de COM desde .NET es incorrecta. El hecho es que el tiempo de ejecución .NET y el recolector de basura rastrean correctamente y limpian las referencias COM. Para su código, esto significa que puede eliminar todo el bucle `while (...) en la parte superior.
En segundo lugar, si desea asegurarse de que las referencias COM a un objeto COM fuera de proceso se limpien cuando finalice su proceso (para que el proceso de Excel se cierre), debe asegurarse de que se ejecuta el recolector de basura. Haces esto correctamente con llamadas a GC.Collect()
y GC.WaitForPendingFinalizers()
. Llamar a esto dos veces es seguro y garantiza que los ciclos también se limpien definitivamente (aunque no estoy seguro de que sea necesario, y apreciaría un ejemplo que lo muestre).
Tercero, cuando se ejecuta bajo el depurador, las referencias locales se mantendrán vivas artificialmente hasta el final del método (para que funcione la inspección de variables locales). Por lo GC.Collect()
llamadas GC.Collect()
no son efectivas para limpiar objetos como rng.Cells
desde el mismo método. Debe dividir el código que realiza la interoperabilidad COM de la limpieza del GC en métodos separados. (Este fue un descubrimiento clave para mí, de una parte de la respuesta publicada aquí por @nightcoder).
El patrón general sería así:
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
Hay mucha información falsa y confusión sobre este problema, incluidas muchas publicaciones en MSDN y en (¡y especialmente esta pregunta!).
Lo que finalmente me convenció para mirar más de cerca y encontrar el consejo correcto fue la publicación del blog Marshal.ReleaseComObject Considered Dangerous junto con encontrar el problema con referencias vivas bajo el depurador que confundía mis pruebas anteriores.
Puede liberar su objeto de aplicación de Excel de manera limpia, pero debe tener cuidado.
El consejo de mantener una referencia con nombre para absolutamente cada objeto COM al que acceda y luego liberarlo explícitamente a través de Marshal.FinalReleaseComObject()
es correcto en teoría, pero, desafortunadamente, es muy difícil de administrar en la práctica. Si alguna vez se desliza en cualquier lugar y usa "dos puntos", o itera celdas a través de a for each
bucle, o cualquier otro tipo de comando similar, entonces tendrá objetos COM sin referencia y corre el riesgo de colgarse. En este caso, no habría manera de encontrar la causa en el código; Tendría que revisar todo su código a simple vista y, con suerte, encontrar la causa, una tarea que podría ser casi imposible para un proyecto grande.
La buena noticia es que realmente no tiene que mantener una referencia variable con nombre para cada objeto COM que utiliza. En su lugar, llame a GC.Collect()
y luego a GC.WaitForPendingFinalizers()
para liberar todos los objetos (generalmente menores) a los que no tiene una referencia, y luego libere explícitamente los objetos a los que sí tiene una referencia de variable nombrada.
También debe liberar sus referencias nombradas en orden inverso de importancia: primero coloque los objetos en un rango, luego las hojas de trabajo, los libros de trabajo y, finalmente, su objeto de aplicación de Excel.
Por ejemplo, suponiendo que tenía una variable de objeto de rango llamada xlRng
, una variable de la xlSheet
trabajo llamada xlSheet
, una variable de xlBook
llamada xlBook
y una variable de la aplicación de Excel llamada xlApp
, entonces su código de limpieza podría ser similar al siguiente:
// 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);
En la mayoría de los ejemplos de código, verá cómo limpiar los objetos COM de .NET, las GC.Collect()
y GC.WaitForPendingFinalizers()
se hacen DOS VECES como en:
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
Sin embargo, esto no debería ser necesario, a menos que esté utilizando Visual Studio Tools para Office (VSTO), que utiliza finalizadores que hacen que se promocione un gráfico completo de objetos en la cola de finalización. Tales objetos no serían liberados hasta la próxima recolección de basura. Sin embargo, si no está utilizando VSTO, debería poder llamar a GC.Collect()
y GC.WaitForPendingFinalizers()
solo una vez.
Sé que llamar explícitamente a GC.Collect()
es un no-no (y ciertamente hacerlo dos veces suena muy doloroso), pero no hay forma de evitarlo, para ser honesto. A través de las operaciones normales, generará objetos ocultos a los que no tiene ninguna referencia que, por lo tanto, no puede liberar a través de ningún otro medio que no sea llamar a GC.Collect()
.
Este es un tema complejo, pero esto es realmente todo lo que hay que hacer. Una vez que establezca esta plantilla para su procedimiento de limpieza, puede codificar normalmente, sin la necesidad de envoltorios, etc. :-)
Tengo un tutorial sobre esto aquí:
Automatización de programas de Office con interoperabilidad VB.Net / COM
Está escrito para VB.NET, pero no se desanime por eso, los principios son exactamente los mismos que cuando usa C #.
"Nunca use dos puntos con objetos COM" es una buena regla general para evitar la fuga de referencias COM, pero Excel PIA puede provocar fugas en más formas de lo que parece a primera vista.
Una de estas formas es suscribirse a cualquier evento expuesto por cualquiera de los objetos COM del modelo de objetos de Excel.
Por ejemplo, suscribirse al evento WorkbookOpen de la clase Application.
Alguna teoría sobre eventos COM
Las clases COM exponen un grupo de eventos a través de interfaces de devolución de llamada. Para suscribirse a eventos, el código del cliente puede simplemente registrar un objeto que implementa la interfaz de devolución de llamada y la clase COM invocará sus métodos en respuesta a eventos específicos. Dado que la interfaz de devolución de llamada es una interfaz COM, es deber del objeto de implementación disminuir el recuento de referencia de cualquier objeto COM que reciba (como parámetro) para cualquiera de los controladores de eventos.
Cómo Excel PIA expone eventos COM
Excel PIA expone eventos COM de la clase de aplicación Excel como eventos .NET convencionales. Cada vez que el código de cliente se suscribe a un evento .NET (énfasis en ''a''), PIA crea una instancia de una clase que implementa la interfaz de devolución de llamada y la registra con Excel.
Por lo tanto, varios objetos de devolución de llamada se registran con Excel en respuesta a diferentes solicitudes de suscripción del código .NET. Un objeto de devolución de llamada por suscripción de evento.
Una interfaz de devolución de llamada para el manejo de eventos significa que, PIA debe suscribirse a todos los eventos de interfaz para cada solicitud de suscripción a eventos .NET. No puede escoger y elegir. Al recibir una devolución de llamada de evento, el objeto de devolución de llamada comprueba si el controlador de eventos .NET asociado está interesado en el evento actual o no, y luego invoca el controlador o ignora silenciosamente la devolución de llamada.
Efecto en los recuentos de referencia de instancia COM
Todos estos objetos de devolución de llamada no disminuyen el recuento de referencia de ninguno de los objetos COM que reciben (como parámetros) para ninguno de los métodos de devolución de llamada (incluso para los que se ignoran en silencio). Se basan únicamente en el recolector de basura CLR para liberar los objetos COM.
Dado que la ejecución de GC no es determinista, esto puede llevar a retrasar el proceso de Excel durante más tiempo que el deseado y crear una impresión de "pérdida de memoria".
Solución
La única solución a partir de ahora es evitar el proveedor de eventos de PIA para la clase COM y escribir su propio proveedor de eventos que libera de manera determinista los objetos COM.
Para la clase de aplicación, esto se puede hacer implementando la interfaz AppEvents y luego registrando la implementación con Excel utilizando la interfaz IConnectionPointContainer . La clase de aplicación (y, de hecho, todos los objetos COM que exponen eventos mediante el mecanismo de devolución de llamada) implementa la interfaz IConnectionPointContainer.
¡Asegúrate de liberar todos los objetos relacionados con Excel!
Pasé unas horas probando varias maneras. Todas son ideas geniales, pero finalmente encontré mi error: si no sueltas todos los objetos, ninguna de las formas anteriores puede ayudarte en mi caso. ¡Asegúrate de liberar todos los objetos, incluido el rango uno!
Excel.Range rng = (Excel.Range)worksheet.Cells[1, 1];
worksheet.Paste(rng, false);
releaseObject(rng);
Las opciones están juntas here .
found una plantilla genérica útil que puede ayudar a implementar el patrón de eliminación correcto para los objetos COM, que necesitan que se llame a Marshal.ReleaseComObject cuando están fuera de alcance:
Uso:
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();
}
}
Modelo:
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;
}
}
}
Referencia:
¨ ° º¤ø „¸ Disparar Excel proc y masticar chicle ¸„ ø¤º ° ¨
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);
}
ACTUALIZACIÓN : Se agregó el código C # y se vincula a los trabajos de Windows
Pasé algún tiempo tratando de resolver este problema, y en ese momento, XtremeVBTalk era el más activo y sensible. Aquí hay un enlace a mi publicación original, Cierre de un proceso de Interoperabilidad de Excel limpiamente, incluso si su aplicación falla . A continuación se muestra un resumen de la publicación y el código copiado en esta publicación.
- Cerrar el proceso de interoperabilidad con
Application.Quit()
yProcess.Kill()
funciona en su mayor parte, pero falla si las aplicaciones se bloquean de forma catastrófica. Es decir, si la aplicación falla, el proceso de Excel todavía se estará ejecutando. - La solución es permitir que el sistema operativo maneje la limpieza de sus procesos a través de los objetos de trabajo de Windows mediante llamadas de Win32. Cuando su aplicación principal muera, los procesos asociados (es decir, Excel) también terminarán.
Descubrí que esta es una solución limpia porque el sistema operativo está haciendo un verdadero trabajo de limpieza. Todo lo que tienes que hacer es registrar el proceso de Excel.
Código de trabajo de Windows
Envuelve las llamadas a la API de Win32 para registrar procesos de interoperabilidad.
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);
}
}
Nota sobre el código Constructor
- En el constructor, el
info.LimitFlags = 0x2000;
se llama.0x2000
es el valor de enumeraciónJOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
, y este valor está definido por MSDN como:
Hace que todos los procesos asociados con el trabajo terminen cuando se cierra el último manejador del trabajo.
Llamada API de Win32 adicional para obtener el ID de proceso (PID)
[DllImport("user32.dll", SetLastError = true)]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
Usando el codigo
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);
Cualquier cosa que esté en el espacio de nombres de Excel debe ser liberada. Período
No puedes estar haciendo:
Worksheet ws = excel.WorkBooks[1].WorkSheets[1];
Tienes que estar haciendo
Workbooks books = excel.WorkBooks;
Workbook book = books[1];
Sheets sheets = book.WorkSheets;
Worksheet ws = sheets[1];
Seguido de la liberación de los objetos.
Prefacio: mi respuesta contiene dos soluciones, así que tenga cuidado al leer y no se pierda nada.
Hay diferentes maneras y consejos sobre cómo hacer que la instancia de Excel se descargue, como por ejemplo:
Liberar CADA objeto com explícitamente con Marshal.FinalReleaseComObject () (sin olvidar los com-objetos creados implícitamente). Para liberar cada objeto com creado, puede usar la regla de los 2 puntos mencionados aquí:
¿Cómo limpio adecuadamente los objetos de interoperabilidad de Excel?Llamando a GC.Collect () y GC.WaitForPendingFinalizers () para hacer que CLR libere objetos de uso no utilizados * (En realidad, funciona, consulte mi segunda solución para obtener más detalles)
Verificando si com-server-application tal vez muestre un cuadro de mensaje esperando a que el usuario responda (aunque no estoy seguro de que pueda evitar que Excel se cierre, pero escuché algunas veces)
Enviando un mensaje WM_CLOSE a la ventana principal de Excel
Ejecutar la función que funciona con Excel en un dominio de aplicación separado. Algunas personas creen que la instancia de Excel se cerrará cuando se descargue AppDomain.
Matar todas las instancias de Excel que se crearon después de que comenzara nuestro código de interoperación de excel.
¡PERO! ¡A veces todas estas opciones simplemente no ayudan o no pueden ser apropiadas!
Por ejemplo, ayer descubrí que en una de mis funciones (que funciona con Excel) Excel se sigue ejecutando después de que finaliza la función. ¡Lo intenté todo! Revisé minuciosamente toda la función 10 veces y añadí Marshal.FinalReleaseComObject () para todo. También tuve GC.Collect () y GC.WaitForPendingFinalizers (). Busqué cajas de mensajes ocultos. Intenté enviar un mensaje WM_CLOSE a la ventana principal de Excel. Ejecuté mi función en un dominio de aplicación separado y descargué ese dominio. ¡Nada ayudó! La opción de cerrar todas las instancias de Excel es inapropiada, ya que si el usuario inicia otra instancia de Excel manualmente, durante la ejecución de mi función que también funciona con Excel, esa función también la cerrará. Apuesto que el usuario no estará feliz! Así que, honestamente, esta es una opción poco convincente (no se ofenda). Así que pasé un par de horas antes de encontrar una buena solución (en mi humilde opinión): matar al proceso de Excel por hWnd desde su ventana principal (es la primera solución).
Aquí está el código simple:
[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();
}
Como puede ver, proporcioné dos métodos, de acuerdo con el patrón Try-Parse (creo que es apropiado aquí): un método no produce una excepción si el Proceso no se puede eliminar (por ejemplo, el proceso ya no existe), y otro método arroja una excepción si el Proceso no fue cancelado. El único lugar débil en este código son los permisos de seguridad. En teoría, el usuario puede no tener permisos para detener el proceso, pero en el 99,99% de todos los casos, el usuario tiene dichos permisos. También lo probé con una cuenta de invitado, funciona perfectamente.
Entonces, su código, trabajando con Excel, puede verse así:
int hWnd = xl.Application.Hwnd;
// ...
// here we try to close Excel as usual, with xl.Quit(),
// Marshal.FinalReleaseComObject(xl) and so on
// ...
TryKillProcessByMainWindowHwnd(hWnd);
Voila! Excel se termina! :)
Bien, volvamos a la segunda solución, como prometí al principio del post. La segunda solución es llamar a GC.Collect () y GC.WaitForPendingFinalizers (). Sí, realmente funcionan, ¡pero debes tener cuidado aquí!
Mucha gente dice (y yo dije) que llamar a GC.Collect () no ayuda. ¡Pero la razón por la que no ayudaría es si todavía hay referencias a objetos COM! Una de las razones más populares por las que GC.Collect () no es útil es ejecutar el proyecto en modo de depuración. En los objetos en modo depuración a los que ya no se hace referencia realmente, no se recogerá la basura hasta el final del método.
Por lo tanto, si probó GC.Collect () y GC.WaitForPendingFinalizers () y no le sirvió, intente hacer lo siguiente:
1) Intente ejecutar su proyecto en modo Release y verifique si Excel se cerró correctamente
2) Envuelva el método que trabaja con Excel en un método separado. Entonces, en lugar de algo como esto:
void GenerateWorkbook(...)
{
ApplicationClass xl;
Workbook xlWB;
try
{
xl = ...
xlWB = xl.Workbooks.Add(...);
...
}
finally
{
...
Marshal.ReleaseComObject(xlWB)
...
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
usted escribe:
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)
...
}
}
Ahora, Excel se cerrará =)
Cuando todas las cosas anteriores no funcionaron, intente darle a Excel algo de tiempo para cerrar sus hojas:
app.workbooks.Close();
Thread.Sleep(500); // adjust, for me it works at around 300+
app.Quit();
...
FinalReleaseComObject(app);
Debe tener en cuenta que Excel también es muy sensible a la cultura con la que se está ejecutando.
Es posible que tenga que configurar la cultura en EN-US antes de llamar a las funciones de Excel. Esto no se aplica a todas las funciones, sino a algunas de ellas.
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;
Esto se aplica incluso si está utilizando VSTO.
Para obtener más información: http://support.microsoft.com/default.aspx?scid=kb;en-us;Q320369
Después de intentar
- Liberar objetos COM en orden inverso
- Añadir
GC.Collect()
yGC.WaitForPendingFinalizers()
dos veces al final. - No más de dos puntos.
- Cerrar libro de trabajo y salir de la aplicación
- Ejecutar en modo de liberación
La solución final que funciona para mí es mover una serie de
GC.Collect();
GC.WaitForPendingFinalizers();
que agregamos al final de la función a un contenedor, de la siguiente manera:
private void FunctionWrapper(string sourcePath, string targetPath)
{
try
{
FunctionThatCallsExcel(sourcePath, targetPath);
}
finally
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
Para agregar a las razones por las que Excel no se cierra, incluso cuando crea referencias directas a cada objeto en la lectura, la creación es el bucle ''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
Actualmente estoy trabajando en la automatización de Office y he encontrado una solución para esto que siempre me funciona. Es simple y no implica matar ningún proceso.
Parece que simplemente haciendo un bucle a través de los procesos activos actuales, y de alguna manera ''accediendo'' a un proceso de Excel abierto, se eliminará cualquier instancia de Excel que se cuelgue. El código siguiente simplemente comprueba los procesos donde el nombre es ''Excel'', luego escribe la propiedad MainWindowTitle del proceso en una cadena. Esta ''interacción'' con el proceso parece hacer que Windows se ponga al día y aborte la instancia congelada de Excel.
Ejecuto el método siguiente justo antes del complemento en el que estoy desarrollando las paradas, ya que dispara el evento de descarga. Elimina todas las instancias colgantes de Excel cada vez. Honestamente, no estoy completamente seguro de por qué funciona esto, pero funciona bien para mí y podría colocarse al final de cualquier aplicación de Excel sin tener que preocuparme por los puntos dobles, Marshal.ReleaseComObject, ni los procesos de muerte. Me interesaría mucho cualquier sugerencia sobre por qué esto es efectivo.
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;
}
}
}
}
Como es probable que algunos ya hayan escrito, no solo es importante cómo cierra Excel (objeto); También es importante cómo lo abres y también por el tipo de proyecto.
En una aplicación WPF, básicamente el mismo código funciona sin o con muy pocos problemas.
Tengo un proyecto en el que el mismo archivo de Excel se procesa varias veces para diferentes valores de parámetros, por ejemplo, analizándolo en base a valores dentro de una lista genérica.
Pongo todas las funciones relacionadas con Excel en la clase base y el analizador en una subclase (diferentes analizadores usan funciones comunes de Excel). No quería que Excel se abriera y volviera a cerrar para cada elemento en una lista genérica, así que lo abrí solo una vez en la clase base y lo cerré en la subclase. Tuve problemas al mover el código a una aplicación de escritorio. He probado muchas de las soluciones mencionadas anteriormente. GC.Collect()
Ya se implementó antes, el doble de lo sugerido.
Luego decidí que movería el código para abrir Excel a una subclase. En lugar de abrir solo una vez, ahora creo un nuevo objeto (clase base), abro Excel para cada elemento y lo cierro al final. Existe una penalización de rendimiento, pero en función de varias pruebas, los procesos de Excel se cierran sin problemas (en el modo de depuración), por lo que también se eliminan los archivos temporales. Continuaré con las pruebas y escribiré un poco más si recibo algunas actualizaciones.
La conclusión es: también debe verificar el código de inicialización, especialmente si tiene muchas clases, etc.
Como han señalado otros, debe crear una referencia explícita para cada objeto de Excel que use y llamar a Marshal.ReleaseComObject a esa referencia, como se describe en este artículo de KB . También debe usar try / finally para asegurarse de que siempre se llame a ReleaseComObject, incluso cuando se lanza una excepción. Es decir, en lugar de:
Worksheet sheet = excelApp.Worksheets(1)
... do something with sheet
Necesitas hacer algo como:
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);
}
También debe llamar a Application.Quit antes de liberar el objeto de la aplicación si desea que Excel se cierre.
Como puede ver, esto rápidamente se vuelve extremadamente difícil de manejar tan pronto como intenta hacer algo incluso moderadamente complejo. He desarrollado con éxito aplicaciones .NET con una clase de envoltura simple que envuelve algunas manipulaciones simples del modelo de objetos de Excel (abrir un libro, escribir en un rango, guardar / cerrar el libro, etc.). La clase envoltura implementa IDisposable, implementa cuidadosamente Marshal.ReleaseComObject en cada objeto que usa, y no expone públicamente ningún objeto de Excel al resto de la aplicación.
Pero este enfoque no se adapta bien a los requisitos más complejos.
Esta es una gran deficiencia de .NET COM Interop. Para escenarios más complejos, consideraría seriamente escribir una DLL ActiveX en VB6 u otro lenguaje no administrado en el que pueda delegar toda la interacción con objetos COM de fuera de proceso, como Office. Luego puede hacer referencia a esta DLL ActiveX desde su aplicación .NET, y las cosas serán mucho más fáciles ya que solo necesitará liberar esta referencia.
Creo que parte de eso es solo la forma en que el marco maneja las aplicaciones de Office, pero podría estar equivocado. En algunos días, algunas aplicaciones limpian los procesos inmediatamente, y otros días parecen esperar hasta que la aplicación se cierre. En general, dejé de prestar atención a los detalles y me aseguré de que no haya ningún proceso adicional flotando al final del día.
Además, y tal vez estoy simplificando las cosas, pero creo que puedes ...
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();
Como dije antes, no tiendo a prestar atención a los detalles de cuándo aparece o desaparece el proceso de Excel, pero eso generalmente funciona para mí. Tampoco me gusta mantener los procesos de Excel por un tiempo que no sea la cantidad mínima de tiempo, pero probablemente solo estoy siendo paranoico con eso.
Debe tener mucho cuidado al utilizar las aplicaciones de interoperabilidad de Word / Excel. Después de probar todas las soluciones, aún quedaba mucho proceso "WinWord" abierto en el servidor (con más de 2000 usuarios).
Después de trabajar en el problema durante horas, me di cuenta de que si abro más de un par de documentos Word.ApplicationClass.Document.Open()
en diferentes subprocesos simultáneamente, ¡el proceso de trabajo de IIS (w3wp.exe) se bloquearía dejando todos los procesos de WinWord abiertos!
Entonces, supongo que no hay una solución absoluta para este problema, pero cambiar a otros métodos como Office Open XML development.
La regla de los dos puntos no funcionó para mí. En mi caso, creé un método para limpiar mis recursos de la siguiente manera:
private static void Clean()
{
workBook.Close();
Marshall.ReleaseComObject(workBook);
excel.Quit();
CG.Collect();
CG.WaitForPendingFinalizers();
}
La respuesta aceptada aquí es correcta, pero también tenga en cuenta que no solo deben evitarse las referencias de "dos puntos", sino también los objetos que se recuperan a través del índice. Tampoco es necesario que espere hasta que haya terminado con el programa para limpiar estos objetos; es mejor crear funciones que los limpien tan pronto como termine con ellos, cuando sea posible. Aquí hay una función que creé que asigna algunas propiedades de un objeto de estilo llamado 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 { }
}
Observe que tuve que establecer xlBorders[Excel.XlBordersIndex.xlEdgeBottom]
una variable para limpiar eso (no por los dos puntos, que se refieren a una enumeración que no necesita ser liberada, sino porque el objeto al que me refiero es en realidad un objeto de borde) que necesita ser liberado).
Este tipo de cosas no son realmente necesarias en las aplicaciones estándar, que hacen un gran trabajo de limpieza después de sí mismas, pero en las aplicaciones ASP.NET, si se olvida de alguno de ellos, no importa la frecuencia con la que llame al recolector de basura, Excel todavía se ejecuta en su servidor.
Requiere mucha atención a los detalles y muchas ejecuciones de prueba al monitorear el Administrador de tareas al escribir este código, pero al hacerlo le ahorra la molestia de buscar desesperadamente en las páginas de código para encontrar la instancia que perdió. Esto es especialmente importante cuando se trabaja en bucles, en los que debe liberar CADA INSTANCIA de un objeto, aunque use el mismo nombre de variable cada vez que realice un bucle.
La respuesta aceptada no funcionó para mí. El siguiente código en el destructor hizo el trabajo.
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(); }
}
Seguí esto exactamente ... Pero todavía tuve problemas 1 de cada 1000 veces. Quien sabe por que Es hora de sacar el martillo ...
Justo después de que se crea una instancia de la clase de la aplicación Excel, obtengo una retención del proceso de Excel que se acaba de crear.
excel = new Microsoft.Office.Interop.Excel.Application();
var process = Process.GetProcessesByName("EXCEL").OrderByDescending(p => p.StartTime).First();
Luego, una vez que haya realizado toda la limpieza de COM anterior, me aseguro de que el proceso no se esté ejecutando. Si todavía está corriendo, ¡mátalo!
if (!process.HasExited)
process.Kill();
Tradicionalmente he seguido los consejos que se encuentran en la respuesta de VVS . Sin embargo, en un esfuerzo por mantener esta respuesta actualizada con las últimas opciones, creo que todos mis proyectos futuros usarán la biblioteca "NetOffice".
NetOffice es un reemplazo completo para los PIA de Office y es completamente independiente de la versión. Es una colección de envoltorios de COM administrados que pueden manejar la limpieza que a menudo causa tales dolores de cabeza cuando se trabaja con Microsoft Office en .NET.
Algunas características clave son:
- La mayoría de las versiones son independientes (y las características dependientes de la versión están documentadas)
- Sin dependencias
- No pia
- No hay registro
- No VSTO
No estoy afiliado de ninguna manera con el proyecto; Simplemente aprecio sinceramente la severa reducción de los dolores de cabeza.
Un gran artículo sobre la liberación de objetos COM es 2.5 Liberar objetos COM (MSDN).
El método que recomendaría es anular las referencias de Excel.Interop si son variables no locales, y luego llamar GC.Collect()
y GC.WaitForPendingFinalizers()
dos veces. Las variables de interoperabilidad de ámbito local serán atendidas automáticamente.
Esto elimina la necesidad de mantener una referencia con nombre para cada objeto COM.
Aquí hay un ejemplo tomado del artículo:
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();
}
}
Estas palabras son directamente del artículo:
En casi todas las situaciones, anular la referencia RCW y forzar una recolección de basura se limpiará correctamente. Si también llama a GC.WaitForPendingFinalizers, la recolección de basura será tan determinista como pueda hacerlo. Es decir, estará bastante seguro exactamente de cuándo se limpió el objeto, en el retorno de la segunda llamada a WaitForPendingFinalizers. Como alternativa, puede utilizar Marshal.ReleaseComObject. Sin embargo, tenga en cuenta que es muy poco probable que necesite utilizar este método.