从互操作库(Doument.Paragraphs 等)中识别对对象的唯一引用 [英] Identify unique references to objects from Interop Libraries (Doument.Paragraphs, etc)

查看:22
本文介绍了从互操作库(Doument.Paragraphs 等)中识别对对象的唯一引用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我希望能够确定两个互操作变量对象何时引用相同实际"对象.实际"是指例如 Microsoft Word 文档中的给定段落或脚注.

测试是否相等

操作:

<块引用>

我希望能够确定两个互操作变量对象何时引用同一个实际"对象.对象

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

Document document =//一个 Word 文档段落段落=文档.段落;//获取集合var punk = Marshal.GetIUnknownForObject(段落);//获取 IUnknown段落 p2 = document.Paragraphs;//再次获取集合var punk2 = Marshal.GetIUnknownForObject(p2);//获取它的 IUnknownDebug.Assert(punk.Equals(punk2));//这是真实的!

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

RCW 和 COM 集合的问题

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

const string Id = "Miss Piggy";var x = 段落[1];//获取第一段Debug.Assert(x.ID == null);//首先确保它是空的x.ID = ID;//分配一个ID朋克 = Marshal.GetIUnknownForObject(x);//获取 IUnknown//再次获取var y = 段落[1];//再次获取第一段Debug.Assert(x.ID == Id);//真的punk2 = Marshal.GetIUnknownForObject(y);//获取 IUnknownDebug.Assert(punk.Equals(punk2));//错误的!!!因此不同的RCW

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

var ParagraphsCopy = Paragraphs.Cast().ToList();

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

最终示例

这是最终的代码:

Document document =//...段落段落=文档.段落;varparagraphsCopy =paragraphs.Cast().ToList();段落 firstParagraph = 段落Copy.First();//这里我明确地选择了一个段落,但你可能已经有了一个//选择第一段var firstRange = firstParagraph.Range;firstRange.Select();var selectedPunk = Marshal.GetIUnknownForObject(firstParagraph);变量 i = 1;foreach (var 段落中的段落复制){var otherPunk = Marshal.GetIUnknownForObject(paragraph);if (selectedPunk.Equals(otherPunk)){Console.WriteLine($"Paragraph {i} 是选定的段落");}我++;}

另见

[1] IUnknown::QueryInterface, MSDN

[2] https://stackoverflow.com/a/9048685/585968

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

I would like to be able to identify when two interop variable objects refer to the same "actual" object. By "actual", I mean for example a given paragraph or footnote in a Microsoft Word document.

Example in : (note answers are ok too, the question is not language related)

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

As you can see, comparing the object variables by reference does not work. Whilst this is a bit frustrating, I would be ok running a comparer over the .ParaID property if the documentation did not say just as little as:

Reserved for internal use.

Any comments are welcome on (1) how to avoid using .ParaID, and (2) the reliability of using .ParaID as a unique identifier (any info on this property is welcome too as Microsoft and Google remain quite silent on the topic)

The question can be generelised for other collections as well, such as Word.Footnotes, Word.Bookmarks. I suppose the same would happen with Excel.Worksheets, etc.

解决方案


My second answer - OK so I was on the right track, however my prior solution failed due to .NET's Runtime Callable Wrappers (RCW), specifically when the COM object represents a collection.


TL;DR: You can compare any COM object via .NET and test for equality simply by comparing the pointers via IntPtr. You can compare objects even if they don’t have Id or ParaId properties.

IUnknown

First a word from MSDN on IUnknown in COM:

For any given COM object (also known as a COM component), a specific query for the IUnknown interface on any of the object's interfaces must always return the same pointer value. This enables a client to determine whether two pointers point to the same component by calling QueryInterface with IID_IUnknown and comparing the results. It is specifically not the case that queries for interfaces other than IUnknown (even the same interface through the same pointer) must return the same pointer value[1]

RCW

Now to see how RCW are a middleman between COM and .NET:

The common language runtime exposes COM objects through a proxy called the runtime callable wrapper (RCW). Although the RCW appears to be an ordinary object to .NET clients, its primary function is to marshal calls between a .NET client and a COM object.

The runtime creates exactly one RCW for each COM object, regardless of the number of references that exist on that object. The runtime maintains a single RCW per process for each object[3]

Note how it said "exactly one", it probably should have had an asterisk (*) as we shall soon see.

RCW. Image courtesy of MSDN[3], used without permission.

Testing for equality

OP:

I would like to be able to identify when two interop variable objects refer to the same "actual" object

In the following example of using Word interop, we deliberately retrieve a pointer to the same child COM object twice in order to demonstrate that COM IUnknown pointers are a means to uniquely identiy COM objects as outlined in the SDK mentioned above. IntPtr.Equals allows us to compare COM pointers quite nicely.

Document document =                                   // a Word document 
Paragraphs paragraphs = document.Paragraphs;          // grab the collection
var punk = Marshal.GetIUnknownForObject(paragraphs);  // get IUnknown
Paragraphs p2 = document.Paragraphs;                  // get the collection again
var punk2 = Marshal.GetIUnknownForObject(p2);         // get its IUnknown
Debug.Assert(punk.Equals(punk2));                     // This is TRUE!

In the above example, we retrieve the Paragraphs COM object via the Paragraphs property. We then retrieve a IntPtr that represents the objects IUnkown interface (that all COM objects must implement, sort of in the same way all .NET classes derive ultimately from Object).

The Problem of RCWs and COM Collections

Though the above example works well with most COM objects, when used with a COM collection, a new RCW is created for an item in the collection each time you fetch it from the collection! We can demonstrate this in the following example:

const string Id = "Miss Piggy";
var x = paragraphs[1];                   // get first paragraph
Debug.Assert(x.ID == null);              // make sure it is empty first 
x.ID = Id;                               // assign an ID 
punk = Marshal.GetIUnknownForObject(x);  // get IUnknown
// get it again
var y = paragraphs[1];                   // get first paragraph AGAIN
Debug.Assert(x.ID == Id);                // true
punk2 = Marshal.GetIUnknownForObject(y); // get IUnknown
Debug.Assert(punk.Equals(punk2));        // FALSE!!! Therefore different RCW

Luckily there is a solution and after much researching eventually stumbled across another post where someone was encountering the same issue. Long story short, in order to compare items in a COM collection when RCW is in the way, the best way is to store a local copy[2] so as to avoid additonal RCWs being created like so:

var paragraphsCopy = paragraphs.Cast<Paragraph>().ToList();

Now the objects in the collection are still RCW so any changes to the COM objects will reflect in COM clients however the local collection isn't so if you need to add/remove items best to refer to the COM collection proper - in this case Word's Paragraphs collection.

Final Example

Here is the final code:

Document document = // ...
Paragraphs paragraphs = document.Paragraphs;
var paragraphsCopy = paragraphs.Cast<Paragraph>().ToList();
Paragraph firstParagraph = paragraphsCopy.First();

// here I explicitly select a paragraph but you might have one already
// select first paragraph
var firstRange = firstParagraph.Range;
firstRange.Select();

var selectedPunk = Marshal.GetIUnknownForObject(firstParagraph);
var i = 1;
foreach (var paragraph in paragraphsCopy)
{
    var otherPunk = Marshal.GetIUnknownForObject(paragraph);
    if (selectedPunk.Equals(otherPunk))
    {
        Console.WriteLine($"Paragraph {i} is the selected paragraph");
    }

    i++;
}
   

See also

[1] IUnknown::QueryInterface, MSDN

[2] https://stackoverflow.com/a/9048685/585968

[3] Runtime Callable Wrapper, MSDN

这篇关于从互操作库(Doument.Paragraphs 等)中识别对对象的唯一引用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆