¿Cómo limpio correctamente los objetos de interoperabilidad de Excel?

747

Estoy usando la interoperabilidad de Excel en C # ( ApplicationClass) y he colocado el siguiente código en mi cláusula finalmente:

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

Aunque este tipo de trabajo funciona, el Excel.exeproceso todavía está en segundo plano incluso después de cerrar Excel. Solo se lanza 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?

Infierno
fuente
1
¿Está intentando cerrar Excel.exe sin cerrar su aplicación? No estoy seguro de entender completamente tu pregunta.
Bryant
3
Estoy tratando de asegurarme de que los objetos de interoperabilidad no administrados se eliminen correctamente. Para que no haya procesos de Excel dando vueltas incluso cuando el usuario haya terminado con la hoja de cálculo de Excel que creamos desde la aplicación.
HAdes
3
Si puede intentar hacerlo mediante la producción de archivos XML Excel, de lo contrario, considere VSTO no / gestión de memoria administrada: jake.ginnivan.net/vsto-com-interop
Jeremy Thompson
¿Esto se traduce a Excel bien?
Coops
2
Consulte (además de las respuestas a continuación) este artículo de soporte de Microsoft, donde específicamente brindan soluciones a este problema: support.microsoft.com/kb/317109
Arjan

Respuestas:

684

Excel no se cierra porque su aplicación aún contiene referencias a objetos COM.

Supongo que estás invocando al menos un miembro de un objeto COM sin asignarlo a una variable.

Para mí fue el objeto excelApp.Worksheets que utilicé directamente sin asignarlo a una variable:

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

No sabía que internamente C # creó un contenedor para el objeto COM de Hojas de trabajo que no fue liberado por mi código (porque no lo sabía) y fue la razón por la cual 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 POSTERIOR A MORTEM:

Quiero que todos los lectores lean esta respuesta de Hans Passant con mucho cuidado, ya que explica la trampa en la que yo y muchos otros desarrolladores nos encontramos. Cuando escribí esta respuesta hace años, no sabía sobre el efecto que tiene el depurador para el recolector de basura y saqué las conclusiones equivocadas. 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

VVS
fuente
16
Entonces sugiero no usar Excel de COM y ahorrarse todos los problemas. Los formatos de Excel 2007 se pueden usar sin abrir Excel, magnífico.
user7116
55
No entendí lo que significan "dos puntos". ¿Puede usted explicar por favor?
A9S6
22
Esto significa que no debe usar el patrón comObject.Property.PropertiesProperty (¿ve los dos puntos?). En su lugar, asigne comObject.Property a una variable y use y elimine esa variable. Una versión más formal de la regla anterior podría ser algo. como "Asignar objeto com a variables antes de usarlas. Esto incluye objetos com que son propiedades de otro objeto com".
VVS
55
@ Nick: En realidad, no necesitas ningún tipo de limpieza, ya que el recolector de basura lo hará por ti. Lo único que debe hacer es asignar cada objeto COM a su propia variable para que el GC lo sepa.
VVS
12
@VSS, eso es todo, el GC limpia todo ya que las variables de envoltura están hechas por el marco .net. El GC podría tardar una eternidad en limpiarlo. Llamar a GC.Collect después de una fuerte interoperabilidad no es un mal concepto
CodingBarfield
280

En realidad, 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 Marshal.FinalReleaseComObject()es correcto en teoría, pero, desafortunadamente, es muy difícil de administrar en la práctica. Si alguna vez se desliza a algún lado y usa "dos puntos", o itera celdas a través de un for eachbucle, o cualquier otro tipo de comando similar, entonces tendrá objetos COM sin referencia y se arriesgará a bloquearse. En este caso, no habría forma 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 gran proyecto.

La buena noticia es que no tiene que mantener una referencia de variable con nombre para cada objeto COM que use. En su lugar, llama GC.Collect()y luego GC.WaitForPendingFinalizers()libera todos los objetos (generalmente menores) a los que no tienes una referencia, y luego libera explícitamente los objetos a los que sí tienes una referencia de variable con nombre.

También debe publicar sus referencias nombradas en orden inverso de importancia: primero los objetos de rango, luego las hojas de trabajo, los libros de trabajo y, finalmente, su objeto de aplicación de Excel.

Por ejemplo, suponiendo que tuviera una variable de objeto Range llamada xlRng, una variable de hoja de trabajo llamada xlSheet, una variable de xlBooklibro de trabajo llamada y una variable de aplicación de Excel llamada xlApp, entonces su código de limpieza podría tener el siguiente aspecto:

// 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 que verá para limpiar objetos COM de .NET, las llamadas GC.Collect()y GC.WaitForPendingFinalizers()se realizan 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 promueva un gráfico completo de objetos en la cola de finalización. Dichos objetos no se liberarán hasta la próxima recolección de basura. Sin embargo, si no está usando VSTO, debería poder llamar GC.Collect()y GC.WaitForPendingFinalizers()solo una vez.

Sé que llamar explícitamente GC.Collect()es un no-no (y ciertamente hacerlo dos veces suena muy doloroso), pero para ser sincero, no hay forma de evitarlo. A través de las operaciones normales, generará objetos ocultos a los que no tiene referencias que, por lo tanto, no puede liberar por ningún otro medio que no sea llamar GC.Collect().

Este es un tema complejo, pero esto es todo lo que hay que hacer. Una vez que establezca esta plantilla para su procedimiento de limpieza, puede codificar normalmente, sin necesidad de envoltorios, etc.

Tengo un tutorial sobre esto aquí:

Automatización de programas de Office con VB.Net / COM Interop

Está escrito para VB.NET, pero no se desanime por eso, los principios son exactamente los mismos que cuando se usa C #.

Mike Rosenblum
fuente
3
Puede encontrar una discusión relacionada en el foro ExtremeVBTalk .NET Office Automation, aquí: xtremevbtalk.com/showthread.php?t=303928 .
Mike Rosenblum
2
Y si todo lo demás falla, Process.Kill () puede usarse (como último recurso) como se describe aquí: stackoverflow.com/questions/51462/…
Mike Rosenblum
1
Me alegro de que funcionó Richard. :-) Y aquí hay un ejemplo sutil donde simplemente evitar "dos puntos" no es suficiente para prevenir un problema: stackoverflow.com/questions/4964663/…
Mike Rosenblum
Aquí hay una opinión sobre esto de Misha Shneerson de Microsoft sobre el tema, dentro de la fecha del comentario del 7 de octubre de 2010. ( blogs.msdn.com/b/csharpfaq/archive/2010/09/28/… )
Mike Rosenblum
Espero que esto ayude a un problema persistente que estamos teniendo. ¿Es seguro hacer la recolección de basura y liberar llamadas dentro de un bloque finalmente?
Brett Green
213

Prefacio: mi respuesta contiene dos soluciones, así que tenga cuidado al leer y no se pierda nada.

Hay diferentes formas y consejos sobre cómo hacer que la instancia de Excel se descargue, como:

  • Liberando CUALQUIER objeto com explícitamente con Marshal.FinalReleaseComObject () (sin olvidar los objetos com creados implícitamente). Para liberar cada objeto com creado, puede usar la regla de los 2 puntos mencionados aquí:
    ¿Cómo limpio correctamente los objetos de interoperabilidad de Excel?

  • Llamar a GC.Collect () y GC.WaitForPendingFinalizers () para hacer que CLR libere objetos com no utilizados * (En realidad, funciona, vea mi segunda solución para más detalles)

  • Verificando si la aplicación com-server puede mostrar un cuadro de mensaje esperando a que el usuario responda (aunque no estoy seguro de que pueda evitar que Excel se cierre, pero escuché sobre ello varias veces)

  • Enviar mensaje WM_CLOSE a la ventana principal de Excel

  • Ejecutando 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 instanciaron después de que comenzara nuestro código de interoperabilidad de Excel.

¡PERO! ¡Algunas 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 sigue ejecutándose después de que finaliza la función. Lo intenté todo! ¡Revisé a fondo toda la función 10 veces y agregué Marshal.FinalReleaseComObject () para todo! También tenía GC.Collect () y GC.WaitForPendingFinalizers (). Revisé los cuadros de mensajes ocultos. Traté de enviar el 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, porque 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 cerrará esa instancia. ¡Apuesto a que el usuario no será feliz! Entonces, honestamente, esta es una opción poco convincente (sin ofender, muchachos).Solución : elimine el proceso de Excel por hWnd de 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 arroja la excepción si el Proceso no puede ser eliminado (por ejemplo, el proceso ya no existe) , y otro método arroja la excepción si el Proceso no se eliminó. El único punto débil en este código son los permisos de seguridad. Teóricamente, el usuario puede no tener permisos para eliminar 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 ha terminado! :)

Ok, volvamos a la segunda solución, como prometí al principio de la publicación. La segunda solución es llamar a GC.Collect () y GC.WaitForPendingFinalizers (). Sí, en realidad funcionan, ¡pero debes tener cuidado aquí!
Mucha gente dice (y 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 para que GC.Collect () no sea útil es ejecutar el proyecto en modo de depuración. En el modo de depuración, los objetos a los que ya no se hace referencia realmente no se recogerán como basura hasta el final del método.
Por lo tanto, si probó GC.Collect () y GC.WaitForPendingFinalizers () y no funcionó, intente hacer lo siguiente:

1) Intente ejecutar su proyecto en modo Release y verifique si Excel se cerró correctamente

2) Envuelva el método de trabajar 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á =)

codificador nocturno
fuente
19
triste que el hilo ya es tan antiguo que su excelente respuesta parece que muy por debajo, que creo que es la única razón de que no fuera más veces upvoted ...
chiccodoro
15
Tengo que admitir que, cuando leí tu respuesta por primera vez, pensé que era un gran error. Después de aproximadamente 6 horas de lucha con esto (todo está publicado, no tengo puntos dobles, etc.), ahora creo que su respuesta es genial.
Mark
3
Gracias por esto. He estado luchando con un Excel que no se cerraría sin importar qué por un par de días antes de encontrar esto. Excelente.
BBlake
2
@nightcoder: respuesta increíble, muy detallada. Sus comentarios con respecto al modo de depuración son muy verdaderos e importantes por lo que señaló. El mismo código en modo de lanzamiento a menudo puede estar bien. Sin embargo, Process.Kill solo es bueno cuando se usa la automatización, no cuando el programa se ejecuta en Excel, por ejemplo, como un complemento COM administrado.
Mike Rosenblum
2
@DanM: Su enfoque es 100% correcto y nunca fallará. Al mantener todas sus variables locales al método, todas las referencias son efectivamente inalcanzables por el código .NET. Esto significa que cuando su sección finalmente llame a GC.Collect (), los finalizadores para todos sus objetos COM serán llamados con certeza. Cada finalizador llama a Marshal.FinalReleaseComObject sobre el objeto que se está finalizando. Su enfoque es, por lo tanto, simple e infalible. No tengas miedo de usar esto. (Solo advertencia: si usa VSTO, que dudo que lo sea, necesitaría llamar a GC.Collect () & GC.WaitForPendingFinalizers TWICE.)
Mike Rosenblum
49

ACTUALIZACIÓN : código C # agregado y enlace a trabajos de Windows

Pasé algún tiempo tratando de resolver este problema, y ​​en ese momento XtremeVBTalk era el más activo y receptivo. Aquí hay un enlace a mi publicación original, Cerrando 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()y Process.Kill()funciona en su mayor parte, pero falla si las aplicaciones se bloquean catastróficamente. Es decir, si la aplicación falla, el proceso de Excel seguirá ejecutándose.
  • La solución es dejar que el sistema operativo maneje la limpieza de sus procesos a través de objetos de trabajo de Windows utilizando llamadas Win32. Cuando su aplicación principal muere, los procesos asociados (es decir, Excel) también finalizarán.

Encontré 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 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 del constructor

  • En el constructor, info.LimitFlags = 0x2000;se llama a. 0x2000es el JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSEvalor de enumeración, y MSDN define este valor como:

Hace que todos los procesos asociados con el trabajo finalicen cuando se cierra el último identificador del trabajo.

Llamada de API Win32 adicional para obtener la ID de proceso (PID)

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

Usando el código

    Excel.Application app = new Excel.ApplicationClass();
    Job job = new Job();
    uint pid = 0;
    Win32.GetWindowThreadProcessId(new IntPtr(app.Hwnd), out pid);
    job.AddProcess(Process.GetProcessById((int)pid).Handle);
joshgo
fuente
2
Es una idea terrible matar explícitamente un servidor COM fuera de proceso (que podría estar sirviendo a otros clientes COM). Si está recurriendo a esto es porque su protocolo COM está roto. Los servidores COM no deben tratarse como una aplicación de ventana normal
MickyD
39

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 todas las referencias a un objeto COM de Excel en nulo cuando terminara con él. Esto incluía celdas, hojas y todo.

Philip Fourie
fuente
30

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.

MagicKat
fuente
3
Cómo aobut xlRange.Interior.Color por ejemplo.
HAdes
El interior debe ser liberado (está en el espacio de nombres) ... El color, por otro lado, no (porque es de System.Drawing.Color, iirc)
MagicKat
2
En realidad, el color es un color de Excel, no un color .Net. acabas de pasar un largo. También libros de trabajo def. necesitan ser lanzados, hojas de trabajo ... menos.
Anónimo tipo
Eso no es cierto, la regla de dos puntos malos y uno bueno no tiene sentido. Además, no tiene que publicar Libros de trabajo, Hojas de trabajo, Rangos, ese es el trabajo del GC. Lo único que nunca debe olvidar es llamar a Quit en el único objeto Excel.Application. Después de llamar a Quit, anule el objeto Excel.Application y llame a GC.Collect (); GC.WaitForPendingFinalizers (); dos veces.
Dietrich Baumgarten
29

Primero, nunca tiene que llamar Marshal.ReleaseComObject(...)ni Marshal.FinalReleaseComObject(...)cuando interopera con Excel. Es un antipatrón confuso, pero cualquier información sobre esto, incluida la de Microsoft, que indique que debe liberar manualmente las referencias COM de .NET es incorrecta. El hecho es que el tiempo de ejecución de .NET y el recolector de basura registran y limpian correctamente las referencias COM. Para su código, esto significa que puede eliminar todo el ciclo `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 se cierre el proceso de Excel), debe asegurarse de que se ejecuta el recolector de basura. Hace 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 agradecería un ejemplo que lo demuestre).

Tercero, cuando se ejecuta bajo el depurador, las referencias locales se mantendrán artificialmente vivas hasta el final del método (para que funcione la inspección de variables locales). Por lo tanto, las GC.Collect()llamadas no son efectivas para limpiar objetos como rng.Cellsdel mismo método. Debería dividir el código haciendo la interoperabilidad COM desde 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 Stack Overflow (¡y especialmente esta pregunta!).

Lo que finalmente me convenció de echar un vistazo más de cerca y descubrir el consejo correcto fue la publicación de blog Marshal .

Govert
fuente
55
Prácticamente TODAS LAS RESPUESTAS EN ESTA PÁGINA SON INCORRECTAS, EXCEPTO ESTA. Funcionan pero son demasiado trabajo. IGNORAR la regla "2 PUNTOS". Deje que el GC haga su trabajo por usted. Evidencia suplementaria de .Net GURU Hans Passant: stackoverflow.com/a/25135685/3852958
Alan Baljeu
1
Govert, qué alivio encontrar la forma correcta de hacerlo. Gracias.
Dietrich Baumgarten
18

Me encontré con una plantilla genérica útil que ayuda puede implementar el patrón de eliminación correcto para objetos COM, esa necesidad Marshal.ReleaseComObject llama cuando salen del ámbito de aplicación:

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:

http://www.deez.info/sengelha/2005/02/11/useful-idisposable-class-3-autoreleasecomobject/

Edward Wilde
fuente
3
Sí, este es bueno. Creo que este código puede actualizarse ahora que FinalReleaseCOMObject está disponible.
Anónimo tipo
Todavía es molesto tener un bloque de uso para cada objeto. Vea aquí stackoverflow.com/questions/2191489/… para una alternativa.
Henrik
16

No puedo creer que este problema haya atormentado al mundo durante 5 años ... Si ha creado una aplicación, primero debe cerrarla antes de eliminar el enlace.

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

al cerrar

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

Cuando nuevas aplicaciones de Excel, se abre un programa de Excel en segundo plano. Debe ordenar que se cierre ese programa de Excel antes de liberar el enlace porque ese programa de Excel no es parte de su control directo. Por lo tanto, permanecerá abierto si se libera el enlace.

Buena programación para todos ~~

Colin
fuente
No, no lo hace, especialmente si desea permitir la interacción del usuario con la aplicación COM (Excel en este caso, OP no especifica si la interacción del usuario está destinada, pero no hace referencia a cerrarla). Solo tiene que hacer que la persona que llama suelte para que el usuario pueda salir de Excel al cerrar, digamos, un solo archivo.
Downwitch
esto funcionó perfectamente para mí, se bloqueó cuando solo tenía objExcel.Quit (), pero cuando también hice objExcel.Application.Quit () de antemano, se cerró limpiamente.
mcmillab
13

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 el objeto de Excel después de nuestro trabajo en el administrador de tareas"

Okay. 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 el PID de los procesos de 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 destruido> <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.

Mohsen Afshin
fuente
1
.... este es un poco un enfoque de trineo de martillos? - es similar a lo que también estoy usando :( pero necesito cambiar. El problema con el uso de este enfoque es que, a menudo, al abrir Excel, en la máquina donde se ejecutó, tiene la alerta en la barra izquierda, que dice algo así como "Excel se cerró incorrectamente": no es muy elegante. Creo que una de las sugerencias anteriores en este hilo es preferible.
whytheq
11

Esto parece seguro que ha sido demasiado complicado. Según 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 debe tener una de todas formas; configúrela en null)

2: llamada GC.Collect()

3: Excel tiene que cerrarse, ya sea por el usuario cerrando manualmente el programa o llamando Quital objeto Excel. (Tenga en cuenta que Quitfuncionará 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 que 2, pero 3 puede suceder en cualquier momento.

Una forma de implementar esto es envolver el objeto de interoperabilidad de Excel con su propia clase, crear la instancia de interoperabilidad en el constructor e implementar IDisposable con Dispose con un aspecto similar a

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

Eso limpiará Excel del lado de las cosas de su programa. Una vez que Excel esté cerrado (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. Sin embargo, tenga en cuenta que si bien esto funciona, en realidad tiene que cerrar con gracia para que funcione. Seguirá acumulando procesos de excel.exe si interrumpe su programa antes de que se limpie Excel (generalmente presionando "detener" mientras se está depurando su programa).

Dave Cousineau
fuente
1
Esta respuesta funciona y es infinitamente más conveniente que la respuesta aceptada. No se preocupa por "dos puntos" con objetos COM, sin uso de Marshal.
andrew.cuthbert
9

Para agregar a las razones por las cuales Excel no se cierra, incluso cuando crea referencias directas a cada objeto al leer, la creación, es el ciclo '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
Siniestro
fuente
Y otra razón, reutilizar una referencia sin liberar primero el valor anterior.
Grimfort
Gracias. También estaba usando un "para cada" su solución funcionó para mí.
Chad Braun-Duin
+1 gracias. Además, tenga en cuenta que si tiene un objeto para un rango de Excel y desea cambiar el rango durante la vida útil del objeto, descubrí que tenía que ReleaseComObject antes de reasignarlo, lo que hace que el código esté un poco desordenado.
AjV Jsy
9

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 utilizará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 COM administrados que pueden manejar la limpieza que a menudo causa dolores de cabeza cuando se trabaja con Microsoft Office en .NET.

Algunas características clave son:

  • Principalmente independiente de la versión (y las características dependientes de la versión están documentadas)
  • Sin dependencias
  • No PIA
  • No hay registro
  • No VSTO

De ninguna manera estoy afiliado al proyecto; Realmente aprecio la reducción absoluta de los dolores de cabeza.

BTownTKD
fuente
2
Esto debería ser marcado como la respuesta, realmente. NetOffice abstrae toda esta complejidad.
C. Augusto Proiete
1
He estado usando NetOffice durante un tiempo considerable escribiendo un complemento de Excel y funcionó perfectamente. Lo único a tener en cuenta es que si no elimina los objetos usados ​​explícitamente, lo hará cuando salga de la aplicación (porque de todos modos los rastrea). Así que la regla de oro con NetOffice es siempre utilizar el "uso" patrón con todos los objetos de Excel como celda, rango u hoja etc
Stas Ivanov
8

La respuesta aceptada aquí es correcta, pero también tenga en cuenta que no solo se deben evitar las referencias de "dos puntos", sino también los objetos que se recuperan a través del índice. Tampoco necesita esperar hasta que haya terminado con el programa para limpiar estos objetos, es mejor crear funciones que los limpien tan pronto como haya terminado con ellos, cuando sea posible. Aquí hay una función que creé que asigna algunas propiedades de un objeto Style 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 { } 
}

Tenga en cuenta que tuve que establecer xlBorders[Excel.XlBordersIndex.xlEdgeBottom]una variable para limpiar eso (no debido a 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 Border eso necesita ser lanzado).

Este tipo de cosas no es realmente necesario en las aplicaciones estándar, que hacen un gran trabajo de limpieza después de sí mismas, pero en las aplicaciones ASP.NET, si omite incluso una de estas, no importa con qué frecuencia llame al recolector de basura, Excel lo hará todavía se está ejecutando en su servidor.

Requiere mucha atención al detalle y muchas ejecuciones de prueba mientras se monitorea el Administrador de tareas al escribir este código, pero hacerlo le ahorra la molestia de buscar desesperadamente a través de páginas de código para encontrar la instancia que se perdió. Esto es especialmente importante cuando se trabaja en bucles, donde necesita liberar CADA INSTANCIA de un objeto, a pesar de que usa el mismo nombre de variable cada vez que se repite.

Chris McGrath
fuente
Inside Release, verifica si hay Null (la respuesta de Joe). Eso evitará excepciones nulas innecesarias. He probado MUCHOS métodos. Esta es la única forma de liberar Excel de manera efectiva.
Gerhard Powell
8

Después de intentar

  1. Liberar objetos COM en orden inverso
  2. Agregar GC.Collect()y GC.WaitForPendingFinalizers()dos veces al final
  3. No más de dos puntos.
  4. Cerrar el libro de trabajo y salir de la aplicación
  5. Ejecutar en modo de liberación

la solución final que funciona para mí es mover un conjunto 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();
    }
}
DG
fuente
Descubrí que no necesitaba anular ComObjects, solo Quit () - Close () y FinalReleaseComObject. No puedo creer que esto sea todo lo que me faltaba para que funcionara. ¡Excelente!
Carol
7

Seguí esto exactamente ... Pero todavía me encontré con problemas 1 de cada 1000 veces. Quién sabe por qué. Es hora de sacar el martillo ...

Inmediatamente después de que se instancia la clase de Aplicación Excel, obtengo el proceso 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 he realizado toda la limpieza COM anterior, me aseguro de que el proceso no se esté ejecutando. Si todavía se está ejecutando, mátalo!

if (!process.HasExited)
   process.Kill();
craig.tadlock
fuente
7

¨ ° º¤ø „¸ Dispara Excel proc y mastica 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);
}
Antoine Meltzheim
fuente
6

Debe tener en cuenta que Excel también es muy sensible a la cultura en la que se está ejecutando.

Es posible que necesite establecer la cultura en EN-US antes de llamar a las funciones de Excel. Esto no se aplica a todas las funciones, pero 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 más detalles: http://support.microsoft.com/default.aspx?scid=kb;en-us;Q320369


fuente
6

"Nunca use dos puntos con objetos COM" es una gran regla general para evitar fugas de referencias COM, pero Excel PIA puede provocar fugas en más formas que aparentes 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 Aplicación.

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 referencias de cualquier objeto COM que reciba (como parámetro) para cualquiera de los controladores de eventos.

Cómo Excel PIA expone los eventos COM

Excel PIA expone los eventos COM de la clase de aplicación Excel como eventos .NET convencionales. Cada vez que el código del 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 tiene que 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 verifica 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 referencias 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). Dependen únicamente del 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 del deseado y crear una impresión de una "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 mediante la interfaz IConnectionPointContainer . La clase de aplicación (y para el caso todos los objetos COM que exponen eventos usando el mecanismo de devolución de llamada) implementa la interfaz IConnectionPointContainer.

Amit Mittal
fuente
4

Como otros han señalado, debe crear una referencia explícita para cada objeto de Excel que use y llamar a Marshal.ReleaseComObject sobre esa referencia, como se describe en este artículo de KB . También debe usar try / finally para asegurarse de que siempre se llama a ReleaseComObject, incluso cuando se produce 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 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 contenedor simple que envuelve algunas manipulaciones simples del modelo de objetos de Excel (abrir un libro de trabajo, escribir en un rango, guardar / cerrar el libro de trabajo, etc.). La clase contenedora 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 escala bien para 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 interacción con objetos COM 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.

Joe
fuente
3

Cuando todo lo anterior no funcionó, 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);
hombre araña
fuente
2
Odio las esperas arbitrarias. Pero hace +1 porque tiene razón sobre la necesidad de esperar a que se cierren los libros. Una alternativa es sondear la colección de libros de trabajo en un bucle y usar la espera arbitraria como el tiempo de espera del bucle.
dFlat
3

¡Asegúrese de liberar todos los objetos relacionados con Excel!

Pasé unas horas intentando de varias maneras. Todas son grandes ideas, pero finalmente encontré mi error: si no liberas 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 aquí .

Ned
fuente
Muy buen punto! Creo que porque estoy usando Office 2007, hay una limpieza realizada en mi nombre. He utilizado el aviso más arriba, pero no he almacenado variables como usted sugirió aquí y EXCEL.EXE se cierra, pero podría tener suerte y si tengo más problemas definitivamente veré esta parte de mi código =)
Coops
3

Un gran artículo sobre la liberación de objetos COM es 2.5 Liberación de objetos COM (MSDN).

El método que recomendaría es anular su Excel. Interoperar referencias si son variables no locales, y luego llamar GC.Collect()yGC.WaitForPendingFinalizers() dos veces. Las variables de interoperabilidad de ámbito local se ocuparán 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. Es decir, estará bastante seguro exactamente cuando se haya limpiado el objeto, al regresar de la segunda llamada a WaitForPendingFinalizers. Como alternativa, puede usar Marshal.ReleaseComObject. Sin embargo, tenga en cuenta que es muy poco probable que necesite usar este método.

Puerco
fuente
2

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

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

Debe tener mucho cuidado al usar aplicaciones de interoperabilidad de Word / Excel. Después de probar todas las soluciones, todavía 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 usando Word.ApplicationClass.Document.Open() diferentes hilos simultáneamente, ¡el proceso de trabajo de IIS (w3wp.exe) se bloquearía dejando todos los procesos de WinWord abiertos!

Así que supongo que no hay una solución absoluta para este problema, sino cambiar a otros métodos, como el desarrollo de Office Open XML .

Arvand
fuente
1

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(); }
}
Martín
fuente
1

Actualmente estoy trabajando en la automatización de Office y he encontrado una solución para esto que siempre funciona para mí. Es simple y no implica matar ningún proceso.

Parece que simplemente recorriendo los procesos activos actuales y de alguna manera 'accediendo' a un proceso abierto de Excel, se eliminará cualquier instancia perdida de Excel. El siguiente código simplemente verifica 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 anule la instancia congelada de Excel.

Ejecuto el siguiente método justo antes del complemento que estoy desarrollando para salir, ya que activa el evento de descarga. Elimina todas las instancias colgantes de Excel cada vez. Honestamente, no estoy completamente seguro de por qué esto funciona, 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 eliminación. Estaría muy interesado en cualquier sugerencia de 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;
                    }
                }
            }
}
Tom Brearley
fuente
1

Creo que algo de eso es solo la forma en que el marco maneja las aplicaciones de Office, pero podría estar equivocado. Algunos días, algunas aplicaciones limpian los procesos de inmediato, y otros días parece esperar hasta que se cierra la aplicación. En general, dejé de prestar atención a los detalles y me aseguré de que no haya procesos adicionales flotando al final del día.

Además, y tal vez haya terminado de simplificar 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 estoy siendo paranoico al respecto.

bill_the_loser
fuente
1

Como algunos probablemente ya hayan escrito, no solo es importante cómo cerrar el Excel (objeto); También es importante cómo 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, analizarlo en función de los valores dentro de una lista genérica.

Puse todas las funciones relacionadas con Excel en la clase base, y analizador en una subclase (diferentes analizadores utilizan funciones comunes de Excel). No quería que Excel se abriera y cerrara nuevamente 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í mover el código para abrir Excel a una subclase. En lugar de abrir solo una vez, ahora creo un nuevo objeto (clase base) y abro Excel para cada elemento y lo cierro al final. Hay algunas penalizaciones de rendimiento, pero en función de varias pruebas, los procesos de Excel se están cerrando sin problemas (en 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.

Blaz Brencic
fuente