Wie bereinige ich Excel-Interop-Objekte ordnungsgemäß?

747

Ich verwende die 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 diese Art von funktioniert, ist der Excel.exeProzess auch nach dem Schließen von Excel noch im Hintergrund. Es wird erst freigegeben, wenn meine Anwendung manuell geschlossen wurde.

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

Hades
quelle
1
Versuchen Sie, die Datei Excel.exe herunterzufahren, ohne Ihre Anwendung zu schließen? Ich bin mir nicht sicher, ob ich Ihre Frage vollständig verstehe.
Bryant
3
Ich versuche sicherzustellen, dass die nicht verwalteten Interop-Objekte ordnungsgemäß entsorgt werden. Damit keine Excel-Prozesse herumhängen, selbst wenn der Benutzer mit der aus der App erstellten Excel-Tabelle fertig ist.
HAdes
3
Wenn Sie versuchen können, dies durch Erstellen von XML-Excel-Dateien zu tun, ziehen Sie ansonsten VSTO un / verwaltete Speicherverwaltung in Betracht: jake.ginnivan.net/vsto-com-interop
Jeremy Thompson
Wird dies gut in Excel übersetzt?
Coops
2
Siehe (neben den Antworten unten) diesen Support-Artikel von Microsoft, in dem speziell Lösungen für dieses Problem angegeben werden: support.microsoft.com/kb/317109
Arjan

Antworten:

684

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

Ich vermute, Sie rufen mindestens ein Mitglied 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 die Arbeitsblätter erstellt hat COM-Objekt erstellt hat, der von meinem Code nicht freigegeben wurde (weil ich es nicht wusste) und der Grund dafür war, dass Excel nicht entladen wurde.

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

Verwenden Sie niemals zwei Punkte mit COM-Objekten.


Mit diesem Wissen ist der richtige Weg, dies 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 sie die Falle erklärt, in die ich und viele andere Entwickler geraten sind. Als ich diese Antwort vor Jahren schrieb, wusste ich nicht, welche Auswirkungen der Debugger auf den Garbage Collector hat, und zog die falschen Schlussfolgerungen. Ich behalte meine Antwort aus Gründen der Geschichte unverändert, aber bitte lesen Sie diesen Link und gehen Sie nicht den Weg der "zwei Punkte": Grundlegendes zur Speicherbereinigung in .NET und Bereinigen von Excel-Interop-Objekten mit IDisposable

VVS
quelle
16
Dann schlage ich vor, Excel von COM nicht zu verwenden und sich den ganzen Ärger zu ersparen. Die Excel 2007-Formate können verwendet werden, ohne jemals Excel zu öffnen.
user7116
5
Ich habe nicht verstanden, was "zwei Punkte" bedeuten. Kannst du bitte Erklären?
A9S6
22
Dies bedeutet, dass Sie das Muster comObject.Property.PropertiesProperty nicht verwenden sollten (sehen Sie die beiden Punkte?). Weisen Sie stattdessen einer Variablen comObject.Property zu und verwenden und entsorgen Sie diese Variable. Eine formellere Version der obigen Regel könnte etw sein. wie "Weisen Sie Variablen ein com-Objekt zu, bevor Sie sie verwenden. Dies schließt com-Objekte ein, die Eigenschaften eines anderen com-Objekts sind."
VVS
5
@ Nick: Eigentlich brauchst du keine Bereinigung, da der Garbage Collector das für dich erledigt. Das einzige, was Sie tun müssen, ist, jedes COM-Objekt einer eigenen Variablen zuzuweisen, damit der GC davon weiß.
VVS
12
@VSS das ist Blödsinn, der GC bereinigt alles, da die Wrapper-Variablen vom .net-Framework erstellt werden. Es kann ewig dauern, bis der GC es bereinigt. GC.Collect nach einem schweren Interop anzurufen ist keine schlechte Idee.
CodingBarfield
280

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

Der Rat, für absolut jedes COM-Objekt, auf das Sie zugreifen, eine benannte Referenz beizubehalten und diese dann explizit freizugeben, Marshal.FinalReleaseComObject()ist theoretisch korrekt, in der Praxis jedoch leider sehr schwierig zu verwalten. Wenn man jemals irgendwohin rutscht und "zwei Punkte" verwendet oder Zellen über a iteriertfor each Schleife oder einen anderen ähnlichen Befehl , 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 den Augen überprüfen und hoffentlich die Ursache finden, eine Aufgabe, die für ein großes Projekt fast unmöglich sein könnte.

Die gute Nachricht ist, dass Sie nicht für jedes von Ihnen verwendete COM-Objekt einen benannten Variablenverweis verwalten müssen. Rufen Sie stattdessen anGC.Collect() und geben Sie dann GC.WaitForPendingFinalizers()alle (normalerweise geringfügigen) Objekte frei, auf die Sie keine Referenz haben, und geben Sie dann explizit die Objekte frei, auf die Sie eine benannte Variablenreferenz halten.

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

Angenommen, Sie hätten beispielsweise eine Range-Objektvariable mit dem Namen xlRng, eine Arbeitsblattvariable mit dem Namen xlSheet, eine Arbeitsmappenvariable mit dem Namen xlBookund eine Excel-Anwendungsvariable mit dem Namen benannt xlApp, könnte Ihr Bereinigungscode folgendermaßen 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 zum Bereinigen von COM-Objekten aus .NET werden die Aufrufe GC.Collect()und GC.WaitForPendingFinalizers()zweimal ausgeführt, wie in:

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 ganzes Diagramm von Objekten in der Finalisierungswarteschlange heraufgestuft wird. Solche Objekte würden erst bei der nächsten Speicherbereinigung freigegeben . Wenn Sie jedoch VSTO nicht verwenden, sollten Sie in der Lage sein, GC.Collect()und anzurufenGC.WaitForPendingFinalizers() nur einmal.

Ich weiß, dass explizites Anrufen GC.Collect()ein Nein-Nein ist (und es klingt sicherlich zweimal sehr schmerzhaft), aber um ehrlich zu sein führt kein Weg daran vorbei. Durch normale Operationen generieren Sie versteckte Objekte, auf die Sie keinen Verweis haben, den Sie daher nur durch Aufrufen freigeben können GC.Collect().

Dies ist ein komplexes Thema, aber das ist wirklich alles, was dazu gehört. Sobald Sie diese Vorlage für Ihren Bereinigungsvorgang erstellt 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 davon nicht abschrecken. Die Prinzipien sind genau die gleichen wie bei der Verwendung von C #.

Mike Rosenblum
quelle
3
Eine entsprechende Diskussion finden Sie im ExtremeVBTalk .NET Office Automation-Forum hier: xtremevbtalk.com/showthread.php?t=303928 .
Mike Rosenblum
2
Und wenn alles andere fehlschlägt, kann Process.Kill () (als letzter Ausweg) wie hier beschrieben verwendet werden: stackoverflow.com/questions/51462/…
Mike Rosenblum
1
Ich bin froh, dass es funktioniert hat, Richard. :-) Und hier ist ein subtiles Beispiel, wo das einfache Vermeiden von "zwei Punkten" nicht ausreicht, um ein Problem zu verhindern: stackoverflow.com/questions/4964663/…
Mike Rosenblum
Hier ist eine Stellungnahme von Misha Shneerson von Microsoft zu diesem Thema innerhalb des Kommentardatums 7. Oktober 2010. ( blogs.msdn.com/b/csharpfaq/archive/2010/09/28/… )
Mike Rosenblum
Ich hoffe, dass dies einem anhaltenden Problem hilft, das wir haben. Ist es sicher, die Garbage Collection- und Release-Aufrufe innerhalb eines finally-Blocks durchzuführen?
Brett Green
213

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 zum Entladen von Excel-Instanzen, z.

  • JEDES com-Objekt explizit mit Marshal.FinalReleaseComObject () freigeben (nicht zu vergessen implizit erstellte com-Objekte). Um jedes erstellte COM-Objekt freizugeben, können Sie die hier genannte Regel von 2 Punkten verwenden:
    Wie bereinige ich Excel-Interop-Objekte ordnungsgemäß?

  • Aufruf von GC.Collect () und GC.WaitForPendingFinalizers (), damit CLR nicht verwendete com-Objekte freigibt * (Tatsächlich funktioniert es, siehe meine zweite Lösung für Details)

  • Wenn Sie überprüfen, ob die com-server-Anwendung möglicherweise ein Meldungsfeld anzeigt, das auf die Antwort des Benutzers wartet (obwohl ich nicht sicher bin, ob das Schließen von Excel verhindert werden kann, habe ich jedoch einige Male davon gehört).

  • Senden der WM_CLOSE-Nachricht an das Hauptfenster von Excel

  • Ausführen der Funktion, die mit Excel funktioniert, in einer separaten AppDomain. Einige Leute glauben, dass die Excel-Instanz geschlossen wird, wenn AppDomain entladen wird.

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

ABER! Manchmal helfen all diese Optionen einfach nicht oder können nicht angemessen sein!

Zum Beispiel habe ich gestern herausgefunden, dass Excel in einer meiner Funktionen (die mit Excel funktioniert) nach dem Ende der Funktion weiter ausgeführt wird. 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 Meldungsfeldern gesucht. Ich habe versucht, eine WM_CLOSE-Nachricht an das Excel-Hauptfenster zu senden. Ich habe meine Funktion in einer separaten AppDomain ausgeführt und diese Domain entladen. Nichts hat geholfen! Die Option zum Schließen aller Excel-Instanzen ist unangemessen. Wenn der Benutzer während der Ausführung meiner Funktion, die auch mit Excel funktioniert, eine andere Excel-Instanz manuell startet, wird diese Instanz auch von meiner Funktion geschlossen. Ich wette, der Benutzer wird nicht glücklich sein! Ehrlich gesagt ist dies eine lahme Option (keine Beleidigung, Leute).Lösung : Beenden Sie den Excel-Prozess mit 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 gemäß dem Try-Parse-Muster bereitgestellt (ich denke, dass dies hier angemessen ist): Eine Methode löst keine Ausnahme aus, wenn der Prozess nicht beendet werden konnte (zum Beispiel 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 zum Beenden des Prozesses, aber in 99,99% aller Fälle verfügt der Benutzer über solche Berechtigungen. Ich habe es auch mit einem Gastkonto getestet - es funktioniert perfekt.

Ihr Code, der mit Excel arbeitet, kann also 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, gehen wir zurück zur zweiten Lösung, wie ich zu Beginn des Beitrags versprochen habe. Die zweite Lösung besteht darin, GC.Collect () und GC.WaitForPendingFinalizers () aufzurufen. Ja, sie funktionieren tatsächlich, aber Sie müssen hier vorsichtig sein!
Viele Leute sagen (und ich sagte), dass das Aufrufen von GC.Collect () nicht hilft. Aber der Grund, warum es nicht helfen würde, ist, wenn es immer noch Verweise auf COM-Objekte gibt! Einer der beliebtesten Gründe dafür, dass 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, erst am Ende der Methode mit Müll gesammelt.
Wenn Sie also GC.Collect () und GC.WaitForPendingFinalizers () ausprobiert haben und es nicht geholfen hat, versuchen Sie Folgendes:

1) Versuchen Sie, Ihr Projekt im Release-Modus auszuführen, und überprüfen Sie, ob Excel korrekt geschlossen wurde

2) Wickeln Sie die Arbeitsweise mit Excel in eine separate Methode ein. Also, anstatt so etwas:

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

Jetzt wird Excel geschlossen =)

Nachtcodierer
quelle
19
traurig, dass der Thread bereits so alt ist, dass Ihre ausgezeichnete Antwort so weit unten erscheint, was meiner Meinung nach der einzige Grund dafür ist, dass er nicht mehrmals upvoted wird ...
chiccodoro
15
Ich muss zugeben, als ich Ihre Antwort zum ersten Mal las, dachte ich, es sei ein riesiger Kludge. Nach ungefähr 6 Stunden Wrestling damit (alles ist veröffentlicht, ich habe keine doppelten Punkte usw.) denke ich jetzt, dass Ihre Antwort genial ist.
Mark
3
Danke dafür. Ich habe ein paar Tage lang mit einem Excel gerungen, das sich nicht schließen ließ, bevor ich das fand. Ausgezeichnet.
BBlake
2
@nightcoder: tolle Antwort, sehr detailliert. Ihre Kommentare zum Debug-Modus sind sehr wahr und wichtig, dass Sie darauf hingewiesen haben. Der gleiche Code im Release-Modus kann oft in Ordnung sein. Process.Kill ist jedoch nur bei Verwendung der Automatisierung gut, nicht wenn Ihr Programm in Excel ausgeführt wird, z. B. als verwaltetes COM-Add-In.
Mike Rosenblum
2
@ DanM: Ihr Ansatz ist 100% korrekt und wird niemals scheitern. Indem Sie alle Ihre Variablen lokal für die Methode halten, sind alle Referenzen für .NET-Code praktisch nicht erreichbar. Dies bedeutet, dass die Finalizer für alle Ihre COM-Objekte mit Sicherheit aufgerufen werden , wenn Ihr Abschnitt finally GC.Collect () aufruft . Jeder Finalizer ruft dann Marshal.FinalReleaseComObject für das zu finalisierende Objekt auf. Ihr Ansatz ist daher einfach und narrensicher. Hab keine Angst damit. (Nur Einschränkung: Wenn Sie VSTO verwenden, was ich bezweifle, dass Sie es sind, müssten Sie GC.Collect () & GC.WaitForPendingFinalizers ZWEIMAL aufrufen.)
Mike Rosenblum
49

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

Ich habe einige Zeit damit verbracht, dieses Problem herauszufinden, und zu der Zeit war XtremeVBTalk am aktivsten und reaktionsschnellsten. Hier ist ein Link zu meinem ursprünglichen Beitrag: Schließen eines Excel Interop-Prozesses sauber, auch wenn Ihre Anwendung abstürzt . Unten 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 . Dh wenn die App abstürzt, läuft der Excel-Prozess immer noch locker.
  • Die Lösung besteht darin, das Betriebssystem die Bereinigung Ihrer Prozesse über Windows-Jobobjekte mithilfe von Win32-Aufrufen durchführen zu lassen. Wenn Ihre Hauptanwendung stirbt, werden auch die zugehörigen Prozesse (z. B. Excel) beendet.

Ich fand, dass dies eine saubere Lösung ist, da das Betriebssystem echte Aufräumarbeiten durchführt. Sie müssen lediglich den Excel-Prozess registrieren .

Windows-Jobcode

Wickelt 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 info.LimitFlags = 0x2000;heißt das. 0x2000ist der JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSEAufzählungswert, und dieser Wert wird von MSDN wie folgt definiert :

Bewirkt, dass alle mit dem Job verknüpften Prozesse beendet werden, wenn das letzte Handle für den Job geschlossen wird.

Zusätzlicher Win32-API-Aufruf zum Abrufen der Prozess-ID (PID)

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

Den Code verwenden

    Excel.Application app = new Excel.ApplicationClass();
    Job job = new Job();
    uint pid = 0;
    Win32.GetWindowThreadProcessId(new IntPtr(app.Hwnd), out pid);
    job.AddProcess(Process.GetProcessById((int)pid).Handle);
Joshgo
quelle
2
Das explizite Beenden eines nicht prozessbedingten COM-Servers (der möglicherweise andere COM-Clients bedient) ist eine schreckliche Idee. Wenn Sie darauf zurückgreifen, liegt dies daran, dass Ihr COM-Protokoll fehlerhaft ist. COM-Server sollten nicht als normale Fenster-App behandelt werden
MickyD
38

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 ist, jeden Verweis auf ein Excel-COM-Objekt auf null zu setzen, wenn Sie damit fertig sind. Dies beinhaltete Zellen, Blätter und alles.

Philip Fourie
quelle
30

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.

MagicKat
quelle
3
Wie zum Beispiel xlRange.Interior.Color.
HAdes
Das Interieur muss freigegeben werden (es befindet sich im Namespace) ... Farbe dagegen nicht (weil es von System.Drawing.Color, iirc stammt)
MagicKat
2
Eigentlich ist Farbe eine Excel-Farbe, keine .Net-Farbe. Sie passieren nur eine lange. Auch Arbeitsmappen def. müssen freigegeben werden, Arbeitsblätter ... weniger.
Anonymer Typ
Das ist nicht wahr, die Zwei-Punkt-Schlecht-, Ein-Punkt-Gut-Regel ist Unsinn. Außerdem müssen Sie keine Arbeitsmappen, Arbeitsblätter, Bereiche freigeben, das ist die Aufgabe des GC. Das einzige, was Sie niemals vergessen dürfen, ist, Quit für das einzige Excel.Application-Objekt aufzurufen. Setzen Sie nach dem Aufrufen von Quit das Excel.Application-Objekt auf Null und rufen Sie GC.Collect () auf. GC.WaitForPendingFinalizers (); zweimal.
Dietrich Baumgarten
29

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

Zweitens müssen Sie sicherstellen, dass der Garbage Collector ausgeführt wird, wenn Sie sicherstellen möchten, dass die COM-Verweise auf ein COM-Objekt außerhalb des Prozesses bereinigt werden, wenn Ihr Prozess endet (damit der Excel-Prozess geschlossen wird). Sie machen das richtig mit Anrufen an GC.Collect()und GC.WaitForPendingFinalizers(). Ein zweimaliger Aufruf ist sicher und stellt sicher, dass die Zyklen definitiv auch bereinigt werden (obwohl ich nicht sicher bin, ob dies erforderlich ist, und würde ein Beispiel begrüßen, das dies zeigt).

Drittens werden beim Ausführen unter dem Debugger lokale Referenzen bis zum Ende der Methode künstlich am Leben erhalten (damit die Überprüfung lokaler Variablen funktioniert). Daher sind GC.Collect()Aufrufe zum Reinigen von Objekten wie rng.Cellsbei derselben Methode nicht wirksam . Sie sollten den Code, der das COM-Interop aus der GC-Bereinigung ausführt, in separate Methoden aufteilen. (Dies war eine wichtige Entdeckung für mich, aus einem Teil der Antwort, die hier von @nightcoder gepostet wurde.)

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 viele falsche Informationen und Verwirrung zu diesem Problem, einschließlich vieler Beiträge zu MSDN und zum Stapelüberlauf (und insbesondere zu dieser Frage!).

Was mich schließlich überzeugte, genauer hinzuschauen und den richtigen Rat zu finden, war der Blog-Beitrag Marshal.ReleaseComObject als gefährlich eingestuft, zusammen mit der Suche nach dem Problem mit Referenzen, die unter dem Debugger am Leben gehalten wurden und meine früheren Tests verwirrten.

Govert
quelle
5
Praktisch alle Antworten auf dieser Seite sind falsch, mit Ausnahme dieser. Sie arbeiten, sind aber viel zu viel Arbeit. Ignorieren Sie die Regel "2 DOTS". Lassen Sie den GC Ihre Arbeit für Sie erledigen. Ergänzende Beweise von .Net GURU Hans Passant: stackoverflow.com/a/25135685/3852958
Alan Baljeu
1
Govert, was für eine Erleichterung, den richtigen Weg zu finden. Vielen Dank.
Dietrich Baumgarten
18

Ich habe eine nützliche generische Vorlage gefunden, mit deren Hilfe das richtige Entsorgungsmuster für COM-Objekte implementiert werden kann, für die Marshal.ReleaseComObject aufgerufen werden muss, 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/

Edward Wilde
quelle
3
yup dieser ist gut. Ich denke, dieser Code kann jetzt aktualisiert werden, da FinalReleaseCOMObject verfügbar ist.
Anonymer Typ
Es ist immer noch ärgerlich, für jedes Objekt einen Verwendungsblock zu haben. Eine Alternative finden Sie hier auf stackoverflow.com/questions/2191489/… .
Henrik
16

Ich kann nicht glauben, dass dieses Problem die Welt seit 5 Jahren heimgesucht 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 im Hintergrund ein Excel-Programm geöffnet. Sie müssen diesem Excel-Programm befehlen, das Programm zu beenden, bevor Sie den Link freigeben, da dieses Excel-Programm nicht Teil Ihrer direkten Kontrolle ist. Daher bleibt es offen, wenn der Link freigegeben wird!

Gute Programmierung an alle ~~

Colin
quelle
Nein, insbesondere nicht, wenn Sie die Benutzerinteraktion mit der COM-Anwendung zulassen möchten (in diesem Fall Excel - OP gibt nicht an, ob eine Benutzerinteraktion beabsichtigt ist, bezieht sich jedoch nicht auf das Herunterfahren). Sie müssen nur den Anrufer dazu bringen, loszulassen, damit der Benutzer Excel beenden kann, wenn er beispielsweise eine einzelne Datei schließt.
Downwitch
Dies funktionierte perfekt für mich - stürzte ab, als ich nur objExcel.Quit () hatte, aber als ich vorher auch objExcel.Application.Quit () tat, sauber geschlossen.
Mcmillab
13

Gemeinsame Entwickler, keine Ihrer Lösungen hat für mich funktioniert, daher entscheide ich mich, einen neuen Trick zu implementieren .

Geben Sie zunächst an: "Was ist unser Ziel?" => "Excel-Objekt nach unserem Job im Task-Manager nicht sehen"

OK. Lassen Sie no herausfordern und anfangen, es zu zerstören, aber denken Sie daran, andere Instanzen von Excel, die parallel ausgeführt werden, nicht zu zerstören.

Holen Sie sich also die Liste der aktuellen Prozessoren und rufen Sie die PID von EXCEL-Prozessen ab. Sobald Ihre Arbeit erledigt ist, haben wir einen neuen Gast in der Prozessliste mit einer eindeutigen PID. Finden und zerstören Sie genau diese.

<Beachten Sie, dass jeder neue Excel-Prozess während Ihres Excel-Jobs als neu und zerstört erkannt wird.> <Eine bessere Lösung besteht darin, 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();

Dies löst mein Problem, hoffe auch deins.

Mohsen Afshin
quelle
1
.... das ist ein bisschen ein Schlittenhammer-Ansatz? - Es ähnelt dem, was ich auch verwende :( muss aber geändert werden. Das Problem bei der Verwendung dieses Ansatzes ist, dass beim Öffnen von Excel auf der Maschine, auf der dies ausgeführt wurde, häufig die Warnung in der linken Leiste angezeigt wird, die so etwas wie sagt "Excel wurde falsch geschlossen": nicht sehr elegant. Ich denke, einer der früheren Vorschläge in diesem Thread ist vorzuziehen.
Whytheq
11

Dies scheint sicher zu kompliziert gewesen zu sein. Nach meiner Erfahrung gibt es nur drei wichtige Dinge, um Excel ordnungsgemäß zu schließen:

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

2: anrufen GC.Collect()

3: Excel muss geschlossen werden, entweder durch den Benutzer, der das Programm manuell schließt, oder durch Ihren Aufruf Quit des Excel-Objekts. (Beachten Sie, dass dies Quitso funktioniert, als ob der Benutzer versucht hätte, das Programm zu schließen, und ein Bestätigungsdialogfeld angezeigt wird, wenn nicht gespeicherte Änderungen vorliegen, auch wenn Excel nicht sichtbar ist. Der Benutzer könnte auf Abbrechen klicken, und Excel wurde dann nicht geschlossen. )

1 muss vor 2 passieren, aber 3 kann jederzeit passieren.

Eine Möglichkeit, dies zu implementieren, besteht darin, das Interop-Excel-Objekt mit Ihrer eigenen Klasse zu versehen, die Interop-Instanz im Konstruktor zu erstellen und IDisposable mit Dispose so zu implementieren

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

Das wird Excel von der Seite Ihres Programms bereinigen. Sobald Excel geschlossen ist (manuell vom Benutzer oder von Ihnen angerufen)Quit ), wird der Prozess beendet. Wenn das Programm bereits geschlossen wurde, verschwindet der Vorgang beim GC.Collect()Aufruf.

(Ich bin nicht sicher, wie wichtig es ist, aber Sie möchten vielleicht einen GC.WaitForPendingFinalizers()Anruf nach demGC.Collect() Anruf, aber es ist nicht unbedingt erforderlich, den Excel-Prozess loszuwerden.)

Das hat bei mir seit Jahren ohne Probleme funktioniert. Beachten Sie jedoch, dass Sie, während dies funktioniert, tatsächlich ordnungsgemäß schließen müssen, damit es funktioniert. Wenn Sie Ihr Programm unterbrechen, bevor Excel bereinigt wird, werden immer noch excel.exe-Prozesse angesammelt (normalerweise durch Drücken von "Stop", während Ihr Programm debuggt wird).

Dave Cousineau
quelle
1
Diese Antwort funktioniert und ist unendlich bequemer als die akzeptierte Antwort. Keine Sorge um "zwei Punkte" mit COM-Objekten, keine Verwendung von Marshal.
Andrew.Cuthbert
9

Um die Gründe hinzuzufügen, warum Excel nicht geschlossen wird, selbst wenn Sie beim Lesen und Erstellen direkte Aktualisierungen für jedes Objekt erstellen, wird die 'For'-Schleife verwendet.

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
Grimfort
quelle
Ein weiterer Grund ist die Wiederverwendung einer Referenz, ohne zuvor den vorherigen Wert freizugeben.
Grimfort
Vielen Dank. Ich habe auch ein "für jeden" verwendet, für das Ihre Lösung funktioniert hat.
Chad Braun-Duin
+1 Danke. Beachten Sie außerdem, dass ich, wenn Sie ein Objekt für einen Excel-Bereich haben und den Bereich während der Lebensdauer des Objekts ändern möchten, vor dem erneuten Zuweisen ReleaseComObject ausführen musste, was den Code etwas unordentlich macht!
AjV Jsy
9

Ich habe traditionell den Rat befolgt, der in der Antwort von VVS enthalten ist . Um diese Antwort 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 vollständig . Es handelt sich um eine Sammlung von verwalteten COM-Wrappern, die die Bereinigung verarbeiten können, die bei der Arbeit mit Microsoft Office in .NET häufig solche Kopfschmerzen verursacht.

Einige Hauptmerkmale sind:

  • Meistens versionunabhängig (und versionabhängige Funktionen sind dokumentiert)
  • Keine Abhängigkeiten
  • Keine PIA
  • Keine Registration
  • Kein VSTO

Ich bin in keiner Weise mit dem Projekt verbunden; Ich schätze die starke Reduzierung der Kopfschmerzen wirklich.

BTownTKD
quelle
2
Dies sollte wirklich als Antwort markiert werden. NetOffice abstrahiert all diese Komplexität.
C. Augusto Proiete
1
Ich benutze NetOffice seit geraumer Zeit, um ein Excel-Add-In zu schreiben, und es hat perfekt funktioniert. Die einzige zu berücksichtigende Sache ist, dass wenn Sie gebrauchte Objekte nicht explizit entsorgen, dies beim Beenden der Anwendung der Fall ist (da sie ohnehin nachverfolgt werden). Die Faustregel bei NetOffice lautet daher immer, "using" -Muster für jedes Excel-Objekt wie Zelle, Bereich oder Blatt usw. zu verwenden.
Stas Ivanov
8

Die hier akzeptierte Antwort ist korrekt, aber beachten Sie auch, dass nicht nur "Zwei-Punkt" -Referenzen 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 Funktion, die ich erstellt habe und die einige Eigenschaften eines Style-Objekts mit dem Namen zuweist 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 { } 
}

Beachten Sie, dass ich einstellen musste xlBorders[Excel.XlBordersIndex.xlEdgeBottom] eine Variable , um dies zu bereinigen (nicht wegen der zwei Punkte, die sich auf eine Aufzählung beziehen, die nicht freigegeben werden muss, sondern weil das Objekt, auf das ich mich beziehe, tatsächlich ein Border-Objekt ist das muss freigegeben werden).

Diese Art von Dingen ist in Standardanwendungen, die eine hervorragende Bereinigung nach sich selbst leisten, nicht unbedingt erforderlich. In ASP.NET-Anwendungen wird Excel jedoch auch nur eine dieser Anwendungen verpassen, unabhängig davon, wie oft Sie den Garbage Collector aufrufen läuft immer noch auf Ihrem Server.

Es erfordert viel Liebe zum Detail und viele Testausführungen, während der Task-Manager beim Schreiben dieses Codes überwacht wird. Dies erspart Ihnen jedoch den Aufwand, verzweifelt nach Codeseiten zu suchen, um die eine Instanz zu finden, die Sie verpasst haben. Dies ist besonders wichtig, wenn Sie in Schleifen arbeiten, in denen Sie JEDE INSTANZ eines Objekts freigeben müssen, obwohl es bei jeder Schleife denselben Variablennamen verwendet.

Chris McGrath
quelle
Suchen Sie in Release nach Null (Joes Antwort). Dadurch werden unnötige Nullausnahmen vermieden. Ich habe viele Methoden getestet. Dies ist die einzige Möglichkeit, Excel effektiv freizugeben.
Gerhard Powell
8

Nach dem Versuch

  1. Geben Sie COM-Objekte in umgekehrter Reihenfolge frei
  2. Fügen Sie GC.Collect()und GC.WaitForPendingFinalizers()zweimal am Ende
  3. Nicht mehr als zwei Punkte
  4. Arbeitsmappe schließen und Anwendung beenden
  5. Im Release-Modus ausführen

Die endgültige Lösung, die für mich funktioniert, besteht darin, einen Satz zu verschieben

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

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

private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}
DG
quelle
Ich stellte fest, dass ich ComObjects nicht aufheben musste, sondern nur Quit () - Close () und FinalReleaseComObject. Ich kann nicht glauben, dass dies alles ist, was mir am Ende gefehlt hat, damit es funktioniert. Großartig!
Carol
7

Ich habe das genau befolgt ... Aber ich bin immer noch auf Probleme 1 von 1000 gestoßen. Wer weiß warum. Zeit, den Hammer herauszubringen ...

Unmittelbar nach der Instanziierung der Excel-Anwendungsklasse erhalte ich den gerade erstellten Excel-Prozess.

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 ausgeführt wird. Wenn es noch läuft, töte es!

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

¨ ° º¤ø „¸ Schießen Sie Excel proc und kauen Sie 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);
}
Antoine Meltzheim
quelle
6

Sie müssen sich bewusst 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 können. Dies gilt nicht für alle Funktionen, sondern für einige.

    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 dann, wenn Sie VSTO verwenden.

Für Details: http://support.microsoft.com/default.aspx?scid=kb;en-us;Q320369


quelle
6

"Verwenden Sie niemals zwei Punkte mit COM-Objekten" ist eine gute Faustregel, um das Auslaufen von COM-Referenzen zu vermeiden. Excel PIA kann jedoch auf mehr als auf den ersten Blick erkennbare Weise zum Auslaufen führen.

Eine dieser Möglichkeiten besteht darin, ein Ereignis zu abonnieren, das von einem der COM-Objekte des Excel-Objektmodells verfügbar gemacht wird.

Abonnieren Sie beispielsweise das WorkbookOpen-Ereignis der Anwendungsklasse.

Einige Theorie zu COM-Ereignissen

COM-Klassen stellen eine Gruppe von Ereignissen über Rückrufschnittstellen bereit. Um Ereignisse zu abonnieren, kann der Clientcode einfach ein Objekt registrieren, das die Rückrufschnittstelle implementiert, und die COM-Klasse ruft ihre Methoden als Antwort auf bestimmte Ereignisse auf. Da die Rückrufschnittstelle eine COM-Schnittstelle ist, ist es die Pflicht des implementierenden Objekts, den Referenzzähler jedes empfangenen COM-Objekts (als Parameter) für einen der Ereignishandler zu verringern.

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 .NET-Ereignis abonniert (Schwerpunkt auf 'a'), erstellt PIA eine Instanz einer Klasse, die die Rückrufschnittstelle implementiert, und registriert sie bei Excel.

Daher wird eine Reihe von Rückrufobjekten als Antwort auf verschiedene Abonnementanforderungen aus dem .NET-Code bei Excel registriert. Ein Rückrufobjekt pro Ereignisabonnement.

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

Auswirkung auf die Referenzanzahl der COM-Instanzen

Alle diese Rückrufobjekte verringern nicht die Referenzanzahl der COM-Objekte, die sie (als Parameter) für eine der Rückrufmethoden erhalten (selbst für diejenigen, die stillschweigend 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 derzeit darin, den PIA-Ereignisanbieter für die COM-Klasse zu vermeiden und einen eigenen Ereignisanbieter zu schreiben, der COM-Objekte deterministisch freigibt.

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

Amit Mittal
quelle
4

Wie bereits erwähnt, müssen Sie für jedes von Ihnen 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. Dh statt:

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 Excel geschlossen werden soll.

Wie Sie sehen, wird dies schnell extrem unhandlich, sobald Sie versuchen, etwas zu tun, das auch nur mäßig komplex ist. Ich habe erfolgreich .NET-Anwendungen mit einer einfachen Wrapper-Klasse entwickelt, die einige einfache Manipulationen des Excel-Objektmodells umfasst (Ö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 lässt sich jedoch nicht besser für komplexere Anforderungen skalieren.

Dies ist ein großer Mangel an .NET COM Interop. Für komplexere Szenarien würde ich ernsthaft in Betracht ziehen, eine ActiveX-DLL in VB6 oder einer anderen nicht verwalteten Sprache zu schreiben, an die Sie die gesamte Interaktion mit Out-Proc-COM-Objekten wie Office delegieren können. Sie können dann in Ihrer .NET-Anwendung auf diese ActiveX-DLL verweisen, und die Dinge werden viel einfacher, da Sie nur diese eine Referenz freigeben müssen.

Joe
quelle
3

Wenn all die oben genannten Dinge nicht funktioniert haben, 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);
Spider Man
quelle
2
Ich hasse willkürliche Wartezeiten. Aber +1, weil Sie zu Recht warten müssen, bis die Arbeitsmappen geschlossen sind. Eine Alternative besteht darin, die Arbeitsmappensammlung in einer Schleife abzufragen und das willkürliche Warten als Zeitlimit für die Schleife zu verwenden.
dFlat
3

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 in meinem Fall keine der oben genannten Möglichkeiten helfen . Stellen Sie sicher, dass Sie alle Objekte einschließlich des ersten Bereichs freigeben!

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

Die Optionen sind hier zusammen .

Ned
quelle
Sehr guter Punkt! Ich denke, weil ich Office 2007 verwende, werden in meinem Namen einige Bereinigungen durchgeführt. Ich habe den Rat weiter oben verwendet, aber keine Variablen gespeichert, wie Sie hier vorgeschlagen haben, und EXCEL.EXE wird beendet, aber ich könnte einfach Glück haben, und wenn ich weitere Probleme habe, werde ich mir diesen Teil meines Codes auf jeden Fall ansehen =)
Coops
3

Ein großartiger Artikel zum Freigeben von COM-Objekten ist 2.5 Freigeben von COM-Objekten (MSDN).

Die Methode, die ich empfehlen würde, besteht darin, Ihre Excel.Interop-Referenzen auf Null zu setzen, wenn es sich um nicht lokale Variablen handelt, und dann GC.Collect()und aufzurufenGC.WaitForPendingFinalizers() zweimal. Interop-Variablen mit lokalem Gültigkeitsbereich werden automatisch berücksichtigt.

Dadurch entfällt die Notwendigkeit, für jeden eine benannte Referenz zu führen COM-Objekt .

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 Wörter 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 Speicherbereinigung so deterministisch wie möglich. Das heißt, Sie sind sich ziemlich sicher, wann das Objekt bereinigt wurde - bei der Rückkehr vom zweiten Aufruf von WaitForPendingFinalizers. Alternativ können Sie Marshal.ReleaseComObject verwenden. Beachten Sie jedoch, dass Sie diese Methode wahrscheinlich nie verwenden müssen.

Porkbutts
quelle
2

Die Zwei-Punkte-Regel hat bei mir nicht funktioniert. In meinem Fall habe ich eine Methode zum Bereinigen meiner Ressourcen wie folgt erstellt:

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

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

Sie sollten bei der Verwendung von Word / Excel-Interop-Anwendungen sehr vorsichtig sein. Nachdem wir alle Lösungen ausprobiert hatten, war noch viel "WinWord" -Prozess auf dem Server offen (mit mehr als 2000 Benutzern).

Nachdem ich stundenlang an dem Problem gearbeitet hatte, wurde mir klar, dass der Word.ApplicationClass.Document.Open()IIS-Arbeitsprozess (w3wp.exe) abstürzen würde , wenn ich mehr als ein paar Dokumente gleichzeitig mit verschiedenen Threads öffnen würde, wobei alle WinWord-Prozesse offen bleiben würden!

Ich denke, es gibt keine absolute Lösung für dieses Problem, sondern den Wechsel zu anderen Methoden wie der Office Open XML- Entwicklung.

Arvand
quelle
1

Die akzeptierte Antwort hat bei mir nicht funktioniert. Der folgende Code im Destruktor hat den Job erledigt.

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

Ich arbeite derzeit an der Office-Automatisierung und bin auf eine Lösung gestoßen, die jedes Mal für mich funktioniert. Es ist einfach und beinhaltet nicht das Beenden von Prozessen.

Es scheint, dass durch bloßes Durchlaufen der aktuell aktiven Prozesse und auf irgendeine Weise "Zugriff" auf einen offenen Excel-Prozess jede streunende hängende Instanz von Excel entfernt wird. Der folgende Code sucht einfach nach Prozessen, bei denen der Name 'Excel' lautet, und schreibt dann die MainWindowTitle-Eigenschaft des Prozesses in eine Zeichenfolge. Diese 'Interaktion' mit dem Prozess scheint Windows dazu zu bringen, die eingefrorene Instanz von Excel einzuholen und abzubrechen.

Ich führe die folgende Methode kurz vor dem Add-In aus, das ich gerade beende, da es das Entladeereignis auslöst. Es entfernt jedes Mal alle hängenden Instanzen von Excel. Ehrlich gesagt bin ich mir nicht ganz sicher, warum dies funktioniert, aber es funktioniert gut für mich und könnte am Ende jeder Excel-Anwendung platziert werden, ohne sich um doppelte Punkte, Marshal.ReleaseComObject oder das Beenden von Prozessen kümmern zu müssen. Ich wäre sehr an Vorschlägen interessiert, 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;
                    }
                }
            }
}
Tom Brearley
quelle
1

Ich denke, dass einige davon nur die Art und Weise sind, wie das Framework mit Office-Anwendungen umgeht, aber ich könnte mich irren. An manchen Tagen bereinigen einige Anwendungen die Prozesse sofort, und an anderen Tagen scheint es zu warten, bis die Anwendung geschlossen wird. Im Allgemeinen höre ich auf, auf die Details zu achten, und stelle nur sicher, dass am Ende des Tages keine zusätzlichen Prozesse im Umlauf sind.

Auch und vielleicht bin ich damit fertig, Dinge zu vereinfachen, aber ich denke, Sie können 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 sagte, neige ich nicht dazu, auf die Details zu achten, wann der Excel-Prozess erscheint oder verschwindet, aber das funktioniert normalerweise bei mir. Ich mag es auch nicht, Excel-Prozesse für etwas anderes als die minimale Zeit zu halten, aber ich bin wahrscheinlich nur paranoid.

bill_the_loser
quelle
1

Wie einige haben wohl schon geschrieben, ist es nicht nur wichtig , wie Sie schließen das Excel (Objekt); Es ist auch wichtig, wie Sie öffnen und auch nach 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 unterschiedliche Parameterwerte verarbeitet wird - z. B. anhand von Werten in einer generischen Liste.

Ich habe alle Excel-bezogenen Funktionen in die Basisklasse und den Parser in eine Unterklasse eingefügt (verschiedene Parser verwenden gemeinsame Excel-Funktionen). Ich wollte nicht, dass Excel für jedes Element in einer generischen Liste geöffnet und wieder geschlossen wird, deshalb 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 zuvor implementiert, zweimal wie vorgeschlagen.

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 einige Updates bekomme.

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

Blaz Brencic
quelle