it-swarm.com.de

Wie kann ich Excel-Interop-Objekte ordnungsgemäß bereinigen?

Ich verwende das Excel-Interop in C # (ApplicationClass) und habe den folgenden Code in meine finally-Klausel eingefügt:

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

Obwohl dies so funktioniert, ist der Excel.exe-Prozess auch nach dem Schließen von Excel immer noch im Hintergrund. Sie wird erst freigegeben, wenn meine Anwendung manuell geschlossen wird.

Was mache ich falsch oder gibt es eine Alternative, um sicherzustellen, dass Interop-Objekte ordnungsgemäß entsorgt werden?

696
HAdes

Excel wird nicht beendet, da Ihre Anwendung immer noch Verweise auf COM-Objekte enthält.

Ich schätze, Sie rufen mindestens ein Member eines COM-Objekts auf, ohne es einer Variablen zuzuweisen.

Für mich war es das excelApp.Worksheets-Objekt, das ich direkt verwendet habe, ohne es einer Variablen zuzuweisen:

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

Ich wusste nicht, dass C # intern einen Wrapper für das COM-Objekt Worksheets erstellt hat, das nicht durch meinen Code freigegeben wurde (weil ich es nicht wusste) und dass Excel nicht entladen wurde.

Ich habe die Lösung für mein Problem auf dieser Seite gefunden, die auch eine Nice-Regel für die Verwendung von COM-Objekten in C # hat:

Verwenden Sie niemals zwei Punkte mit COM-Objekten.


Mit diesem Wissen ist der richtige Weg, um das oben Genannte zu tun:

Worksheets sheets = excelApp.Worksheets; // <-- The important part
Worksheet sheet = sheets.Open(...);
...
Marshal.ReleaseComObject(sheets);
Marshal.ReleaseComObject(sheet);

POST MORTEM UPDATE:

Ich möchte, dass jeder Leser diese Antwort von Hans Passant sehr sorgfältig liest, da er die Falle erklärt, in die ich und viele andere Entwickler gestoßen sind. Als ich vor Jahren diese Antwort schrieb, wusste ich nicht, welche Auswirkungen der Debugger auf den Garbage-Collector hat, und zog die falschen Schlussfolgerungen. Ich halte meine Antwort aus Gründen der Geschichte unverändert, aber lesen Sie bitte diesen Link und Don't gehen Sie den Weg der "zwei Punkte": Garbage Collection in .NET verstehen und Excel Interop bereinigen Objekte mit IDisposable

656
VVS

Sie können Ihr Excel-Anwendungsobjekt tatsächlich sauber freigeben, müssen jedoch aufpassen. 

Die Empfehlung, für jedes COM-Objekt, auf das Sie zugreifen, eine benannte Referenz beizubehalten und diese explizit über Marshal.FinalReleaseComObject() freizugeben, ist theoretisch korrekt, aber in der Praxis leider sehr schwer zu verwalten. Wenn Sie jemals irgendwo ausrutschen und "zwei Punkte" verwenden oder Zellen über eine for each-Schleife oder einen anderen ähnlichen Befehlstyp durchlaufen, haben Sie nicht referenzierte COM-Objekte und riskieren einen Hang. In diesem Fall gibt es keine Möglichkeit, die Ursache im Code zu finden. Sie müssten Ihren gesamten Code mit dem Auge überprüfen und hoffentlich die Ursache finden, eine Aufgabe, die für ein großes Projekt nahezu unmöglich sein könnte.

Die gute Nachricht ist, dass Sie nicht wirklich eine benannte Variablenreferenz für jedes verwendete COM-Objekt verwalten müssen. Rufen Sie stattdessen GC.Collect() und dann GC.WaitForPendingFinalizers() auf, um alle (in der Regel untergeordneten) Objekte freizugeben, für die Sie keine Referenz besitzen, und geben Sie dann explizit die Objekte frei, für die Sie eine benannte Variablenreferenz enthalten. 

Sie sollten auch Ihre benannten Verweise in umgekehrter Reihenfolge freigeben: zuerst Bereichsobjekte, dann Arbeitsblätter, Arbeitsmappen und schließlich Ihr Excel-Anwendungsobjekt.

Angenommen, Sie hatten eine Range-Objektvariable mit dem Namen xlRng, eine Arbeitsblattvariable mit dem Namen xlSheet, eine Workbook-Variable mit dem Namen xlBook und eine Excel-Anwendungsvariable mit dem Namen xlApp, dann könnte Ihr Bereinigungscode wie folgt aussehen:

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

In den meisten Codebeispielen, die Sie zum Bereinigen von COM-Objekten von .NET sehen, werden die Aufrufe GC.Collect() und GC.WaitForPendingFinalizers() ZWEIMAL wie folgt gemacht:

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

Dies sollte jedoch nicht erforderlich sein, es sei denn, Sie verwenden Visual Studio Tools für Office (VSTO), das Finalizer verwendet, die bewirken, dass ein ganzer Diagramm von Objekten in der Finalisierungswarteschlange hochgestuft wird. Solche Objekte werden erst in der Garbage Collection next freigegeben. Wenn Sie jedoch nicht VSTO verwenden, sollten Sie GC.Collect() und GC.WaitForPendingFinalizers() nur einmal aufrufen können.

Ich weiß, dass das explizite Aufrufen von GC.Collect() ein Nein-Nein ist (und das zweifach zu tun, hört sich sehr schmerzhaft an), aber es gibt keinen Weg, um ehrlich zu sein. Im normalen Betrieb generieren Sie versteckte Objekte, auf die Sie keinen Verweis haben, den Sie daher nicht auf andere Weise freigeben können, als den Aufruf von GC.Collect().

Dies ist ein komplexes Thema, aber das ist wirklich alles, was dazu gehört. Wenn Sie diese Vorlage für Ihre Bereinigungsprozedur festgelegt haben, können Sie normal codieren, ohne dass Wrapper usw. erforderlich sind. :-)

Ich habe hier ein Tutorial dazu:

Automatisieren von Office-Programmen mit VB.Net/COM Interop

Es ist für VB.NET geschrieben, aber lassen Sie sich nicht davon abschrecken, die Prinzipien sind genau die gleichen wie bei der Verwendung von C #.

268
Mike Rosenblum

Vorwort: Meine Antwort enthält zwei Lösungen. Seien Sie also beim Lesen vorsichtig und verpassen Sie nichts.

Es gibt verschiedene Möglichkeiten und Ratschläge, wie das Entladen einer Excel-Instanz durchgeführt werden kann, z. 

  • Jedes COM-Objekt explizit Mit Marshal.FinalReleaseComObject () Freigeben (nicht implizit vergessen.... Erstellte Com-Objekte). Um Jedes erstellte Com-Objekt freizugeben, können Sie Die hier erwähnte Regel von 2 Punkten verwenden:
    Wie bereinige ich Excel-Interop-Objekte ordnungsgemäß?

  • Aufruf von GC.Collect () und GC.WaitForPendingFinalizers () zur Erstellung von CLR-Freigabe nicht verwendeter Com-Objekte * (Eigentlich funktioniert es, siehe meine zweite Lösung für Details)

  • Wenn Sie prüfen, ob die COM-Server-Anwendung Möglicherweise ein Meldungsfeld anzeigt, das auf den Benutzer ___ wartet, um zu antworten (obwohl ich nicht Sicher bin, kann dies verhindern, dass Excel Schließt. Ich habe jedoch einige davon gehört mal)

  • Senden einer WM_CLOSE-Nachricht an das Hauptfenster von Excel

  • Ausführen der Funktion, die mit Excel in einer separaten AppDomain ausgeführt wird Einige Leute glauben, dass die Excel-Instanz Geschlossen wird, wenn AppDomain Entladen ist.

  • Töten Sie alle Excel-Instanzen, die nach dem Start unseres Excel-Interoping-Codes instanziiert wurden.

ABER! Manchmal helfen all diese Optionen nicht oder sind nicht geeignet!

Zum Beispiel habe ich gestern herausgefunden, dass in einer meiner Funktionen (die mit Excel funktioniert) Excel nach dem Ende der Funktion weiterläuft. Ich habe alles versucht! Ich habe die gesamte Funktion 10 Mal gründlich überprüft und Marshal.FinalReleaseComObject () für alles hinzugefügt! Ich hatte auch GC.Collect () und GC.WaitForPendingFinalizers (). Ich habe nach versteckten Nachrichtenfeldern gesucht. Ich habe versucht, die WM_CLOSE-Nachricht an das Hauptfenster von Excel zu senden. Ich habe meine Funktion in einer separaten AppDomain ausgeführt und diese Domäne entladen. Nichts hat geholfen! Die Option zum Schließen aller Excel-Instanzen ist ungeeignet, da der Benutzer, wenn er eine andere Excel-Instanz manuell startet, während die Ausführung meiner Funktion, die auch mit Excel arbeitet, manuell startet, diese Instanz auch von meiner Funktion geschlossen wird. Ich wette, der Benutzer wird nicht glücklich sein! Also, ehrlich gesagt, ist dies eine lahme Option (keine Beleidigung, Jungs). Ich verbrachte also ein paar Stunden, bevor ich eine gute (meiner bescheidenen Meinung nach) gefunden hatte: Lösung: Kill Excel-Prozess durch hWnd des Hauptfensters (es ist die erste Lösung).

Hier ist der einfache Code:

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

Wie Sie sehen, habe ich zwei Methoden bereitgestellt, entsprechend dem Try-Parse-Muster (ich denke, dass es hier angebracht ist): Eine Methode löst keine Ausnahme aus, wenn der Prozess nicht beendet werden konnte (beispielsweise existiert der Prozess nicht mehr). , und eine andere Methode löst die Ausnahme aus, wenn der Prozess nicht beendet wurde. Die einzige Schwachstelle in diesem Code sind Sicherheitsberechtigungen. Theoretisch hat der Benutzer möglicherweise keine Berechtigungen, um den Prozess zu beenden, aber in 99,99% aller Fälle hat der Benutzer solche Berechtigungen. Ich habe es auch mit einem Gastkonto getestet - es funktioniert perfekt.

So kann Ihr Code, der mit Excel arbeitet, folgendermaßen aussehen:

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 ist beendet! :)

Ok, lass uns zur zweiten Lösung zurückgehen, wie ich es am Anfang des Beitrags versprochen habe .Die zweite Lösung ist, GC.Collect () und GC.WaitForPendingFinalizers ().) Aufzurufen.Ja eigentlich funktionieren, aber hier muss man vorsichtig sein!
Viele Leute sagen (und ich sagte), dass das Aufrufen von GC.Collect () nicht hilft. Aber es hilft nichts, wenn noch Verweise auf COM-Objekte vorhanden sind! Einer der häufigsten Gründe, warum GC.Collect () nicht hilfreich ist, ist das Ausführen des Projekts im Debug-Modus. Im Debug-Modus werden Objekte, auf die nicht mehr wirklich verwiesen wird, bis zum Ende der Methode nicht gesammelt.
Wenn Sie also GC.Collect () und GC.WaitForPendingFinalizers () ausprobiert haben und dies nicht geholfen hat, versuchen Sie Folgendes: 

1) Versuchen Sie, Ihr Projekt im Freigabemodus auszuführen, und überprüfen Sie, ob Excel ordnungsgemäß geschlossen wurde 

2) Umfassen Sie die Methode der Arbeit mit Excel in einer separaten Methode . Statt so etwas wie folgt:

void GenerateWorkbook(...)
{
  ApplicationClass xl;
  Workbook xlWB;
  try
  {
    xl = ...
    xlWB = xl.Workbooks.Add(...);
    ...
  }
  finally
  {
    ...
    Marshal.ReleaseComObject(xlWB)
    ...
    GC.Collect();
    GC.WaitForPendingFinalizers();
  }
}

du schreibst:

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

Nun wird Excel geschlossen =)

202
nightcoder

UPDATE: C # -Code hinzugefügt und Link zu Windows-Jobs

Ich habe einige Zeit damit verbracht, dieses Problem zu klären, und zu dieser Zeit war XtremeVBTalk das aktivste und ansprechbarste. Hier ist ein Link zu meinem ursprünglichen Beitrag, Schließen Sie einen Excel-Interop-Prozess sauber, auch wenn Ihre Anwendung abstürzt . Nachfolgend finden Sie eine Zusammenfassung des Beitrags und den in diesen Beitrag kopierten Code. 

  • Das Schließen des Interop-Prozesses mit Application.Quit() und Process.Kill() funktioniert größtenteils, schlägt jedoch fehl, wenn die Anwendungen katastrophal abstürzen. D.h. Wenn die App abstürzt, läuft der Excel-Prozess weiterhin lose.
  • Die Lösung besteht darin, dass das Betriebssystem die Bereinigung Ihrer Prozesse über Windows-Auftragsobjekte mit Win32-Aufrufen erledigt. Wenn Ihre Hauptanwendung stirbt, werden auch die zugehörigen Prozesse (z. B. Excel) beendet. 

Ich habe festgestellt, dass dies eine saubere Lösung ist, da das Betriebssystem eine echte Aufräumungsarbeit leistet. Alles was Sie tun müssen, ist registrieren den Excel-Prozess.

Windows-Auftragscode

Schließt die Win32-API-Aufrufe ein, um Interop-Prozesse zu registrieren.

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

}

Hinweis zum Konstruktorcode

  • Im Konstruktor wird der info.LimitFlags = 0x2000; aufgerufen. 0x2000 ist der en_wert von JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, und dieser Wert wird durch MSDN als definiert:

Bewirkt, dass alle mit dem Job verknüpften Prozesse beendet werden, wenn der Der letzte Griff des Jobs ist geschlossen.

Extra Win32 API-Aufruf zum Abrufen der Prozess-ID (PID)

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

Verwenden des Codes

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

Dies funktionierte für ein Projekt, an dem ich arbeitete:

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

Wir haben gelernt, dass es wichtig war, every auf ein Excel COM-Objekt auf null zu setzen, wenn Sie damit fertig waren. Dies beinhaltete Zellen, Blätter und alles.

35
Philip Fourie

Alles, was sich im Excel-Namespace befindet, muss freigegeben werden. Zeitraum

Sie können nicht tun:

Worksheet ws = Excel.WorkBooks[1].WorkSheets[1];

Du musst es tun

Workbooks books = Excel.WorkBooks;
Workbook book = books[1];
Sheets sheets = book.WorkSheets;
Worksheet ws = sheets[1];

gefolgt von der Freigabe der Objekte.

29
MagicKat

Ich found eine nützliche generische Vorlage, die helfen kann, das korrekte Entsorgungsmuster für COM-Objekte zu implementieren, die Marshal.ReleaseComObject aufrufen müssen, wenn sie den Gültigkeitsbereich verlassen:

Verwendungszweck:

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

Vorlage:

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

Referenz:

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

18
Edward Wilde

Zuerst müssen Sie neverMarshal.ReleaseComObject(...) oder Marshal.FinalReleaseComObject(...) aufrufen, wenn Sie Excel-Interop ausführen. Es ist ein verwirrendes Anti-Pattern, aber alle Informationen dazu, einschließlich von Microsoft, die besagen, dass Sie COM-Referenzen manuell aus .NET freigeben müssen, ist falsch. Tatsache ist, dass die .NET-Laufzeitumgebung und der Garbage-Collector COM-Verweise korrekt verfolgen und bereinigen. Für Ihren Code bedeutet dies, dass Sie die gesamte `while (...) - Schleife oben entfernen können.

Zweitens: Wenn Sie sicherstellen möchten, dass die COM-Verweise auf ein Out-of-Process-COM-Objekt bereinigt werden, wenn Ihr Prozess endet (damit der Excel-Prozess geschlossen wird), müssen Sie sicherstellen, dass der Garbage Collection-Kollektor ausgeführt wird. Das machen Sie mit Aufrufen von GC.Collect() und GC.WaitForPendingFinalizers() richtig. Ein zweimaliges Aufrufen ist sicher und stellt sicher, dass die Zyklen definitiv aufgeräumt werden (obwohl ich nicht sicher bin, ob dies erforderlich ist, und ich würde ein Beispiel schätzen, das dies zeigt).

Drittens werden lokale Verweise, wenn sie unter dem Debugger ausgeführt werden, bis zum Ende der Methode künstlich am Leben erhalten (so dass die Überprüfung lokaler Variablen funktioniert). GC.Collect()-Aufrufe sind also nicht wirksam, um Objekte wie rng.Cells von derselben Methode zu entfernen. Sie sollten den Code für das COM-Interop von der GC-Bereinigung in separate Methoden aufteilen. (Dies war eine Schlüsselentdeckung für mich aus einem Teil der Antwort, die @nightcoder hier veröffentlicht hat.)

Das allgemeine Muster wäre also:

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

Es gibt eine Menge falscher Informationen und Verwirrung über dieses Problem, einschließlich vieler Posts in MSDN und Stack Overflow (und insbesondere dieser Frage!).

Was mich schließlich überzeugte, genauer hinzuschauen und den richtigen Rat zu finden, war der Blogbeitrag Marshal.ReleaseComObject Considered Dangerous zusammen mit dem Finden des Problems, dessen Referenzen unter dem Debugger lebendig waren, der meine früheren Tests verwirrte.

16
Govert

Ich kann nicht glauben, dass dieses Problem die Welt seit 5 Jahren verfolgt hat ... Wenn Sie eine Anwendung erstellt haben, müssen Sie sie zuerst herunterfahren, bevor Sie den Link entfernen.

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

beim Schließen

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

Wenn Sie eine Excel-Anwendung neu erstellen, wird ein Excel-Programm im Hintergrund geöffnet. Sie müssen dieses Excel-Programm zum Beenden anweisen, bevor Sie die Verknüpfung freigeben, da dieses Excel-Programm nicht Teil Ihrer direkten Kontrolle ist. Daher bleibt es offen, wenn der Link freigegeben wird!

Gute Programmierung alle ~~

15
Colin

Herkömmliche Entwickler, keine Ihrer Lösungen hat für mich funktioniert, also entscheide ich mich, einen neuen trick einzuführen.

Lassen Sie uns zunächst "Was ist unser Ziel?" => "Excel-Objekt nach dem Job im Task-Manager nicht sehen" 

OK. Lassen Sie Nein herausfordern und beginnen, es zu zerstören, denken Sie jedoch daran, andere parallele Instanzen os Excel nicht zu zerstören.

Holen Sie sich also die Liste der aktuellen Prozessoren und holen Sie die PID von Excel-Prozessen ab. Sobald Ihre Aufgabe abgeschlossen ist, haben wir einen neuen Gast in der Prozessliste mit einer eindeutigen PID. Finden und zerstören Sie genau diesen.

<Denken Sie daran, dass neue Excel-Prozesse während Ihres Excel-Jobs als neu erkannt und zerstört werden.> <Eine bessere Lösung ist, die PID eines neu erstellten Excel-Objekts zu erfassen und diese einfach zu zerstören

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

Das löst mein Problem, ich hoffe auch Ihr.

12
Mohsen Afshin

Dies scheint sicher zu kompliziert zu sein. Aus meiner Erfahrung gibt es nur drei wichtige Punkte, damit Excel richtig geschlossen wird:

1: Stellen Sie sicher, dass keine Verweise auf die von Ihnen erstellte Excel-Anwendung vorhanden sind (Sie sollten sowieso nur eine haben; setzen Sie sie auf null).

2: Aufruf GC.Collect()

3: Excel muss geschlossen werden, entweder durch den Benutzer, der das Programm manuell schließt, oder indem Sie Quit für das Excel-Objekt aufrufen. (Beachten Sie, dass Quit so funktioniert, als würde der Benutzer versuchen, das Programm zu schließen, und zeigt ein Bestätigungsdialogfeld an, wenn ungespeicherte Änderungen vorhanden sind, auch wenn Excel nicht sichtbar ist. Der Benutzer könnte Abbrechen drücken, und Excel wurde nicht geschlossen .)

1 muss vor 2 geschehen, 3 können jedoch jederzeit vorkommen.

Eine Möglichkeit, dies zu implementieren, besteht darin, das Interop-Excel-Objekt mit einer eigenen Klasse zu umschließen, die Interop-Instanz im Konstruktor zu erstellen und IDisposable mit Dispose zu implementieren

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

Das reinigt Excel von der Seite Ihres Programms. Nach dem Schließen von Excel (manuell vom Benutzer oder durch Aufrufen von Quit) wird der Prozess abgebrochen. Wenn das Programm bereits geschlossen wurde, verschwindet der Prozess beim Aufruf von GC.Collect().

(Ich bin nicht sicher, wie wichtig es ist, aber Sie möchten möglicherweise einen GC.WaitForPendingFinalizers()-Aufruf nach dem GC.Collect()-Aufruf, es ist jedoch nicht unbedingt erforderlich, den Excel-Prozess zu löschen.)

Das hat bei mir jahrelang ohne Probleme funktioniert. Denken Sie jedoch daran, dass Sie, während dies funktioniert, tatsächlich anmutig schließen müssen, damit es funktioniert. Sie erhalten immer noch akkumulierende Excel.exe-Prozesse, wenn Sie Ihr Programm unterbrechen, bevor Excel bereinigt wird (normalerweise durch Drücken von "Stop", während das Programm debuggt wird).

10
Dave Cousineau

Um Gründe hinzuzufügen, warum Excel nicht geschlossen wird, selbst wenn Sie beim Lesen direkte Verweise auf jedes Objekt erstellen, ist das Erstellen die For-Schleife.

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
9
Grimfort

Nach dem Versuch

  1. COM-Objekte in umgekehrter Reihenfolge freigeben
  2. Fügen Sie am Ende zweimal GC.Collect() und GC.WaitForPendingFinalizers() hinzu
  3. Nicht mehr als zwei Punkte
  4. Schließen Sie die Arbeitsmappe und beenden Sie die Anwendung
  5. Im Freigabemodus ausführen

die letzte Lösung, die für mich funktioniert, besteht darin, einen Satz von zu verschieben

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

dass wir am Ende der Funktion einen Wrapper hinzugefügt haben, wie folgt:

private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}
9
D.G.

Die akzeptierte Antwort hier ist korrekt, beachte aber auch, dass nicht nur "zwei Punkte" -Verweise vermieden werden müssen, sondern auch Objekte, die über den Index abgerufen werden. Sie müssen auch nicht warten, bis Sie mit dem Programm fertig sind, um diese Objekte zu bereinigen. Am besten erstellen Sie Funktionen, die sie bereinigen, sobald Sie mit ihnen fertig sind, wenn möglich. Hier ist eine von mir erstellte Funktion, die einige Eigenschaften eines Style-Objekts namens xlStyleHeader zuweist:

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

Beachten Sie, dass ich xlBorders[Excel.XlBordersIndex.xlEdgeBottom] auf eine Variable setzen musste, um das zu bereinigen Objekt, das freigegeben werden muss).

In Standardanwendungen, die eine große Aufräumaktion nach sich selbst erledigen, ist dies nicht unbedingt erforderlich. Wenn Sie jedoch auch nur eine dieser Anwendungen in ASP.NET-Anwendungen verpassen, wird dies, egal wie oft Sie den Garbage Collector verwenden, Excel noch auf Ihrem Server laufen. 

Es erfordert viel Aufmerksamkeit für Details und viele Testausführungen, während der Task-Manager beim Schreiben dieses Codes überwacht wird. Dies erspart Ihnen jedoch die Mühe, durch Code-Seiten verzweifelt nach der einen fehlenden Instanz zu suchen. Dies ist besonders wichtig, wenn Sie in Schleifen arbeiten, in denen Sie JEDES INSTANZ eines Objekts freigeben müssen, obwohl es bei jeder Schleife den gleichen Variablennamen verwendet.

9
Chris McGrath

Ich bin traditionell dem Rat in VVSs Antwort gefolgt. Um diese Antwort jedoch mit den neuesten Optionen auf dem neuesten Stand zu halten, denke ich, dass alle meine zukünftigen Projekte die "NetOffice" -Bibliothek verwenden werden.

NetOffice ist ein vollständiger Ersatz für die Office-PIAs und ist vollständig versionsunabhängig. Es handelt sich um eine Sammlung verwalteter COM-Wrapper, die mit der Bereinigung umgehen kann, die bei der Arbeit mit Microsoft Office in .NET häufig zu Kopfschmerzen führt.

Einige Hauptmerkmale sind:

  • Meistens versionsunabhängig (und versionsabhängige Features sind dokumentiert)
  • Keine Abhängigkeiten
  • Kein PIA
  • Keine Registration
  • Kein VSTO

Ich bin in keiner Weise mit dem Projekt verbunden. Ich schätze einfach die starke Verringerung der Kopfschmerzen.

8
BTownTKD

Ich habe das genau verfolgt ... Aber ich habe immer noch Probleme mit einem von 1000 Punkten. Wer weiß warum? Zeit, den Hammer herauszubringen ...

Direkt nachdem die Excel-Anwendungsklasse instanziiert wurde, kann ich den soeben erstellten Excel-Prozess abrufen.

Excel = new Microsoft.Office.Interop.Excel.Application();
var process = Process.GetProcessesByName("Excel").OrderByDescending(p => p.StartTime).First();

Sobald ich alle oben genannten COM-Bereinigungen durchgeführt habe, stelle ich sicher, dass der Prozess nicht läuft. Wenn es noch läuft, töte es!

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

¨ ° º¤ø „¸ Schießen Sie Excel Proc und kauen Kaugummi ¸„ ø¤º ° ¨

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

"Verwenden Sie niemals zwei Punkte mit COM-Objekten" ist eine sehr gute Faustregel, um ein Durchsickern von COM-Referenzen zu vermeiden. Excel PIA kann jedoch auf mehr als dem auf den ersten Blick augenscheinlichen Wege zum Durchsickern führen.

Eine dieser Möglichkeiten ist das Abonnieren von Ereignissen, die von COM-Objekten des Excel-Objektmodells bereitgestellt werden.

Beispielsweise Abonnieren des WorkbookOpen-Ereignisses der Application-Klasse.

Einige Theorie über COM-Ereignisse

COM-Klassen machen eine Gruppe von Ereignissen über Rückrufschnittstellen verfügbar. Um Ereignisse zu abonnieren, kann der Clientcode einfach ein Objekt registrieren, das die Rückrufschnittstelle implementiert, und die COM-Klasse ruft ihre Methoden als Reaktion auf bestimmte Ereignisse auf. Da es sich bei der Rückrufschnittstelle um eine COM-Schnittstelle handelt, ist es die Aufgabe des implementierenden Objekts, den Referenzzähler aller COM-Objekte, die er als Parameter erhält, für jeden Event-Handler zu dekrementieren.

Wie Excel PIA COM-Ereignisse verfügbar macht

Excel PIA macht COM-Ereignisse der Excel-Anwendungsklasse als herkömmliche .NET-Ereignisse verfügbar. Immer wenn der Clientcode ein a .NET-Ereignis (Schwerpunkt 'a') abonniert, erstellt PIA eine - Instanz einer Klasse, die die Rückrufschnittstelle implementiert, und registriert sie in Excel.

Daher werden einige Rückrufobjekte in Excel als Reaktion auf unterschiedliche Abonnementanfragen aus dem .NET-Code registriert. Ein Rückrufobjekt pro Ereignisabonnement.

Eine Rückrufschnittstelle für die Ereignisbehandlung bedeutet, dass PIA für jede .NET-Ereignisabonnementanforderung alle Schnittstellenereignisse abonnieren muss. Es kann nicht auswählen und wählen. Beim Empfang eines Ereignisrückrufs überprüft das Rückrufobjekt, ob der zugehörige .NET-Ereignishandler an dem aktuellen Ereignis interessiert ist oder nicht, und ruft dann entweder den Handler auf oder ignoriert den Rückruf unbemerkt.

Auswirkungen auf die Anzahl der COM-Instanzreferenzen

Alle diese Rückrufobjekte dekrementieren nicht die Referenzzählung der von ihnen empfangenen COM-Objekte (als Parameter) für eine der Rückrufmethoden (auch für diejenigen, die still ignoriert werden). Sie verlassen sich ausschließlich auf den CLR - Garbage Collector, um die COM-Objekte freizugeben.

Da der GC-Lauf nicht deterministisch ist, kann dies dazu führen, dass der Excel-Prozess länger als gewünscht unterbrochen wird und der Eindruck eines "Speicherverlusts" entsteht.

Lösung

Die einzige Lösung besteht ab jetzt darin, den Event-Provider der PIA für die COM-Klasse zu vermeiden und einen eigenen Event-Provider zu schreiben, der COM-Objekte deterministisch freigibt.

Für die Application-Klasse kann dies durch Implementieren der AppEvents-Schnittstelle und anschließendes Registrieren der Implementierung in Excel mithilfe der IConnectionPointContainer-Schnittstelle erfolgen. Die Application-Klasse (und damit auch alle COM-Objekte, die Ereignisse mithilfe des Rückrufmechanismus verfügbar machen) implementiert die IConnectionPointContainer-Schnittstelle.

6
Amit Mittal

Sie müssen sich darüber im Klaren sein, dass Excel auch sehr empfindlich auf die Kultur reagiert, unter der Sie arbeiten.

Möglicherweise müssen Sie die Kultur auf EN-US einstellen, bevor Sie Excel-Funktionen aufrufen. Dies gilt nicht für alle Funktionen - aber einige davon.

    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;

Dies gilt auch, wenn Sie VSTO verwenden.

Weitere Informationen: http://support.Microsoft.com/default.aspx?scid=kb;en-us;Q320369

6

Wie bereits erwähnt, müssen Sie für jedes verwendete Excel-Objekt eine explizite Referenz erstellen und Marshal.ReleaseComObject für diese Referenz aufrufen, wie in diesem KB-Artikel beschrieben. Sie müssen auch try/finally verwenden, um sicherzustellen, dass ReleaseComObject immer aufgerufen wird, auch wenn eine Ausnahme ausgelöst wird. D.h. anstatt:

Worksheet sheet = excelApp.Worksheets(1)
... do something with sheet

sie müssen etwas tun wie:

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

Sie müssen auch Application.Quit aufrufen, bevor Sie das Application-Objekt freigeben, wenn Sie möchten, dass Excel geschlossen wird.

Wie Sie sehen, wird dies schnell extrem unhandlich, sobald Sie versuchen, etwas zu tun, das auch nur einigermaßen komplex ist. Ich habe erfolgreich .NET-Anwendungen mit einer einfachen Wrapper-Klasse entwickelt, die einige einfache Manipulationen des Excel-Objektmodells einschließt (Öffnen einer Arbeitsmappe, Schreiben in einen Bereich, Speichern/Schließen der Arbeitsmappe usw.). Die Wrapper-Klasse implementiert IDisposable, implementiert Marshal.ReleaseComObject sorgfältig für jedes verwendete Objekt und macht keine Excel-Objekte öffentlich für den Rest der App verfügbar.

Dieser Ansatz eignet sich jedoch nicht für komplexere Anforderungen. 

Dies ist ein großer Mangel von .NET COM Interop. Für komplexere Szenarien würde ich ernsthaft in Betracht ziehen, ein ActiveX DLL in VB6 oder eine andere nicht verwaltete Sprache zu schreiben, an die Sie die gesamte Interaktion mit out-proc-COM-Objekten wie z. B. Office delegieren können. Sie können dann von Ihrer .NET-Anwendung aus auf dieses ActiveX DLL verweisen. Die Dinge werden viel einfacher, da Sie nur diese eine Referenz freigeben müssen.

5
Joe

Wenn alle oben genannten Dinge nicht funktionierten, geben Sie Excel etwas Zeit, um die Blätter zu schließen:

app.workbooks.Close();
Thread.Sleep(500); // adjust, for me it works at around 300+
app.Quit();

...
FinalReleaseComObject(app);
4
spiderman

Stellen Sie sicher, dass Sie alle Objekte freigeben, die sich auf Excel beziehen!

Ich habe ein paar Stunden damit verbracht, verschiedene Möglichkeiten auszuprobieren. Alle sind großartige Ideen, aber ich habe endlich meinen Fehler gefunden: Wenn Sie nicht alle Objekte freigeben, kann Ihnen keiner der oben genannten Wege helfen , wie in meinem Fall . Stellen Sie sicher, dass Sie alle Objekte einschließlich Bereich eins freigeben!

Excel.Range rng = (Excel.Range)worksheet.Cells[1, 1];
worksheet.Paste(rng, false);
releaseObject(rng);

Die Optionen sind zusammen hier .

3
Ned

Sie sollten sehr vorsichtig mit Word/Excel-Interop-Anwendungen sein. Nachdem wir alle Lösungen ausprobiert hatten, hatten wir noch viele "WinWord" -Prozesse auf dem Server (mit mehr als 2000 Benutzern) geöffnet.

Nachdem ich stundenlang an dem Problem gearbeitet hatte, wurde mir klar, dass der Arbeitsprozess IIS - Worker-Prozess (w3wp.exe) abstürzen würde, wenn ich mehr als ein paar Dokumente gleichzeitig mit Word.ApplicationClass.Document.Open() auf verschiedenen Threads öffne, und alle WinWord-Prozesse geöffnet wären!

Ich denke, dass es keine absolute Lösung für dieses Problem gibt, sondern auf andere Methoden wie Office Open XML development wechseln.

2
Arvand

Ein guter Artikel zum Freigeben von COM-Objekten ist 2.5 Release COM-Objekte (MSDN).

Die Methode, die ich befürworten würde, besteht darin, Ihre Excel.Interop-Referenzen auf Null zu setzen, wenn sie nicht lokale Variablen sind, und dann zweimal GC.Collect() und GC.WaitForPendingFinalizers() aufzurufen. Interop-Variablen mit lokalem Umfang werden automatisch berücksichtigt.

Dadurch entfällt die Notwendigkeit, eine benannte Referenz für das COM-Objekt every aufzubewahren.

Hier ist ein Beispiel aus dem Artikel:

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

Diese Worte stammen direkt aus dem Artikel:

In fast allen Situationen wird das Aufheben der RCW-Referenz und das Erzwingen einer Speicherbereinigung ordnungsgemäß bereinigt. Wenn Sie auch GC.WaitForPendingFinalizers aufrufen, ist die Garbage Collection so deterministisch, wie Sie es machen können. Das heißt, Sie sind ziemlich genau sicher, wann das Objekt bereinigt wurde - bei der Rückkehr vom zweiten Aufruf an WaitForPendingFinalizers. Alternativ können Sie Marshal.ReleaseComObject verwenden. Beachten Sie jedoch, dass Sie diese Methode wahrscheinlich nie verwenden müssen.

2
Porkbutts

Die Zwei-Punkte-Regel hat bei mir nicht funktioniert. In meinem Fall habe ich eine Methode zum Säubern meiner Ressourcen erstellt:

private static void Clean()
{
    workBook.Close();
    Marshall.ReleaseComObject(workBook);
    Excel.Quit();
    CG.Collect();
    CG.WaitForPendingFinalizers();
}
2
Hahnemann

Ich arbeite derzeit an Office-Automatisierung und bin dabei auf eine Lösung gestoßen, die jedes Mal für mich funktioniert. Es ist einfach und beinhaltet keine Prozesse zu töten.

Es scheint, dass durch das Durchlaufen der aktuell aktiven Prozesse und durch "Zugreifen" auf einen geöffneten Excel-Prozess jegliche überflüssige hängende Instanz von Excel entfernt wird. Mit dem folgenden Code wird einfach nach Prozessen gesucht, bei denen der Name 'Excel' lautet, und dann die MainWindowTitle-Eigenschaft des Prozesses in eine Zeichenfolge geschrieben. Diese "Interaktion" mit dem Prozess scheint Windows dazu zu bringen, die eingefrorene Instanz von Excel aufzuholen und abzubrechen. 

Ich führe die unten stehende Methode direkt vor dem Add-In aus, das ich entwickle, da es das Entladeereignis auslöst. Es entfernt jedes Mal hängende Instanzen von Excel. Ehrlich gesagt bin ich mir nicht ganz sicher, warum das funktioniert, aber es funktioniert gut für mich und könnte am Ende jeder Excel-Anwendung platziert werden, ohne sich um Doppelpunkte sorgen zu müssen, Marshal.ReleaseComObject oder um Prozesse zu beenden. Ich würde mich sehr für Vorschläge interessieren, warum dies effektiv ist.

public static void SweepExcelProcesses()
{           
            if (Process.GetProcessesByName("Excel").Length != 0)
            {
                Process[] processes = Process.GetProcesses();
                foreach (Process process in processes)
                {
                    if (process.ProcessName.ToString() == "Excel")
                    {                           
                        string title = process.MainWindowTitle;
                    }
                }
            }
}
1
Tom Brearley

Ich denke, dass ein Teil davon nur die Art und Weise ist, wie das Framework Office-Anwendungen verarbeitet, aber ich könnte falsch liegen. An einigen Tagen bereinigen einige Anwendungen die Prozesse sofort, und an anderen Tagen scheint es zu warten, bis die Anwendung geschlossen wird. Im Allgemeinen habe ich aufgehört, auf die Details zu achten, und stelle sicher, dass am Ende des Tages keine zusätzlichen Prozesse in der Umgebung herumlaufen.

Auch und vielleicht bin ich über die Vereinfachung der Dinge hinweg, aber ich denke, du kannst einfach ...

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

Wie ich bereits gesagt habe, neige ich nicht dazu, auf die Details zu achten, wann der Excel-Prozess erscheint oder verschwindet, aber das funktioniert normalerweise für mich. Ich mag es auch nicht, Excel-Prozesse für etwas anderes als die minimale Zeit beizubehalten, aber ich bin wahrscheinlich nur paranoid.

1
bill_the_loser

„Dies scheint sicher zu kompliziert zu sein. Aus meiner Erfahrung gibt es nur drei wichtige Punkte, damit Excel richtig geschlossen wird:

1: Stellen Sie sicher, dass keine Verweise auf die von Ihnen erstellte Excel-Anwendung vorhanden sind (Sie sollten sowieso nur einen haben; setzen Sie ihn auf null)

2: GC.Collect () aufrufen

3: Excel muss geschlossen werden, entweder durch den Benutzer, der das Programm manuell beendet, oder indem Sie für das Excel-Objekt Quit aufrufen. (Beachten Sie, dass Quit genauso funktioniert, als wenn der Benutzer versucht hat, das Programm zu schließen, und es wird ein Bestätigungsdialogfeld angezeigt, wenn nicht gespeicherte Änderungen vorhanden sind, auch wenn Excel nicht sichtbar ist. Der Benutzer könnte Abbrechen drücken, und Excel wurde nicht geschlossen .)

1 muss vor 2 geschehen, 3 können jedoch jederzeit vorkommen.

Eine Möglichkeit, dies zu implementieren, besteht darin, das Interop-Excel-Objekt mit einer eigenen Klasse zu umschließen, die Interop-Instanz im Konstruktor zu erstellen und IDisposable mit Dispose zu implementieren

Das reinigt Excel von der Seite Ihres Programms. Nach dem Schließen von Excel (manuell durch den Benutzer oder durch Aufrufen von Quit) wird der Prozess abgebrochen. Wenn das Programm bereits geschlossen wurde, wird der Prozess beim Aufruf von GC.Collect () ausgeblendet.

(Ich bin nicht sicher, wie wichtig es ist, aber Sie möchten möglicherweise einen Aufruf von GC.WaitForPendingFinalizers () nach dem Aufruf von GC.Collect (), aber es ist nicht unbedingt erforderlich, den Excel-Prozess zu entfernen.)

Das hat bei mir jahrelang ohne Probleme funktioniert. Denken Sie jedoch daran, dass Sie, während dies funktioniert, tatsächlich anmutig schließen müssen, damit es funktioniert. Sie erhalten immer noch akkumulierende Excel.exe-Prozesse, wenn Sie Ihr Programm unterbrechen, bevor Excel bereinigt wird (normalerweise durch Klicken auf "Stopp", während das Programm debuggt wird.)

Die akzeptierte Antwort hat bei mir nicht funktioniert. Der folgende Code im Destruktor hat die Aufgabe erfüllt.

if (xlApp != null)
{
    xlApp.Workbooks.Close();
    xlApp.Quit();
}

System.Diagnostics.Process[] processArray = System.Diagnostics.Process.GetProcessesByName("Excel");
foreach (System.Diagnostics.Process process in processArray)
{
    if (process.MainWindowTitle.Length == 0) { process.Kill(); }
}
1
Martin

Benutzen:

[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

Deklarieren Sie es, fügen Sie Code im finally-Block hinzu:

finally
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
    if (excelApp != null)
    {
        excelApp.Quit();
        int hWnd = excelApp.Application.Hwnd;
        uint processID;
        GetWindowThreadProcessId((IntPtr)hWnd, out processID);
        Process[] procs = Process.GetProcessesByName("Excel");
        foreach (Process p in procs)
        {
            if (p.Id == processID)
                p.Kill();
        }
        Marshal.FinalReleaseComObject(excelApp);
    }
}
1

Meine Lösung

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

Wie einige wahrscheinlich schon geschrieben haben, ist es nicht nur wichtig, wie Sie schließen das Excel (Objekt); Es ist auch wichtig, wie Sie öffnen es und auch von der Art des Projekts.

In einer WPF-Anwendung funktioniert im Grunde derselbe Code ohne oder mit sehr wenigen Problemen.

Ich habe ein Projekt, in dem dieselbe Excel-Datei mehrmals für einen anderen Parameterwert verarbeitet wird - z. Analysieren basierend auf Werten in einer generischen Liste.

Ich habe alle Excel-bezogenen Funktionen in die Basisklasse und den Parser in eine Unterklasse gestellt (verschiedene Parser verwenden gemeinsame Excel-Funktionen). Ich wollte nicht, dass Excel für jedes Element in einer generischen Liste erneut geöffnet und geschlossen wird, also habe ich es nur einmal in der Basisklasse geöffnet und in der Unterklasse geschlossen. Ich hatte Probleme beim Verschieben des Codes in eine Desktop-Anwendung. Ich habe viele der oben genannten Lösungen ausprobiert. GC.Collect() wurde bereits zweimal wie vorgeschlagen implementiert.

Dann habe ich beschlossen, den Code zum Öffnen von Excel in eine Unterklasse zu verschieben. Anstatt nur einmal zu öffnen, erstelle ich jetzt ein neues Objekt (Basisklasse) und öffne Excel für jedes Element und schließe es am Ende. Es gibt einige Leistungseinbußen, aber basierend auf mehreren Tests werden Excel-Prozesse problemlos geschlossen (im Debug-Modus), sodass auch temporäre Dateien entfernt werden. Ich werde mit dem Testen fortfahren und weitere schreiben, wenn ich ein paar Updates bekomme.

Die Quintessenz lautet: Sie müssen auch den Initialisierungscode überprüfen, insbesondere wenn Sie viele Klassen usw. haben.

1
Blaz Brencic

Dort habe ich eine Idee. Versuche, den geöffneten Excel-Prozess zu beenden:

  1. rufen Sie vor dem Öffnen einer Excelapplikation alle Prozess-IDs mit dem Namen oldProcessIds ab.
  2. Öffnen Sie die Excel-Anwendung.
  3. holen Sie sich jetzt alle Excel-Anwendungs-IDs mit dem Namen nowProcessIds.
  4. wenn Sie den Vorgang beenden müssen, beenden Sie die Ausnahme-IDs zwischen oldProcessIds und nowProcessIds.

    private static Excel.Application GetExcelApp()
         {
            if (_excelApp == null)
            {
                var processIds = System.Diagnostics.Process.GetProcessesByName("Excel").Select(a => a.Id).ToList();
                _excelApp = new Excel.Application();
                _excelApp.DisplayAlerts = false;
    
                _excelApp.Visible = false;
                _excelApp.ScreenUpdating = false;
                var newProcessIds = System.Diagnostics.Process.GetProcessesByName("Excel").Select(a => a.Id).ToList();
                _excelApplicationProcessId = newProcessIds.Except(processIds).FirstOrDefault();
            }
    
            return _excelApp;
        }
    
    public static void Dispose()
        {
            try
            {
                _excelApp.Workbooks.Close();
                _excelApp.Quit();
                System.Runtime.InteropServices.Marshal.ReleaseComObject(_excelApp);
                _excelApp = null;
                GC.Collect();
                GC.WaitForPendingFinalizers();
                if (_excelApplicationProcessId != default(int))
                {
                    var process = System.Diagnostics.Process.GetProcessById(_excelApplicationProcessId);
                    process?.Kill();
                    _excelApplicationProcessId = default(int);
                }
            }
            catch (Exception ex)
            {
                _excelApp = null;
            }
    
        }
    
0
Aloha

Um nur eine weitere Lösung hinzuzufügen, die hier aufgeführt ist, mit C++/ATL-Automatisierung (ich könnte mir vorstellen, dass Sie etwas ähnliches von VB/C # verwenden könnten?)

Excel::_ApplicationPtr pXL = ...
  :
SendMessage ( ( HWND ) m_pXL->GetHwnd ( ), WM_DESTROY, 0, 0 ) ;

Das funktioniert für mich wie ein Zauber ...

0
dicksters

Ich hatte das gleiche Problem beim Schließen von PowerPoint, nachdem ich das Anwendungsobjekt in meinem VSTO-AddIn neu erstellt hatte. Ich habe alle Antworten hier mit begrenztem Erfolg ausprobiert.

Dies ist die Lösung, die ich für meinen Fall gefunden habe - verwenden Sie NICHT 'new Application', die AddInBase-Basisklasse von ThisAddIn hat bereits ein Handle für 'Application'. Wenn Sie dieses Handle dort verwenden, wo Sie es benötigen (statisch machen, wenn Sie müssen), müssen Sie sich keine Sorgen um die Bereinigung machen, und PowerPoint bleibt nicht lange in der Nähe.

0
swax

Das geht ganz einfach:

[DllImport("User32.dll")]
static extern uint GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);
...

int objExcelProcessId = 0;

Excel.Application objExcel = new Excel.Application();

GetWindowThreadProcessId(new IntPtr(objExcel.Hwnd), out objExcelProcessId);

Process.GetProcessById(objExcelProcessId).Kill();
0
tjarrett

Getestet mit Microsoft Excel 2016

Eine wirklich getestete Lösung.

C # -Referenz finden Sie unter: https://stackoverflow.com/a/1307180/10442623

Zur VB.net-Referenz siehe: https://stackoverflow.com/a/54044646/10442623

1 beinhaltet den Klassenjob

2 Implementieren Sie die Klasse so, dass sie mit den entsprechenden Excel-Prozessen umgehen kann

0
SamSar

Bisher scheinen alle Antworten einige dieser Punkte zu beinhalten:

  1. Töte den Prozess
  2. GC.Collect () verwenden
  3. Verfolgen Sie jedes COM-Objekt und geben Sie es ordnungsgemäß frei.

Was mich zu schätzen weiß, wie schwierig dieses Problem ist :)

Ich habe an einer Bibliothek gearbeitet, um den Zugriff auf Excel zu vereinfachen, und ich versuche sicherzustellen, dass die Benutzer, die sie verwenden, kein Durcheinander hinterlassen (Daumen drücken).

Anstatt direkt auf die Schnittstellen zu schreiben, die Interop bietet, mache ich Erweiterungsmethoden, um das Leben einfacher zu machen. Wie ApplicationHelpers.CreateExcel () oder workbook.CreateWorksheet ("mySheetNameThatWillBeValidated"). Natürlich kann alles, was geschaffen wird, später zu einem Problem beim Aufräumen führen, daher ziehe ich es vor, den Prozess als letzten Ausweg zu beenden. Richtiges Aufräumen (dritte Option) ist jedoch wahrscheinlich das am wenigsten zerstörerische und am meisten kontrollierte.

In diesem Zusammenhang fragte ich mich, ob es nicht das Beste wäre, so etwas zu machen:

public abstract class ReleaseContainer<T>
{
    private readonly Action<T> actionOnT;

    protected ReleaseContainer(T releasible, Action<T> actionOnT)
    {
        this.actionOnT = actionOnT;
        this.Releasible = releasible;
    }

    ~ReleaseContainer()
    {
        Release();
    }

    public T Releasible { get; private set; }

    private void Release()
    {
        actionOnT(Releasible);
        Releasible = default(T);
    }
}

Ich habe "Releasible" verwendet, um Verwechslungen mit Disposable zu vermeiden. Die Erweiterung auf IDisposable sollte jedoch einfach sein.

Eine Implementierung wie diese:

public class ApplicationContainer : ReleaseContainer<Application>
{
    public ApplicationContainer()
        : base(new Application(), ActionOnExcel)
    {
    }

    private static void ActionOnExcel(Application application)
    {
        application.Show(); // extension method. want to make sure the app is visible.
        application.Quit();
        Marshal.FinalReleaseComObject(application);
    }
}

Und man könnte für alle Arten von COM-Objekten etwas Ähnliches tun.

In der Fabrikmethode:

    public static Application CreateExcelApplication(bool hidden = false)
    {
        var Excel = new ApplicationContainer().Releasible;
        Excel.Visible = !hidden;

        return Excel;
    }

Ich würde erwarten, dass jeder Container vom GC ordnungsgemäß zerstört wird, und führt daher automatisch den Aufruf von Quit und Marshal.FinalReleaseComObject aus.

Bemerkungen? Oder ist das eine Antwort auf die Frage der dritten Art?

0
akirakonenu