Come si puliscono correttamente gli oggetti di interoperabilità di Excel?


747

Sto usando l'interoperabilità di Excel in C # ( ApplicationClass) e ho inserito il seguente codice nella mia clausola finally:

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

Sebbene questo tipo di lavori, il Excel.exeprocesso è ancora in background anche dopo aver chiuso Excel. Viene rilasciato solo quando la mia applicazione viene chiusa manualmente.

Che cosa sto facendo di sbagliato o esiste un'alternativa per garantire che gli oggetti di interoperabilità vengano smaltiti correttamente?


1
Stai cercando di chiudere Excel.exe senza chiudere l'applicazione? Non sono sicuro di aver compreso appieno la tua domanda.
Bryant,

3
Sto cercando di assicurarmi che gli oggetti di interoperabilità non gestiti siano eliminati correttamente. In modo che non ci siano processi Excel in giro anche quando l'utente ha finito con il foglio di calcolo Excel che abbiamo creato dall'app.
HAdes,

3
Se puoi provare a farlo producendo file XML Excel, altrimenti considera la gestione della memoria non gestita VSTO
Jeremy Thompson

Questo si traduce bene in Excel?
Coops il

2
Vedere (oltre alle risposte di seguito) questo articolo di supporto di Microsoft, in cui forniscono soluzioni specifiche a questo problema: support.microsoft.com/kb/317109
Arjan

Risposte:


684

Excel non si chiude perché l'applicazione contiene ancora riferimenti a oggetti COM.

Immagino che stai invocando almeno un membro di un oggetto COM senza assegnarlo a una variabile.

Per me è stato l' oggetto excelApp.Worksheets che ho usato direttamente senza assegnarlo a una variabile:

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

Non sapevo che internamente C # creato un wrapper per l' Fogli di lavoro oggetto COM che non ha ottenuto rilasciato dal mio codice (perché non ero a conoscenza di esso) ed è stato il motivo per cui Excel non è stato scaricato.

Ho trovato la soluzione al mio problema in questa pagina , che ha anche una bella regola per l'uso degli oggetti COM in C #:

Non usare mai due punti con oggetti COM.


Quindi, con questa conoscenza, il modo giusto di fare quanto sopra è:

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

AGGIORNAMENTO POST MORTEM:

Voglio che tutti i lettori leggano con attenzione questa risposta di Hans Passant in quanto spiega la trappola in cui ci siamo imbattuti in molti altri sviluppatori. Quando ho scritto questa risposta anni fa non sapevo degli effetti del debugger sul garbage collector e ho tratto conclusioni errate. Mantengo la mia risposta inalterata per il bene della storia, ma per favore leggi questo link e non seguire la strada dei "due punti": Comprensione della garbage collection in .NET e Pulizia degli oggetti di interoperabilità di Excel con IDisposable


16
Quindi suggerisco di non usare Excel da COM e di salvarti tutti i problemi. I formati di Excel 2007 possono essere utilizzati senza mai aprire Excel, stupendo.
user7116

5
Non ho capito cosa significano "due punti". Puoi spiegare per favore?
A9S6,

22
Questo significa che non dovresti usare il modello comObject.Property.PropertiesProperty (vedi i due punti?). Assegna invece comObject.Property a una variabile e usa e smaltisci quella variabile. Una versione più formale della regola sopra potrebbe essere sth. come "Assegna l'oggetto com alle variabili prima di usarle. Ciò include gli oggetti com che sono proprietà di un altro oggetto com."
VVS

5
@Nick: In realtà, non hai bisogno di alcun tipo di pulizia, poiché il garbage collector lo farà per te. L'unica cosa che devi fare è assegnare ogni oggetto COM alla propria variabile in modo che il GC lo sappia.
VVS,

12
@VSS ecco i bollock, il GC pulisce tutto poiché le variabili wrapper sono create dal framework .net. Potrebbe volerci solo un'eternità per ripulirlo dal GC. Chiamare GC.Collect dopo l'interoperabilità pesante non è un cattivo idea.
CodingBarfield

280

Puoi effettivamente rilasciare l'oggetto Applicazione Excel in modo pulito, ma devi prenderti cura.

Il consiglio di mantenere un riferimento denominato per qualsiasi oggetto COM a cui si accede e di rilasciarlo esplicitamente tramite Marshal.FinalReleaseComObject()è in teoria corretto, ma, sfortunatamente, molto difficile da gestire in pratica. Se uno scivola da nessuna parte e usa "due punti", o scorre le celle tramite un for eachciclo o qualsiasi altro tipo di comando simile, allora avrai oggetti COM senza riferimento e rischierai un blocco. In questo caso, non ci sarebbe modo di trovare la causa nel codice; dovresti rivedere tutto il tuo codice a occhio e sperare di trovare la causa, un compito che potrebbe essere quasi impossibile per un grande progetto.

La buona notizia è che in realtà non è necessario mantenere un riferimento alla variabile denominata per ogni oggetto COM che si utilizza. Invece, chiama GC.Collect()e quindi GC.WaitForPendingFinalizers()rilasciare tutti gli oggetti (di solito minori) su cui non hai un riferimento, quindi rilascia esplicitamente gli oggetti su cui hai un riferimento a una variabile denominata.

Dovresti anche rilasciare i riferimenti nominati in ordine inverso di importanza: prima intervallo oggetti, quindi fogli di lavoro, cartelle di lavoro e infine l'oggetto Applicazione Excel.

Ad esempio, supponendo che tu avessi una variabile oggetto Range denominata xlRng, una variabile Foglio di lavoro denominata xlSheet, una variabile Workbook denominata xlBooke una variabile Applicazione Excel denominata xlApp, il tuo codice di pulizia potrebbe essere simile al seguente:

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

Nella maggior parte degli esempi di codice che vedrai per ripulire gli oggetti COM da .NET, le chiamate GC.Collect()e GC.WaitForPendingFinalizers()vengono effettuate DUE VOLTE come in:

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

Ciò non dovrebbe essere richiesto, tuttavia, a meno che non si utilizzi Visual Studio Tools for Office (VSTO), che utilizza finalizzatori che promuovono un intero grafico di oggetti nella coda di finalizzazione. Tali oggetti non sarebbero stati rilasciati fino alla prossima raccolta dei rifiuti. Tuttavia, se non si utilizza VSTO, si dovrebbe essere in grado di chiamare GC.Collect()e GC.WaitForPendingFinalizers()solo una volta.

So che chiamare esplicitamente GC.Collect()è un no-no (e certamente farlo due volte suona molto doloroso), ma non c'è modo di aggirarlo, a dire il vero. Attraverso le normali operazioni genererai oggetti nascosti ai quali non detieni alcun riferimento che, pertanto, non puoi rilasciare con nessun altro mezzo diverso dalla chiamata GC.Collect().

Questo è un argomento complesso, ma questo è davvero tutto quello che c'è da fare. Una volta stabilito questo modello per la procedura di pulizia, è possibile codificare normalmente, senza la necessità di wrapper, ecc. :-)

Ho un tutorial su questo qui:

Automatizzare i programmi di Office con VB.Net / COM Interop

È scritto per VB.NET, ma non scoraggiarlo, i principi sono esattamente gli stessi di quando si usa C #.


3
Una discussione correlata è disponibile sul forum ExtremeVBTalk .NET Office Automation, qui: xtremevbtalk.com/showthread.php?t=303928 .
Mike Rosenblum,

2
E se tutto il resto fallisce, allora Process.Kill () può essere usato (come ultima risorsa) come descritto qui: stackoverflow.com/questions/51462/…
Mike Rosenblum,

1
Sono contento che abbia funzionato Richard. :-) Ed ecco un sottile esempio in cui semplicemente evitare "due punti" non è sufficiente per prevenire un problema: stackoverflow.com/questions/4964663/…
Mike Rosenblum,

Ecco un'opinione al riguardo di Misha Shneerson di Microsoft sull'argomento, entro la data del commento del 7 ottobre 2010. ( blogs.msdn.com/b/csharpfaq/archive/2010/09/28/… )
Mike Rosenblum,

Spero che questo aiuti un problema persistente che stiamo riscontrando. È sicuro eseguire la garbage collection e rilasciare le chiamate in un blocco finally?
Brett Green,

213

Prefazione: la mia risposta contiene due soluzioni, quindi fai attenzione quando leggi e non perdere nulla.

Esistono diversi modi e consigli su come scaricare l'istanza di Excel, ad esempio:

  • Rilasciare OGNI oggetto com esplicitamente con Marshal.FinalReleaseComObject () (senza dimenticare gli oggetti com creati in modo implicito). Per rilasciare ogni oggetto com creato, è possibile utilizzare la regola di 2 punti menzionati qui:
    Come si puliscono correttamente gli oggetti di interoperabilità di Excel?

  • Chiamare GC.Collect () e GC.WaitForPendingFinalizers () per fare in modo che CLR rilasci oggetti com inutilizzati * (In realtà, funziona, vedere la mia seconda soluzione per i dettagli)

  • Verifica se l'applicazione com-server mostra forse una finestra di messaggio in attesa che l'utente risponda (anche se non sono sicuro che possa impedire la chiusura di Excel, ma ne ho sentito parlare alcune volte)

  • Invio del messaggio WM_CLOSE alla finestra principale di Excel

  • Esecuzione della funzione che funziona con Excel in un AppDomain separato. Alcune persone credono che l'istanza di Excel verrà chiusa quando AppDomain viene scaricato.

  • Uccidere tutte le istanze di Excel che sono state istanziate dopo l'inizio del nostro codice di interoperabilità di Excel.

MA! A volte tutte queste opzioni non aiutano o non possono essere appropriate!

Ad esempio, ieri ho scoperto che in una delle mie funzioni (che funziona con Excel) Excel continua a funzionare al termine della funzione. Ho provato di tutto! Ho controllato a fondo l'intera funzione 10 volte e ho aggiunto Marshal.FinalReleaseComObject () per tutto! Ho anche avuto GC.Collect () e GC.WaitForPendingFinalizers (). Ho controllato le finestre di messaggio nascoste. Ho provato a inviare il messaggio WM_CLOSE alla finestra principale di Excel. Ho eseguito la mia funzione in un AppDomain separato e scaricato quel dominio. Niente ha aiutato! L'opzione con la chiusura di tutte le istanze di Excel è inappropriata, perché se l'utente avvia manualmente un'altra istanza di Excel, durante l'esecuzione della mia funzione che funziona anche con Excel, anche quell'istanza verrà chiusa dalla mia funzione. Scommetto che l'utente non sarà felice! Quindi, onestamente, questa è un'opzione zoppa (senza offesa ragazzi).soluzione : uccidi il processo Excel da hWnd della sua finestra principale (è la prima soluzione).

Ecco il semplice codice:

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

Come puoi vedere, ho fornito due metodi, secondo il modello Try-Parse (penso che sia appropriato qui): un metodo non genera l'eccezione se il Processo non può essere ucciso (ad esempio il processo non esiste più) e un altro metodo genera l'eccezione se il processo non è stato ucciso. L'unico punto debole in questo codice sono le autorizzazioni di sicurezza. Teoricamente, l'utente potrebbe non disporre delle autorizzazioni per terminare il processo, ma nel 99,99% dei casi l'utente dispone di tali autorizzazioni. L'ho anche testato con un account ospite - funziona perfettamente.

Quindi, il tuo codice, lavorando con Excel, può assomigliare a questo:

int hWnd = xl.Application.Hwnd;
// ...
// here we try to close Excel as usual, with xl.Quit(),
// Marshal.FinalReleaseComObject(xl) and so on
// ...
TryKillProcessByMainWindowHwnd(hWnd);

Ecco! Excel è terminato! :)

Ok, torniamo alla seconda soluzione, come ho promesso all'inizio del post. La seconda soluzione è chiamare GC.Collect () e GC.WaitForPendingFinalizers (). Sì, funzionano davvero, ma devi stare attento qui!
Molte persone dicono (e io ho detto) che chiamare GC.Collect () non aiuta. Ma il motivo per cui non sarebbe d'aiuto è se ci sono ancora riferimenti a oggetti COM! Uno dei motivi più popolari per cui GC.Collect () non è utile è l'esecuzione del progetto in modalità Debug. Negli oggetti in modalità debug a cui non viene più realmente fatto riferimento, i dati non verranno raccolti in modo inutile fino alla fine del metodo.
Quindi, se hai provato GC.Collect () e GC.WaitForPendingFinalizers () e non è stato d'aiuto, prova a fare quanto segue:

1) Prova a eseguire il progetto in modalità Rilascio e controlla se Excel è stato chiuso correttamente

2) Avvolgere il metodo di lavorare con Excel in un metodo separato. Quindi, invece di qualcosa del genere:

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

Scrivi:

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

Ora Excel chiuderà =)


19
triste che il thread sia già così vecchio che la tua risposta eccellente appare molto più in basso, che penso sia l'unica ragione per cui non è stato votato più volte ...
Chiccodoro,

15
Devo ammettere che, quando ho letto per la prima volta la tua risposta, ho pensato che fosse un kludge gigante. Dopo circa 6 ore di lotta con questo (tutto viene rilasciato, non ho punti doppi, ecc.), Ora penso che la tua risposta sia geniale.
Segna il

3
Grazie per questo. Ho lottato con un Excel che non si chiudeva, non importa cosa per un paio di giorni prima di trovarlo. Eccellente.
BBlake,

2
@nightcoder: risposta fantastica, davvero dettagliata. I tuoi commenti in merito alla modalità di debug sono molto veri e importanti che hai sottolineato. Lo stesso codice in modalità di rilascio può spesso andare bene. Process.Kill, tuttavia, è utile solo quando si utilizza l'automazione, non quando il programma è in esecuzione in Excel, ad esempio come componente aggiuntivo COM gestito.
Mike Rosenblum,

2
@DanM: il tuo approccio è corretto al 100% e non fallirà mai. Mantenendo tutte le variabili locali sul metodo, tutti i riferimenti sono effettivamente irraggiungibili dal codice .NET. Ciò significa che quando la tua sezione infine chiama GC.Collect (), i finalizzatori di tutti i tuoi oggetti COM verranno chiamati con certezza. Ogni finalizzatore chiama quindi Marshal.FinalReleaseComObject sull'oggetto da finalizzare. Il tuo approccio è quindi semplice e sicuro. Non aver paura di usare questo. (Unico avvertimento: se usi VSTO, che dubito che tu sia, dovresti chiamare GC.Collect () e GC.WaitForPendingFinalizers DUE VOLTE.)
Mike Rosenblum,

49

AGGIORNAMENTO : aggiunto codice C # e collegamento ai lavori di Windows

Ho trascorso qualche tempo a cercare di capire questo problema e all'epoca XtremeVBTalk era il più attivo e reattivo. Ecco un link al mio post originale, Chiudere in modo pulito un processo di interoperabilità di Excel, anche se l'applicazione si arresta in modo anomalo . Di seguito è riportato un riepilogo del post e il codice copiato in questo post.

  • Chiudere il processo di interoperabilità con Application.Quit()e Process.Kill()funziona per la maggior parte, ma fallisce se le applicazioni si bloccano in modo catastrofico. Vale a dire se l'app si arresta in modo anomalo, il processo di Excel continuerà comunque a perdere.
  • La soluzione è consentire al sistema operativo di gestire la pulizia dei processi tramite Windows Job Objects utilizzando le chiamate Win32. Quando la tua applicazione principale muore, anche i processi associati (ad es. Excel) verranno chiusi.

Ho trovato che questa è una soluzione pulita perché il sistema operativo sta facendo un vero lavoro di pulizia. Tutto quello che devi fare è registrare il processo di Excel.

Codice lavoro di Windows

Avvolge le chiamate API Win32 per registrare i processi di interoperabilità.

public enum JobObjectInfoType
{
    AssociateCompletionPortInformation = 7,
    BasicLimitInformation = 2,
    BasicUIRestrictions = 4,
    EndOfJobTimeInformation = 6,
    ExtendedLimitInformation = 9,
    SecurityLimitInformation = 5,
    GroupInformation = 11
}

[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
    public int nLength;
    public IntPtr lpSecurityDescriptor;
    public int bInheritHandle;
}

[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
    public Int64 PerProcessUserTimeLimit;
    public Int64 PerJobUserTimeLimit;
    public Int16 LimitFlags;
    public UInt32 MinimumWorkingSetSize;
    public UInt32 MaximumWorkingSetSize;
    public Int16 ActiveProcessLimit;
    public Int64 Affinity;
    public Int16 PriorityClass;
    public Int16 SchedulingClass;
}

[StructLayout(LayoutKind.Sequential)]
struct IO_COUNTERS
{
    public UInt64 ReadOperationCount;
    public UInt64 WriteOperationCount;
    public UInt64 OtherOperationCount;
    public UInt64 ReadTransferCount;
    public UInt64 WriteTransferCount;
    public UInt64 OtherTransferCount;
}

[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
    public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
    public IO_COUNTERS IoInfo;
    public UInt32 ProcessMemoryLimit;
    public UInt32 JobMemoryLimit;
    public UInt32 PeakProcessMemoryUsed;
    public UInt32 PeakJobMemoryUsed;
}

public class Job : IDisposable
{
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
    static extern IntPtr CreateJobObject(object a, string lpName);

    [DllImport("kernel32.dll")]
    static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);

    private IntPtr m_handle;
    private bool m_disposed = false;

    public Job()
    {
        m_handle = CreateJobObject(null, null);

        JOBOBJECT_BASIC_LIMIT_INFORMATION info = new JOBOBJECT_BASIC_LIMIT_INFORMATION();
        info.LimitFlags = 0x2000;

        JOBOBJECT_EXTENDED_LIMIT_INFORMATION extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
        extendedInfo.BasicLimitInformation = info;

        int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
        IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length);
        Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false);

        if (!SetInformationJobObject(m_handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length))
            throw new Exception(string.Format("Unable to set information.  Error: {0}", Marshal.GetLastWin32Error()));
    }

    #region IDisposable Members

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    #endregion

    private void Dispose(bool disposing)
    {
        if (m_disposed)
            return;

        if (disposing) {}

        Close();
        m_disposed = true;
    }

    public void Close()
    {
        Win32.CloseHandle(m_handle);
        m_handle = IntPtr.Zero;
    }

    public bool AddProcess(IntPtr handle)
    {
        return AssignProcessToJobObject(m_handle, handle);
    }

}

Nota sul codice del costruttore

  • Nel costruttore, info.LimitFlags = 0x2000;viene chiamato. 0x2000è il JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSEvalore enum e questo valore è definito da MSDN come:

Fa terminare tutti i processi associati al lavoro quando viene chiuso l'ultimo handle per il lavoro.

Chiamata API Win32 aggiuntiva per ottenere l'ID processo (PID)

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

Usando il codice

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

2
Uccidere esplicitamente un server COM out-of-process (che potrebbe servire altri client COM) è un'idea terribile. Se stai ricorrendo a questo è perché il tuo protocollo COM è rotto. I server COM non devono essere trattati come normali app con finestre
MickyD

39

Questo ha funzionato per un progetto a cui stavo lavorando:

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

Abbiamo appreso che era importante impostare su null ogni riferimento a un oggetto COM di Excel al termine. Ciò includeva Celle, Fogli e tutto.


30

Tutto ciò che si trova nello spazio dei nomi di Excel deve essere rilasciato. Periodo

Non puoi fare:

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

Devi fare

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

seguito dal rilascio degli oggetti.


3
Come ad esempio xlRange.Interior.Color per esempio.
HAdes,

L'interno deve essere rilasciato (è nello spazio dei nomi) ... Il colore d'altra parte non lo fa (causa suo da System.Drawing.Color, iirc)
MagicKat

2
in realtà il colore è un colore Excel, non un colore .Net. hai appena passato un lungo. Anche le cartelle di lavoro def. devono essere rilasciati, fogli di lavoro ... meno.
Tipo anonimo

Ciò non è vero, la regola dei due punti cattivi e di un punto buoni non ha senso. Inoltre, non è necessario rilasciare cartelle di lavoro, fogli di lavoro, intervalli, questo è il lavoro del GC. L'unica cosa che non devi mai dimenticare è chiamare Esci sul solo e unico oggetto Excel.Application. Dopo aver chiamato Esci, annulla l'oggetto Excel.Application e chiama GC.Collect (); GC.WaitForPendingFinalizers (); due volte.
Dietrich Baumgarten,

29

Prima tu non mai chiamare Marshal.ReleaseComObject(...)o Marshal.FinalReleaseComObject(...)quando esegui l'interoperabilità di Excel. È un anti-pattern confuso, ma qualsiasi informazione al riguardo, incluso da Microsoft, che indica che è necessario rilasciare manualmente i riferimenti COM da .NET non è corretta. Il fatto è che il runtime .NET e il garbage collector tengono correttamente traccia e puliscono i riferimenti COM. Per il tuo codice, questo significa che puoi rimuovere l'intero ciclo `while (...) in alto.

In secondo luogo, se si desidera assicurarsi che i riferimenti COM a un oggetto COM out-of-process vengano ripuliti al termine del processo (in modo che il processo Excel si chiuda), è necessario assicurarsi che Garbage Collector sia in esecuzione. Lo si fa correttamente con le chiamate a GC.Collect()e GC.WaitForPendingFinalizers(). Chiamarlo due volte è sicuro e garantisce che anche i cicli vengano definitivamente ripuliti (anche se non sono sicuro che sia necessario, e apprezzerei un esempio che lo mostri).

In terzo luogo, quando si esegue con il debugger, i riferimenti locali verranno mantenuti artificialmente in vita fino alla fine del metodo (in modo che l'ispezione delle variabili locali funzioni). Così GC.Collect() chiamate non sono efficaci per la pulizia dell'oggetto come rng.Cellsdallo stesso metodo. È necessario dividere il codice eseguendo l'interoperabilità COM dalla pulizia del GC in metodi separati. (Questa è stata una scoperta chiave per me, da una parte della risposta pubblicata qui da @nightcoder.)

Lo schema generale sarebbe quindi:

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

Ci sono molte informazioni false e confusione su questo problema, inclusi molti post su MSDN e Stack Overflow (e soprattutto questa domanda!).

Ciò che alla fine mi ha convinto a dare un'occhiata più da vicino e capire il consiglio giusto è stato il post sul blog Marshal.ReleaseComObject considerato pericoloso insieme a trovare il problema con riferimenti mantenuti vivi sotto il debugger che stava confondendo i miei test precedenti.


5
Praticamente TUTTE LE RISPOSTE SU QUESTA PAGINA SONO ERRATE TRANNE QUESTA PAGINA. Funzionano ma sono troppo lavoro. IGNORA la regola "2 DOTS". Lascia che il GC faccia il tuo lavoro per te. Prove supplementari da Net GURU Hans Passant: stackoverflow.com/a/25135685/3852958
Alan Baljeu

1
Govert, che sollievo trovare il modo corretto di farlo. Grazie.
Dietrich Baumgarten,

18

Ho trovato un modello generico utile che può aiutare a implementare il modello di smaltimento corretto per gli oggetti COM, che necessitano di Marshal.ReleaseComObject chiamato quando escono dall'ambito:

Uso:

using (AutoReleaseComObject<Application> excelApplicationWrapper = new AutoReleaseComObject<Application>(new Application()))
{
    try
    {
        using (AutoReleaseComObject<Workbook> workbookWrapper = new AutoReleaseComObject<Workbook>(excelApplicationWrapper.ComObject.Workbooks.Open(namedRangeBase.FullName, false, false, missing, missing, missing, true, missing, missing, true, missing, missing, missing, missing, missing)))
        {
           // do something with your workbook....
        }
    }
    finally
    {
         excelApplicationWrapper.ComObject.Quit();
    } 
}

Modello:

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

Riferimento:

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


3
sì questo è buono. Penso che questo codice possa essere aggiornato anche se ora FinalReleaseCOMObject è disponibile.
Tipo anonimo

È ancora fastidioso avere un blocco using per ogni oggetto. Vedi qui stackoverflow.com/questions/2191489/… per un'alternativa.
Henrik,

16

Non posso credere che questo problema abbia tormentato il mondo per 5 anni .... Se hai creato un'applicazione, devi prima spegnerla prima di rimuovere il collegamento.

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

alla chiusura

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

Quando si crea una nuova applicazione Excel, si apre un programma Excel in background. Devi comandare a quel programma Excel di uscire prima di rilasciare il collegamento perché quel programma Excel non fa parte del tuo controllo diretto. Pertanto, rimarrà aperto se il collegamento viene rilasciato!

Buona programmazione a tutti ~~


No, in particolare se si desidera consentire l'interazione dell'utente con l'applicazione COM (Excel in questo caso - OP non specifica se l'interazione dell'utente è prevista, ma non fa alcun riferimento alla sua chiusura). Devi solo convincere il chiamante a lasciarsi andare affinché l'utente sia in grado di uscire da Excel quando si chiude, per esempio, un singolo file.
downwitch

questo ha funzionato perfettamente per me - si è schiantato quando avevo solo objExcel.Quit (), ma quando ho anche fatto objExcel.Application.Quit () in precedenza, chiuso in modo pulito.
mcmillab,

13

Sviluppatori comuni, nessuna delle vostre soluzioni ha funzionato per me, quindi decido di implementare un nuovo trucco .

Innanzitutto, specifica "Qual è il nostro obiettivo?" => "Non vedere l'oggetto Excel dopo il nostro lavoro nel task manager"

Ok. Non lasciarti sfidare e inizia a distruggerlo, ma considera di non distruggere altre istanze di Excel che sono in esecuzione in parallelo.

Quindi, ottieni l'elenco dei processori attuali e recupera il PID dei processi EXCEL, quindi una volta terminato il tuo lavoro, abbiamo un nuovo guest nell'elenco dei processi con un PID unico, trova e distruggi solo quello.

<tieni presente che qualsiasi nuovo processo Excel durante il tuo lavoro Excel verrà rilevato come nuovo e distrutto> <Una soluzione migliore è catturare il PID del nuovo oggetto Excel creato e distruggerlo>

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

Questo risolve il mio problema, spero anche il tuo.


1
.... questo è un po 'un approccio con il martello? - è simile a quello che sto usando anche :( ma è necessario cambiare. Il problema con l'utilizzo di questo approccio è che spesso quando si apre Excel, sulla macchina su cui è stato eseguito, ha l'avviso nella barra di sinistra, dicendo qualcosa come "Excel è stato chiuso in modo errato": non molto elegante. Penso che uno dei precedenti suggerimenti in questo thread sia da preferire.
whytheq,

11

Questo sembra sicuramente complicato. Dalla mia esperienza, ci sono solo tre cose chiave per far chiudere correttamente Excel:

1: assicurati che non vi siano riferimenti rimanenti all'applicazione Excel creata (dovresti comunque averne uno solo; impostalo su null)

2: chiama GC.Collect()

3: Excel deve essere chiuso dall'utente o chiudendo manualmente il programma oppure chiamando Quitl'oggetto Excel. (Nota cheQuit funzionerà come se l'utente tentasse di chiudere il programma e presenterà una finestra di dialogo di conferma in caso di modifiche non salvate, anche se Excel non è visibile. L'utente può premere Annulla, quindi Excel non sarà chiuso. )

1 deve accadere prima di 2, ma 3 può accadere in qualsiasi momento.

Un modo per implementarlo è avvolgere l'oggetto Excel interop con la propria classe, creare l'istanza interop nel costruttore e implementare IDisposable con Dispose simile a

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

Ciò eliminerà l'eccellenza dal lato del programma. Una volta chiuso Excel (manualmente dall'utente o chiamando l'utente Quit) il processo andrà via. Se il programma è già stato chiuso, il processo scomparirà sulGC.Collect() chiamata.

(Non sono sicuro di quanto sia importante, ma potresti desiderare una GC.WaitForPendingFinalizers()chiamata dopo la GC.Collect()chiamata, ma non è strettamente necessario eliminare il processo di Excel.)

Questo ha funzionato per me senza problemi per anni. Tieni presente però che mentre funziona, in realtà devi chiudere con grazia per farlo funzionare. Continuerai ad accumulare processi excel.exe se interrompi il programma prima che Excel venga ripulito (in genere premendo "stop" mentre il programma è in fase di debug).


1
Questa risposta funziona ed è infinitamente più conveniente della risposta accettata. Nessun problema con "due punti" con oggetti COM, nessun uso di Marshal.
andrew.cuthbert,

9

Per aggiungere ai motivi per cui Excel non si chiude, anche quando si creano riferimenti diretti a ciascun oggetto al momento della lettura, la creazione è il ciclo "For".

For Each objWorkBook As WorkBook in objWorkBooks 'local ref, created from ExcelApp.WorkBooks to avoid the double-dot
   objWorkBook.Close 'or whatever
   FinalReleaseComObject(objWorkBook)
   objWorkBook = Nothing
Next 

'The above does not work, and this is the workaround:

For intCounter As Integer = 1 To mobjExcel_WorkBooks.Count
   Dim objTempWorkBook As Workbook = mobjExcel_WorkBooks.Item(intCounter)
   objTempWorkBook.Saved = True
   objTempWorkBook.Close(False, Type.Missing, Type.Missing)
   FinalReleaseComObject(objTempWorkBook)
   objTempWorkBook = Nothing
Next

E un altro motivo, riutilizzare un riferimento senza rilasciare prima il valore precedente.
Grimfort,

Grazie. Stavo anche usando un "per ciascuno" che la tua soluzione ha funzionato per me.
Chad Braun-Duin,

+1 Grazie. Inoltre, nota che se hai un oggetto per un intervallo di Excel e vuoi cambiare l'intervallo durante la vita dell'oggetto, ho scoperto che dovevo rilasciare ReleaseComObject prima di riassegnarlo, il che rende il codice un po 'disordinato!
AjV Jsy,

9

Tradizionalmente ho seguito i consigli trovati nella risposta di VVS . Tuttavia, nel tentativo di mantenere aggiornata questa risposta con le ultime opzioni, penso che tutti i miei progetti futuri utilizzeranno la libreria "NetOffice".

NetOffice sostituisce completamente le PIA di Office ed è completamente indipendente dalla versione. È una raccolta di wrapper COM gestiti in grado di gestire la pulizia che spesso causa tali mal di testa quando si lavora con Microsoft Office in .NET.

Alcune caratteristiche chiave sono:

  • Principalmente indipendenti dalla versione (e le funzionalità dipendenti dalla versione sono documentate)
  • Nessuna dipendenza
  • No PIA
  • Nessuna registrazione
  • No VSTO

Non sono in alcun modo affiliato al progetto; Apprezzo davvero la forte riduzione del mal di testa.


2
Questo dovrebbe essere contrassegnato come la risposta, davvero. NetOffice elimina tutta questa complessità.
C. Augusto Proiete,

1
Sto usando NetOffice da molto tempo scrivendo un componente aggiuntivo Excel e ha funzionato perfettamente. L'unica cosa da considerare è che se non si eliminano esplicitamente oggetti usati, lo farà quando si esce dall'applicazione (perché ne tiene traccia comunque). Quindi la regola empirica con NetOffice è sempre quella di utilizzare il modello "using" con ogni oggetto Excel come cella, intervallo o foglio ecc.
Stas Ivanov,

8

La risposta accettata qui è corretta, ma si noti anche che non solo i riferimenti a "due punti" devono essere evitati, ma anche gli oggetti che vengono recuperati tramite l'indice. Inoltre, non è necessario attendere fino al termine del programma per ripulire questi oggetti, è consigliabile creare funzioni che li ripuliscano non appena si è terminato con essi, quando possibile. Ecco una funzione che ho creato che assegna alcune proprietà di un oggetto Style chiamato 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 { } 
}

Si noti che ho dovuto impostare xlBorders[Excel.XlBordersIndex.xlEdgeBottom] una variabile per ripulirla (non a causa dei due punti, che si riferiscono a un elenco che non ha bisogno di essere rilasciato, ma perché l'oggetto a cui mi riferisco è in realtà un oggetto Border che deve essere rilasciato).

Questo genere di cose non è davvero necessario nelle applicazioni standard, che fanno un ottimo lavoro di pulizia dopo se stesse, ma nelle applicazioni ASP.NET, se perdi anche una di queste, indipendentemente da quanto spesso chiami il garbage collector, Excel essere ancora in esecuzione sul tuo server.

Richiede molta attenzione ai dettagli e molte esecuzioni di test durante il monitoraggio del Task Manager durante la scrittura di questo codice, ma in tal modo si evita il fastidio di cercare disperatamente pagine di codice per trovare l'istanza persa. Ciò è particolarmente importante quando si lavora in loop, in cui è necessario rilasciare OGNI ISTANZA di un oggetto, anche se utilizza lo stesso nome di variabile ogni volta che viene eseguito il loop.


In Release, controlla Null (la risposta di Joe). Ciò eviterà inutili eccezioni nulle. Ho testato MOLTI metodi. Questo è l'unico modo per rilasciare efficacemente Excel.
Gerhard Powell,

8

Dopo aver provato

  1. Rilasciare gli oggetti COM in ordine inverso
  2. Aggiungi GC.Collect()eGC.WaitForPendingFinalizers() due volte alla fine
  3. Non più di due punti
  4. Chiudi la cartella di lavoro ed esci dall'applicazione
  5. Esegui in modalità di rilascio

la soluzione finale che funziona per me è spostare un set di

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

che abbiamo aggiunto alla fine della funzione a un wrapper, come segue:

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

Ho scoperto che non avevo bisogno di annullare ComObjects, solo Quit () - Close () e FinalReleaseComObject. Non riesco a credere che questo sia tutto ciò che mi mancava per farlo funzionare. Grande!
Carol

7

L'ho seguito esattamente ... Ma ho ancora riscontrato problemi 1 su 1000 volte. Chissà perché. È ora di tirare fuori il martello ...

Subito dopo l'istanza della classe Applicazione Excel ottengo una sospensione del processo Excel appena creato.

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

Quindi, una volta eseguita la pulizia COM di cui sopra, mi assicuro che il processo non sia in esecuzione. Se è ancora in esecuzione, uccidilo!

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

7

¨ ° º¤ø „¸ Spara Excel proc e mastica gomma da masticare ¸„ ø¤º ° ¨

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

6

Devi essere consapevole del fatto che Excel è molto sensibile alla cultura in cui stai eseguendo.

Potrebbe essere necessario impostare la cultura su EN-US prima di chiamare le funzioni di Excel. Questo non si applica a tutte le funzioni, ma ad alcune.

    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;

Questo vale anche se si utilizza VSTO.

Per dettagli: http://support.microsoft.com/default.aspx?scid=kb;en-us;Q320369


6

"Non usare mai due punti con oggetti COM" è un'ottima regola empirica per evitare perdite di riferimenti COM, ma Excel PIA può portare a perdite in più di quanto appaia a prima vista.

Uno di questi modi è la sottoscrizione a qualsiasi evento esposto da uno qualsiasi degli oggetti COM del modello a oggetti Excel.

Ad esempio, iscrivendosi all'evento WorkbookOpen della classe Application.

Qualche teoria sugli eventi COM

Le classi COM espongono un gruppo di eventi tramite interfacce di call-back. Per iscriversi agli eventi, il codice client può semplicemente registrare un oggetto che implementa l'interfaccia di richiamata e la classe COM invocherà i suoi metodi in risposta a eventi specifici. Poiché l'interfaccia di richiamata è un'interfaccia COM, è compito dell'oggetto implementante ridurre il conteggio di riferimento di qualsiasi oggetto COM che riceve (come parametro) per qualsiasi gestore di eventi.

Come Excel PIA espone eventi COM

Excel PIA espone gli eventi COM della classe di applicazione Excel come eventi .NET convenzionali. Ogni volta che il codice del client è abbonata a un evento di .NET (enfasi sulla 'a'), PIA crea un'istanza di una classe che implementa l'interfaccia call-back e lo registra con Excel.

Pertanto, numerosi oggetti di richiamata vengono registrati con Excel in risposta a diverse richieste di abbonamento dal codice .NET. Un oggetto call-back per abbonamento evento.

Un'interfaccia di call-back per la gestione degli eventi significa che PIA deve sottoscrivere tutti gli eventi dell'interfaccia per ogni richiesta di abbonamento agli eventi .NET. Non può scegliere. Alla ricezione di un call-back di eventi, l'oggetto call-back verifica se il gestore di eventi .NET associato è interessato o meno all'evento corrente e quindi invoca il gestore o ignora silenziosamente il call-back.

Effetto sui conteggi dei riferimenti all'istanza COM

Tutti questi oggetti di richiamata non diminuiscono il conteggio dei riferimenti di nessuno degli oggetti COM che ricevono (come parametri) per nessuno dei metodi di richiamata (anche per quelli che vengono silenziosamente ignorati). Si basano esclusivamente sul Garbage Collector CLR per liberare gli oggetti COM.

Poiché l'esecuzione del GC non è deterministica, ciò può comportare il blocco del processo di Excel per una durata superiore a quella desiderata e creare un'impressione di "perdita di memoria".

Soluzione

Al momento l'unica soluzione è evitare il provider di eventi PIA per la classe COM e scrivere il proprio provider di eventi che rilascia in modo deterministico oggetti COM.

Per la classe Application, questo può essere fatto implementando l'interfaccia AppEvents e quindi registrando l'implementazione con Excel utilizzando l' interfaccia IConnectionPointContainer . La classe Application (e per questo motivo tutti gli oggetti COM che espongono eventi utilizzando il meccanismo di callback) implementa l'interfaccia IConnectionPointContainer.


4

Come altri hanno sottolineato, è necessario creare un riferimento esplicito per ogni oggetto Excel utilizzato e chiamare Marshal.ReleaseComObject su tale riferimento, come descritto in questo articolo KB . È inoltre necessario utilizzare try / finally per assicurarsi che ReleaseComObject sia sempre chiamato, anche quando viene generata un'eccezione. Cioè invece di:

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

devi fare qualcosa del tipo:

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

È inoltre necessario chiamare Application.Quit prima di rilasciare l'oggetto Application se si desidera chiudere Excel.

Come puoi vedere, questo diventa rapidamente estremamente ingombrante non appena provi a fare qualcosa anche moderatamente complesso. Ho sviluppato con successo applicazioni .NET con una semplice classe wrapper che avvolge alcune semplici manipolazioni del modello a oggetti di Excel (apri una cartella di lavoro, scrivi in ​​un intervallo, salva / chiudi la cartella di lavoro ecc.). La classe wrapper implementa IDisposable, implementa attentamente Marshal.ReleaseComObject su ogni oggetto che utilizza e non espone pubblicamente alcun oggetto Excel al resto dell'app.

Ma questo approccio non si adatta bene a requisiti più complessi.

Questa è una grande carenza di .NET COM Interop. Per scenari più complessi, prenderei in seria considerazione la possibilità di scrivere una DLL ActiveX in VB6 o altro linguaggio non gestito a cui è possibile delegare tutte le interazioni con oggetti COM out-proc come Office. È quindi possibile fare riferimento a questa DLL ActiveX dall'applicazione .NET e le cose saranno molto più semplici in quanto sarà sufficiente rilasciare questo riferimento.


3

Quando tutto quanto sopra non ha funzionato, prova a dare ad Excel un po 'di tempo per chiudere i suoi fogli:

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

...
FinalReleaseComObject(app);

2
Odio le attese arbitrarie. Ma +1 perché hai ragione sul bisogno di aspettare la chiusura delle cartelle di lavoro. Un'alternativa è eseguire il polling della raccolta delle cartelle di lavoro in un ciclo e utilizzare l'attesa arbitraria come timeout del ciclo.
dFlat

3

Assicurati di rilasciare tutti gli oggetti relativi a Excel!

Ho trascorso alcune ore provando diversi modi. Sono tutte grandi idee ma alla fine ho trovato il mio errore: se non rilasci tutti gli oggetti, nessuno dei modi sopra può aiutarti nel mio caso. Assicurati di rilasciare tutti gli oggetti compreso l'intervallo uno!

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

Le opzioni sono insieme qui .


Ottimo punto! Penso che, poiché sto usando Office 2007, ci sia un po 'di pulizia da parte mia. Ho usato il consiglio più avanti ma non ho memorizzato le variabili come hai suggerito qui ed EXCEL.EXE esce ma potrei essere solo fortunato e se avessi ulteriori problemi guarderò sicuramente questa parte del mio codice =)
Coops

3

Un ottimo articolo sul rilascio di oggetti COM è 2.5 Rilascio di oggetti COM (MSDN).

Il metodo che vorrei raccomandare è di annullare i riferimenti a Excel.Interop se sono variabili non locali, quindi chiamare GC.Collect()e GC.WaitForPendingFinalizers()due volte. Le variabili Interop con ambito locale verranno gestite automaticamente.

Ciò elimina la necessità di mantenere un riferimento denominato per ogni oggetto COM.

Ecco un esempio tratto dall'articolo:

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

Queste parole sono direttamente dall'articolo:

In quasi tutte le situazioni, l'annullamento del riferimento RCW e la forzatura di una garbage collection ripuliranno correttamente. Se chiami anche GC.WaitForPendingFinalizers, la garbage collection sarà deterministica quanto puoi farla. Cioè, sarai abbastanza sicuro esattamente quando l'oggetto sarà stato ripulito, al ritorno dalla seconda chiamata a WaitForPendingFinalizers. In alternativa, è possibile utilizzare Marshal.ReleaseComObject. Tuttavia, tieni presente che è molto improbabile che tu abbia mai bisogno di usare questo metodo.


2

La regola dei due punti non ha funzionato per me. Nel mio caso ho creato un metodo per pulire le mie risorse come segue:

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

2

La mia soluzione

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

2

Dovresti stare molto attento usando le applicazioni di interoperabilità di Word / Excel. Dopo aver provato tutte le soluzioni, il processo "WinWord" era ancora aperto sul server (con oltre 2000 utenti).

Dopo aver lavorato sul problema per ore, mi sono reso conto che se avessi aperto più di un paio di documenti usando Word.ApplicationClass.Document.Open() contemporaneamente su thread diversi, il processo di lavoro IIS (w3wp.exe) si sarebbe arrestato in modo anomalo lasciando aperti tutti i processi di WinWord!

Quindi suppongo che non ci sia una soluzione assoluta a questo problema, ma il passaggio ad altri metodi come lo sviluppo di Office Open XML .


1

La risposta accettata non ha funzionato per me. Il seguente codice nel distruttore ha fatto il lavoro.

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

Attualmente sto lavorando sull'automazione di Office e mi sono imbattuto in una soluzione per questo che funziona sempre per me. È semplice e non comporta l'uccisione di alcun processo.

Sembra che semplicemente eseguendo il ciclo continuo degli attuali processi attivi e in qualsiasi modo "accedendo" a un processo Excel aperto, qualsiasi istanza sospesa sospesa di Excel verrà rimossa. Il codice seguente controlla semplicemente i processi in cui il nome è "Excel", quindi scrive la proprietà MainWindowTitle del processo in una stringa. Questa 'interazione' con il processo sembra fare in modo che Windows raggiunga e interrompa l'istanza congelata di Excel.

Eseguo il metodo seguente appena prima che il componente aggiuntivo in cui sto sviluppando venga chiuso, poiché genera l'evento di scaricamento. Rimuove ogni istanza sospesa di Excel ogni volta. In tutta onestà non sono del tutto sicuro del perché funzioni, ma funziona bene per me e potrebbe essere posizionato alla fine di qualsiasi applicazione Excel senza doversi preoccupare di doppi punti, Marshal.ReleaseComObject, né di uccidere i processi. Sarei molto interessato a qualsiasi suggerimento sul perché questo sia efficace.

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

Penso che parte di questo sia solo il modo in cui il framework gestisce le applicazioni di Office, ma potrei sbagliarmi. In alcuni giorni, alcune applicazioni puliscono immediatamente i processi e altri giorni sembrano attendere fino alla chiusura dell'applicazione. In generale, ho smesso di prestare attenzione ai dettagli e mi sono assicurato che alla fine della giornata non vi fossero processi aggiuntivi.

Inoltre, e forse sto finendo di semplificare le cose, ma penso che puoi semplicemente ...

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

Come ho detto prima, non tendo a prestare attenzione ai dettagli di quando appare o scompare il processo di Excel, ma di solito funziona per me. Inoltre non mi piace mantenere i processi di Excel in giro per qualcosa di diverso dal minimo tempo, ma probabilmente sono solo paranoico su questo.


1

Come probabilmente alcuni hanno già scritto, non è solo importante il modo in cui si chiude Excel (oggetto); è anche importante come lo apri e anche dal tipo di progetto.

In un'applicazione WPF, sostanzialmente lo stesso codice funziona senza o con pochissimi problemi.

Ho un progetto in cui lo stesso file Excel viene elaborato più volte per valori di parametro diversi, ad esempio analizzandolo in base a valori all'interno di un elenco generico.

Ho inserito tutte le funzioni relative a Excel nella classe base e il parser in una sottoclasse (diversi parser usano le comuni funzioni di Excel). Non volevo che Excel fosse aperto e chiuso di nuovo per ogni elemento in un elenco generico, quindi l'ho aperto solo una volta nella classe base e lo ho chiuso nella sottoclasse. Ho avuto problemi durante lo spostamento del codice in un'applicazione desktop. Ho provato molte delle soluzioni sopra menzionate. GC.Collect()era già stato implementato prima, due volte come suggerito.

Quindi ho deciso di spostare il codice per l'apertura di Excel in una sottoclasse. Invece di aprirlo solo una volta, ora creo un nuovo oggetto (classe base) e apro Excel per ogni elemento e lo chiudo alla fine. C'è una certa penalità delle prestazioni, ma sulla base di numerosi test i processi di Excel si chiudono senza problemi (in modalità debug), quindi anche i file temporanei vengono rimossi. Continuerò con i test e scriverò ancora se avrò degli aggiornamenti.

La linea di fondo è: devi anche controllare il codice di inizializzazione, specialmente se hai molte classi, ecc.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.