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