识别对互操作库中对象的唯一引用(Doument.Paragraphs 等)

Ama*_*Ama 2 c# vb.net vsto ms-word office-interop

我希望能够识别两个互操作变量对象何时引用同一个实际”对象。我所说的“实际”是指Microsoft Word文档中的给定段落或脚注。

中的示例:(注意答案也可以,问题与语言无关)

Imports Microsoft.Office.Interop

Sub Tests()

    Dim WordApp as Word.Application = Globals.ThisAddIn.Application         
    Dim ThisDoc as Word.Document = WordApp.ActiveDocument
    Dim ThisSelection As Word.Selection = ThisDoc .Application.Selection
    If ThisSelection.Range Is Nothing Then Exit Sub

    Dim SelectedPara As Word.Paragraph = ThisSelection.Range.Paragraphs.First


    For Each MyPara As Word.Paragraph In ThisDoc.Paragraphs

        'Reference equality: Never finds a match
        If MyPara.Equals(SelectedPara) Then MsgBox("Paragraph Found by ref") 

        'Property equality: Seems to works ok with .ParaID
        If MyPara.ParaID = SelectedPara.ParaID Then MsgBox("Paragraph Found by Id")

    Next

End Sub
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,通过引用比较对象变量是行不通的。.ParaID虽然这有点令人沮丧,但如果文档没有说明以下内容,我可以在该属性上运行比较器:

保留供内部使用。

欢迎对 (1) 如何避免使用.ParaID以及 (2).ParaID作为唯一标识符使用的可靠性提出任何评论(也欢迎有关此属性的任何信息,因为 Microsoft 和 Google 在该主题上保持沉默)

这个问题也可以推广到其他集合,例如Word.Footnotes, Word.Bookmarks。我想同样的情况也会发生在Excel.Worksheets等身上。

小智 6


\n

我的第二个答案 - 好的,所以我走在正确的轨道上,但是由于 .NET 的运行时可调用包装器 (RCW),特别是当 COM 对象表示集合时,我之前的解决方案失败

\n
\n

TL;DR:您可以通过 .NET 比较任何COM 对象,并通过 .NET 比较指针来测试是否相等IntPtr。即使对象没有\xe2\x80\x99t 属性,您也可以比较Id它们ParaId

\n

未知

\n

首先来自 MSDN 上关于 COM 的一句话IUnknown

\n
\n

对于任何给定的 COM 对象(也称为 COM 组件),对该对象的任何接口上的接口的特定查询必须始终返回相同的指针值。这使得客户端能够通过调用并比较结果来确定两个指针是否指向同一个组件。具体来说,查询除此之外的接口(即使是通过相同指针的相同接口)必须返回相同的指针值[1]IUnknownQueryInterfaceIID_IUnknownIUnknown

\n
\n

RCW

\n

现在看看 RCW 如何充当 COM 和 .NET 之间的中间人:

\n
\n

公共语言运行时通过称为运行时可调用包装器 (RCW) 的代理公开 COM 对象。尽管 RCW 对于 .NET 客户端来说似乎是一个普通对象,但它的主要功能是编组 .NET 客户端和 COM 对象之间的调用。

\n

运行时为每个 COM 对象创建一个 RCW,无论该对象上存在多少引用。运行时为每个对象的每个进程维护一个 RCW [3]

\n
\n

请注意它是如何说“恰好一个”的,它可能应该有一个星号(*),我们很快就会看到。

\n

RCW。图片由 MSDN [3]提供,未经许可使用。

\n

在此输入图像描述

\n

测试平等性

\n

操作:

\n
\n

我希望能够识别两个互操作变量对象何时引用同一个“实际”对象

\n
\n

在下面使用 Word 互操作的示例中,我们故意两次检索指向同一子 COM 对象的指针,以证明 COMIUnknown指针是唯一标识 COM 对象的一种方法,如上面提到的 SDK 中所述。 IntPtr.Equals允许我们很好地比较 COM 指针。

\n
Document document =                                   // a Word document \nParagraphs paragraphs = document.Paragraphs;          // grab the collection\nvar punk = Marshal.GetIUnknownForObject(paragraphs);  // get IUnknown\nParagraphs p2 = document.Paragraphs;                  // get the collection again\nvar punk2 = Marshal.GetIUnknownForObject(p2);         // get its IUnknown\nDebug.Assert(punk.Equals(punk2));                     // This is TRUE!\n
Run Code Online (Sandbox Code Playgroud)\n

在上面的示例中,我们Paragraphs通过Paragraphs属性检索 COM 对象。然后,我们检索IntPtr代表对象IUnkown接口的 一个(所有 COM 对象都必须实现该接口,类似于所有 .NET 类最终派生自 的方式Object)。

\n

RCW 和 COM 集合的问题

\n

虽然上面的示例适用于大多数 COM 对象,但当与 COM 集合一起使用时,每次从集合中获取项目时,都会为集合中的项目创建一个新的 RCW! 我们可以在下面的例子中证明这一点:

\n
const string Id = "Miss Piggy";\nvar x = paragraphs[1];                   // get first paragraph\nDebug.Assert(x.ID == null);              // make sure it is empty first \nx.ID = Id;                               // assign an ID \npunk = Marshal.GetIUnknownForObject(x);  // get IUnknown\n// get it again\nvar y = paragraphs[1];                   // get first paragraph AGAIN\nDebug.Assert(x.ID == Id);                // true\npunk2 = Marshal.GetIUnknownForObject(y); // get IUnknown\nDebug.Assert(punk.Equals(punk2));        // FALSE!!! Therefore different RCW\n
Run Code Online (Sandbox Code Playgroud)\n

幸运的是,有一个解决方案,经过大量研究,最终偶然发现了另一篇文章,其中有人遇到了同样的问题。长话短说,为了在 RCW 妨碍时比较 COM 集合中的项目,最好的方法是存储本地副本[2]以避免创建额外的 RCW,如下所示:

\n
var paragraphsCopy = paragraphs.Cast<Paragraph>().ToList();\n
Run Code Online (Sandbox Code Playgroud)\n

现在集合中的对象仍然是RCW,因此对 COM 对象的任何更改都会反映在 COM 客户端中,但是如果您需要添加/删除项目,则 本地集合不是这样,最好正确引用 COM 集合 - 在这种情况下Word 的Paragraphs集合。

\n

最后的例子

\n

这是最终的代码:

\n
Document document = // ...\nParagraphs paragraphs = document.Paragraphs;\nvar paragraphsCopy = paragraphs.Cast<Paragraph>().ToList();\nParagraph firstParagraph = paragraphsCopy.First();\n\n// here I explicitly select a paragraph but you might have one already\n// select first paragraph\nvar firstRange = firstParagraph.Range;\nfirstRange.Select();\n\nvar selectedPunk = Marshal.GetIUnknownForObject(firstParagraph);\nvar i = 1;\nforeach (var paragraph in paragraphsCopy)\n{\n    var otherPunk = Marshal.GetIUnknownForObject(paragraph);\n    if (selectedPunk.Equals(otherPunk))\n    {\n        Console.WriteLine($"Paragraph {i} is the selected paragraph");\n    }\n\n    i++;\n}\n   \n
Run Code Online (Sandbox Code Playgroud)\n

也可以看看

\n

[1] IUnknown::QueryInterface,MSDN

\n

[2] /sf/answers/633407981/

\n

[3] 运行时可调用包装器,MSDN

\n

  • 创建本地集合“paragraphsCopy”后,不再需要使用“GetIUnknownForObject”来比较其成员。您可以使用正常的引用相等。为了完整起见,您还应该对从“GetIUnknownForObject”获得的指针调用“Marshal.Release”;对于 Word 来说,这通常不是问题,但其他 Office 应用程序(例如 Excel)可能不会容忍未完成的引用计数。总的来说,很好的答案。+1 (3认同)