c#和excel自动化 - 结束正在运行的实例

Sea*_*ean 13 c# excel automation

我正在通过C#尝试Excel自动化.我已经按照微软的所有指示来解决这个问题,但是我仍然在努力放弃Excel的最终引用,以便关闭它并使GC能够收集它.

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

Sheet.Cells[iRowCount, 1] = data["fullname"].ToString();
Run Code Online (Sandbox Code Playgroud)

然后文件保存并退出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;
    }
}
Run Code Online (Sandbox Code Playgroud)

更新

无论我尝试什么,"干净"方法或"丑陋"方法(见下面的答案),excel实例仍然会在此行被击中时挂起:

wSheet.Cells[iRowCount, 1] = data["fullname"].ToString();
Run Code Online (Sandbox Code Playgroud)

如果我评论该行(以及其下面的其他类似的那些,显然)Excel应用程序优雅地退出.只要每行上面一行被取消注释,Excel就会徘徊.

我想我必须在分配xl变量之前检查是否存在正在运行的实例,而是将其挂钩.我忘了提到这是一个Windows服务,但这应该不重要,不是吗?


Gar*_*ill 12

更新(2016年11月)

我刚刚读到Hans Passant的一个令人信服的论点,即使用GC.Collect它实际上是正确的方法.我不再使用Office(谢天谢地),但如果我这样做,我可能想再试一次 - 它肯定会简化很多(数千行)我编写的代码,试图做"正确" "方式(就像我之前看到的那样).

我会给后人留下原来的答案......


正如迈克在答案中所说,有一种简单的方法和一种难以解决的问题.迈克建议使用简单的方法,因为......它更容易.我个人认为这不是一个足够好的理由,我不相信这是正确的方式.对我来说,它有点"关掉它".

我有几年在.NET中开发Office自动化应用程序的经验,这些COM互操作问题在我第一次遇到这个问题的前几周和几个月困扰着我,尤其是因为微软非常腼腆承认存在问题.第一个地方,当时很难在网上找到好的建议.

我有一种工作方式,我现在几乎没有考虑过这种方式,而且自从我遇到问题以来已经有好几年了.对你可能正在创造的所有隐藏物体保持活力仍然很重要 - 是的,如果你错过了一个,你可能会有一个泄漏,只有在很晚才会变得明显.但它不逊于用过的东西要在坏日子malloc/ free.

我确实认为,随着时间的推移,而不是最后的清理,可以说清理.如果你只是启动Excel来填充几个单元格,那么也许没关系 - 但如果你要做一些繁重的工作,那么这是另一回事.

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

至关重要的是,它会被处理/释放,即使我的函数提前返回,或者有异常等.此外,它只会被处置/释放,如果它实际上是在第一时间创建的 - 请叫我一个学究但是建议试图释放可能实际上没有创建的对象的代码看起来像草率代码.我对使用FinalReleaseComObject有类似的异议 - 您应该知道导致创建COM引用的次数,因此应该能够释放相同的次数.

我的代码的典型代码段可能看起来像这样(或者,如果我使用的是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();
  }
}
Run Code Online (Sandbox Code Playgroud)

现在,我不打算假装这不是罗嗦,如果你不把东西分成更小的方法,那么由对象创建引起的缩进可能会失控.这个例子是最坏的情况,因为我们所做的就是创建对象.通常情况下,大括号之间会有更多的内容,而开销则要小得多.

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

ComWrapper<T>这里使用的课程希望很少解释.它只是存储对包装的COM对象的引用,并ReleaseComObject在其Dispose方法中显式(使用)释放它.该ComObject方法只返回对包装的COM对象的类型化引用.

希望这可以帮助!

编辑:我现在只是跟着迈克回答另一个问题的链接,我看到那个问题的另一个答案有一个包装类的链接,就像我上面提到的那样.

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

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

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

object saveChanges = Excel.XlSaveAction.xlSaveChanges;

workbook.ComObject.Close(saveChanges, Type.Missing, Type.Missing);
Run Code Online (Sandbox Code Playgroud)

...并且保存更改,只需更改xlSaveChangesxlDoNotSaveChanges.


Mik*_*lum 6

发生的事情是你打电话给:

Sheet.Cells[iRowCount, 1] = data["fullname"].ToString();
Run Code Online (Sandbox Code Playgroud)

基本上与以下相同:

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

通过这种方式,您可以看到您正在创建一个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...
}
Run Code Online (Sandbox Code Playgroud)

在上面,我们通过调用释放每个范围对象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);
Run Code Online (Sandbox Code Playgroud)

简而言之,您现有的代码非常接近.如果您只是在'NAR'调用之前添加对GC.Collect()和GC.WaitForPendingFinalizers()的调用,我认为它应该适合您.(简而言之,Jamie的代码和Ahmad的代码都是正确的.Jamie的代码更清晰,但Ahmad的代码对你来说更容易"快速修复",因为你只需要调用对GC.Collect()和GC.WaitForPendingFinalizers的调用. ()到你现有的代码.)

Jamie和Amhad还列出了我参与的.NET自动化论坛的链接(感谢大家!)以下是我在StackOverflow上发表的一些相关帖子:

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

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

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

麦克风


Ahm*_*eed 2

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

GC.Collect(); 
GC.WaitForPendingFinalizers(); 
Run Code Online (Sandbox Code Playgroud)

您还可以在 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);
}
Run Code Online (Sandbox Code Playgroud)

更新的 NAR 方法:

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

我不久前研究过这个问题,在示例中我发现通常与 GC 相关的调用是在关闭应用程序后结束的。然而,有一位 MVP(Mike Rosenblum)提到应该在一开始就调用它。我已经尝试过这两种方法并且它们都有效。我也在没有 WaitForPendingFinalizers 的情况下尝试过,它确实有效,但不会造成任何伤害。YMMV。

以下是我提到的 MVP 的相关链接(它们是用 VB 编写的,但并没有那么不同):