C#和Excel自动化 – 结束正在运行的实例

我正在通过C#尝试Excel自动化。 我已经遵循了微软关于如何解决这个问题的所有指示,但是我仍然在努力放弃最后的参考资料给Excel来closures并使GC收集它。

代码示例如下。 当我注释掉包含类似于以下行的代码块时:

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

然后该文件保存并退出Excel。 否则,文件会保存,但Excel仍将作为一个进程运行。 下次运行代码时,会创build一个新的实例并最终build立起来。

任何帮助表示赞赏。 谢谢。

这是我的代码的准系统:

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

更新

无论我尝试什么,“干净”的方法或“丑陋”的方法(见下面的答案),excel实例仍然挂起一旦这条线被击中:

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

如果我注释到这一行(显然,下面的其他类似的行),Excel应用程序将正常退出。 一旦上面的一行没有注释,Excel就会一直存在。

我想我将不得不检查是否有一个正在运行的实例,而不是分配xlvariables和挂钩。 我忘了提及这是一个Windows服务,但这应该不重要,应该吗?


更新 (2016年11月)

我刚刚读到Hans Passant的一个令人信服的说法 ,那就是使用GC.Collect实际上是正确的select。 我不再和Office一起工作(谢天谢地),但是如果我这样做的话,我可能会想再试一次 – 这肯定会简化很多我写的代码(数千行),尝试做“正确的“(就像我看到的那样)。

我会留下我对于后代的原始答案…


正如迈克在回答中所说的,对付这个问题有一个简单的方法和一个很难的方法。 迈克build议使用简单的方法,因为…更容易。 我个人不相信这是一个足够好的理由,我不相信这是正确的。 它带着“把它关掉”的感觉。

我有几年在.NET中开发Office自动化应用程序的经验,当我第一次遇到这个问题时,这些COM互操作问题困扰着我头几个星期和几个月,不仅仅是因为微软承认有一个问题第一,当时在网上很难find好的build议。

我有一种工作方式,现在我几乎不用考虑这个问题,而且这个问题已经有好几年了。 活着所有你可能创造的隐藏物体仍然是重要的 – 是的,如果你错过了一个,你可能会有一个泄漏,只是在以后才会出现。 但是这并不比过去在malloc / free这个不好的日子里更糟。

我认为在你走后,有些事情可以说清理,而不是最后。 如果你只是开始在Excel中填写一些单元格,那么也许并不重要 – 但是如果你要做一些繁重的工作,那么这就是另外一回事了。

无论如何,我使用的技术是使用实现IDisposable的包装类,并在Dispose方法中调用ReleaseComObject 。 这样,我可以使用using语句来确保一旦完成对象处理(和COM对象释放)。

最重要的是,即使我的函数返回提前,或者有一个exception等,它也会被处置/释放。而且,如果它实际上是在第一个地方创build的,它只会被释放/释放 – 叫我一个僧侣,但是build议试图释放可能实际上没有被创build的对象的代码在我看来就像一些草率的代码。 我有类似的反对使用FinalReleaseComObject – 你应该知道多less次你创build一个COM引用,因此应该能够释放它相同的次数。

我的代码的一个典型片段可能是这样的(或者,如果我使用C#v2,可以使用generics:-)):

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

现在,我不会假装这不是罗嗦,如果你不把东西分成更小的方法,由对象创build引起的缩进可能会失控。 这个例子是最坏的情况,因为我们所做的只是创build对象。 通常情况下,大括号之间还有很多事情要做,开销也less得多。

请注意,根据上面的示例,我总是在方法之间传递“包装”对象,而不是一个裸体的COM对象,调用者有责任处理它(通常使用using语句)。 同样,我总是会返回一个包装的对象,从来不是一个裸体的对象,并且调用者也有责任释放它。 你可以使用一个不同的协议,但是有一个清晰的规则是很重要的,就像我们以前需要做自己的内存pipe理一样。

这里使用的ComWrapper<T>类希望只需要很less的解释。 它只是存储对包装COM对象的引用,并在Dispose方法中显式释放它(使用ReleaseComObject )。 ComObject方法只是返回一个types化引用到包装的COM对象。

希望这可以帮助!

编辑 :我现在只是跟随迈克的另一个问题的答案链接,我看到另一个问题的答案有一个包装类的链接,正如我上面的build议。

而且,关于迈克对另一个问题的回答,我不得不说我被“只用GC.Collect ”的观点所吸引。 但是,我主要是在一个错误的前提下提到的, 乍看之下就好像根本不需要担心COM的参考。 但是,正如Mike所说的,您仍然需要显式地释放与所有范围内variables相关的COM对象 – 因此,您所做的只是减less而不是删除对COM对象pipe理的需求。 就个人而言,我宁愿去整个猪。

我还注意到有很多答案的倾向,在一个大的ReleaseComObject调用块中,写一些代码放在一个方法的末尾。 如果一切按计划进行,这一切都是非常好的,但我会敦促任何人编写严肃的代码来考虑如果抛出exception会发生什么情况,或者如果方法有几个退出点(代码不会执行,因此COM对象不会被释放)。 这就是为什么我喜欢使用“包装”和using s。 这是罗嗦,但它确实是防弹的代码。

编辑2 :我已经更新上面的代码,以指示工作簿应该closures或不保存更改。 以下是保存更改的代码:

 object saveChanges = Excel.XlSaveAction.xlSaveChanges; workbook.ComObject.Close(saveChanges, Type.Missing, Type.Missing); 

并不保存更改,只需将xlSaveChanges更改为xlDoNotSaveChanges

正在发生的事情是您的电话:

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

基本上是一样的:

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

通过这样做,你可以看到你正在创build一个Excel.Range对象,然后给它赋值。 这种方式也给了我们一个对我们的范围variables( cell variable)的命名引用,它允许我们直接发布它,如果我们想要的话。 所以你可以用下面两种方法清理你的对象:

(1)困难和难看的方式:

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

在上面,我们通过调用Marshal.FinalReleaseComObject(cell)来释放每个范围对象。

(2)简单干净的方法:

保持你的代码与你目前的代码完全一样,然后最后你可以清理如下:

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

总之,你现有的代码是非常接近的 。 如果你只是在你的'NAR'调用之前添加到GC.Collect()和GC.WaitForPendingFinalizers()的调用,我认为它应该为你工作。 (简而言之,Jamie的代码和Ahmad的代码都是正确的,Jamie's更干净,但Ahmad的代码对于你来说是一个更简单的“快速修复”,因为你只需要将调用添加到GC.Collect()和GC.WaitForPendingFinalizers ()到您现有的代码。)

Jamie和Amhad也列出了我参与的.NET自动化论坛的链接(谢谢你们!)下面是我在StackOverflow上发布的一些相关文章:

(1) 如何正确清理C#中的Excel互操作对象

(2) C#自动化PowerPoint Excel – PowerPoint不会退出

我希望这可以帮助,肖恩…

麦克风

在调用xl.Quit()之前添加以下内容:

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

你也可以在你的NAR方法中使用Marshal.FinalReleaseComObject()而不是ReleaseComObject。 ReleaseComObject将引用计数递减1,而FinalReleaseComObject释放所有引用,因此计数为0。

所以你的finally块看起来像:

 finally { GC.Collect(); GC.WaitForPendingFinalizers(); NAR(wSheet); if (wBook != null) wBook.Close(false, m_objOpt, m_objOpt); NAR(wBook); xl.Quit(); NAR(xl); } 

更新的NAR方法:

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

我前面已经研究过这个例子,在我发现的例子中,通常在closures应用程序之后,GC相关的调用已经结束了。 然而,有一个MVP(Mike Rosenblum)提到它应该被称为一开始。 我已经尝试了两种方式,他们已经工作。 我也试过没有WaitForPendingFinalizers,它的工作虽然不应该伤害任何东西。 因人而异。

这里是我提到的MVP的相关链接(他们在VB中,但没有什么不同):

正如其他人已经覆盖InterOp我会build议,如果你处理与XLSX扩展名的Excel文件,你应该使用EPPlus ,这将使你的Excel噩梦消失。

我刚刚在这里回答了这个问题:

由主窗口hWnd杀死excel进程

自从发布了4年以上,但我遇到了同样的问题,并能够解决这个问题。 显然只是访问Cells数组创build一个COM对象。 所以,如果你这样做:

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

然后其余的清理它应该解决这个问题。

我最终做了什么来解决类似的问题是得到进程ID和杀死作为最后的手段…

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

这里是我的hasn't失败的内容,但最后阻止清理Excel自动化。 我的申请离开Excel,所以没有退出电话。 评论中的参考是我的来源。

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

你有没有考虑过使用纯.NET的解决scheme,如SpreadsheetGear for .NET ? 这里是你的代码可能喜欢使用SpreadsheetGear:

 // open the template using (IWorkbookSet workbookSet = SpreadsheetGear.Factory.GetWorkbookSet()) { IWorkbook wBook = workbookSet.Workbooks.Open(excelTemplatePath + _report.ExcelTemplate); IWorksheet wSheet = wBook.ActiveWorksheet; int iRowCount = 2; // enumerate and drop the values straight into the Excel file while (data.Read()) { wSheet.Cells[iRowCount, 1].Value = data["fullname"].ToString(); wSheet.Cells[iRowCount, 2].Value = data["brand"].ToString(); wSheet.Cells[iRowCount, 3].Value = data["agency"].ToString(); wSheet.Cells[iRowCount, 4].Value = data["advertiser"].ToString(); wSheet.Cells[iRowCount, 5].Value = data["product"].ToString(); wSheet.Cells[iRowCount, 6].Value = data["comment"].ToString(); wSheet.Cells[iRowCount, 7].Value = data["brief"].ToString(); wSheet.Cells[iRowCount, 8].Value = data["responseDate"].ToString(); wSheet.Cells[iRowCount, 9].Value = data["share"].ToString(); wSheet.Cells[iRowCount, 10].Value = data["status"].ToString(); wSheet.Cells[iRowCount, 11].Value = data["startDate"].ToString(); wSheet.Cells[iRowCount, 12].Value = data["value"].ToString(); iRowCount++; } DirectoryInfo saveTo = Directory.CreateDirectory(excelTemplatePath + _report.FolderGuid.ToString() + "\\"); _report.ReportLocation = saveTo.FullName + _report.ExcelTemplate; wBook.SaveAs(_report.ReportLocation, FileFormat.OpenXMLWorkbook); } 

如果您的行数超过了几行,您可能会惊讶于它的运行速度。 而且你将永远不必担心Excel的挂起实例。

您可以在这里下载免费试用版并自己尝试。

免责声明:我自己的SpreadsheetGear LLC

粘贴代码最简单的方法是通过一个问题 – 这并不意味着我已经回答了我自己的问题(不幸的是)。 对那些试图帮助我的人道歉 – 直到现在我还没有回到这个地步。 它仍然有我难住…我已经完全隔离到一个函数的Excel代码

 private bool GenerateDailyProposalsReport(ScheduledReport report) { // start of test Excel.Application xl = null; Excel._Workbook wBook = null; Excel._Worksheet wSheet = null; Excel.Range xlrange = null; object m_objOpt = System.Reflection.Missing.Value; xl = new Excel.Application(); wBook = (Excel._Workbook)xl.Workbooks.Open(@"E:\Development\Romain\APN\SalesLinkReportManager\ExcelTemplates\DailyProposalReport.xls", 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; xlrange = wSheet.Cells[2, 1] as Excel.Range; // PROBLEM LINE ************ xlrange.Value2 = "fullname"; //************************** wBook.Close(true, @"c:\temp\DailyProposalReport.xls", m_objOpt); xl.Quit(); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); GC.WaitForPendingFinalizers(); Marshal.FinalReleaseComObject(xlrange); Marshal.FinalReleaseComObject(wSheet); Marshal.FinalReleaseComObject(wBook); Marshal.FinalReleaseComObject(xl); xlrange = null; wSheet = null; wBook = null; xl = null; // end of test return true; } 

If I comment out the PROBLEM LINE above, the instance of Excel is released from memory. As it stands, it does not. I'd appreciate any further help on this as time is fleeting and a deadline looms (don't they all).

Please ask if you need more information. Thanks in anticipation.

Addendum

A bit more information that may or may not shed more light on this. I have resorted to killing the process (stopgap measure) after a certain time lapse (5-10 seconds to give Excel time to finish it's processes). I have two reports scheduled – the first report is created and saved to disk and the Excel process is killed, then emailed. The second is created, saved to disk, the process is killed but suddenly there is an error when attempting the email. The error is: The process cannot access the file'….' etc

So even when the Excel app has been killed, the actual Excel file is still being held by the windows service. I have to kill the service to delete the file…

I'm afraid I am running out of ideas here Sean. 🙁

Gary could have some thoughts, but although his wrapper approach is very solid, it won't actually help you in this case because you are already doing everything pretty much correctly.

I'll list a few thoughts here. I don't see how any of them will actually work because your mystery line

 xlrange.Value2 = "fullname"; 

would not seem to be impacted by any of these ideas, but here goes:

(1) Don't make use of the _Workbook and _Worksheet interfaces. Use Workbook and Worksheet instead. (For more on this see: Excel interop: _Worksheet or Worksheet? .)

(2) Any time you have two dots (".") on the same line when accessing an Excel object, break it up into two lines, assigning each object to a named variable. Then, within the cleanup section of your code, explicitly release each variable using Marshal.FinalReleaseComObject().

For example, your code here:

  wBook = (Excel._Workbook)xl.Workbooks.Open(@"E:\Development\Romain\APN\SalesLinkReportManager\ExcelTemplates\DailyProposalReport.xls", 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); 

could be broken up to:

 Excel.Workbooks wBooks = xl.Workbooks; wBook = wBooks.Open("@"E:\Development\...\DailyProposalReport.xls", etc...); 

And then later, within the cleanup section, you would have:

 Marshal.FinalReleaseComObject(xlrange); Marshal.FinalReleaseComObject(wSheet); Marshal.FinalReleaseComObject(wBook); Marshal.FinalReleaseComObject(wBooks); // <-- Added Marshal.FinalReleaseComObject(xl); 

(3) I am not sure what is going on with your Process.Kill approach. If you call wBook.Close() and then xl.Quit() before calling Process.Kill(), you should have no troubles. Workbook.Close() does not return execution to you until the workbook is closed, and Excel.Quit() will not return execution until Excel has finished shutting down (although it might still be hanging).

After calling Process.Kill(), you can check the Process.HasExited property in a loop, or, better, call the Process.WaitForExit() method which will pause until it has exited for you. I would guess that this will generally take well under a second to occur. It is better to wait less time and be certain than to wait 5 – 10 seconds and only be guessing.

(4) You should try these cleanup ideas that I've listed above, but I am starting to suspect that you might have an issue with other processes that might be working with Excel, such as an add-in or anti-virus program. These add-ins can cause Excel to hang if they are not done correctly. If this occurs, it can be very difficult or impossible to get Excel to release. You would need to figure out the offending program and then disable it. Another possibility is that operating as a Windows Service somehow is an issue. I don't see why it would be, but I do not have experience automating Excel via a Windows Service, so I can't say. If your problems are related to this, then using Process.Kill will likely be your only resort here.

This is all I can think of off-hand, Sean. I hope this helps. Let us know how it goes…

— Mike

Sean,

I'm going to re-post your code again with my changes (below). I've avoided changing your code too much, so I haven't added any exception handling, etc. This code is not robust.

 private bool GenerateDailyProposalsReport(ScheduledReport report) { Excel.Application xl = null; Excel.Workbooks wBooks = null; Excel.Workbook wBook = null; Excel.Worksheet wSheet = null; Excel.Range xlrange = null; Excel.Range xlcell = null; xl = new Excel.Application(); wBooks = xl.Workbooks; wBook = wBooks.Open(@"DailyProposalReport.xls", false, false, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing, Type.Missing); wSheet = wBook.ActiveSheet; xlrange = wSheet.Cells; xlcell = xlrange[2, 1] as Excel.Range; xlcell.Value2 = "fullname"; Marshal.ReleaseComObject(xlcell); Marshal.ReleaseComObject(xlrange); Marshal.ReleaseComObject(wSheet); wBook.Close(true, @"c:\temp\DailyProposalReport.xls", Type.Missing); Marshal.ReleaseComObject(wBook); Marshal.ReleaseComObject(wBooks); xl.Quit(); Marshal.ReleaseComObject(xl); return true; } 

Points to note:

  1. The Workbooks method of the Application class creates a Workbooks object which holds a reference to the corresponding COM object, so we need to ensure we subsequently release that reference, which is why I added the variable wBooks and the corresponding call to ReleaseComObject .

  2. Similarly, the Cells method of the Worksheet object returns a Range object with another COM reference, so we need to clean that up too. Hence the need for 2 separate Range variables.

  3. I've released the COM references (using ReleaseComObject ) as soon as they're no longer needed, which I think is good practice even if it isn't strictly necessary. Also, (and this may be superstition) I've released all the objects owned by the workbook before closing the workbook, and released the workbook before closing Excel.

  4. I'm not calling GC.Collect etc. because it shouldn't be necessary. Really!

  5. I'm using ReleaseComObject rather than FinalReleaseComObject, because it should be perfectly sufficient.

  6. I'm not null-ing the variables after use; once again, it's not doing anything worthwhile.

  7. Not relevant here, but I'm using Type.Missing instead of System.Reflection.Missing.Value for convenience. Roll on C#v4 where optional parameters will be supported by the compiler!

I've not been able to compile or run this code, but I'm pretty confident it'll work. Good luck!

There's no need to use the excel com objects from C#. You can use OleDb to modify the sheets.

http://www.codeproject.com/KB/office/excel_using_oledb.aspx

I was having a similar problem. I removed _worksheet and _workbook and all was well.