Pregunta

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

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

Aunque este tipo de trabajos, el Excel.exe El proceso todavía está en segundo plano incluso después de cerrar Excel.Solo se publica una vez que mi aplicación se cierra manualmente.

¿Qué estoy haciendo mal o existe una alternativa para garantizar que los objetos de interoperabilidad se eliminen correctamente?

¿Fue útil?

Solución

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 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 Hojas de trabajo que no fue liberado por mi código (porque no estaba al tanto de ello) 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 POST MORTEM:

Quiero que cada lector lea esta respuesta de Hans Passant con mucho cuidado, ya que explica la trampa en la que yo y muchos otros desarrolladores nos topamos. 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 sin cambios por el bien de la historia, pero lea este enlace y no siga el camino de & Quot; los dos puntos & Quot ;: Comprensión de la recolección de basura en .NET y Limpiar objetos de interoperabilidad de Excel con IDisposable

Otros consejos

De hecho, puedes liberar tu objeto de aplicación de Excel limpiamente, pero debes 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, lamentablemente, muy difícil de gestionar en la práctica.Si alguna vez uno se desliza en alguna parte y usa "dos puntos", o itera celdas a través de un for each bucle, o cualquier otro tipo de comando similar, entonces tendrá objetos COM sin referencia y correrá el riesgo de bloquearse.En este caso, no habría manera de encontrar la causa en el código;Tendría que revisar todo su código a ojo y, con suerte, encontrar la causa, una tarea que podría ser casi imposible para un proyecto grande.

La buena noticia es que en realidad no es necesario mantener una referencia de variable con nombre para cada objeto COM que utilice.En lugar de eso, llama GC.Collect() y luego GC.WaitForPendingFinalizers() para liberar todos los objetos (generalmente menores) a los que no tiene una referencia, y luego liberar explícitamente los objetos a los que sí tiene 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 el objeto de aplicación de Excel.

Por ejemplo, suponiendo que tuviera una variable de objeto Rango llamada xlRng, una variable de hoja de cálculo denominada xlSheet, una variable de libro llamada xlBook y una variable de aplicación de Excel denominada xlApp, entonces su código de limpieza podría verse así:

// 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, el GC.Collect() y GC.WaitForPendingFinalizers() las llamadas 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.Estos objetos no se liberarían hasta que próximo recolección de basura.Sin embargo, si no está utilizando VSTO, debería poder llamar GC.Collect() y GC.WaitForPendingFinalizers() sólo una vez.

Sé que llamar explícitamente GC.Collect() es un no-no (y ciertamente hacerlo dos veces suena muy doloroso), pero, para ser honesto, no hay forma de evitarlo.A través de operaciones normales generarás objetos ocultos a los que no tienes ninguna referencia y que, por lo tanto, no puedes 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, podrá 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 te desanimes, los principios son exactamente los mismos que cuando se usa C#.

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:

  • Liberación de CADA objeto com explícitamente con Marshal.FinalReleaseComObject () (sin olvidar implícitamente Com-objetos creados). Liberar cada objeto com creado, puede usar la regla de 2 puntos mencionada aquí:
    ¿Cómo limpio correctamente la interoperabilidad de Excel? objetos?

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

  • Comprobando si com-server-application tal vez muestra un cuadro de mensaje esperando que el usuario responda (aunque no soy Seguro que puede evitar que Excel cerrando, pero escuché sobre esto veces)

  • Enviando mensaje WM_CLOSE al principal Ventana 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 AppDomain sea descargado.

  • Matar todas las instancias de Excel que se instanciaron después de que comenzara nuestro código de interoperabilidad 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 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). Así que pasé un par de horas antes de encontrar una buena (en mi humilde opinión) solución : Eliminar el proceso de Excel por 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 no existe más), 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 comienzo de la publicación. La segunda solución es llamar a GC.Collect () y GC.WaitForPendingFinalizers (). Sí, realmente 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.
Entonces, si probó GC.Collect () y GC.WaitForPendingFinalizers () y no ayudó, 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();
  }
}

escribes:

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á =)

ACTUALIZAR:Se agregó código C# 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, Cerrar un proceso de Excel Interop 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.

  • Cerrando el proceso de Interop con Application.Quit() y Process.Kill() funciona en su mayor parte, pero falla si las aplicaciones fallan catastróficamente.Es decir.Si la aplicación falla, el proceso de Excel seguirá funcionando libremente.
  • La solución es dejar que el sistema operativo se encargue de la limpieza de sus procesos a través de Objetos de trabajo de Windows usando llamadas Win32.Cuando su aplicación principal muere, los procesos asociados (es decir,Excel) también será cancelado.

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 registro el proceso de Excel.

Código de trabajo de Windows

Envuelve las llamadas 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 del constructor

  • En el constructor, el info.LimitFlags = 0x2000; se llama. 0x2000 es el JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE valor de enumeración, y este valor está definido por MSDN como:

Hace que todos los procesos asociados con el trabajo terminen cuando el último manejo del trabajo está cerrado.

Llamada adicional a la API de Win32 para obtener el ID del 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);

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 cada referencia a un objeto COM de Excel como nulo cuando haya terminado con él. Esto incluía celdas, hojas y todo.

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.

Primero tú nunca tengo que llamar Marshal.ReleaseComObject(...) o Marshal.FinalReleaseComObject(...) al realizar la interoperabilidad de Excel.Es un antipatrón confuso, pero cualquier información al respecto, incluida la de Microsoft, que indique que debe liberar manualmente las referencias COM desde .NET es incorrecta.El hecho es que el tiempo de ejecución de .NET y el recolector de basura realizan un seguimiento y limpian correctamente 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 ejecute el recolector de basura.Esto lo haces 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 muestre esto).

En tercer lugar, cuando se ejecuta bajo el depurador, las referencias locales se mantendrán vivas artificialmente hasta el final del método (para que la inspección de variables locales funcione).Entonces GC.Collect() Las llamadas no son efectivas para limpiar objetos como rng.Cells del mismo método.Debe dividir el código que realiza 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 entonces:

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 tema, incluidas muchas publicaciones en MSDN y Stack Overflow (¡y especialmente esta pregunta!).

Lo que finalmente me convenció de mirar más de cerca y encontrar el consejo correcto fue la publicación del blog. Marshal.ReleaseComObject considerado peligroso junto con encontrar el problema con las referencias mantenidas activas bajo el depurador que confundía mis pruebas anteriores.

I encontrado a útil plantilla genérica que puede ayudar a implementar el patrón de eliminación correcto para los objetos COM, que necesitan llamar a Marshal.ReleaseComObject cuando salen del 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();
    } 
}

Plantilla :

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 / Helpful-idisposable-class-3-autoreleasecomobject /

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 a ese programa de Excel que se cierre 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 ~~

Desarrolladores comunes, ninguna de sus soluciones funcionó para mí, así que decido implementar un nuevo truco .

Primero, especifique " ¿Cuál es nuestro objetivo? " = > & "; No ver el objeto de Excel después de nuestro trabajo en el administrador de tareas &";

Ok. 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 Excel creado y simplemente destruir ese & Gt;

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.

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 quedan referencias a la aplicación de Excel que creó (solo debe tener una de todas formas; configúrela en null)

2: llame a GC.Collect()

3: Excel debe cerrarse, ya sea por el usuario cerrando manualmente el programa o llamando al Quit en el objeto Excel. (Tenga en cuenta que GC.WaitForPendingFinalizers() 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 tendrá 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 llamando a <=>), el proceso desaparecerá. Si el programa ya se ha cerrado, el proceso desaparecerá en la llamada <=>.

(No estoy seguro de lo importante que es, pero es posible que desee una llamada <=> después de la 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 excel.exe si interrumpe su programa antes de que se limpie Excel (generalmente presionando & Quot; stop & Quot; mientras su programa se está depurando).

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

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 & Quot; NetOffice & Quot; biblioteca.

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 al trabajar 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
  • Sin registro
  • No VSTO

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

La respuesta aceptada aquí es correcta, pero también tenga en cuenta que no solo " two dot " deben evitarse las referencias, pero 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] en 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 to es en realidad un objeto Border que necesita ser liberado).

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 seguirá ejecutándose 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.

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 y salir de la aplicación
  5. Ejecutar en modo de lanzamiento

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

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

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

if (!process.HasExited)
   process.Kill();

& # 168; & # 176; & # 186; & # 164; & # 248; & # 8222; & # 184; Dispara el proceso de Excel y mastica chicle & # 184; & # 8222; & # 248; & # 164; & # 186; & # 176; & # 168;

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

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

Es posible que necesite configurar 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

" Nunca use dos puntos con objetos COM " es una gran regla general para evitar fugas de referencias COM, pero Excel PIA puede provocar fugas de una manera más que aparente 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 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 CLR recolector de basura para liberar los objetos COM.

Dado que la ejecución del 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 PIA & # 8217; s 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 usando 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.

Como otros han señalado, debe crear una referencia explícita para cada objeto de Excel que utilice 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 wrapper 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 de ActiveX desde su aplicación .NET, y las cosas serán mucho más fáciles, ya que solo necesitará liberar esta referencia.

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

¡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 como 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í .

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

Debe tener mucho cuidado al usar aplicaciones de interoperabilidad de Word / Excel. Después de probar todas las soluciones, todavía teníamos un montón de & "; WinWord &"; proceso dejado 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() en diferentes hilos simultáneamente, ¡el proceso de trabajo de IIS (w3wp.exe) se bloqueará 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 Desarrollo XML.

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

El método que yo 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 Interop de ámbito local se gestionará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 provienen directamente del artículo:

En casi todas las situaciones, anular la referencia RCW y forzar una recolección de basura se limpiará adecuadamente.Si también llama a GC.WaitForPendingFinalizers, la recolección de basura será lo más determinista posible.Es decir, estará bastante seguro exactamente de cuándo se ha limpiado el objeto: al regresar de la segunda llamada a WaitForPendingFinalizers.Como alternativa, puede utilizar Marshal.ReleaseComObject.Sin embargo, tenga en cuenta que es muy poco probable que alguna vez necesite utilizar este método.

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

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

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

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 solo estoy paranoico con eso.

Como algunos probablemente ya hayan escrito, no solo es importante cómo cierra el Excel (objeto); También es importante cómo abrirlo y también según 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 un valor de parámetro diferente, p. 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é 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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top