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

时间:2014-10-09 22:29:28

标签: c# excel vba pdf vsto

我的组织有时需要使用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.GCobj.test pdf,我会被告知weakref尚未生效。

但是,如果我仅取消注释obj.test pdfweakref在之前和之后都有效(并且我发出“ReferenceEquals:true”)。

请注意,pdf在VBA 的整个时间范围内。我最初测试过如果你让pdf逃避VBA范围会发生什么,但结果并不重要。

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

2 个答案:

答案 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#