【问题标题】:c# and excel automation - ending the running instancec#和excel自动化——结束正在运行的实例
【发布时间】:2009-06-24 22:05:45
【问题描述】:

我正在尝试通过 C# 实现 Excel 自动化。我已按照 Microsoft 的所有说明执行此操作,但我仍在努力放弃对 Excel 的最终引用以关闭它并让 GC 收集它。

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

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

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

感谢任何帮助。谢谢。

这是我的代码的准系统:

        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 就会继续存在。

我想我必须在分配 xl 变量之前检查是否有一个正在运行的实例,然后改为挂钩。我忘了说这是一个 Windows 服务,但这不重要,不是吗?


【问题讨论】:

  • 在你完全正确之前,你不会让“丑陋”的方法起作用。没有折衷办法,因为只要有任何 COM 引用未发布,Excel 就会继续存在。请发布您的“丑陋”代码,以便我们发现错误 - 我可以保证会有一个。请注意,在发布的代码中,您在主块中将 wb 设置为 null,因此 防止 它在您的 finally 块中正确释放。但这只是一个问题(我们在下面讨论了 Workbooks 对象和 Range 对象)。
  • 我不得不提一下(尽管它与讨论不是特别相关)你的 NAR 方法可能没有按照你的想法做。行“o = null;”在 finally 块中将局部参数变量 o 设置为 null,但这对调用方法中的变量没有影响。所以在 NAR(xl) 之后,变量 xl 不会为空!话虽如此,无论如何都不需要将变量设置为 null - 除非您对此感到迷信。
  • 我刚刚注意到您提到它作为 Windows 服务运行。它不应该与手头的问题有任何关系,但让我给你一个提示。我的应用程序(或其中的一部分)运行 Windows 服务,并且在 Windows XP 或 Windows 2003 Server 下运行良好,但我在 Vista 上遇到了问题,并且完全无法让它在 Windows Server 2008 下运行。只是想我要提一下,如果您的开发机器是 XP,而您的目标平台是 Vista/2008...
  • 如果您仍然对此感到疑惑(我非常怀疑,希望不会),我遇到了this post by Hans Passant,其中 (a) 解释了为什么 GC.Collect 是这样做的正确方法,并且( b) 为什么它可能看起来在调试器中运行时工作。如果这些年前我们有汉斯来帮忙就好了:-)
  • 很好的发现——你是对的,我很久以前就不再想它了:) 我会读一下那个链接,谢谢。不断学习总是好的。

标签: c# excel automation


【解决方案1】:

更新(2016 年 11 月)

我刚刚阅读了 Hans Passant 的 convincing argument,其中提到使用 GC.Collect 实际上是正确的方法。我不再使用 Office(谢天谢地),但如果我这样做了,我可能想再试一次 - 它肯定会简化我写的很多(数千行)代码,试图做“正确的事情” " 方式(就像我当时看到的那样)。

我会把我原来的答案留给后代......


正如迈克在他的回答中所说,有一个简单的方法和一个艰难的方法来处理这个问题。 Mike 建议使用简单的方法,因为……它更容易。我个人不认为这是一个足够好的理由,我也不认为这是正确的方法。对我来说,它有点“关掉它然后再打开”的味道。

我有几年在 .NET 中开发 Office 自动化应用程序的经验,当我第一次遇到这个问题时,这些 COM 互操作问题一直困扰着我,尤其是因为微软很害羞地承认存在首先是一个问题,当时很难在网络上找到好的建议。

我有一种工作方式,我现在几乎不用考虑就使用它,而且我已经好几年没有遇到问题了。对您可能正在创建的所有隐藏对象保持活力仍然很重要 - 是的,如果您错过了一个,您可能会有一个泄漏,直到很久以后才会变得明显。但这并不比 malloc/free 过去的糟糕日子更糟糕。

我确实认为有一些事情可以说是在你走的时候清理自己,而不是在最后。如果您只是启动 Excel 来填充几个单元格,那可能没关系 - 但如果您要进行一些繁重的工作,那就另当别论了。

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

至关重要的是,即使我的函数提前返回或出现异常等情况,它也会被释放/释放。此外,只有如果它实际上是在第一名 - 称我为书呆子,但尝试释放实际上可能尚未创建的对象的建议代码在我看来就像草率的代码。我对使用 FinalReleaseComObject 也有类似的反对意见 - 您应该知道您导致创建 COM 引用的次数,因此应该能够以相同的次数释放它。

我的代码的典型 sn-p 可能如下所示(或者,如果我使用 C# v2 并且可以使用泛型 :-)):

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

现在,我不打算假装这不是罗嗦,如果你不把东西分成更小的方法,对象创建造成的缩进可能会失控。这个例子是最坏的情况,因为我们所做的只是创建对象。通常,大括号之间会发生更多事情,而开销要少得多。

请注意,根据上面的示例,我将始终在方法之间传递“包装”对象,而不是裸 COM 对象,并且调用者有责任处理它(通常使用 using 语句) .同样,我总是返回一个包裹的对象,而不是一个裸露的对象,再次释放它是调用者的责任。您可以使用不同的协议,但重要的是要有明确的规则,就像我们过去必须自己进行内存管理一样。

这里使用的ComWrapper&lt;T&gt; 类希望不需要解释。它只是存储对包装 COM 对象的引用,并在其 Dispose 方法中显式释放它(使用 ReleaseComObject)。 ComObject 方法只返回一个对包装 COM 对象的类型化引用。

希望这会有所帮助!

编辑:我现在才点击链接到Mike's answer to another question,我看到该问题的另一个答案有一个包装类的链接,就像我上面建议的那样。

另外,关于迈克对另一个问题的回答,我不得不说我几乎被“只使用GC.Collect”的论点所诱惑。然而,我主要是在一个错误的前提下被吸引到的。乍一看,根本不需要担心 COM 引用。但是,正如 Mike 所说,您仍然需要显式释放与所有范围内变量关联的 COM 对象 - 因此您所做的只是减少而不是消除对 COM 对象管理的需要。就个人而言,我宁愿全力以赴。

我还注意到许多答案倾向于编写代码,其中所有内容都在方法结束时发布,在ReleaseComObject 调用的大块中。如果一切都按计划进行,那就太好了,但我会敦促任何编写严肃代码的人考虑如果抛出异常,或者该方法有多个退出点会发生什么(代码不会被执行,因此 COM 对象不会被释放)。这就是为什么我赞成使用“包装器”和usings。这很罗嗦,但它确实是防弹代码。

EDIT2:我更新了上面的代码,以指示在保存或不保存更改的情况下应关闭工作簿的位置。这是保存更改的代码:

object saveChanges = Excel.XlSaveAction.xlSaveChanges;

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

...要保存更改,只需将 xlSaveChanges 更改为 xlDoNotSaveChanges

【讨论】:

  • 非常好的加里,我非常喜欢这个。作为一种折衷方案,可以只为 Excel.Application 实例使用一个包装器。然后在 Excel.Application 包装器的 Dispose 例程中,可以使用 Process.Kill 方法,该方法使用 Application.Hwnd 和 GetWindowThreadProcessId (stackoverflow.com/questions/51462/…) 获取进程 ID。这基本上是我为我的生产代码所做的,并且问题为零。它节省了您展示的大量嵌套,因此代码更自然地流动。
  • 我在使用 GC.Collect() 和 GC.WaitForPendingFinalizers() 方法时也从未遇到过问题,并且我在 xtremevbtalk.com/forumdisplay.php?f=105">XVBT .NET 上帮助了无数编码人员自动化论坛。不过,这可能是第一个...... :-(
  • Mike,虽然杀死这样的进程可能在 99.99% 的情况下都有效,但迟早会在它正在执行不应该半途而废的事情时被杀死。例如,如果它是 Word 而不是 Excel,它可能正在向 Normal.dot 写入更改......杀死一个进程也使它没有机会自行清理;我们在这个问题线程中有一个例子,在 Excel 被杀死后文件被保留为只读。
  • 确实,这就是我让他展示他的代码的原因。这绝对不应该发生,并且与“悬挂实例”没有任何关系。代码的最后清理部分需要检查'Workbook'是否为空,如果不是,关闭它,然后调用Marshall.FinalReleaseComObject(),然后调用Excel.Application.Quit(),再调用Process.Kill。这使您从 99.99% 提高到 100%。但我们需要看看他的代码。
  • 感谢两位的精彩讨论。我被包装器的想法(也有利于代码重用)所吸引,并将在周末尝试它。如果有机会,我会在今晚发布我的代码,尽管我怀疑周末在 StackOverflow 上会比较安静——或者更确切地说,它们应该是 :)
【解决方案2】:

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

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

本质上等同于:

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

通过这种方式,您可以看到您正在创建一个Excel.Range 对象,然后为其分配一个值。这种方式还为我们提供了对范围变量 cell 变量的命名引用,这允许我们在需要时直接释放它。因此,您可以通过以下两种方式之一清理对象:

(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 的代码更简洁,但 Ahmad 的代码对您来说更容易“快速修复”,因为您只需添加对 GC.Collect() 和 GC.WaitForPendingFinalizers 的调用() 到您现有的代码。)

Jamie 和 Amhad 还列出了我参与的 .NET Automation Forum 的链接(谢谢大家!)这是我在 StackOverflow 上发布的一些相关帖子:

(1)How to properly clean up Excel interop objects in C#

(2)C# Automate PowerPoint Excel -- PowerPoint does not quit

我希望这会有所帮助,肖恩...

迈克

【讨论】:

  • 太好了,谢谢 - 我尝试在 stackoverflow 上进行搜索。显然,我的搜索措辞不够好,无法找到您的帖子,迈克。感谢您花时间重新解释。我了解隐藏的引用以及为什么它没有被清理,但我在网络上找不到显示“丑陋”方式的样本。今晚我会尝试这一切。届时我将标记为已回答。 (我在爱尔兰,所以时差会造成严重破坏!)。感谢以上所有提供帮助的人。
  • 肖恩,祝你好运,让我们知道进展如何。我发布了“丑陋的方式”只是为了解释“引擎盖下”发生了什么。您可以将它用作实验来帮助您了解它是如何工作的,但我不会将它用于实际的生产代码。原因是在代码的某个地方很容易犯一个小错误,Excel 就会挂起。编译器和任何代码分析程序都无法为您找到此错误。这种方法还会通过对 Marshal.FinalReleaseComObject() 的大量调用使您的代码膨胀,从而分散代码的意图。 (续...)
  • ... 因此,实际上,我相信要走的路是在代码的最后清理部分调用 GC.Collect() 和 GC.WaitForPendingFinaliers,然后是在每个引用上使用 Marshal.FinalReleaseComObject() 逐一释放所有命名变量引用。
  • 嗨,迈克,很遗憾,我无法让它工作。我已经对我的原始帖子添加了更新,但如果您从未见过,我现在正在考虑仅在没有运行实例可挂接的情况下创建一个新的 Excel 实例。这将作为 Windows 服务运行,并且 Excel 将安装在服务器上纯粹用于此应用程序,因此不应对其他 Excel 用户产生不利影响(因为不会有任何影响)。不过感谢您的指点 - 这让我很难过。
  • 此应用程序是否旨在支持网站等服务器端?如果是这样,这可能是一个很大的问题。 (请参阅:support.microsoft.com/default.aspx?scid=kb;EN-US;257757.)您可以使用 VSTO 进行服务器端自动化,或者考虑为此使用第 3 方程序。 Excel 本身并不适合这种情况。但你可能会没事——我不知道。至于挂起的问题,我会发布您更新的代码,希望我们会看到改进。如果没有,您可以求助于 Process.Kill(请参阅:xtremevbtalk.com/showthread.php?p=1326018#post1326018)。
【解决方案3】:

在调用 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;
    }
}

我前段时间对此进行了研究,在示例中我发现与 GC 相关的调用通常在关闭应用程序后结束。然而,有一个 MVP (Mike Rosenblum) 提到它应该在一开始就被调用。我已经尝试了两种方法,并且它们都有效。我也在没有 WaitForPendingFinalizers 的情况下尝试过它,虽然它不应该伤害任何东西,但它仍然有效。 YMMV。

以下是我提到的 MVP 的相关链接(它们在 VB 中,但没什么不同):

【讨论】:

  • 没有必要以这种方式使用垃圾收集器。我有一个用 .NET 编写的极其复杂的办公自动化系统,我承认在一种情况下我确实不得不这样做,但是对于任何中小型项目,您应该能够清理 COM 引用,所以这是没有必要的。请注意,如果您确实使用此方法,则需要执行两次收集/等待,因为第一次迭代只是将内容标记为可收集。
  • 嗨,加里,我同意你没有必须这样做,但实际上我发现很容易错过一个(或多个)显式 Marshal.FinalReleaseCom() 对象调用,然后您将挂起。在大中型项目中发现错误几乎是不可能的。 (编译器和代码分析工具都不会帮助你。)所以我发现使用“最终清理”方法要安全得多。
  • Gary,你不必调用 GC.Collect() 和 GC.WaitForPendingFinalizers() 两次。发生的情况是,当您调用 GC.Collect() 时,RCW 的终结器都会被调用,这会减少 COM 引用计数并释放栅栏的 COM 侧的引用(这就是您在这里关心的全部内容)。然而,RCW 本身在 NEXT 垃圾收集之前将保持未被收集。但我们不在乎——RCW 已经放弃了它的有效载荷。 (但是,如果使用 VSTO,则必须调用 Collect/Wait 两次,因为 VSTO 使用了额外的终结器层。)
  • Mike,有趣的是 GC x 2 在 VSTO 之外不需要,谢谢。
  • 是的,我从来没有在任何地方看到过这样的说法,但是如果你了解终结器是如何工作的,以及 RCW 如何在终结器中调用 Marshal.ReleaseComObject(),那么 RCW 被提升的事实并不重要...所以我一直想知道为什么所有代码​​示例都显示它被称为 2x。答案是 VSTO 需要它。这是我目前能找到的唯一来源:social.msdn.microsoft.com/forums/en-US/vsto/thread/…
【解决方案4】:

由于其他人已经介绍了 InterOp,我建议如果您处理带有 XLSX 扩展名的 Excel 文件,您应该使用EPPlus,这将使您的 Excel 噩梦消失。

【讨论】:

    【解决方案5】:

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

    Killing excel process by its main window hWnd

    【讨论】:

      【解决方案6】:

      自发布以来已超过 4 年,但我遇到了同样的问题并且能够解决它。显然,仅访问 Cells 数组就会创建一个 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);
      

      然后其余的清理工作应该可以解决问题。

      【讨论】:

        【解决方案7】:

        我最终解决类似问题的方法是获取进程 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();
        

        【讨论】:

        • 嗨,安迪,我尝试了你的方法。但是,即使 Excel 实例被终止,我仍然留下一个 Excel 文件,该文件认为它正在被另一个应用程序访问。当我打开文件时,系统会提示我输入通知/只读消息...
        • 天哪,不要那样做(杀死进程)。清理您的 COM 引用是一件痛苦的事,但非常可行。终止这个过程就像射杀你不爱的女朋友以避免与她分手的麻烦。
        • Gary - 也许可以,但在我的情况下根本不可靠。在执行一个小时左右后,所有其他方法都让我有进程运行(其中 20-30 个)。
        【解决方案8】:

        这是我的尚未失败的内容,用于清理 Excel 自动化。我的应用程序使 Excel 保持打开状态,因此没有退出呼叫。 reference in the comment 是我的来源。

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

        【讨论】:

          【解决方案9】:

          您是否考虑过使用纯 .NET 解决方案,例如 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 实例会挂起。

          您可以下载免费试用版here 并亲自试用。

          免责声明:我拥有 SpreadsheetGear LLC

          【讨论】:

          • 谢谢乔 - 如果我有更多的 Excel 工作和/或更高收入的客户,我可能会花时间评估它。不过,在这个时间点,这是一个小项目,而我所采取的路线似乎是最可行的。不过我会记住的。
          【解决方案10】:

          粘贴代码的最简单方法是通过一个问题 - 这并不意味着我已经回答了我自己的问题(很遗憾)。 向那些试图帮助我的人道歉——直到现在我才能回到这个问题上。它仍然让我难过... 我已将 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;
          
          }
          

          如果我注释掉上面的问题行,Excel 的实例就会从内存中释放。就目前而言,事实并非如此。 我会感谢任何进一步的帮助,因为时间转瞬即逝,最后期限迫在眉睫(不是全部)。

          如果您需要更多信息,请询问。 感谢期待。

          附录

          更多信息可能会或可能不会更清楚地说明这一点。在一段时间后(5-10 秒让 Excel 有时间完成它的进程)后,我采取了终止进程(权宜之计)。我安排了两个报告——第一个报告被创建并保存到磁盘,Excel 进程被终止,然后通过电子邮件发送。第二个被创建,保存到磁盘,进程被终止,但在尝试发送电子邮件时突然出现错误。错误是: 进程无法访问文件'....'等

          因此,即使 Excel 应用程序已被终止,实际的 Excel 文件仍由 Windows 服务持有。我必须终止服务才能删除文件...

          【讨论】:

            【解决方案11】:

            肖恩恐怕我的想法已经用完了。 :-(

            Gary 可能有一些想法,但尽管他的包装方法非常可靠,但在这种情况下它实际上对你没有帮助,因为你已经在做所有事情了。

            我将在这里列出一些想法。我看不出它们中的任何一个实际上会如何工作,因为你的神秘线

            xlrange.Value2 = "fullname";
            

            似乎不会受到任何这些想法的影响,但这里是:

            (1) 不要使用 _Workbook 和 _Worksheet 接口。请改用工作簿和工作表。 (有关更多信息,请参阅:Excel interop: _Worksheet or Worksheet?。)

            (2) 访问 Excel 对象时,如果同一行上有两个点(“.”),请将其分成两行,将每个对象分配给一个命名变量。然后,在代码的清理部分中,使用 Marshal.FinalReleaseComObject() 显式释放每个变量。

            例如,您的代码:

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

            可以分解为:

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

            然后,在清理部分中,您将:

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

            (3) 我不确定您的 Process.Kill 方法发生了什么。如果在调用 Process.Kill() 之前调用 wBook.Close() 和 xl.Quit(),则应该没有问题。 Workbook.Close() 在工作簿关闭之前不会向您返回执行,并且 Excel.Quit() 在 Excel 完成关闭之前不会返回执行(尽管它可能仍处于挂起状态)。

            在调用 Process.Kill() 之后,您可以在循环中检查 Process.HasExited 属性,或者更好的是调用 Process.WaitForExit() 方法,该方法将暂停,直到它为您退出。我猜这通常需要不到一秒钟的时间才能发生。等待的时间少一点,确定性好,而不是等待 5 到 10 秒,然后只是猜测。

            (4) 您应该尝试我上面列出的这些清理方法,但我开始怀疑您可能对可能与 Excel 一起使用的其他进程有问题,例如加载项或反病毒程序。如果未正确完成这些加载项,可能会导致 Excel 挂起。如果发生这种情况,要发布 Excel 可能会非常困难或不可能。您需要找出有问题的程序,然后将其禁用。另一种可能性是,以某种方式作为 Windows 服务运行是一个问题。我不明白为什么会这样,但我没有通过 Windows 服务自动化 Excel 的经验,所以我不能说。如果您的问题与此相关,那么使用 Process.Kill 可能是您唯一的选择。

            这就是我能想到的,肖恩。我希望这有帮助。让我们知道进展如何...

            -- 迈克

            【讨论】:

            • 再次感谢迈克,感谢您提供全面的回答和帮助。关于上面的 1) 和 2) - 在这个阶段,我已经尝试了所有这些和许多其他排列来试图解决这个问题!我什至产生了一个新线程来处理 Excel 工作,以防那里存在一些潜在的问题,但没有乐趣。令我感到奇怪的是,实际的 windowsService.exe 保留了文件的句柄,直到我退出服务本身才允许我删除它。我现在可能会求助于 Excel 的 xml 来完成这项工作。不过,我会先尝试上面的建议。再次感谢。
            • 在上述 3) 的基础上,调试代码时 wBook.Close 和 xl.Quit 会快速返回且不会出错。之后 process.kill 被调用,但文件仍处于打开状态。我可以打开该文件,但系统提示该文件已打开 - 即使在杀死孤立的 Excel.exe 之后,我也必须选择只读或通知。比小说更奇怪——也许我需要重写,以防它是显而易见的,而我只是看得太久了。叹息。
            • 呃...很抱歉听到这一切,肖恩。 应该这么难。但它确实发生了。 :-(
            【解决方案12】:

            肖恩,

            我将再次发布您的代码并进行更改(如下)。我已经避免过多地更改您的代码,所以我没有添加任何异常处理等。这段代码健壮。

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

            注意事项:

            1. Workbooks 方法 Application 类创建一个 Workbooks 持有一个 参考对应的COM 对象,所以我们需要确保我们 随后发布该引用, 这就是我添加变量的原因 wBooks 以及对 ReleaseComObject 的相应调用。

            2. 类似地,Cells 方法的 Worksheet 对象返回 Range 具有另一个 COM 引用的对象, 所以我们也需要清理它。 因此需要 2 个单独的 Range 变量。

            3. 我已经发布了 COM 引用 (使用ReleaseComObject)尽快 因为它们不再需要,我 认为是好的做法,即使它 不是绝对必要的。此外,(和 这可能是迷信)我已经 释放所有拥有的对象 关闭之前的工作簿 工作簿,并发布工作簿 在关闭 Excel 之前。

            4. 我没有打电话给GC.Collect 等等。 因为它不应该是必要的。 真的!

            5. 我使用的是 ReleaseComObject 而不是 比 FinalReleaseComObject,因为它应该是完全足够的。

            6. 之后我不会将变量归零 采用;再一次,它没有做 任何有价值的东西。

            7. 不相关 在这里,但我使用的是Type.Missing 代替 System.Reflection.Missing.Value 为 方便。在 C#v4 上滚动 可选参数将是 编译器支持!

            我无法编译或运行这段代码,但我很有信心它会工作。 祝你好运!

            【讨论】:

            • 嗨,Gary - 谢谢你,代码很好,只是缺少一个演员表。无论如何,代码运行但仍然没有自行清理 Excel。我已经失去了继续战斗的意愿,我将不得不求助于 Open Xml 来创建和保存 xls 文件。正如 MS 所说:微软不推荐也不支持 Office 的服务器端自动化。现在我明白为什么了!它必须与通过 Windows 服务运行有关。感谢所有人的帮助——尤其是迈克和加里。很高兴知道有人愿意花时间帮助开发人员。
            • Gary 的积分列表不错,“Cells”方法也不错,这可能是票...奇怪的是 Seans 代码没有挂在“xlrange = wSheet”行上。 Cells[2, 1] as Excel.Range',但是从添加行'xlrange.Value2 = "fullname"',这没有任何意义。
            • 肖恩,我猜这可能是由于通过 Windows 服务工作,我不明白为什么会这样,但我没有这方面的经验,所以我不知道。它也可能是与 Excel 或防病毒软件等一起运行的加载项,导致 Excel 挂起——我自己也看到过。不管它是什么,这很奇怪......对不起,它对你来说是这样的。 :-(
            • Sean,我的应用程序作为 Windows 服务运行没有任何问题,所以虽然这肯定是一个额外的复杂性,但我不会认为这是一个阻碍。很抱歉,我们无法让这个工作。我准备再坚持一段时间,但我很理解你现在已经受够了。
            • 感谢您测试 Gary。在这种情况下,您已经夺走了我必须坚持的最后一根稻草:) 我已经开始使用 XML,因为它只是我需要创建的几个具有最小格式的报告。如果我以后有机会重新调查这个问题,我会的。再次感谢您的帮助。顺便说一句,我在 Windows XP 上使用 2007 PIA - 我认为你们也在 2007 上没有问题...
            【解决方案13】:

            无需使用 C# 中的 excel com 对象。您可以使用 OleDb 来修改工作表。

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

            【讨论】:

              【解决方案14】:

              我遇到了类似的问题。 我删除了_worksheet_workbook,一切都很好。

              【讨论】:

              • 删除这些是什么意思?
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2010-11-20
              • 2022-08-03
              • 2023-03-05
              • 1970-01-01
              相关资源
              最近更新 更多