VSTO: manipulating COM objects (“one dot good, two

2019-02-24 23:44发布

Coming from an Excel VBA background I would frequently write code such as:

Range("myRange").Offset(0, 1).Resize(1, ccData).EntireColumn.Delete

I'm now moving to VSTO, and have been reading about RCW counters, etc., and the need to explicitly release COM objects. The basic advice seems to be: don't chain together references to Excel objects (as I have above) - hence "one dot good, two dots bad". My question is, am I correct that the above code is not the way to go in VSTO? If so, does that mean that I would need to explicitly declare the 3 ranges implied in the above chain (Offset, Resize & EntireColumn)?

Or even how about something like:

rng.Columns.Count

where rng is a declared Range? Should I be assigning a name to rng.Columns in order to obtain the number of columns in the range??

标签: c# excel vba com vsto
3条回答
Viruses.
2楼-- · 2019-02-25 00:18

am I correct that the above code is not the way to go in VSTO?

Yes, you are on the right avenue. You need to declare different objects to release them later. Use System.Runtime.InteropServices.Marshal.ReleaseComObject to release an Office object when you have finished using it. Then set a variable to Nothing in Visual Basic (null in C#) to release the reference to the object.

You can read more about that in the Systematically Releasing Objects article. It is related to Outlook, but the same principles can be applied to all Office applications.

BTW: it doesn't depend on VSTO. It comes from the COM world...

查看更多
手持菜刀,她持情操
3楼-- · 2019-02-25 00:23

There is very detrimental cargo cult behind that "two dot rule" silliness, it completely fails to keep C# programmers out of trouble since version 4. And it endlessly more painful than the simple way to make Office programs quit on demand.

But this is not a problem you have at all, the cargo cult only applies to an out-of-process program that use Automation to activate an Office program. Your code in fact runs inside the Office program, you of course don't care when the program terminates. Because that terminates your code as well.

Just write your code the way you'd write regular C# code, the GC does not need any help.

查看更多
一纸荒年 Trace。
4楼-- · 2019-02-25 00:23

As often happens with small affirmations, the double-dot rule needs further explanation. You can use it as a mnemonic.

The reason is that each new RCW you get in .NET will hold a reference to the respective COM object. So, if you have (assuming obj is an RCW and this is the first time you're getting the other objects):

obj.Property[0].MethodThatReturnsAnotherObject()
//     1     2                  3

you're getting 3 additional RCWs. As you can see, there is only 1 extra dot. Although properties are probably the most common way of obtaining other COM objects, it's not the only way.

Usually, each RCW will only release the underlying COM object when it's garbage collected, unless you use Marshal.ReleaseComObject. Only use this method if you are completely sure you're the only one using the RCW you're releasing.


To be perfectly clear about this subject:

Only use ReleaseComObject or FinalReleaseComObject if you really, demonstrably have to, and you are completely, totally sure your piece of code is the only one referring to the RCW.


<type> propObj;
try
{
    propObj = obj.Property;
    <type> propArrayObj;
    try
    {
        propArrayObj = propObj[0];
        <type> propArrayObjReturn;
        try
        {
            propArrayObjReturn = propArrayObj.MethodThatReturnsAnotherObject();
        }
        finally
        {
            if (propArrayObjReturn != null) Marshal.ReleaseComObject(propArrayObjReturn);
        }
    }
    finally
    {
        if (propArrayObj != null) Marshal.ReleaseComObject(propArrayObj);
    }
}
finally
{
    if (propObj != null) Marshal.ReleaseComObject(propObj);
}

This is tedious, a wrapper might help here:

using System;
using System.Runtime.InteropServices;
using System.Threading;

public class ComPtr<T> : IDisposable where T : class
{
    public ComPtr(T comObj)
    {
        if (comObj == null) throw new ArgumentNullException("comObj");

        if (!typeof(T).IsInterface)
        {
            throw new ArgumentException("COM type must be an interface.", "T");
        }
        // TODO: check interface attributes: ComImport or ComVisible, and Guid

        this.comObj = comObj;
    }

    private T comObj;

    public T ComObj
    {
        get
        {
            // It's not best practice to throw exceptions in getters
            // But the alternative might lead to a latent NullReferenceException
            if (comObj == null)
            {
                throw new ObjectDisposedException("ComObj");
            }

            return comObj;
        }
    }

    ~ComPtr()
    {
        Dispose(false);
    }

    // IDisposable
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected void Dispose(bool disposing)
    {
        #if !RELEASECOMPTR

        // Option 1: Safe.  It might force the GC too often.
        // You can probably use a global limiter, e.g. don't force GC
        // for less than 5 seconds apart.
        if (Interlocked.Exchange(ref comObj, null) != null)
        {
             // Note: GC all generations
            GC.Collect();
            // WARNING: Wait for ALL pending finalizers
            // COM objects in other STA threads will require those threads
            // to process messages in a timely manner.
            // However, this is the only way to be sure GCed RCWs
            // actually invoked the COM object's Release.
            GC.WaitForPendingFinalizers();
        }

        #else

        // Option 2: Dangerous!  You must be sure you have no other
        // reference to the RCW (Runtime Callable Wrapper).
        T currentComObj = Interlocked.Exchange(ref comObj, null);
        if (currentComObj != null)
        {
            // Note: This might (and usually does) invalidate the RCW
            Marshal.ReleaseComObject(currentComObj);
            // WARNING: This WILL invalidate the RCW, no matter how many
            // times the object reentered the managed world.
            // However, this is the only way to be sure the RCW's
            // COM object is not referenced by our .NET instance.
            //Marshal.FinalReleaseComObject(currentComObj);
        }

        #endif
    }
}

This would make the previous example a bit friendlier:

using (var prop = new ComObj<type>(obj.Property))
{
    using (var propArray = new ComObj<type>(prop.ComObj[0]))
    {
        using (var propArrayReturn = new ComPtr<type>(propArray.ComObj.MethodThatReturnsAnotherObject()))
        {
        }
    }
}

To avoid the ComObj property, you could implement a proxy, but I'll leave that as an exercise. Specifically, make an efficient proxy generation instead of forwarding by reflection.

查看更多
登录 后发表回答