从.NET传递到VBA的COM对象的对象生命周期

我的组织有时需要使用Excel来生成一堆格式化的语句(在文档中指出“您的帐户余额为$ X”),将其打印为PDF,然后将它们合并为一个大的PDF。 通常使用的方法涉及由索引单元驱动的单张纸以及另一张纸上的人/数据列表。 VBAmacros将索引单元格从1迭代到N,然后使用Adobe Distiller API每次打印格式化的表格并合并结果。

出于各种原因,我希望在C#中实现我们的VSTO Excel加载macros中大部分的这个macros的逻辑,这样进程的VBA端就会减less到几行。

我决定公开一个大致如下所示的API:

AcroPDDoc PdfBegin(Worksheet worksheet, string filename); void PdfAddPage(AcroPDDoc pdf, Worksheet worksheet); void PdfComplete(AcroPDDoc pdf); 

你的想法是写VBA的forms:

 Sub PrintToPdf() Dim obj As IMySharedObject Set obj = Application.COMAddIns("MyAddIn").Object Dim pdf As Acrobat.AcroPDDoc Dim i As Long For i = 1 To 10 Range("counter").Value = i If i = 1 Then Set pdf = obj.PdfBegin(Sheets("Statement"), "C:\myFile.pdf") Else PdfAddPage pdf, Sheets("Statement") End If Next i PdfComplete pdf End Sub 

我很好奇/担心AcroPDDoc对象的生命周期,以及有关打开文件句柄,Acrobat.exe进程等,如果macros命中错误或中止执行。 不要担心,因为“closuresExcel并重新打开”是必要的可接受的解决scheme。 我用C#编写了下面的代码:

 internal static class Printing { private static WeakReference weakref; public static AcroPDDoc PdfBegin(Worksheet worksheet, string filename) { SetAdobeOutputFile(filename); worksheet.PrintOut(ActivePrinter: "Adobe PDF"); AcroPDDoc pdf = new AcroPDDoc(); pdf.Open(filename); weakref = new WeakReference(pdf); return pdf; } public static void GC() { System.GC.Collect(); } public static void test(AcroPDDoc pdf) { if (weakref != null) { System.Diagnostics.Debug.WriteLine("IsAlive pre: " + weakref.IsAlive); if (weakref.IsAlive) System.Diagnostics.Debug.WriteLine("ReferenceEquals: " + Object.ReferenceEquals(pdf, weakref.Target)); } GC.Collect(); if (weakref != null) System.Diagnostics.Debug.WriteLine("IsAlive post: " + weakref.IsAlive); } } 

我已经消除了一堆额外的Debug.WriteLine和一些其他无关的代码。 我用下面的VBAtesting了它:

 Sub foo() Dim obj As IUDFSharedObject Set obj = Application.COMAddIns("MyAddIn").Object Dim pdf As Acrobat.AcroPDDoc Set pdf = obj.PdfBegin(Sheets("Statement"), "C:\myFile.pdf") 'obj.GC 'obj.test pdf End Sub 

我发现的一般情况是.NET不包括VBA-land在垃圾收集引用计数中的引用。

例如,如果我只取消注释obj.GCobj.test pdf ,我被告知weakref不存在。

但是,如果我只取消obj.test pdf注释,那么weakref在之前和之后都是有效的(我发出“ReferenceEquals:true”)。

注意pdf整个时间在VBA范围 。 我最初testing,看看会发生什么事情,如果让pdf逃脱VBA范围,但事实certificate并不重要。

对于我来说这是一个比资源链接更大的问题。 是否有任何解决scheme没有永久存储在List生成的每个AcroPDDoc对象,以保持引用计数大于零?

感谢上面的@yms,我已经知道发生了什么,并提出了一个我很满意的解决scheme。 首先,对API的轻微修改:

 void PdfBegin(AcroPDDoc pdf, Worksheet worksheet, string filename); void PdfAddPage(AcroPDDoc pdf, Worksheet worksheet); void PdfComplete(AcroPDDoc pdf); 

每个C#方法在返回之前都会调用Mashal.ReleaseComObject(pdf) 。 我没有阅读Marshal.ReleaseComObject被认为是危险的 ,但是我已经testing了他所调用的特定故障模式,并且发现它在实践中并不会出现。

VBA现在必须从一开始就提供AcroPDDoc对象。 因此,典型的用法如下所示:

 Sub PrintToPdf() Dim obj As IMySharedObject Set obj = Application.COMAddIns("MyAddIn").Object Dim pdf As New AcroPDDoc Dim i As Long For i = 1 To 10 Range("counter").Value = i If i = 1 Then obj.PdfBegin pdf, Sheets("Statement"), "C:\myFile.pdf" Else obj.PdfAddPage pdf, Sheets("Statement") End If Next i obj.PdfComplete pdf End Sub 

基本上只是声明现在是As New AcroPDDoc而不是As AcroPDDoc与以后的Set

testing表明,VBA一旦超出范围就将减lessAcroPDDoc的refcount,或者将参考设置为Nothing 。 这包括在子例程中引发错误并且用户结束执行的情况。

最后,Acrobat.exe进程还会在其引用计数达到零时立即自动closures,即使文件已打开。

请注意,您在.Net中对AcroPDDoc的引用实际上是对正在通过不同生态系统传递的进程外COM对象的封装.Net框架并不完全控制底层对象的生存期,引用计数由COM服务器控制,只要有一个COM对一个对象的引用,无论是从VBA还是从.Net,对象将保持活着。

我相信你会发现这个问题,它的答案是有趣的: RCW和引用计数,当在C#中使用COM互操作