编程语言
首页 > 编程语言> > c#-从.NET传递到VBA的COM对象的对象生命周期

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

作者:互联网

我的组织有时需要使用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.WriteLines和一些其他无关的代码.我使用以下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对象永久存储在某个地方以使引用计数保持在零以上之外,还有什么解决方案吗?

解决方法:

感谢上面的@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,而不是带有更高Set的As AcroPDDoc.

测试显示,一旦VBA超出范围或将引用设置为Nothing,VBA就会非常迅速地减少AcroPDDoc的引用计数.这包括在子例程中引发错误并且用户结束执行的情况.

最后,即使其refcount达到零,Acrobat.exe进程也会立即提示自己杀死自己,即使它已打开文件.

标签:pdf,c,excel,vba,vsto
来源: https://codeday.me/bug/20191121/2049775.html