识别对互操作库中对象的唯一引用(Doument.Paragraphs,等等)
Identify unique references to objects from Interop Libraries (Doument.Paragraphs, etc)
我希望能够识别两个互操作变量对象何时引用相同 "actual" 对象。 "actual" 是指 Microsoft Word 文档中的给定段落或脚注。
vb 中的示例:
(注意 c# 答案也可以,问题 与 语言无关)
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
如您所见,通过引用比较对象变量不起作用。虽然这有点令人沮丧,但如果 documentation 没有说的那么少,我会 运行 比较 .ParaID
属性:
Reserved for internal use.
欢迎就 (1) 如何避免使用 .ParaID
,以及 (2) 使用 .ParaID
作为唯一标识符的可靠性提出任何意见(有关此 属性 的任何信息也很受欢迎,因为 Microsoft 和 Google 在这个话题上保持沉默)
这个问题也可以推广到其他集合,例如Word.Footnotes
、Word.Bookmarks
。我想 Excel.Worksheets
等也会发生同样的情况
在 Word 中可以通过多种方式完成此操作。一种相当 straight-forward 的方法是使用 InRange
方法比较 Range
属性。例如:
Sub Tests()
Dim WordApp as Word.Application = Globals.ThisAddIn.Application
Dim ThisDoc as Word.Document = WordApp.ActiveDocument
Dim ThisSelection As Word.Selection = WordApp.Selection
If ThisSelection.Range Is Nothing Then Exit Sub
Dim SelectedPara As Word.Range = ThisSelection.Range.Paragraphs.First.Range
For Each MyPara As Word.Paragraph In ThisDoc.Paragraphs
Dim rng as Word.Range = myPara.Range
If rng.InRange(SelectedPara) And SelectedPara.InRange(rng) Then
'They're the same
Else
'They're not the same
End If
rng = Nothing
Next
End Sub
我的第二个答案 - 好的,所以我在正确的轨道上,但是由于 .NET 的运行时可调用包装器 (RCW),我之前的解决方案失败了,特别是当 COM object 代表 时collection.
TL;DR: 您可以通过 .NET 比较 any COM object 并通过比较通过 IntPtr
的指针。您可以比较 object,即使它们没有 Id
或 ParaId
属性。
未知
首先来自 MSDN 关于 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
现在看看 RCW 如何成为 COM 和 .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]
请注意它是怎么说的“正好是一个”,它可能应该有一个星号 (*),我们很快就会看到。
RCW。图片由 MSDN[3] 提供,未经许可使用。
相等性测试
OP:
I would like to be able to identify when two interop variable objects refer to the same "actual" object
在下面使用 Word 互操作的示例中,我们故意检索指向相同 child COM object 两次 的指针,以证明 COM IUnknown
指针是一种唯一标识 COM object 的方法,如上述 SDK 中所述。 IntPtr.Equals
让我们可以很好地比较 COM 指针。
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!
在上面的示例中,我们通过 Paragraphs
属性 检索 Paragraphs
COM object。然后我们检索一个 IntPtr
表示 objects IUnkown
接口(所有 COM objects 必须实现,类似于所有 .NET 类 最终派生自 Object
).
RCWs和COM的问题Collections
虽然上面的示例适用于大多数 COM objects,但当与 COM collection 一起使用时,将为 [=121] 中的项目创建一个新的 RCW =] 每次从 collection! 获取它时我们可以在下面的示例中演示这一点:
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
幸运的是有一个解决方案,经过大量研究最终偶然发现另一个 post 有人遇到了同样的问题。长话短说,为了在 RCW 挡路时比较 COM collection 中的项目,最好的方法是存储本地副本 [2] 以避免像这样创建额外的 RCW:
var paragraphsCopy = paragraphs.Cast<Paragraph>().ToList();
现在 collection 中的 object 仍然是 RCW 所以对 COM object 的任何更改 将反映 在 COM 客户端 然而 本地 collection 不是 所以如果你需要 add/remove 项最好引用 COM collection 本身 - 在本例中是 Word 的 Paragraphs
collection.
最终示例
这是最终代码:
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++;
}
另见
[1]IUnknown::QueryInterface, MSDN
[2] https://whosebug.com/a/9048685/585968
[3]Runtime Callable Wrapper, MSDN
我希望能够识别两个互操作变量对象何时引用相同 "actual" 对象。 "actual" 是指 Microsoft Word 文档中的给定段落或脚注。
vb 中的示例: (注意 c# 答案也可以,问题 与 语言无关)
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
如您所见,通过引用比较对象变量不起作用。虽然这有点令人沮丧,但如果 documentation 没有说的那么少,我会 运行 比较 .ParaID
属性:
Reserved for internal use.
欢迎就 (1) 如何避免使用 .ParaID
,以及 (2) 使用 .ParaID
作为唯一标识符的可靠性提出任何意见(有关此 属性 的任何信息也很受欢迎,因为 Microsoft 和 Google 在这个话题上保持沉默)
这个问题也可以推广到其他集合,例如Word.Footnotes
、Word.Bookmarks
。我想 Excel.Worksheets
等也会发生同样的情况
在 Word 中可以通过多种方式完成此操作。一种相当 straight-forward 的方法是使用 InRange
方法比较 Range
属性。例如:
Sub Tests()
Dim WordApp as Word.Application = Globals.ThisAddIn.Application
Dim ThisDoc as Word.Document = WordApp.ActiveDocument
Dim ThisSelection As Word.Selection = WordApp.Selection
If ThisSelection.Range Is Nothing Then Exit Sub
Dim SelectedPara As Word.Range = ThisSelection.Range.Paragraphs.First.Range
For Each MyPara As Word.Paragraph In ThisDoc.Paragraphs
Dim rng as Word.Range = myPara.Range
If rng.InRange(SelectedPara) And SelectedPara.InRange(rng) Then
'They're the same
Else
'They're not the same
End If
rng = Nothing
Next
End Sub
我的第二个答案 - 好的,所以我在正确的轨道上,但是由于 .NET 的运行时可调用包装器 (RCW),我之前的解决方案失败了,特别是当 COM object 代表 时collection.
TL;DR: 您可以通过 .NET 比较 any COM object 并通过比较通过 IntPtr
的指针。您可以比较 object,即使它们没有 Id
或 ParaId
属性。
未知
首先来自 MSDN 关于 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 callingQueryInterface
withIID_IUnknown
and comparing the results. It is specifically not the case that queries for interfaces other thanIUnknown
(even the same interface through the same pointer) must return the same pointer value[1]
RCW
现在看看 RCW 如何成为 COM 和 .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]
请注意它是怎么说的“正好是一个”,它可能应该有一个星号 (*),我们很快就会看到。
RCW。图片由 MSDN[3] 提供,未经许可使用。
相等性测试
OP:
I would like to be able to identify when two interop variable objects refer to the same "actual" object
在下面使用 Word 互操作的示例中,我们故意检索指向相同 child COM object 两次 的指针,以证明 COM IUnknown
指针是一种唯一标识 COM object 的方法,如上述 SDK 中所述。 IntPtr.Equals
让我们可以很好地比较 COM 指针。
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!
在上面的示例中,我们通过 Paragraphs
属性 检索 Paragraphs
COM object。然后我们检索一个 IntPtr
表示 objects IUnkown
接口(所有 COM objects 必须实现,类似于所有 .NET 类 最终派生自 Object
).
RCWs和COM的问题Collections
虽然上面的示例适用于大多数 COM objects,但当与 COM collection 一起使用时,将为 [=121] 中的项目创建一个新的 RCW =] 每次从 collection! 获取它时我们可以在下面的示例中演示这一点:
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
幸运的是有一个解决方案,经过大量研究最终偶然发现另一个 post 有人遇到了同样的问题。长话短说,为了在 RCW 挡路时比较 COM collection 中的项目,最好的方法是存储本地副本 [2] 以避免像这样创建额外的 RCW:
var paragraphsCopy = paragraphs.Cast<Paragraph>().ToList();
现在 collection 中的 object 仍然是 RCW 所以对 COM object 的任何更改 将反映 在 COM 客户端 然而 本地 collection 不是 所以如果你需要 add/remove 项最好引用 COM collection 本身 - 在本例中是 Word 的 Paragraphs
collection.
最终示例
这是最终代码:
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++;
}
另见
[1]IUnknown::QueryInterface, MSDN
[2] https://whosebug.com/a/9048685/585968
[3]Runtime Callable Wrapper, MSDN