VSTO: manipulating COM objects (“one dot good, two dots bad”)

前端 未结 3 562
感情败类
感情败类 2020-12-12 00:02

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

Range(\"myRange\").Offset(0, 1).Resize(1, ccData).EntireColumn.Delete
相关标签:
3条回答
  • 2020-12-12 00:05

    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...

    0 讨论(0)
  • 2020-12-12 00:17

    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.

    0 讨论(0)
  • 2020-12-12 00:19

    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.

    0 讨论(0)
提交回复
热议问题