Comment nettoyer correctement les objets d'interopérabilité Excel?

747

J'utilise l'interopérabilité Excel en C # ( ApplicationClass) et j'ai placé le code suivant dans ma clause finally:

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

Bien que ce genre de travaux, le Excel.exeprocessus est toujours en arrière-plan même après avoir fermé Excel. Il n'est publié que lorsque ma candidature est fermée manuellement.

Que fais-je de mal ou existe-t-il une alternative pour garantir que les objets d'interopérabilité sont correctement éliminés?

Enfers
la source
1
Essayez-vous de fermer Excel.exe sans fermer votre application? Je ne suis pas sûr de bien comprendre votre question.
Bryant
3
J'essaie de m'assurer que les objets d'interopérabilité non gérés sont éliminés correctement. Pour qu'il n'y ait pas de processus Excel qui traînent même lorsque l'utilisateur a terminé avec la feuille de calcul Excel que nous avons créée à partir de l'application.
HAdes
3
Si vous pouvez essayer de le faire en produisant des fichiers XML Excel, sinon veuillez considérer la gestion de la mémoire non gérée VSTO
Jeremy Thompson
Est-ce que cela se traduit bien dans Excel?
Coops
2
Voir (en plus des réponses ci-dessous) cet article de support de Microsoft, où ils donnent spécifiquement des solutions à ce problème: support.microsoft.com/kb/317109
Arjan

Réponses:

684

Excel ne se ferme pas car votre application contient toujours des références à des objets COM.

Je suppose que vous invoquez au moins un membre d'un objet COM sans l'assigner à une variable.

Pour moi, c'était l' objet excelApp.Worksheets que j'ai directement utilisé sans l'assigner à une variable:

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

Je ne savais pas qu'en interne C # avait créé un wrapper pour l' objet Worksheets COM qui n'était pas libéré par mon code (parce que je ne le savais pas) et était la raison pour laquelle Excel n'était pas déchargé.

J'ai trouvé la solution à mon problème sur cette page , qui a également une bonne règle pour l'utilisation des objets COM en C #:

N'utilisez jamais deux points avec des objets COM.


Donc, avec cette connaissance, la bonne façon de faire ce qui précède est:

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

MISE À JOUR POST MORTEM:

Je veux que chaque lecteur lise très attentivement cette réponse de Hans Passant car elle explique le piège dans lequel je suis tombé et beaucoup d'autres développeurs. Lorsque j'ai écrit cette réponse il y a des années, je ne connaissais pas l'effet du débogueur sur le garbage collector et j'ai tiré des conclusions erronées. Je garde ma réponse inchangée pour le bien de l'histoire mais s'il vous plaît lisez ce lien et n'allez pas dans le sens des "deux points": Comprendre le garbage collection dans .NET et nettoyer les objets Interop Excel avec IDisposable

VVS
la source
16
Ensuite, je suggère de ne pas utiliser Excel de COM et de vous éviter tous les problèmes. Les formats Excel 2007 peuvent être utilisés sans jamais ouvrir Excel, magnifique.
user7116
5
Je n'ai pas compris ce que "deux points" signifient. Pouvez-vous s'il vous plaît expliquer?
A9S6
22
Cela signifie que vous ne devez pas utiliser le modèle comObject.Property.PropertiesProperty (vous voyez les deux points?). Attribuez plutôt comObject.Property à une variable et utilisez et supprimez cette variable. Une version plus formelle de la règle ci-dessus pourrait être qch. comme "Attribuer un objet com à des variables avant de les utiliser. Cela inclut les objets com qui sont les propriétés d'un autre objet com."
VVS
5
@ Nick: En fait, vous n'avez besoin d'aucun type de nettoyage, car le garbage collector le fera pour vous. La seule chose que vous devez faire est d'assigner chaque objet COM à sa propre variable pour que le GC le sache.
VVS
12
@VSS thats bollocks, le GC nettoie tout puisque les variables wrapper sont faites par le framework .net. Cela pourrait prendre une éternité au GC pour le nettoyer. Appeler GC.Collect après une interopérabilité intense n'est pas une mauvaise idée.
CodingBarfield
280

Vous pouvez réellement libérer votre objet Application Excel proprement, mais vous devez faire attention.

Le conseil de conserver une référence nommée pour absolument chaque objet COM auquel vous accédez et de le libérer explicitement via Marshal.FinalReleaseComObject()est correct en théorie, mais malheureusement très difficile à gérer dans la pratique. Si quelqu'un glisse quelque part et utilise "deux points", ou itère des cellules via une for eachboucle, ou tout autre type de commande similaire, alors vous aurez des objets COM non référencés et risquerez un blocage. Dans ce cas, il n'y aurait aucun moyen de trouver la cause dans le code; vous devrez revoir tout votre code à l'œil nu et, espérons-le, trouver la cause, une tâche qui pourrait être presque impossible pour un grand projet.

La bonne nouvelle est que vous n'avez pas réellement besoin de conserver une référence de variable nommée à chaque objet COM que vous utilisez. Au lieu de cela, appelez GC.Collect()puis GC.WaitForPendingFinalizers()libérez tous les objets (généralement mineurs) auxquels vous ne détenez pas de référence, puis libérez explicitement les objets auxquels vous détenez une référence de variable nommée.

Vous devez également libérer vos références nommées dans l'ordre inverse de leur importance: les objets de plage d'abord, puis les feuilles de calcul, les classeurs, puis enfin votre objet Application Excel.

Par exemple, en supposant que vous disposiez d'une variable d'objet Range nommée xlRng, d'une variable de feuille de calcul nommée xlSheet, d'une variable de classeur nommée xlBooket d'une variable d'application Excel nommée xlApp, votre code de nettoyage pourrait ressembler à ceci:

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

Dans la plupart des exemples de code que vous verrez pour nettoyer les objets COM de .NET, les appels GC.Collect()et GC.WaitForPendingFinalizers()sont effectués DEUX FOIS comme dans:

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

Cela ne devrait toutefois pas être requis, sauf si vous utilisez Visual Studio Tools pour Office (VSTO), qui utilise des finaliseurs qui provoquent la promotion d'un graphique entier d'objets dans la file d'attente de finalisation. Ces objets ne seront pas libérés avant la prochaine collecte de place. Cependant, si vous n'utilisez pas VSTO, vous devriez pouvoir appeler GC.Collect()et GC.WaitForPendingFinalizers()une seule fois.

Je sais qu'appeler explicitement GC.Collect()est un non-non (et le faire deux fois semble très douloureux), mais il n'y a aucun moyen de contourner cela, pour être honnête. Grâce à des opérations normales, vous générerez des objets cachés auxquels vous ne détenez aucune référence que vous ne pouvez donc pas libérer par d'autres moyens que l'appel GC.Collect().

C'est un sujet complexe, mais c'est vraiment tout ce qu'il y a à faire. Une fois que vous avez établi ce modèle pour votre procédure de nettoyage, vous pouvez coder normalement, sans avoir besoin de wrappers, etc. :-)

J'ai un tutoriel à ce sujet ici:

Automatisation des programmes Office avec VB.Net / COM Interop

Il est écrit pour VB.NET, mais ne soyez pas rebuté par cela, les principes sont exactement les mêmes que lors de l'utilisation de C #.

Mike Rosenblum
la source
3
Une discussion connexe peut être trouvée sur le forum ExtremeVBTalk .NET Office Automation, ici: xtremevbtalk.com/showthread.php?t=303928 .
Mike Rosenblum,
2
Et si tout le reste échoue, alors Process.Kill () peut être utilisé (en dernier recours) comme décrit ici: stackoverflow.com/questions/51462/…
Mike Rosenblum
1
Heureux que cela ait fonctionné Richard. :-) Et voici un exemple subtil où éviter simplement "deux points" n'est pas suffisant pour éviter un problème: stackoverflow.com/questions/4964663/…
Mike Rosenblum
Voici un avis à ce sujet de Misha Shneerson de Microsoft sur le sujet, dans la date du commentaire le 7 octobre 2010. ( blogs.msdn.com/b/csharpfaq/archive/2010/09/28/… )
Mike Rosenblum
J'espère que cela aide à résoudre un problème persistant. Est-il sûr de faire le garbage collection et de libérer les appels dans un bloc finally?
Brett Green
213

Préface: ma réponse contient deux solutions, alors soyez prudent lors de la lecture et ne manquez rien.

Il existe différentes manières et conseils pour effectuer le déchargement d'instance Excel, tels que:

  • Libération de CHAQUE objet com explicitement avec Marshal.FinalReleaseComObject () (sans oublier les objets com créés implicitement). Pour libérer chaque objet com créé, vous pouvez utiliser la règle des 2 points mentionnés ici:
    Comment nettoyer correctement les objets d'interopérabilité Excel?

  • Appel de GC.Collect () et GC.WaitForPendingFinalizers () pour que CLR libère des objets com inutilisés * (En fait, cela fonctionne, voir ma deuxième solution pour plus de détails)

  • Vérifier si l'application com-server affiche peut-être une boîte de message attendant que l'utilisateur réponde (bien que je ne suis pas sûr que cela puisse empêcher Excel de se fermer, mais j'en ai entendu parler plusieurs fois)

  • Envoi d'un message WM_CLOSE à la fenêtre principale d'Excel

  • Exécution de la fonction qui fonctionne avec Excel dans un AppDomain distinct. Certaines personnes pensent que l'instance Excel sera fermée lorsque AppDomain sera déchargé.

  • Tuer toutes les instances Excel qui ont été instanciées après le début de notre code d'interaction Excel.

MAIS! Parfois, toutes ces options ne sont tout simplement pas utiles ou ne peuvent pas être appropriées!

Par exemple, hier, j'ai découvert que dans l'une de mes fonctions (qui fonctionne avec Excel), Excel continue de fonctionner après la fin de la fonction. J'ai tout essayé! J'ai soigneusement vérifié l'ensemble de la fonction 10 fois et ajouté Marshal.FinalReleaseComObject () pour tout! J'ai également eu GC.Collect () et GC.WaitForPendingFinalizers (). J'ai vérifié les boîtes de message cachées. J'ai essayé d'envoyer un message WM_CLOSE à la fenêtre principale d'Excel. J'ai exécuté ma fonction dans un AppDomain distinct et déchargé ce domaine. Rien n'a aidé! L'option de fermeture de toutes les instances Excel est inappropriée, car si l'utilisateur démarre une autre instance Excel manuellement, lors de l'exécution de ma fonction qui fonctionne également avec Excel, cette instance sera également fermée par ma fonction. Je parie que l'utilisateur ne sera pas content! Donc, honnêtement, c'est une option boiteuse (pas de gars offensifs).solution : Tuez le processus Excel par hWnd de sa fenêtre principale (c'est la première solution).

Voici le code simple:

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

/// <summary> Tries to find and kill process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <returns>True if process was found and killed. False if process was not found by hWnd or if it could not be killed.</returns>
public static bool TryKillProcessByMainWindowHwnd(int hWnd)
{
    uint processID;
    GetWindowThreadProcessId((IntPtr)hWnd, out processID);
    if(processID == 0) return false;
    try
    {
        Process.GetProcessById((int)processID).Kill();
    }
    catch (ArgumentException)
    {
        return false;
    }
    catch (Win32Exception)
    {
        return false;
    }
    catch (NotSupportedException)
    {
        return false;
    }
    catch (InvalidOperationException)
    {
        return false;
    }
    return true;
}

/// <summary> Finds and kills process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <exception cref="ArgumentException">
/// Thrown when process is not found by the hWnd parameter (the process is not running). 
/// The identifier of the process might be expired.
/// </exception>
/// <exception cref="Win32Exception">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="NotSupportedException">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="InvalidOperationException">See Process.Kill() exceptions documentation.</exception>
public static void KillProcessByMainWindowHwnd(int hWnd)
{
    uint processID;
    GetWindowThreadProcessId((IntPtr)hWnd, out processID);
    if (processID == 0)
        throw new ArgumentException("Process has not been found by the given main window handle.", "hWnd");
    Process.GetProcessById((int)processID).Kill();
}

Comme vous pouvez le voir, j'ai fourni deux méthodes, selon le modèle Try-Parse (je pense que c'est approprié ici): une méthode ne lève pas l'exception si le processus n'a pas pu être tué (par exemple, le processus n'existe plus) et une autre méthode lève l'exception si le processus n'a pas été tué. Le seul point faible dans ce code est les autorisations de sécurité. Théoriquement, l'utilisateur peut ne pas avoir l'autorisation de tuer le processus, mais dans 99,99% de tous les cas, l'utilisateur a de telles autorisations. Je l'ai également testé avec un compte invité - cela fonctionne parfaitement.

Ainsi, votre code, en travaillant avec Excel, peut ressembler à ceci:

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 est terminé! :)

Ok, revenons à la deuxième solution, comme je l'ai promis au début de l'article. La deuxième solution consiste à appeler GC.Collect () et GC.WaitForPendingFinalizers (). Oui, ils fonctionnent réellement, mais vous devez faire attention ici!
Beaucoup de gens disent (et j'ai dit) qu'appeler GC.Collect () n'aide pas. Mais la raison pour laquelle cela n'aiderait pas est qu'il existe encore des références aux objets COM! L'une des raisons les plus courantes pour lesquelles GC.Collect () n'est pas utile est d'exécuter le projet en mode débogage. En mode débogage, les objets qui ne sont plus vraiment référencés ne seront pas récupérés jusqu'à la fin de la méthode.
Donc, si vous avez essayé GC.Collect () et GC.WaitForPendingFinalizers () et que cela n'a pas aidé, essayez de faire ce qui suit:

1) Essayez d'exécuter votre projet en mode Release et vérifiez si Excel s'est bien fermé

2) Enveloppez la méthode de travail avec Excel dans une méthode distincte. Donc, au lieu de quelque chose comme ça:

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

vous écrivez:

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

Maintenant, Excel fermera =)

codeur de nuit
la source
19
triste que le fil soit déjà si vieux que votre excellente réponse n'apparaisse que bien en dessous, ce qui est selon moi la seule raison pour laquelle il n'a pas été voté plus souvent ...
chiccodoro
15
Je dois admettre que lorsque j'ai lu votre réponse pour la première fois, je pensais que c'était un coup de coude géant. Après environ 6 heures de lutte avec ça (tout est sorti, je n'ai pas de double point, etc.), je pense maintenant que votre réponse est géniale.
Mark
3
Merci pour cela. J'ai lutté avec un Excel qui ne fermait pas quoi qu'il arrive pendant quelques jours avant de le trouver. Excellent.
BBlake
2
@nightcoder: réponse impressionnante, vraiment détaillée. Vos commentaires concernant le mode de débogage sont très vrais et importants que vous l'ayez souligné. Le même code en mode de libération peut souvent convenir. Process.Kill cependant, n'est bon que lorsque vous utilisez l'automatisation, pas lorsque votre programme s'exécute dans Excel, par exemple, en tant que complément COM géré.
Mike Rosenblum
2
@DanM: Votre approche est correcte à 100% et n'échouera jamais. En gardant toutes vos variables locales à la méthode, toutes les références sont effectivement inaccessibles par le code .NET. Cela signifie que lorsque votre section finally appelle GC.Collect (), les finaliseurs de tous vos objets COM seront appelés avec certitude. Chaque finaliseur appelle ensuite Marshal.FinalReleaseComObject sur l'objet en cours de finalisation. Votre approche est donc simple et infaillible. N'ayez pas peur d'utiliser cela. (Seule mise en garde: si vous utilisez VSTO, ce que je doute que vous soyez, vous devrez appeler GC.Collect () & GC.WaitForPendingFinalizers TWICE.)
Mike Rosenblum
49

MISE À JOUR : Code C # ajouté et lien vers les travaux Windows

J'ai passé quelque temps à essayer de comprendre ce problème, et à l'époque XtremeVBTalk était le plus actif et le plus réactif. Voici un lien vers mon article d'origine, Fermer un processus Excel Interop proprement, même si votre application se bloque . Vous trouverez ci-dessous un résumé du message et le code copié dans ce message.

  • La fermeture du processus Interop avec Application.Quit()et Process.Kill()fonctionne pour la plupart, mais échoue si les applications se bloquent de manière catastrophique. Autrement dit, si l'application se bloque, le processus Excel sera toujours en cours d'exécution.
  • La solution consiste à laisser le système d'exploitation gérer le nettoyage de vos processus via les objets de travail Windows à l' aide d'appels Win32. Lorsque votre application principale meurt, les processus associés (c'est-à-dire Excel) seront également arrêtés.

J'ai trouvé que c'était une solution propre parce que le système d'exploitation faisait un vrai travail de nettoyage. Il vous suffit d' enregistrer le processus Excel.

Code de travail Windows

Encapsule les appels API Win32 pour enregistrer les processus Interop.

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

}

Remarque sur le code constructeur

  • Dans le constructeur, le info.LimitFlags = 0x2000;est appelé. 0x2000est la JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSEvaleur énumérée, et cette valeur est définie par MSDN comme:

Entraîne la fin de tous les processus associés au travail lorsque le dernier descripteur du travail est fermé.

Appel API Win32 supplémentaire pour obtenir l'ID de processus (PID)

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

Utiliser le code

    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
la source
2
Tuer explicitement un serveur COM hors processus (qui pourrait servir d'autres clients COM) est une idée terrible. Si vous avez recours à cela, c'est parce que votre protocole COM est rompu. Les serveurs COM ne doivent pas être traités comme une application
Windows standard
39

Cela a fonctionné pour un projet sur lequel je travaillais:

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

Nous avons appris qu'il était important de définir chaque référence à un objet COM Excel sur null lorsque vous en avez terminé. Cela comprenait les cellules, les feuilles et tout le reste.

Philip Fourie
la source
30

Tout ce qui se trouve dans l'espace de noms Excel doit être libéré. Période

Vous ne pouvez pas faire:

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

Vous devez faire

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

suivi de la libération des objets.

MagicKat
la source
3
Comment aobut xlRange.Interior.Color par exemple.
HAdes
L'intérieur doit être libéré (son dans l'espace de noms) ... La couleur, en revanche, ne le fait pas (car elle provient de System.Drawing.Color, iirc)
MagicKat
2
en fait, Color est une couleur Excel et non une couleur .Net. vous passez juste un long. Aussi les classeurs def. besoin d'être publié, des feuilles de calcul ... moins.
Type anonyme
Ce n'est pas vrai, la règle deux points mauvais, un point bon est un non-sens. De plus, vous n'avez pas besoin de publier des classeurs, des feuilles de calcul, des plages, c'est le travail du GC. La seule chose que vous ne devez jamais oublier est d'appeler Quit sur le seul et unique objet Excel.Application. Après avoir appelé Quit, annulez l'objet Excel.Application et appelez GC.Collect (); GC.WaitForPendingFinalizers (); deux fois.
Dietrich Baumgarten du
29

Tout d'abord - vous n'avez jamais à appeler Marshal.ReleaseComObject(...)ou Marshal.FinalReleaseComObject(...)à faire de l'interopérabilité Excel. Il s'agit d'un anti-modèle déroutant, mais toute information à ce sujet, y compris de Microsoft, qui indique que vous devez libérer manuellement les références COM à partir de .NET est incorrecte. Le fait est que le runtime .NET et le garbage collector conservent et nettoient correctement les références COM. Pour votre code, cela signifie que vous pouvez supprimer la boucle `while (...) en haut.

Deuxièmement, si vous souhaitez vous assurer que les références COM à un objet COM hors processus sont nettoyées à la fin de votre processus (afin que le processus Excel se ferme), vous devez vous assurer que le garbage collector s'exécute. Vous faites cela correctement avec les appels à GC.Collect()et GC.WaitForPendingFinalizers(). Appeler cela deux fois est sûr et garantit que les cycles sont définitivement nettoyés aussi (bien que je ne sois pas sûr que ce soit nécessaire, et j'apprécierais un exemple qui le montre).

Troisièmement, lors de l'exécution sous le débogueur, les références locales seront maintenues artificiellement en vie jusqu'à la fin de la méthode (pour que l'inspection des variables locales fonctionne). Les GC.Collect()appels ne sont donc pas efficaces pour nettoyer un objet comme à rng.Cellspartir de la même méthode. Vous devez diviser le code effectuant l'interopérabilité COM du nettoyage du GC en méthodes distinctes. (Ce fut une découverte clé pour moi, à partir d'une partie de la réponse publiée ici par @nightcoder.)

Le schéma général serait donc:

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

Il y a beaucoup de fausses informations et de confusion sur ce problème, y compris de nombreux messages sur MSDN et sur Stack Overflow (et surtout cette question!).

Ce qui m'a finalement convaincu de regarder de plus près et de trouver le bon conseil, c'est le blog Marshal.ReleaseComObject Considéré dangereux, ainsi que de trouver le problème avec des références maintenues en vie sous le débogueur qui confondait mes tests précédents.

Govert
la source
5
TOUTES LES RÉPONSES SUR CETTE PAGE SONT FAUX SAUF CELLE-CI. Ils fonctionnent mais sont beaucoup trop de travail. IGNOREZ la règle "2 POINTS". Laissez le GC faire votre travail pour vous. Preuve supplémentaire de .Net GURU Hans Passant: stackoverflow.com/a/25135685/3852958
Alan Baljeu
1
Govert, quel soulagement de trouver la bonne façon de le faire. Je vous remercie.
Dietrich Baumgarten
18

J'ai trouvé un modèle générique utile qui peut aider à implémenter le modèle d'élimination correct pour les objets COM, qui ont besoin de Marshal.ReleaseComObject appelé lorsqu'ils sortent du domaine:

Usage:

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

Modèle:

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

Référence:

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

Edward Wilde
la source
3
yup celui-ci est bon. Je pense que ce code peut être mis à jour mais maintenant que FinalReleaseCOMObject est disponible.
Type anonyme
C'est toujours ennuyeux d'avoir un bloc d'utilisation pour chaque objet. Voir ici stackoverflow.com/questions/2191489/… pour une alternative.
Henrik
16

Je ne peux pas croire que ce problème hante le monde depuis 5 ans ... Si vous avez créé une application, vous devez la fermer avant de supprimer le lien.

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

à la fermeture

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

Lorsque vous créez une nouvelle application Excel, elle ouvre un programme Excel en arrière-plan. Vous devez commander à ce programme Excel de se fermer avant de libérer le lien, car ce programme Excel ne fait pas partie de votre contrôle direct. Par conséquent, il restera ouvert si le lien est libéré!

Bonne programmation tout le monde ~~

Colin
la source
Non, en particulier si vous souhaitez autoriser l'interaction de l'utilisateur avec l'application COM (Excel dans ce cas - OP ne spécifie pas si une interaction utilisateur est prévue, mais ne fait aucune référence à son arrêt). Il vous suffit de demander à l'appelant de lâcher prise pour que l'utilisateur puisse quitter Excel lors de la fermeture, par exemple, d'un seul fichier.
downwitch
cela a fonctionné parfaitement pour moi - s'est écrasé lorsque je n'avais que objExcel.Quit (), mais quand j'ai également fait objExcel.Application.Quit () au préalable, a fermé proprement.
mcmillab
13

Développeurs communs, aucune de vos solutions n'a fonctionné pour moi, alors je décide de mettre en œuvre une nouvelle astuce .

Précisons d'abord "Quel est notre objectif?" => "Ne pas voir l'objet Excel après notre travail dans le gestionnaire de tâches"

D'accord. Laissez no le contester et commencez à le détruire, mais pensez à ne pas détruire les autres instances d'OS Excel qui s'exécutent en parallèle.

Donc, obtenez la liste des processeurs actuels et récupérez le PID des processus EXCEL, puis une fois votre travail terminé, nous avons un nouvel invité dans la liste des processus avec un PID unique, trouvez et détruisez juste celui-là.

<gardez à l'esprit que tout nouveau processus Excel au cours de votre travail Excel sera détecté comme nouveau et détruit> <Une meilleure solution consiste à capturer le PID du nouvel objet Excel créé et à le détruire>

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

Cela résout mon problème, j'espère que le vôtre aussi.

Mohsen Afshin
la source
1
.... c'est un peu une approche de sledghammer? - il est similaire à ce que j'utilise également :( mais je dois changer. Le problème avec cette approche est que souvent lors de l'ouverture d'Excel, sur la machine où cela a fonctionné, il a l'alerte dans la barre de gauche, disant quelque chose comme "Excel a été fermé de manière incorrecte": pas très élégant. Je pense qu'une des suggestions précédentes dans ce fil est à préférer.
whytheq
11

Cela semble bien sûr trop compliqué. D'après mon expérience, il n'y a que trois éléments clés pour que Excel se ferme correctement:

1: assurez-vous qu'il ne reste aucune référence à l'application Excel que vous avez créée (vous ne devriez en avoir qu'une de toute façon; définissez-la sur null)

2: appeler GC.Collect()

3: Excel doit être fermé, soit par l'utilisateur fermant manuellement le programme, soit par l'appel Quitde l'objet Excel. (Notez que Quitcela fonctionnera comme si l'utilisateur tentait de fermer le programme et présentera une boîte de dialogue de confirmation s'il y a des modifications non enregistrées, même si Excel n'est pas visible. L'utilisateur peut appuyer sur Annuler, puis Excel n'aura pas été fermé. )

1 doit se produire avant 2, mais 3 peut survenir à tout moment.

Une façon de l'implémenter est d'envelopper l'objet Excel d'interopérabilité avec votre propre classe, de créer l'instance d'interopérabilité dans le constructeur et d'implémenter IDisposable avec Dispose ressemblant à quelque chose comme

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

Cela nettoiera excellent du côté des choses de votre programme. Une fois Excel fermé (manuellement par l'utilisateur ou par vous appelant Quit), le processus disparaîtra. Si le programme a déjà été fermé, le processus disparaîtra lors de l' GC.Collect()appel.

(Je ne sais pas à quel point c'est important, mais vous voudrez peut-être un GC.WaitForPendingFinalizers()appel après l' GC.Collect()appel, mais il n'est pas strictement nécessaire de se débarrasser du processus Excel.)

Cela a fonctionné pour moi sans problème pendant des années. Gardez à l'esprit cependant que pendant que cela fonctionne, vous devez en fait fermer correctement pour que cela fonctionne. Vous obtiendrez toujours des processus excel.exe accumulés si vous interrompez votre programme avant le nettoyage d'Excel (généralement en appuyant sur "stop" pendant le débogage de votre programme).

Dave Cousineau
la source
1
Cette réponse fonctionne et est infiniment plus pratique que la réponse acceptée. Pas de soucis pour "deux points" avec des objets COM, pas d'utilisation de Marshal.
andrew.cuthbert
9

Pour ajouter aux raisons pour lesquelles Excel ne se ferme pas, même lorsque vous créez des références directes à chaque objet lors de la lecture, la création est la boucle `` Pour ''.

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
la source
Et une autre raison, réutiliser une référence sans libérer d'abord la valeur précédente.
Grimfort
Je vous remercie. J'utilisais également un "pour chacun", votre solution fonctionnait pour moi.
Chad Braun-Duin
+1 Merci. De plus, notez que si vous avez un objet pour une plage Excel et que vous souhaitez modifier la plage pendant la durée de vie de l'objet, j'ai constaté que je devais ReleaseComObject avant de le réaffecter, ce qui rend le code un peu désordonné!
AjV Jsy
9

J'ai traditionnellement suivi les conseils trouvés dans la réponse de VVS . Cependant, dans un effort pour garder cette réponse à jour avec les dernières options, je pense que tous mes futurs projets utiliseront la bibliothèque "NetOffice".

NetOffice est un remplacement complet des PIA Office et est totalement indépendant de la version. Il s'agit d'une collection d'encapsuleurs COM gérés qui peuvent gérer le nettoyage qui provoque souvent de tels maux de tête lorsque vous travaillez avec Microsoft Office dans .NET.

Certaines fonctionnalités clés sont:

  • Généralement indépendant de la version (et les fonctionnalités dépendantes de la version sont documentées)
  • Pas de dépendances
  • Pas de PIA
  • Aucune inscription
  • Pas de VSTO

Je ne suis en aucun cas affilié au projet; J'apprécie vraiment sincèrement la nette réduction des maux de tête.

BTownTKD
la source
2
Cela devrait être marqué comme la réponse, vraiment. NetOffice résume toute cette complexité.
C.Augusto Proiete
1
J'utilise NetOffice depuis un temps considérable pour écrire un complément Excel et cela a parfaitement fonctionné. La seule chose à considérer est que si vous ne supprimez pas explicitement les objets utilisés, il le fera lorsque vous quitterez l'application (car il en garde la trace quand même). Donc, la règle de base avec NetOffice est toujours d'utiliser le modèle "using" avec chaque objet Excel comme une cellule, une plage ou une feuille, etc.
Stas Ivanov
8

La réponse acceptée ici est correcte, mais notez également que non seulement les références "à deux points" doivent être évitées, mais également les objets qui sont récupérés via l'index. Vous n'avez pas non plus besoin d'attendre que vous ayez terminé avec le programme pour nettoyer ces objets, il est préférable de créer des fonctions qui les nettoieront dès que vous en aurez terminé, lorsque cela sera possible. Voici une fonction que j'ai créée qui attribue certaines propriétés d'un objet Style appelé 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 { } 
}

Notez que je devais définir xlBorders[Excel.XlBordersIndex.xlEdgeBottom]une variable pour nettoyer cela (pas à cause des deux points, qui font référence à une énumération qui n'a pas besoin d'être publiée, mais parce que l'objet auquel je fais référence est en fait un objet Border qui doit être libéré).

Ce genre de chose n'est pas vraiment nécessaire dans les applications standard, qui font un excellent travail de nettoyage après elles-mêmes, mais dans les applications ASP.NET, si vous en manquez même une, quelle que soit la fréquence à laquelle vous appelez le garbage collector, Excel toujours en cours d'exécution sur votre serveur.

Cela nécessite beaucoup d'attention aux détails et de nombreuses exécutions de test lors de la surveillance du Gestionnaire des tâches lors de l'écriture de ce code, mais cela vous évite d'avoir à chercher désespérément dans les pages de code pour trouver l'instance que vous avez manquée. Ceci est particulièrement important lorsque vous travaillez dans des boucles, où vous devez libérer CHAQUE INSTANCE d'un objet, même s'il utilise le même nom de variable à chaque fois qu'il boucle.

Chris McGrath
la source
Inside Release, vérifiez Null (réponse de Joe). Cela évitera des exceptions nulles inutiles. J'ai testé BEAUCOUP de méthodes. C'est le seul moyen de libérer efficacement Excel.
Gerhard Powell
8

Après avoir essayé

  1. Libérez les objets COM dans l'ordre inverse
  2. Ajouter GC.Collect()et GC.WaitForPendingFinalizers()deux fois à la fin
  3. Pas plus de deux points
  4. Fermer le classeur et quitter l'application
  5. Exécuter en mode release

la solution finale qui fonctionne pour moi est de déplacer un ensemble de

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

que nous avons ajouté à la fin de la fonction dans un wrapper, comme suit:

private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}
DG
la source
J'ai trouvé que je n'avais pas besoin d'annuler ComObjects, seulement Quit () - Close () et FinalReleaseComObject. Je ne peux pas croire que c'est tout ce qui manquait de mon côté pour que ça marche. Génial!
Carol
7

J'ai suivi cela exactement ... Mais j'ai quand même rencontré des problèmes 1 fois sur 1000. Qui sait pourquoi. Il est temps de sortir le marteau ...

Juste après l'instanciation de la classe d'application Excel, je saisis le processus Excel qui vient d'être créé.

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

Ensuite, une fois que j'ai effectué tout le nettoyage COM ci-dessus, je m'assure que ce processus n'est pas en cours d'exécution. S'il est toujours en cours d'exécution, tuez-le!

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

¨ ° º¤ø „¸ Tirez sur Excel proc et mâchez du chewing-gum ¸„ ø¤º ° ¨

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
la source
6

Vous devez être conscient qu'Excel est également très sensible à la culture sous laquelle vous travaillez.

Vous pouvez constater que vous devez définir la culture sur EN-US avant d'appeler les fonctions Excel. Cela ne s'applique pas à toutes les fonctions - mais à certaines d'entre elles.

    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;

Cela s'applique même si vous utilisez VSTO.

Pour plus de détails: http://support.microsoft.com/default.aspx?scid=kb;en-us;Q320369


la source
6

«Ne jamais utiliser deux points avec des objets COM» est une bonne règle pour éviter les fuites de références COM, mais Excel PIA peut entraîner des fuites de plus de façons qu’apparentes à première vue.

L'une de ces méthodes consiste à s'abonner à tout événement exposé par l'un des objets COM du modèle d'objet Excel.

Par exemple, s'abonner à l'événement WorkbookOpen de la classe Application.

Un peu de théorie sur les événements COM

Les classes COM exposent un groupe d'événements via des interfaces de rappel. Afin de souscrire à des événements, le code client peut simplement enregistrer un objet implémentant l'interface de rappel et la classe COM invoquera ses méthodes en réponse à des événements spécifiques. Étant donné que l'interface de rappel est une interface COM, il est du devoir de l'objet d'implémentation de décrémenter le nombre de références de tout objet COM qu'il reçoit (en tant que paramètre) pour l'un des gestionnaires d'événements.

Comment Excel PIA expose les événements COM

Excel PIA expose les événements COM de la classe Application Excel en tant qu'événements .NET conventionnels. Chaque fois que le code client s'abonne à un événement .NET (accent sur 'a'), PIA crée une instance d'une classe implémentant l'interface de rappel et l'enregistre auprès d'Excel.

Par conséquent, un certain nombre d'objets de rappel sont enregistrés auprès d'Excel en réponse à différentes demandes d'abonnement à partir du code .NET. Un objet de rappel par abonnement à un événement.

Une interface de rappel pour la gestion des événements signifie que PIA doit s'abonner à tous les événements d'interface pour chaque demande d'abonnement aux événements .NET. Il ne peut pas choisir. Lors de la réception d'un rappel d'événement, l'objet de rappel vérifie si le gestionnaire d'événements .NET associé est intéressé par l'événement en cours ou non, puis appelle le gestionnaire ou ignore silencieusement le rappel.

Effet sur le nombre de références d'instance COM

Tous ces objets de rappel ne décrémentent pas le nombre de références des objets COM qu'ils reçoivent (en tant que paramètres) pour l'une des méthodes de rappel (même pour celles qui sont silencieusement ignorées). Ils s'appuient uniquement sur le garbage collector CLR pour libérer les objets COM.

Étant donné que l'exécution du GC n'est pas déterministe, cela peut entraîner la suspension du processus Excel pendant une durée plus longue que souhaitée et créer une impression de «fuite de mémoire».

Solution

La seule solution pour l'instant consiste à éviter le fournisseur d'événements PIA pour la classe COM et à écrire votre propre fournisseur d'événements qui libère de manière déterministe des objets COM.

Pour la classe Application, cela peut être fait en implémentant l'interface AppEvents puis en enregistrant l'implémentation avec Excel à l'aide de l' interface IConnectionPointContainer . La classe Application (et d'ailleurs tous les objets COM exposant des événements à l'aide du mécanisme de rappel) implémente l'interface IConnectionPointContainer.

Amit Mittal
la source
4

Comme d'autres l'ont souligné, vous devez créer une référence explicite pour chaque objet Excel que vous utilisez et appeler Marshal.ReleaseComObject sur cette référence, comme décrit dans cet article de la base de connaissances . Vous devez également utiliser try / finally pour vous assurer que ReleaseComObject est toujours appelé, même lorsqu'une exception est levée. C'est-à-dire au lieu de:

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

vous devez faire quelque chose comme:

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

Vous devez également appeler Application.Quit avant de libérer l'objet Application si vous souhaitez qu'Excel se ferme.

Comme vous pouvez le voir, cela devient rapidement extrêmement compliqué dès que vous essayez de faire quoi que ce soit, même modérément complexe. J'ai développé avec succès des applications .NET avec une classe wrapper simple qui encapsule quelques manipulations simples du modèle d'objet Excel (ouvrir un classeur, écrire dans une plage, enregistrer / fermer le classeur, etc.). La classe wrapper implémente IDisposable, implémente soigneusement Marshal.ReleaseComObject sur chaque objet qu'elle utilise et n'expose publiquement aucun objet Excel au reste de l'application.

Mais cette approche ne s'adapte pas bien aux exigences plus complexes.

Il s'agit d'une grande déficience de .NET COM Interop. Pour des scénarios plus complexes, j'envisagerais sérieusement d'écrire une DLL ActiveX en VB6 ou dans un autre langage non géré auquel vous pouvez déléguer toutes les interactions avec des objets COM sortants tels qu'Office. Vous pouvez ensuite référencer cette DLL ActiveX à partir de votre application .NET, et les choses seront beaucoup plus faciles car vous n'aurez qu'à publier cette seule référence.

Joe
la source
3

Lorsque toutes les choses ci-dessus n'ont pas fonctionné, essayez de laisser à Excel le temps de fermer ses feuilles:

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

...
FinalReleaseComObject(app);
homme araignée
la source
2
Je déteste les attentes arbitraires. Mais +1 parce que vous avez raison de devoir attendre la fermeture des classeurs. Une alternative consiste à interroger la collection de classeurs dans une boucle et à utiliser l'attente arbitraire comme délai d'expiration de la boucle.
dFlat
3

Assurez-vous de libérer tous les objets liés à Excel!

J'ai passé quelques heures en essayant plusieurs façons. Ce sont toutes de bonnes idées mais j'ai finalement trouvé mon erreur: si vous ne libérez pas tous les objets, aucune des façons ci-dessus ne peut vous aider comme dans mon cas. Assurez-vous de libérer tous les objets, y compris la première plage!

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

Les options sont réunies ici .

Ned
la source
Très bon point! Je pense que parce que j'utilise Office 2007, un nettoyage est effectué en mon nom. J'ai utilisé le conseil plus haut mais je n'ai pas stocké de variables comme vous l'avez suggéré ici et EXCEL.EXE se ferme, mais je pourrais avoir de la chance et si j'ai d'autres problèmes, je regarderai certainement cette partie de mon code =)
Coops
3

Un excellent article sur la libération d'objets COM est 2.5 Releasing COM Objects (MSDN).

La méthode que je préconiserais est d'annuler vos références Excel.Interop si ce sont des variables non locales, puis d'appeler GC.Collect()et GC.WaitForPendingFinalizers()deux fois. Les variables Interop étendues localement seront prises en charge automatiquement.

Cela supprime la nécessité de conserver une référence nommée pour chaque objet COM.

Voici un exemple tiré de l'article:

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

Ces mots sont directement tirés de l'article:

Dans presque toutes les situations, l'annulation de la référence RCW et le forçage d'un garbage collection seront nettoyés correctement. Si vous appelez également GC.WaitForPendingFinalizers, la récupération de place sera aussi déterministe que possible. Autrement dit, vous serez à peu près sûr exactement quand l'objet a été nettoyé - au retour du deuxième appel à WaitForPendingFinalizers. Comme alternative, vous pouvez utiliser Marshal.ReleaseComObject. Cependant, notez qu'il est très peu probable que vous ayez jamais besoin d'utiliser cette méthode.

Porkbutts
la source
2

La règle des deux points n'a pas fonctionné pour moi. Dans mon cas, j'ai créé une méthode pour nettoyer mes ressources comme suit:

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

Ma solution

[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
la source
2

Vous devez être très prudent lorsque vous utilisez des applications d'interopérabilité Word / Excel. Après avoir essayé toutes les solutions, nous avions encore beaucoup de processus "WinWord" ouverts sur le serveur (avec plus de 2000 utilisateurs).

Après avoir travaillé sur le problème pendant des heures, je me suis rendu compte que si Word.ApplicationClass.Document.Open()j'ouvrais plus de quelques documents en utilisant simultanément différents threads, le processus de travail IIS (w3wp.exe) plantait, laissant tous les processus WinWord ouverts!

Je suppose donc qu'il n'y a pas de solution absolue à ce problème, mais le passage à d'autres méthodes telles que le développement d' Office Open XML .

Arvand
la source
1

La réponse acceptée n'a pas fonctionné pour moi. Le code suivant dans le destructeur a fait le travail.

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
la source
1

Je travaille actuellement sur la bureautique et suis tombé sur une solution pour cela qui fonctionne à chaque fois pour moi. C'est simple et n'implique pas de tuer aucun processus.

Il semble qu'en parcourant simplement les processus actifs actuels et en "accédant" de quelque manière à un processus Excel ouvert, toute instance suspendue errante d'Excel sera supprimée. Le code ci-dessous vérifie simplement les processus dont le nom est «Excel», puis écrit la propriété MainWindowTitle du processus dans une chaîne. Cette «interaction» avec le processus semble amener Windows à rattraper son retard et à abandonner l'instance figée d'Excel.

J'exécute la méthode ci-dessous juste avant le complément que je développe, se termine, car il déclenche l'événement de déchargement. Il supprime toutes les instances suspendues d'Excel à chaque fois. En toute honnêteté, je ne sais pas vraiment pourquoi cela fonctionne, mais cela fonctionne bien pour moi et pourrait être placé à la fin de toute application Excel sans avoir à se soucier des points doubles, de Marshal.ReleaseComObject, ni des processus de mise à mort. Je serais très intéressé par toute suggestion quant à la raison pour laquelle cela est 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;
                    }
                }
            }
}
Tom Brearley
la source
1

Je pense que c'est en partie la façon dont le cadre gère les applications Office, mais je peux me tromper. Certains jours, certaines applications nettoient immédiatement les processus et d'autres jours, il semble attendre la fermeture de l'application. En général, j'arrête de prêter attention aux détails et je m'assure simplement qu'aucun processus supplémentaire ne flotte à la fin de la journée.

Aussi, et peut-être que je simplifie trop les choses, mais je pense que vous pouvez simplement ...

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

Comme je l'ai dit plus tôt, je n'ai pas tendance à faire attention aux détails du moment où le processus Excel apparaît ou disparaît, mais cela fonctionne généralement pour moi. Je n'aime pas non plus garder les processus Excel pour autre chose que le minimum de temps, mais je suis probablement juste paranoïaque à ce sujet.

bill_the_loser
la source
1

Comme certains l'ont probablement déjà écrit, il n'est pas seulement important de savoir comment fermer Excel (objet); il est également important de savoir comment l' ouvrir et également en fonction du type de projet.

Dans une application WPF, fondamentalement, le même code fonctionne sans ou avec très peu de problèmes.

J'ai un projet dans lequel le même fichier Excel est traité plusieurs fois pour différentes valeurs de paramètres - par exemple, en l'analysant en fonction des valeurs à l'intérieur d'une liste générique.

J'ai mis toutes les fonctions liées à Excel dans la classe de base et l'analyseur dans une sous-classe (différents analyseurs utilisent des fonctions Excel communes). Je ne voulais pas qu'Excel soit ouvert et fermé à nouveau pour chaque élément d'une liste générique, donc je ne l'ai ouvert qu'une seule fois dans la classe de base et je l'ai fermé dans la sous-classe. J'ai eu des problèmes lors du déplacement du code dans une application de bureau. J'ai essayé plusieurs des solutions mentionnées ci-dessus.GC.Collect()a déjà été mis en œuvre auparavant, deux fois comme suggéré.

J'ai ensuite décidé de déplacer le code pour ouvrir Excel dans une sous-classe. Au lieu d'ouvrir une seule fois, maintenant je crée un nouvel objet (classe de base) et ouvre Excel pour chaque élément et le ferme à la fin. Il y a une certaine perte de performances, mais sur la base de plusieurs tests, les processus Excel se ferment sans problème (en mode débogage), donc les fichiers temporaires sont également supprimés. Je continuerai les tests et j'écrirai plus si j'obtiens des mises à jour.

L'essentiel est: Vous devez également vérifier le code d'initialisation, surtout si vous avez plusieurs classes, etc.

Blaz Brencic
la source