我的组织有时需要使用Excel生成一堆格式化的语句(在文档中说明“您的帐户余额为$ X”),将它们打印为PDF,然后将它们组合成一个大的PDF。通常使用的方法涉及由索引单元驱动的单张纸和另一张纸上的人/数据列表。 VBA宏将索引单元格从1迭代到N,然后使用Adobe Distiller API每次打印格式化的表单并合并结果。
由于各种原因,我想在VSTO Excel加载项中实现C#中大部分宏的逻辑,这样过程的VBA端就会减少到几行。
我决定公开一个看起来大致如下的API:
AcroPDDoc PdfBegin(Worksheet worksheet, string filename);
void PdfAddPage(AcroPDDoc pdf, Worksheet worksheet);
void PdfComplete(AcroPDDoc pdf);
你想写下以下形式的VBA:
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进程等感到好奇/担心。不超级担心,因为“关闭Excel并重新打开它”是必要的可接受的解决方案。我在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
和其他一些无关的代码。我用以下VBA测试了它:
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.GC
和obj.test pdf
,我会被告知weakref
尚未生效。
但是,如果我仅取消注释obj.test pdf
,weakref
在之前和之后都有效(并且我发出“ReferenceEquals:true”)。
请注意,pdf
在VBA 的整个时间范围内。我最初测试过如果你让pdf
逃避VBA范围会发生什么,但结果并不重要。
对于我来说,这是一个比资源链接更大的问题。是否有任何解决方案不能永久存储AcroPDDoc
某处生成的List
对象,以使引用计数保持在零以上?
答案 0 :(得分:2)
感谢上面的@yms,我已经知道发生了什么,并提出了一个我非常满意的解决方案。首先,对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
considered dangerous,但我已经测试了他调出的特定故障模式,并发现它似乎没有在实践中发生。
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
。
测试显示,只要VBA超出范围或参考设置为Nothing
,VBA就会立即减少AcroPDDoc的引用计数。这包括在子例程中引发错误并且用户结束执行的情况。
最后,Acrobat.exe进程也会在refcount达到零时立即自动关闭,即使文件已打开。
答案 1 :(得分:1)
请注意,您在.Net中对AcroPDDoc的引用实际上是您正在通过不同生态系统的进程外COM对象的包装,.Net框架不能完全控制底层对象的生命周期,引用计数由COM服务器,只要存在对一个对象的COM引用,无论是来自VBA还是来自.Net,该对象都将保持活动状态。
我相信你会发现这个问题及其答案很有趣: RCW & reference counting when using COM interop in C#