Identify unique references to objects from Interop Libraries (Doument.Paragraphs, etc)

前端 未结 2 694
孤独总比滥情好
孤独总比滥情好 2021-01-23 02:38

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 o

相关标签:
2条回答
  • 2021-01-23 03:19

    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

    0 讨论(0)
  • 2021-01-23 03:30

    There are various ways this could be accomplished in Word. A fairly straight-forward way is to compare the Range properties using the InRange method. For example:

    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
    
    0 讨论(0)
提交回复
热议问题