Comment nettoyer correctement les objets Excel interop?
j'utilise L'interop Excel en C# ( ApplicationClass
) et j'ai placé le code suivant dans ma dernière clause:
while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();
bien que ce genre de travaux, le Excel.exe
processus est toujours à l'arrière-plan même après que je ferme Excel. Il n'est libéré que lorsque mon application est fermée manuellement.
Qu'est-ce que je fais de mal, ou y a-t-il une alternative pour s'assurer que les objets interop sont correctement éliminés?
30 réponses
Excel N'arrête pas parce que votre application contient toujours des références aux objets COM.
je suppose que vous invoquez au moins un membre d'un objet COM sans l'affecter à une variable.
pour moi c'était le excelApp.Feuilles de travail objet que j'ai directement utilisé sans l'affecter à une variable:
Worksheet sheet = excelApp.Worksheets.Open(...);
...
Marshal.ReleaseComObject(sheet);
Je ne savais pas qu'à l'interne C # a créé un wrapper pour le feuilles de travail objet COM qui n'a pas été libéré par mon code (parce que je n'étais pas au courant de celui-ci) et était 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'usage 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);
vous pouvez réellement libérer votre objet D'application Excel proprement, mais vous devez prendre soin.
le Conseil de maintenir une référence nommée pour absolument chaque objet COM que vous accédez et puis de le libérer explicitement via Marshal.FinalReleaseComObject()
est correct en théorie, mais, malheureusement, très difficile à gérer dans la pratique. Si l'un d'eux glisse n'importe où et utilise "deux points", ou itère des cellules via une boucle for each
, ou tout autre type de commande similaire, alors vous aurez objets COM non référencés et risque de pendre. Dans ce cas, il n'y aurait aucun moyen de trouver la cause dans le code; vous auriez à passer en revue tout votre code à l'oeil et avec un peu de chance 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 maintenir une référence de variable nommée à chaque objet COM que vous utilisez. Au lieu de cela, appelez GC.Collect()
et ensuite GC.WaitForPendingFinalizers()
pour libérer tous les objets (généralement mineurs) auxquels vous faites ne pas tenir une référence, puis libérer explicitement les objets auxquels vous tenez une référence de variable nommée.
vous devriez également publier vos références nommées dans l'ordre inverse de l'importance: les objets de gamme d'abord, puis feuilles de travail, Classeurs, et enfin votre objet D'application Excel.
par exemple, en présumant que vous aviez une variable D'objet de portée nommée xlRng
, une variable de feuille de travail nommée xlSheet
, une variable de classeur nommée xlBook
et une variable D'application Excel nommée xlApp
, alors votre code de nettoyage pourrait ressembler à quelque chose comme 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, vous verrez pour nettoyer les objets COM de .NET, les appels GC.Collect()
et GC.WaitForPendingFinalizers()
sont faits deux fois comme dans:
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
ceci ne devrait pas être requis, cependant, à moins que vous n'utilisiez Visual Studio Tools for Office( VSTO), qui utilise des finaliseurs qui causent un graphique des objets à promouvoir dans la file d'attente de finalisation. Ces objets ne seraient pas libérés avant la collecte des ordures suivante . 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 certainement le faire deux fois semble très douloureux), mais il n'y a aucun moyen de l'éviter, pour être honnête. Par des opérations normales, vous générerez caché objets auxquels vous ne tenez pas de référence que vous, par conséquent, ne pouvez pas libérer par d'autres moyens que l'appel GC.Collect()
.
C'est un sujet complexe, mais c'est vraiment tout là est à lui. Une fois que vous avez établi ce modèle pour votre procédure de nettoyage, vous pouvez coder normalement, sans avoir besoin de papier d'emballage, etc. :- )
j'ai un tutoriel sur ce ici:
"1519390920 de l'Automatisation de l'Office des Programmes avec VB.Net / COM Interop
c'est écrit pour VB.NET, mais ne soyez pas rebutés par cela, les principes sont exactement les mêmes que lors de L'utilisation de C#.
préface: ma réponse contient deux solutions, alors soyez prudent lorsque vous lisez et ne manquez rien.
il y a différentes façons et conseils sur la façon de décharger Excel instance, tels que:
-
la libération de TOUS les objets com explicitement avec le Maréchal.FinalReleaseComObject() (sans oublier implicitement créée com-objets). Publier chaque objet com créé, vous pouvez utiliser la règle de 2 points mentionnés ici:
Comment nettoyer correctement les objets Excel interop? -
appel GC.Collect () et GC.Waitforpendingfinalisers () to make CLR libération inutilisés com-objets * (en Fait, il fonctionne, voir mon deuxième solution pour les détails)
-
vérification si com-server-application peut-être montre une boîte de message en attente l'utilisateur à répondre (même si je suis pas sûr qu'il peut empêcher Excel de de clôture, mais j'en ai entendu parler il y a quelques times)
-
envoyer WM_CLOSE message à la main Fenêtre Excel
-
exécuter la fonction qui fonctionne avec Excel dans un AppDomain séparé. Certaines personnes croient que L'instance Excel sera fermé, quand AppDomain est déchargement.
-
tuer toutes les instances excel qui ont été instanciées après notre le code excel-interoping a commencé.
mais! parfois, toutes ces options n'aident pas ou ne peuvent pas être appropriées!
par exemple, hier j'ai découvert que dans une de mes fonctions (qui fonctionne avec excel) Excel continue à fonctionner après la fin de la fonction. J'ai tout essayé! J'ai vérifié toute la fonction 10 fois et J'ai ajouté Marshal.FinalReleaseComObject() pour tout! J'ai aussi eu GC.Recueillir() et GC.Waitforpendingfinalisers (). J'ai vérifié les boîtes à messages cachées. J'ai essayé d'envoyer le message WM_CLOSE à la fenêtre principale Excel. J'ai exécuté ma fonction dans un AppDomain séparé et j'ai déchargé ce domaine. Rien n'y fait! L'option avec la fermeture de toutes les instances excel est inappropriée, parce que si l'utilisateur lance une autre instance Excel manuellement, pendant l'exécution de ma fonction qui fonctionne aussi avec Excel, alors cette instance sera également fermée par ma fonction. Je parie que l'utilisateur ne sera pas heureux! Si, honnêtement, c'est une option boiteuse (sans offense). J'ai donc passé quelques heures avant de trouver un bon (à mon humble avis) solution : Kill excel process par hWnd de sa 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, selon le motif Try-Parse (je pense qu'il est approprié ici): une méthode ne jette pas l'exception si le Processus ne pouvait pas être tué (par exemple le processus n'existe plus), et une autre méthode jette l'exception si le processus n'a pas été tué. Le seul point faible de ce code est les permissions 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. Je l'ai également testé avec un compte client - il fonctionne parfaitement.
ainsi, votre code, 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);
voilà! Excel est terminé! :)
Ok, revenons à 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.Waitforpendingfinalisers (). Oui, ils fonctionnent vraiment, mais vous devez être prudent 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 y a encore des références aux objets COM! Une des raisons les plus populaires de GC.Collect() n'étant pas utile est en cours d'exécution du projet en mode Debug. En mode de débogage, les objets qui ne sont plus vraiment référencés ne seront pas collectés avant 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 de faire ce qui suit:
1) essayer d'exécuter votre projet en mode de publication et vérifier si Excel fermé correctement
2) envelopper la méthode de travail avec Excel dans une méthode séparée. Donc, 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 =)
UPDATE : code C Ajouté, et lien vers Jobs Windows
j'ai passé un certain temps à essayer de comprendre ce problème, et à L'époque XtremeVBTalk était le plus actif et réactif. Voici un lien vers mon article original, fermer un processus Interop Excel proprement, même si votre application plante . Ci-dessous est un résumé du post, et le code copié à ce post.
- fermeture Le processus Interop avec
Application.Quit()
etProcess.Kill()
fonctionne pour la plupart, mais échoue si les applications s'effondrent de manière catastrophique. I. e. si l'application se bloque, le processus Excel sera toujours en cours en vrac. - la solution est de laisser l'OS gérer le nettoyage de vos processus via Windows Job Objects en utilisant Win32 calls. Lorsque votre application principale meurt, le processus (c'est à dire Excel) est interrompu.
j'ai trouvé que c'était une solution propre parce que L'OS fait un vrai travail de nettoyage. Tout ce que vous avez à faire est registre le processus Excel.
Windows Code Du Travail
enroule les appels D'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);
}
}
Note concernant le code de constructeur
- dans le constructeur, le
info.LimitFlags = 0x2000;
est appelé.0x2000
est la valeurJOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
enum, et cette valeur est définie par MSDN comme:
provoque tous les processus associés à l'emploi de se terminer lorsque le dernière poignée à l'emploi est fermé.
appel supplémentaire de L'API Win32 pour obtenir l'ID de processus (PID)
[DllImport("user32.dll", SetLastError = true)]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
à l'Aide de 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);
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 Excel COM à null quand vous en aviez fini avec lui. Cela incluait des cellules, des draps, et tout.
Tout ce qui est dans L'espace de noms Excel doit être publié. Période
Vous ne pouvez pas être en train de faire:
Worksheet ws = excel.WorkBooks[1].WorkSheets[1];
Vous avez à faire
Workbooks books = excel.WorkBooks;
Workbook book = books[1];
Sheets sheets = book.WorkSheets;
Worksheet ws = sheets[1];
suivi de la libération des objets.
je trouvé utile modèle générique qui peut aider à mettre en œuvre la mise au rebut correcte de modèle pour les objets COM, qui ont besoin de Maréchal.ReleaseComObject appelé quand ils sortent de la portée:
Utilisation:
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 /
Je ne peux pas croire que ce problème hante le monde depuis 5 ans.... Si vous avez créé une application, vous devez arrêter d'abord avant de retirer le lien.
objExcel = new Excel.Application();
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing));
lors de la fermeture
objBook.Close(true, Type.Missing, Type.Missing);
objExcel.Application.Quit();
objExcel.Quit();
lorsque vous introduisez une application excel, elle ouvre un programme excel à l'arrière-plan. Vous devez commander ce programme excel pour quitter avant de libérer le lien parce que ce programme excel ne fait pas partie de votre contrôle direct. Donc, il restera ouvert si le lien est publié!
bonne programmation tout le monde~
Premier - vous jamais appeler Marshal.ReleaseComObject(...)
ou Marshal.FinalReleaseComObject(...)
Excel interop. C'est un anti-pattern confus, 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 collecteur de déchets gardent correctement la trace et nettoient les références COM. Pour votre code, cela signifie que vous pouvez supprimer l'ensemble ' while (...) boucle sur le dessus.
deuxièmement, si vous voulez vous assurer que les références COM à un objet COM hors processus sont nettoyées lorsque votre processus se termine (de sorte que le processus Excel se ferme), vous devez vous assurer que le collecteur d'ordures fonctionne. 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 suis pas sûr que ce soit nécessaire, et apprécierait un exemple qui montre cela).
Troisièmement, lors de l'exécution sous le débogueur, les références locales seront artificiellement maintenues vivantes jusqu'à la fin de la méthode (de sorte que l'inspection variable locale fonctionne). Ainsi, les appels GC.Collect()
ne sont pas efficaces pour nettoyer un objet comme rng.Cells
de la même méthode. Vous devez diviser le code faisant le COM interop du nettoyage GC en méthodes séparées. (Ce fut une découverte clé pour moi, à partir d'une partie de la réponse postée ici par @nightcoder.)
le général modèle 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 à propos de cette question, 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 était le billet de blog Marshal.ReleaseComObject Considered Dangerous ainsi que de trouver la question avec des références maintenu vivant sous le débogueur qui perturbait mes tests précédents.
développeurs communs, aucune de vos solutions n'a fonctionné pour moi, j'ai donc décidé de mettre en place un nouveau truc .
premier let spécifier "quel est notre objectif?"=> "Ne pas voir l'objet excel après notre travail dans le gestionnaire des tâches"
Ok. Que non pour le défier et commencer à le détruire, mais envisager de ne pas détruire d'autres instances OS Excel qui sont en cours d'exécution en parallèle.
donc, obtenir la liste des processeurs actuels et fetch PID des processus EXCEL, puis une fois votre travail terminé, nous avons un nouvel invité dans la liste des processus avec un PID unique ,trouver et détruire juste celui-là.
< gardez à l'esprit que tout nouveau processus excel pendant votre travail excel sera détecté comme nouveau et détruit > < Une meilleure solution est de capturer le PID d'un nouvel objet excel créé et de 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();
ceci résout mon problème, espérons le vôtre aussi.
la réponse acceptée ici est correcte, mais notez aussi que non seulement les références à" deux points " doivent être évitées, mais aussi les objets qui sont récupérés via l'index. Vous n'avez pas non plus besoin d'attendre jusqu'à ce 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 fini avec eux, lorsque c'est possible. Voici une fonction que j'ai créée qui attribue certaines propriétés D'un objet de 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 { }
}
remarquez que j'ai dû mettre xlBorders[Excel.XlBordersIndex.xlEdgeBottom]
à une variable pour la nettoyer (non pas à cause des deux points, qui font référence à une énumération qui n'a pas besoin d'être libérée, mais parce que l'objet auquel je fais référence est en fait un objet de bordure qui doit être libérée).
ce genre de chose n'est pas vraiment nécessaire dans les applications standard, qui font un excellent travail de nettoyage après eux-mêmes, mais en ASP.NET applications, Si vous manquez même L'un D'eux, peu importe combien de fois vous appelez le collecteur de déchets, Excel sera toujours en cours d'exécution sur votre serveur.
il exige beaucoup d'attention aux détails et de nombreuses exécutions de test tout en surveillant le Gestionnaire des tâches lors de l'écriture de ce code, mais en faisant cela vous épargne la peine de chercher désespérément à travers les pages de code pour trouver la seule 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 si elle utilise le même nom de variable à chaque fois qu'il en boucle.
pour ajouter aux raisons pour lesquelles Excel ne se ferme pas, même si vous créez des réfrences directes à chaque objet en 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
cela semble sûr comme il a été trop compliqué. D'après mon expérience, il n'y a que trois choses clés pour obtenir Excel de fermer 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 à null
)
2: appeler GC.Collect()
3: Excel doit être fermé, soit par l'utilisateur fermant manuellement le programme, soit par vous appelant Quit
sur L'objet Excel. (Notez que Quit
fonctionnera comme si l'utilisateur essayait de fermer le programme, et présentera une boîte de dialogue de confirmation s'il y a des changements non sauvegardés, même si Excel n'est pas visible. L'utilisateur peut appuyer sur annuler, puis Excel n'a pas été fermé.)
1 doit se produire avant 2, mais 3 peut se produire à tout moment.
une façon de mettre en œuvre ceci est d'envelopper L'objet Excel interop avec votre propre classe, créer le interop instance dans le constructeur, et implémenter IDisposable avec Dispose ressemblant à quelque chose comme
if (!mDisposed) {
mExcel = null;
GC.Collect();
mDisposed = true;
}
qui nettoiera excel du côté de votre programme. Une fois Excel fermé (manuellement par l'utilisateur ou en appelant Quit
), le processus disparaîtra. Si le programme est déjà terminé, le processus disparaîtra à l'appel GC.Collect()
.
(Je ne suis pas sûr à quel point il est important, mais vous pouvez vouloir un GC.WaitForPendingFinalizers()
appel après l'appel GC.Collect()
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 que tout cela fonctionne, vous devez fermer gracieusement pour que cela fonctionne. Vous obtiendrez toujours l'accumulation d'excel.processus exe si vous interrompez votre programme avant que Excel ne soit nettoyé (généralement en appuyant sur "stop" pendant que votre programme est en cours de débogage).
après avoir essayé
- la Libération des objets COM dans l'ordre inverse
- ajouter
GC.Collect()
etGC.WaitForPendingFinalizers()
deux fois à la fin - pas plus de deux points
- fermer le cahier D'exercices et la demande d'abandon
- 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 d'un wrapper, comme suit:
private void FunctionWrapper(string sourcePath, string targetPath)
{
try
{
FunctionThatCallsExcel(sourcePath, targetPath);
}
finally
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
j'ai traditionnellement suivi les conseils trouvés dans réponse de VVS . Cependant, dans un effort pour garder cette réponse à jour avec les dernières options, je pense que tous mes projets futurs utiliseront la bibliothèque "NetOffice".
NetOffice est un remplacement complet pour le bureau PIAs et est complètement version-agnostique. C'est une collection D'emballages COM gérés qui peuvent gérer le nettoyage qui provoque souvent de tels maux de tête quand travailler avec Microsoft Office in .NET.
quelques caractéristiques clés sont:
- la plupart du temps version-indépendant (et la version-les caractéristiques dépendantes sont documentées)
- Pas de dépendances
- No PIA
- pas d'enregistrement
- No VSTO
Je ne suis en aucun cas affilié au projet; je suis vraiment reconnaissant de la réduction brutale dans les maux de tête.
°¤ø "Shoot Excel proc and chew bubble 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);
}
vous devez être conscient que Excel est très sensible à la culture dans laquelle vous évoluez aussi.
vous pouvez trouver que vous avez besoin de mettre la culture à EN-US avant D'appeler des fonctions Excel. Cela ne s'applique pas à toutes les fonctions - mais certains d'entre eux.
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;
ceci s'applique même si vous utilisez VSTO.
pour plus de détails: http://support.microsoft.com/default.aspx?scid=kb;en-us; Q320369
j'ai suivi exactement cela... Mais j'ai quand même rencontré des numéros 1 fois sur 1 000. Qui sait pourquoi. Moment de sortir le marteau...
juste après l'Application Excel classe est instanciée je choppe 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();
puis une fois que j'ai fait tout le nettoyage COM ci-dessus, je m'assure que le processus ne fonctionne pas. S'il est toujours en marche, tuez-le!
if (!process.HasExited)
process.Kill();
"ne Jamais utiliser deux points avec les objets COM" est une grande règle de base pour éviter les fuites de COM références, mais Excel PIA peut conduire à une fuite de plus de manières qu'apparent à première vue.
une de ces façons est de souscrire à 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 de COM
Les classes COM exposent un groupe d'événements à travers des interfaces de rappel. Pour s'abonner à 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. Comme l'interface de rappel est une interface COM, il est du devoir de l'objet d'exécution de décrémenter le nombre de référence de tout objet COM qu'il reçoit (comme paramètre) pour n'importe lequel des gestionnaires d'événements.
How Excel PIA expose com Events
Excel PIA expose les événements COM de la classe D'application Excel comme les événements conventionnels .NET. Chaque fois que le code client s'abonne à un événement .NET (emphasis on 'a'), PIA crée un instance d'une classe implémentant l'interface de rappel et l'enregistre avec Excel.
ainsi, un certain nombre d'objets de rappel sont enregistrés avec Excel en réponse à différentes demandes d'abonnement du code .NET. Un objet de rappel par abonnement d'événement.
une interface de rappel pour le traitement des événements signifie que, PIA doit s'abonner à tous les événements d'interface pour chaque demande d'abonnement d'événement .NET. Il ne peut pas choisir. Lors de la réception d'un rappel d'événement, l'objet call-back vérifie si le gestionnaire D'événements associé à .NET est intéressé par l'événement en cours ou non et ensuite, il invoque le gestionnaire ou ignore silencieusement le rappel.
Effet sur le nombre de références de L'instance
tous ces objets de rappel ne décrémentent le compte de référence d'aucun des objets COM qu'ils reçoivent (comme paramètres) pour l'une des méthodes de rappel (même pour ceux qui sont ignorés silencieusement). Ils s'appuient uniquement sur le collecteur d'ordures CLR pour libérer les objets COM.
étant donné que L'exécution par CG n'est pas déterministe, cela peut conduire à retarder le processus Excel pendant une plus longue période. durée plus longue que désirée et créer une impression de "fuite de mémoire".
Solution
la seule solution à ce jour est d'éviter le fournisseur d'événements PIA pour la classe COM et d'é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 D'AppEvents puis en enregistrant L'implémentation avec Excel en utilisant IConnectionPointContainer interface . La classe Application (et d'ailleurs tous les objets COM exposant des événements en utilisant le mécanisme de rappel) implémente L'interface IConnectionPointContainer.
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 le présent article . Vous devez également utiliser try / finally pour vous assurer que ReleaseComObject est toujours appelé, même si une exception est lancé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.Quittez avant de libérer l'objet Application si vous voulez que Excel ferme.
comme vous pouvez le voir, cela devient rapidement extrêmement difficile dès que vous essayez de faire quelque chose, même modérément complexe. J'ai développé avec succès des applications .NET avec une classe wrapper simple qui enveloppe quelques manipulations simples du modèle D'objet Excel (ouvrir un classeur, écrire sur une plage, sauvegarder/fermer le classeur, etc.). La classe wrapper implémente IDisposable, implémente avec soin Maréchal.ReleaseComObject sur tous les objets qu'il utilise, et n'expose pas publiquement D'objets Excel au reste de l'application.
mais cette approche n'est pas adaptée aux besoins plus complexes.
c'est une grande lacune de .NET COM Interop. Pour des scénarios plus complexes, je considérerais sérieusement écrire une DLL ActiveX dans VB6 ou un autre langage non géré auquel vous pouvez déléguer toute interaction avec des objets COM out-proc tels que Office. Vous pouvez puis référencez cette DLL ActiveX à partir de votre application.net, et les choses seront beaucoup plus faciles que vous n'aurez besoin de libérer cette seule référence.
lorsque toutes les choses ci-dessus n'ont pas fonctionné, essayez de donner à Excel un certain temps pour fermer ses feuilles:
app.workbooks.Close();
Thread.Sleep(500); // adjust, for me it works at around 300+
app.Quit();
...
FinalReleaseComObject(app);
assurez-vous que vous libérez tous les objets liés à Excel!
j'ai passé quelques heures à essayer plusieurs méthodes. Toutes sont de grandes idées mais j'ai finalement trouvé mon erreur: si vous ne relâchez 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 gamme un!
Excel.Range rng = (Excel.Range)worksheet.Cells[1, 1];
worksheet.Paste(rng, false);
releaseObject(rng);
les options sont ensemble ici .
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();
}
vous devez être très prudent en utilisant Word/Excel interop applications. Après avoir essayé toutes les solutions, nous avons encore eu beaucoup de processus "WinWord" laissé ouvert 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 j'ouvrais plus d'un couple de documents en utilisant Word.ApplicationClass.Document.Open()
sur différents threads simultanément, IIS worker process (w3wp.exe) s'écraserait en laissant tous les processus de WinWord ouverts!
donc je suppose il n'y a pas de solution absolue à ce problème, mais passer à d'autres méthodes telles que Office Open XML développement.
un grand article sur la libération des objets COM est 2.5 libération des objets COM (MSDN).
la méthode que je préconiserais est d'annuler votre Excel.Les références Interop s'il s'agit de variables non locales, puis appeler GC.Collect()
et GC.WaitForPendingFinalizers()
deux fois. Les variables Interop à portée locale seront traitées 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 tirés directement de l'article:
dans presque toutes les situations, l'annulation de la référence à la RCW et l'imposition d'une collecte des ordures permettra de nettoyer correctement. Si vous appelez aussi GC.Waitforpendingfinalisers, la collecte des ordures sera aussi déterministe que vous pouvez le faire. Qui est, vous serez sûr exactement quand l' l'objet a été nettoyé-sur le retour du second appel à attendre des finaliseurs. Comme alternative, vous pouvez utiliser Marshal.ReleaseComObject. Toutefois, notez que vous êtes très peu probable qu'on ait besoin d'utiliser cette méthode.
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();
}
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(); }
}
je travaille actuellement sur la bureautique et je suis tombé sur une solution pour cela qui fonctionne à chaque fois pour moi. Il est simple et ne nécessite pas de tuer tous les processus.
il semble qu'en faisant simplement une boucle à travers les processus actifs actuels, et de quelque manière que ce soit en 'accédant' à un processus Excel ouvert, toute instance pendante errante D'Excel sera supprimée. Le code ci-dessous vérifie simplement les processus dont le nom est "Excel", puis écrit le MainWindowTitle propriété du processus d'une chaîne de caractères. Cette 'interaction' avec le processus semble faire que Windows rattrape et annule L'instance gelée D'Excel.
j'exécute la méthode ci-dessous juste avant l'add-in dans lequel je développe quitts, comme il déclenche l'événement de déchargement. Il supprime toutes les instances de suspension D'Excel à chaque fois. En toute honnêteté je ne suis pas entièrement sûr pourquoi cela fonctionne, mais il fonctionne bien pour moi et pourrait être placé à la fin de n'importe quelle application Excel sans avoir à se soucier de double point, Marshal.ReleaseComObject, ni tuer un processus. Je serais très intéressé par toute suggestion quant aux raisons pour lesquelles 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;
}
}
}
}
je pense qu'une partie de cela est juste la façon dont le cadre traite les applications de bureau, mais je pourrais me tromper. Certains jours, certaines applications nettoient les processus immédiatement, et d'autres jours, il semble attendre jusqu'à ce que l'application ferme. En général, j'arrête de prêter attention aux détails et je m'assure qu'il n'y a pas de processus supplémentaires qui flottent autour à la fin de la journée.
aussi, et peut-être que je simplifie trop les choses, mais je pense que vous pouvez juste...
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 de quand le processus Excel apparaît ou disparaît, mais cela fonctionne généralement pour moi. Je n'aime pas à garder Excel processus autour de pour autre chose que le minimum de temps, mais je suis probablement juste être paranoïaque.
comme certains l'ont probablement déjà écrit, ce n'est pas seulement important comment vous fermer L'Excel (objet); il est également important comment vous ouvrir il et aussi par le type du 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 une valeur de paramètre différente-par ex. l'analyse sur la base 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 que Excel soit ouvert et refermé pour chaque élément d'une liste générique, donc je ne l'ai ouvert qu'une fois dans la classe de base et je l'ai fermé dans la sous-classe. J'ai eu des problèmes en déplaçant le code dans une application de bureau. J'ai essayé beaucoup de solutions. GC.Collect()
a déjà été mis en œuvre avant, deux fois comme suggéré.
alors j'ai décidé de déplacer le code pour ouvrir Excel à une sous-classe. Au lieu de n'ouvrir qu'une seule fois, je crée maintenant un nouvel objet (classe de base) et ouvre Excel pour chaque élément et le ferme à la fin. Il y a une certaine pénalité de performance, mais sur la base de plusieurs tests, les processus Excel se ferment sans problème (en mode debug), donc les fichiers temporaires sont également supprimés. Je vais continuer à l'essai et à écrire un peu plus si je obtiendrez des mises à jour.
la ligne de fond est: vous devez également vérifier le code d'initialisation, surtout si vous avez beaucoup de classes, etc.