805 votes

Comment bien nettoyer Excel interopérabilité des objets

Je suis à l'aide d'Excel interop en C# (ApplicationClass) et ont placé le code suivant dans mon clause finally:

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

Bien que, ce genre de travaux, les Excel.exe le processus est toujours en arrière-plan, même après que j'ai fermer Excel. Il est sorti seulement une fois que ma demande est fermé manuellement.

Quiconque de comprendre ce que je fais de mal, ou a une alternative afin d'assurer l'interopérabilité des objets sont correctement éliminés.

711voto

VVS Points 11528

Excel n'a pas cessé de fumer parce que votre application est toujours en tenue de références à des objets COM.

Je suppose que vous êtes en invoquant au moins un membre d'un objet COM, sans l'affecter à une variable.

Pour moi, c'était la excelApp.Les feuilles de calcul d'un objet qui j'ai directement utilisé sans l'affecter à une variable:

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

Ce que je ne savais pas c'est qu'en interne C# créé un wrapper pour les Feuilles de calcul de l'objet COM qui ne se sont pas libérés par mon code (parce que je n'en étais pas consciente) et a été la cause pourquoi Excel n'a pas été déchargé.

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

Ne jamais utiliser de 2 points avec les objets com.


Donc, avec cette connaissance de la bonne façon de le faire ci-dessus est:

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

286voto

Mike Rosenblum Points 7063

Vous pouvez réellement version de votre Application Excel objet proprement, mais vous ne devez prendre soin.

Les conseils pour maintenir un nom de référence pour absolument tous les objets COM vous permet d'accéder et ensuite libérer explicitement par le Maréchal.FinalReleaseComObject() est correct en théorie, mais, malheureusement, très difficile à gérer dans la pratique. Si jamais se glisse n'importe où et utilise des "deux points", ou itère cellules via une boucle for each, ou tout autre type de commande, puis vous aurez non référencées objets COM et le risque d'un blocage. Dans ce cas, il n'y aurait pas moyen de trouver la cause dans le code, et vous auriez à passer en revue toutes vos code par l'oeil et j'espère trouver la cause, une tâche qui pourrait être à peu près impossible pour un grand projet.

La bonne nouvelle est que vous n'avez pas réellement avoir à maintenir un nom de variable de référence pour chaque objet COM que vous utilisez. Au lieu de cela, appelez le GC.Collect() et puis GC.WaitForPendingFinalizers() pour libérer l'ensemble de l' (souvent mineurs) des objets dont vous ne détenez pas une référence, puis relâchez explicitement les objets pour lesquels vous détenez un nom de variable de référence.

Vous devriez également communiquer vos nommé références dans l'ordre inverse d'importance: les objets range d'abord, puis des feuilles de calcul, des classeurs, et puis, finalement, votre Application Excel objet.

Par exemple, en supposant que vous avez un objet de la Plage variable nommée "xlRng", une Feuille de calcul variable nommée "xlSheet", un Classeur variable nommée "xlBook" et une Application Excel variable nommée "xlApp", puis votre code de nettoyage pourrait ressembler à quelque chose comme ce qui suit:

// 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 allez voir pour le nettoyage des objets COM .NET, le GC.Collect() et GC.WaitForPendingFinalizers() les appels sont effectués deux fois dans:

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

Cela ne devrait pas être nécessaire, cependant, sauf si vous utilisez Visual Studio Tools pour Office (VSTO), qui utilise les finaliseurs qui cause l'ensemble d'un graphe d'objets pour être promu dans la file d'attente de finalisation. De tels objets ne seraient pas libérés jusqu'à ce que la prochaine collecte des ordures. Toutefois, si vous n'êtes pas à l'aide de VSTO, vous devriez être en mesure d'appeler GC.Collect() et GC.WaitForPendingFinalizers() une seule fois.

Je sais que l'appelant explicitement GC.Collect() n'est pas (et certainement de faire deux fois les sons très douloureux), mais il n'y a pas moyen de contourner cela, pour être honnête. Par le biais de l'exploitation normale, vous allez générer des objets cachés dont vous détenez pas de référence que vous avez, par conséquent, ne peut pas par tout moyen autre que l'appel de GC.Collect().

C'est un sujet complexe, mais c'est vraiment tout là est à lui. Une fois que vous établissez ce modèle pour votre procédure de nettoyage, vous pouvez code normalement, sans la nécessité pour les emballages, etc. :-)

J'ai un tutoriel sur ce sujet ici:

L'automatisation des Programmes Office avec VB.Net / COM Interop

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

230voto

nightcoder Points 4604

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

Il y a différentes façons et des conseils pour rendre l'instance d'Excel décharger, tels que:

  • De libérer TOUS les objets com explicitement avec le Maréchal.FinalReleaseComObject() (ne pas oublier implicitement créée com-objets). À la libération tous créé objet com, vous pouvez utiliser la règle de 2 points mentionnés ici:
    Comment bien nettoyer Excel interopérabilité des objets en C#

  • L'appel de GC.Collect() et GC.WaitForPendingFinalizers() pour faire CLR libération inutilisés com-objets * (en Fait, il fonctionne, voir mon deuxième solution pour plus de détails)

  • Vérifier si com-serveur d'application peut-être affiche une boîte de message en attente pour à l'utilisateur de répondre (même si je ne suis pas sûr qu'il peut empêcher Excel de de clôture, mais j'en ai entendu parler il y a quelques fois)

  • L'envoi de message WM_CLOSE à la principale Fenêtre Excel

  • L'exécution de la fonction qui fonctionne avec Excel dans un autre domaine d'application. Certaines personnes croient instance d'Excel aura fermé, lorsque le domaine d'application est déchargé.

  • Tuer toutes les instances d'excel qui ont été instancié après notre excel-interoping code commencé.

MAIS! Parfois, toutes ces options, il suffit de ne pas aider ou ne peut pas être approprié!

Par exemple, hier, j'ai constaté qu'un de mes fonctions (qui fonctionne avec excel) Excel cesse de courir après la fonction se termine. J'ai tout essayé! J'ai vérifié l'ensemble de la fonction 10 fois et ajouté Maréchal.FinalReleaseComObject() pour tout! J'ai également eu la GC.Collect() et GC.WaitForPendingFinalizers(). J'ai vérifié pour les boîtes de message. J'ai essayé d'envoyer un message WM_CLOSE à la principale de la fenêtre Excel. J'ai exécuté ma fonction dans un autre domaine d'application et déchargé de ce domaine. Rien n'y fait! L'option avec la fermeture de toutes les instances d'excel est inappropriée, car si l'utilisateur démarre une autre instance d'Excel manuellement, lors de l'exécution de ma fonction qui fonctionne aussi avec Excel, alors que l'instance sera également fermé par ma fonction. Je parie que l'utilisateur ne va pas être content! Donc, honnêtement, c'est un boiteux option (n'en déplaise gars). J'ai donc passé une couple d'heures avant que je trouve un bon (à mon humble avis) solution: Tuer le processus excel par hWnd de la fenêtre principale (c'est une 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, en fonction de Essayez-Analyser le modèle (je pense que c'est approprié ici): une méthode ne lance pas d'exception si le Processus ne peut pas être tué (par exemple le processus n'existe plus), et une autre méthode lève une exception si le Processus n'a pas été tué. La seule faiblesse de la place de ce code est d'autorisations de sécurité. Théoriquement, l'utilisateur peut ne pas avoir les autorisations pour tuer le processus, mais dans 99,99% des cas, l'utilisateur a de telles autorisations. J'ai aussi testé avec un compte invité -, il fonctionne parfaitement.

Ainsi, votre code, travailler 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-en à la deuxième solution, comme je l'ai promis au début du post. La deuxième solution est d'appeler GC.Collect() et GC.WaitForPendingFinalizers(). Oui, ils ont fait le travail, mais vous devez être prudent ici!
Beaucoup de gens disent (et je l'ai dit) que l'appel de GC.Collect() ne l'aide pas. Mais la raison pour laquelle il ne serait pas l'aider si il y a encore des références à des objets COM! L'un des plus populaires des raisons pour GC.Collect() n'étant pas utile est en cours d'exécution du projet en mode Debug. En mode debug objets qui ne sont vraiment pas référencé plus ne seront pas nettoyés jusqu'à la fin de la méthode.
Donc, si vous avez essayé de GC.Collect() et GC.WaitForPendingFinalizers() et ça n'a pas l'aide, essayez d'effectuer les opérations suivantes:

1) Essayez d'exécuter votre projet en mode Release et de vérifier si Excel fermé correctement

2) Synthèse de la méthode de travail avec Excel dans une méthode distincte. Ainsi, au lieu de quelque chose comme ceci:

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 va fermer =)

51voto

joshgo Points 774

Mise à JOUR: Ajout de code C#, et un lien vers Windows Emplois

J'ai passé quelque temps à essayer de comprendre ce problème, et au moment XtremeVBTalk a été la plus active et réactive. Voici un lien vers mon post original, la Fermeture d'un Excel Interop processus proprement, même si votre application se bloque. Ci-dessous est un résumé de la poste, et le code copié à ce post.

  • La fermeture de l'Interopérabilité des processus avec Application.Quit() et Process.Kill() fonctionne pour la plupart, mais échoue si les applications se bloque de façon catastrophique. I. e. si l'application se bloque, le processus Excel sera toujours en cours en vrac.
  • La solution est de laisser le système d'exploitation gérer le nettoyage de vos processus grâce à Windows Travail Objets à l'aide des appels Win32. Lorsque votre application principale meurt, le processus (c'est à dire Excel) est interrompu.

J'ai trouvé ceci pour être une solution propre parce que l'OS est en train de faire un réel travail de nettoyage. Tout ce que vous avez à faire est de vous inscrire le processus Excel.

Windows Code Du Travail

Encapsule les Appels Win32 API pour enregistrer l'Interopérabilité des processus.

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 du Constructeur

  • Dans le constructeur, l' info.LimitFlags = 0x2000; est appelé. 0x2000 est le JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE enum valeur, et cette valeur est définie par MSDN :

Les Causes de tous les processus associés au travail à la fin lorsque l' dernière poignée à l'emploi est fermé.

Extra Appel Win32 API pour obtenir l'ID de Processus (PID)

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

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

46voto

Philip Fourie Points 12889

Il a travaillé pour un projet que je travaillais sur:

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

Nous avons appris qu'il était important que chaque référence à un fichier Excel COM objet a dû être mis à null lorsque vous avez terminé avec elle. Cela comprend des Cellules, des Feuilles, de tout.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X