C# et excel automation - fin de l'instance en cours d'exécution

j'essaie Excel automation par C#. J'ai suivi toutes les instructions de Microsoft sur la façon de procéder, mais j'ai encore du mal à rejeter la ou les références finales pour Qu'elles excellent pour qu'elles se ferment et pour permettre au GC de les collecter.

un exemple de code suit. Lorsque je commente le bloc de code qui contient des lignes similaires à:

Sheet.Cells[iRowCount, 1] = data["fullname"].ToString();

puis le fichier enregistre et Excel quitte. Dans le cas contraire, le fichier enregistre, mais Excel reste en cours d'exécution comme un processus. La prochaine fois que ce code s'exécute, il crée une nouvelle instance et elles finissent par s'accumuler.

Toute aide est appréciée. Grâce.

ce sont les barebones de mon code:

        Excel.Application xl = null;
        Excel._Workbook wBook = null;
        Excel._Worksheet wSheet = null;
        Excel.Range range = null;

        object m_objOpt = System.Reflection.Missing.Value;

        try
        {
            // open the template
            xl = new Excel.Application();
            wBook = (Excel._Workbook)xl.Workbooks.Open(excelTemplatePath + _report.ExcelTemplate, false, false, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt, m_objOpt);
            wSheet = (Excel._Worksheet)wBook.ActiveSheet;

            int iRowCount = 2;

            // enumerate and drop the values straight into the Excel file
            while (data.Read())
            {

                wSheet.Cells[iRowCount, 1] = data["fullname"].ToString();
                wSheet.Cells[iRowCount, 2] = data["brand"].ToString();
                wSheet.Cells[iRowCount, 3] = data["agency"].ToString();
                wSheet.Cells[iRowCount, 4] = data["advertiser"].ToString();
                wSheet.Cells[iRowCount, 5] = data["product"].ToString();
                wSheet.Cells[iRowCount, 6] = data["comment"].ToString();
                wSheet.Cells[iRowCount, 7] = data["brief"].ToString();
                wSheet.Cells[iRowCount, 8] = data["responseDate"].ToString();
                wSheet.Cells[iRowCount, 9] = data["share"].ToString();
                wSheet.Cells[iRowCount, 10] = data["status"].ToString();
                wSheet.Cells[iRowCount, 11] = data["startDate"].ToString();
                wSheet.Cells[iRowCount, 12] = data["value"].ToString();

                iRowCount++;
            }

            DirectoryInfo saveTo = Directory.CreateDirectory(excelTemplatePath + _report.FolderGuid.ToString() + "");
            _report.ReportLocation = saveTo.FullName + _report.ExcelTemplate;
            wBook.Close(true, _report.ReportLocation, m_objOpt);
            wBook = null;

        }
        catch (Exception ex)
        {
            LogException.HandleException(ex);
        }
        finally
        {
            NAR(wSheet);
            if (wBook != null)
                wBook.Close(false, m_objOpt, m_objOpt);
            NAR(wBook);
            xl.Quit();
            NAR(xl);
            GC.Collect();
        }

private void NAR(object o)
{
    try
    {
        System.Runtime.InteropServices.Marshal.ReleaseComObject(o);
    }
    catch { }
    finally
    {
        o = null;
    }
}

mise à Jour

peu importe ce que j'essaie, la méthode 'clean' ou la méthode 'ugly' (voir les réponses ci-dessous), l'instance excel est toujours là dès que cette ligne est activée:

wSheet.Cells[iRowCount, 1] = data["fullname"].ToString();

Si je commente cette ligne (et les autres semblables dessous, évidemment) L'appli Excel sort avec élégance. Dès qu'une ligne par ci-dessus est décommentée, Excel reste dans le coin.

je pense que je vais devoir vérifier s'il y a une instance en cours d'exécution avant d'assigner la variable xl et de l'accrocher à la place. J'ai oublié de mentionner que c'est un service windows, mais qui ne devrait pas, devrait-il?


13
demandé sur Sean 2009-06-25 02:05:45

14 réponses

UPDATE (novembre 2016)

je viens de lire un argument convaincant par Hans Passant que l'utilisation de GC.Collect est en fait la bonne façon d'aller. Je ne travaille plus avec Office (Dieu merci), mais si je le faisais, je voudrais probablement donner un autre essai - il serait certainement simplifier beaucoup de (milliers de lignes) du code que j'ai écrit en essayant de faire les choses de la "bonne" manière (comme je l'ai vu alors).

je laisse mon original réponse pour la postérité...


Mike dit, dans sa réponse, il est un moyen facile et d'une manière difficile à gérer. Mike suggère d'utiliser la méthode facile parce que... c'est plus facile. Personnellement, je ne pense pas que ce soit une raison suffisante, et je ne pense pas que ce soit la bonne façon. Ça sent "éteins et rallume" pour moi.

j'ai plusieurs années d'expérience dans le développement d'une application de bureautique en. net, et ces problèmes COM interop m'ont tourmenté pendant des années. les premières semaines et les premiers mois quand je suis tombé sur le problème, en particulier parce que Microsoft sont très timides sur admettre qu'il ya un problème en premier lieu, et à l'époque de bons conseils était difficile à trouver sur le web.

j'ai une façon de travailler que j'utilise désormais presque sans y penser, et c'est des années depuis que j'ai eu un problème. Il est toujours important d'être en vie pour tous les objets cachés que vous pourriez créer - et oui, si vous en manquez un, vous pourriez avoir une fuite que seulement devient évident que beaucoup plus tard. Mais ce n'est pas pire que les choses étaient dans le mauvais vieux temps de malloc/free.

je pense qu'il y a quelque chose à dire pour nettoyer après vous au fur et à mesure, plutôt qu'à la fin. Si vous commencez seulement Excel pour remplir quelques cellules, alors peut - être que cela n'a pas d'importance-mais si vous allez faire un peu de levage lourd, alors c'est une question différente.

quoi qu'il en soit, la technique que j'utilise est d'utiliser une classe de wrapper qui implémente IDisposable, et qui, dans son Dispose les appels de méthode ReleaseComObject. De cette façon, je peux utiliser using des instructions pour s'assurer que l'objet est éliminé (et l'objet COM libéré) dès que j'en ai fini avec lui.

ce qui est crucial, c'est qu'il sera éliminé/libéré même si ma fonction revient plus tôt, ou s'il y a une Exception, etc. Aussi, il va sont disposées/libérée si elle a effectivement été créé en premier lieu - appelez-moi un pédant, mais l'a suggéré le code qui tente de libérer les objets qui n'ont peut-être pas été créés me semblent du code bâclé. J'ai une objection similaire à l'utilisation de FinalReleaseComObject - vous devez savoir combien de fois vous avez causé la création d'une référence COM, et devrait donc être en mesure de le publier le même nombre de fois.

typique d'Un extrait de mon code pourrait ressembler à ceci (ou il serait, si j'ai été à l'aide de C# v2 et pourrait utiliser des génériques :-)):

using (ComWrapper<Excel.Application> application = new ComWrapper<Excel.Application>(new Excel.Application()))
{
  try
  {
    using (ComWrapper<Excel.Workbooks> workbooks = new ComWrapper<Excel.Workbooks>(application.ComObject.Workbooks))
    {
      using (ComWrapper<Excel.Workbook> workbook = new ComWrapper<Excel.Workbook>(workbooks.ComObject.Open(...)))
      {
        using (ComWrapper<Excel.Worksheet> worksheet = new ComWrapper<Excel.Worksheet>(workbook.ComObject.ActiveSheet))
        {
          FillTheWorksheet(worksheet);
        }
        // Close the workbook here (see edit 2 below)
      }
    }
  }
  finally
  {
    application.ComObject.Quit();
  }
}

Maintenant, je ne vais pas prétendre que ce n'est pas verbeux, et l'entaille causée par la création de l'objet hors de contrôle si vous ne divisez pas les choses en méthodes plus petites. Cet exemple est quelque chose du pire des cas, puisque tout ce que nous faisons est de créer des objets. Normalement, il y en a beaucoup plus entre les bretelles et le plafond est beaucoup moins.

notez que selon l'exemple ci-dessus, je passerais toujours les objets "enveloppés" entre les méthodes, jamais un objet nu COM, et il serait de la responsabilité de l'appelant à en disposer (généralement avec un using déclaration). De même, je serais toujours retourner un objet enveloppé, jamais un nu, et encore, il serait de la responsabilité de l'appelant à la libération. Vous pourriez utiliser un protocole différent, mais il est important d'avoir des règles claires, tout comme c'était quand nous avions l'habitude de faire notre propre gestion de mémoire.

ComWrapper<T> classe utilisée ici j'espère que nécessite peu d'explications. Il stocke simplement une référence à l'objet wrapped COM, et le libère explicitement (en utilisant ReleaseComObject) dans son Dispose méthode. ComObject la méthode retourne simplement une référence dactylographiée à L'objet wrapped COM.

J'espère que cela vous aidera!

EDIT: Je n'ai que maintenant suivi le lien vers la réponse de Mike à une autre question, et je vois qu'une autre réponse à cette question a un lien avec une classe d'emballage, comme je le suggère ci-dessus.

aussi, en ce qui concerne la réponse de Mike à cette autre question, j'ai pour dire que j'ai été presque séduit par le " Just use GC.Collect " argument. Cependant, j'ai été principalement attiré par ce fait sur une fausse prémisse; il a regardé à première vue comme il n'y aurait pas besoin de se soucier des références COM du tout. Cependant, comme Mike dit que vous avez encore besoin de libérer explicitement les objets COM associés à toutes vos variables in-scope - et donc tout ce que vous avez fait est de réduire plutôt que de supprimer le besoin de gestion COM-object. Personnellement, je préfère aller jusqu'au bout.

je notez également une tendance dans beaucoup de réponses à écrire du code où tout est publié à la fin d'une méthode, dans un grand bloc de ReleaseComObject appels. C'est très bien si tout fonctionne comme prévu, mais je conseillerais à quiconque écrit du code sérieux de considérer ce qui se passerait si une exception était lancée, ou si la méthode avait plusieurs points de sortie (le code ne serait pas exécuté, et donc les objets COM ne seraient pas libérés). C'est pourquoi je préfère l'utilisation de "wrappers" et usings. C'est verbeux, mais il ne faire pour pare-balles code.

EDIT2: j'ai mis à jour le code ci-dessus pour indiquer où le classeur doit être fermé avec ou sans enregistrer les modifications. Voici le code pour enregistrer les modifications:

object saveChanges = Excel.XlSaveAction.xlSaveChanges;

workbook.ComObject.Close(saveChanges, Type.Missing, Type.Missing);

...et enregistrer les modifications, il suffit de remplacer xlSaveChangesxlDoNotSaveChanges.

12
répondu Gary McGill 2017-05-23 10:30:15

Ce qui se passe, c'est que votre appel à:

Sheet.Cells[iRowCount, 1] = data["fullname"].ToString();

Est essentiellement la même que:

Excel.Range cell = Sheet.Cells[iRowCount, 1];
cell.Value = data["fullname"].ToString();

en le faisant de cette façon, vous pouvez voir que vous créez un Excel.Range objet, puis lui assigner une valeur. De cette façon, nous obtenons aussi une référence nommée à notre variable de portée, la cell variable, qui nous permet de nous libérer directement si nous le voulions. Ainsi, vous pouvez nettoyer vos objets de deux façons:

(1) La difficile et moche façon:

while (data.Read())
{
    Excel.Range cell = Sheet.Cells[iRowCount, 1];
    cell.Value = data["fullname"].ToString();
    Marshal.FinalReleaseComObject(cell);

    cell = Sheet.Cells[iRowCount, 2];
    cell.Value = data["brand"].ToString();
    Marshal.FinalReleaseComObject(cell);

    cell = Sheet.Cells[iRowCount, 3];
    cell.Value = data["agency"].ToString();
    Marshal.FinalReleaseComObject(cell);

    // etc...
}

dans ce qui précède, nous libérons chaque objet range via un appel à Marshal.FinalReleaseComObject(cell) à mesure que nous avançons.

(2) La simple et propre:

Quitter votre code exactement comme vous avez actuellement, et puis à la fin vous pouvez nettoyer comme suit:

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

if (wSheet != null)
{
    Marshal.FinalReleaseComObject(wSheet)
}
if (wBook != null)
{
    wBook.Close(false, m_objOpt, m_objOpt);
    Marshal.FinalReleaseComObject(wBook);
}
xl.Quit();
Marshal.FinalReleaseComObject(xl);

En bref, votre code est très proche. Si vous ajoutez des appels au GC.Collect() et GC.WaitForPendingFinalizers() avant le " NAR " appels, je pense que cela devrait fonctionner pour vous. (En résumé, le code de Jamie et celui d'Ahmad sont corrects. Jamie est plus propre, mais le code D'Ahmad est une "solution rapide" plus facile pour vous parce que vous n'auriez qu'à ajouter les appels aux appels au GC.Collect() et GC.WaitForPendingFinalizers() dans votre code existant.)

Jamie et Amhad également répertorié les liens vers le .NET Automatisation Forum que je participe sur (merci les gars!) Ici sont un couple de related posts que j'ai fait ici débordement des piles :

(1) comment nettoyer correctement les objets Excel interop dans C#

(2)C# Automatiser PowerPoint Excel-PowerPoint ne quitte pas

j'espère que cette aide, Sean...

Mike

6
répondu Mike Rosenblum 2017-05-23 12:26:29

ajouter ce qui suit avant votre appel à xl.Quit ():

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

vous pouvez aussi utiliser Marshal.FinalReleaseComObject() dans votre méthode NAR au lieu de ReleaseComObject. ReleaseComObject décrète le nombre de référence de 1 tandis que FinalReleaseComObject renvoie toutes les références de sorte que le compte est 0.

donc votre bloc final ressemblerait à:

finally
{
    GC.Collect(); 
    GC.WaitForPendingFinalizers(); 

    NAR(wSheet);
    if (wBook != null)
        wBook.Close(false, m_objOpt, m_objOpt);
    NAR(wBook);
    xl.Quit();
    NAR(xl);
}

mise à Jour de NAR méthode:

private void NAR(object o)
{
    try
    {
        System.Runtime.InteropServices.Marshal.FinalReleaseComObject(o);
    }
    catch { }
    finally
    {
        o = null;
    }
}

j'avais fait des recherches sur ce sujet il y a quelque temps et dans des exemples je habituellement, les appels liés au GC étaient à la fin après la fermeture de l'application. Cependant, il y a un MVP (Mike Rosenblum) qui mentionne qu'il devrait être appelé au début. J'ai essayé les deux méthodes et elles ont fonctionné. Je l'ai également essayé sans les finaliseurs Waitforpending et il a fonctionné bien qu'il ne devrait pas blesser quoi que ce soit. YMMV.

Voici les liens pertinents par le MVP que j'ai mentionné (ils sont en VB mais ce n'est pas que les différentes):

2
répondu Ahmad Mageed 2009-06-24 22:35:03

comme D'autres ont déjà couvert InterOp, je suggère que si vous vous occupez de fichiers Excel avec l'extension XLSX, vous utilisiez EPPlus qui fera de votre Excel cauchemars s'en aller.

2
répondu MadBoy 2010-11-08 22:37:58

je viens de répondre à cette question ici:

Tuer des processus excel par sa fenêtre principale hWnd

1
répondu nightcoder 2017-05-23 12:34:51

Il y a plus de 4 ans que cela a été posté mais j'ai rencontré le même problème et j'ai pu le résoudre. Apparemment, le simple fait d'accéder au tableau de cellules crée un objet COM. Donc, si vous décidez de faire:

    wSheet = (Excel._Worksheet)wBook.ActiveSheet;
    Microsoft.Office.Interop.Excel.Range cells = wSheet.Cells;

    int iRowCount = 2;

    // enumerate and drop the values straight into the Excel file
    while (data.Read())
    {
        Microsoft.Office.Interop.Excel.Range cell = cells[iRowCount, 1];
        cell  = data["fullname"].ToString();
        Marshal.FinalReleaseComObject(cell);
    }
    Marshal.FinalReleaseComObject(cells);

et puis le reste de votre nettoyage il devrait résoudre le problème.

1
répondu Scott 2013-12-17 22:13:51

ce que j'ai fini par faire pour résoudre un problème similaire était d'obtenir L'Id du processus et de le tuer en dernier recours...

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

...

objApp = new Excel.Application();

IntPtr processID;
GetWindowThreadProcessId(objApp.Hwnd, out processID);
excel = Process.GetProcessById(processID.ToInt32());

...

objApp.Application.Quit();
Marshal.FinalReleaseComObject(objApp);
_excel.Kill();
0
répondu Andy Mikula 2009-06-24 22:21:45

voici le contenu de mon n'a pas-échoué-enfin bloquer pour nettoyer L'automatisation Excel. Mon application laisse Excel ouvert donc il n'y a pas D'appel D'abandon. référence dans le commentaire était ma source.

finally
{
    // Cleanup -- See http://www.xtremevbtalk.com/showthread.php?t=160433
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
    GC.WaitForPendingFinalizers();
    // Calls are needed to avoid memory leak
    Marshal.FinalReleaseComObject(sheet);
    Marshal.FinalReleaseComObject(book);
    Marshal.FinalReleaseComObject(excel);
}
0
répondu Jamie Ide 2009-06-24 22:45:57

avez-vous envisagé d'utiliser une solution pure .NET telle que Tableatgear for .NET