C# COM object with a dynamic interface

2019-07-03 22:44发布

问题:

I'd like to build a COM visible C# class, say DynamicComponent, that would provide a dynamic interface through COM.

Internally this class would maintain a dictionary of delegates:

"GetTheAnswer" -> () => { return 42; }
"Add" -> (int a, int b) => { return a + b; }
...

The client code would be some VBA.

Here is the workflow I naively imagine:

  • from the Excel/VBA editor the user references the TLB
  • the user instantiates a new DynamicComponent (well at least get a stub provided by Excel/VBA)
  • Excel/VBA COM infrastructure queries the component through its IDispatch interface
  • the component answers with a disp-ids map like ["GetTheAnswer" -> 1, "Add" -> 2]
  • the user can benefit from auto-completion and sees two methods: GetTheAnswer and Add
  • the user invokes any of these methods as if it was statically defined

My first question: is it possible?

If no: why?

If yes: how?

From what I know about COM, if it's possible, the IDispatch COM interface is my best friend.

Moreover, from what I understand, the ICustomQueryInterface interface from .Net 4 could greatly help too.

But as nowadays COM is not really cutting-edge ;) it's quite hard to find resources like code samples.

I've found this interesting sample: https://clrinterop.codeplex.com/releases/view/32350 which implements COM aggregation using the ICustomQueryInterface interface

But it's not dynamic and based on statically defined types and interfaces.

Any help would be greatly appreciated.

Thanks.

回答1:

Exposing IDispatchEx would work for JavaScript, but I don't think VBA makes any use of it. AFAIK, VBA relies upon IDispatch for late binding. Furthermore, C# dynamic is great for consumption of COM IDispatch-based objects on .NET side, but not vice versa. For some reason (.NET designers decision?), dynamic properties and methods of ExpandoObject and DynamicObject are not exposed to COM by default.

Fortunately, there's a way to override this: by implementing IReflect interface. Refer to this excellent blog post for implementation details. I've myself looked at exposing properties of C# anonymous class to COM, and ended up using IReflect. This is how you can expose dynamic methods and properties to COM. Figuratively speaking, IReflect is exposed to COM as IDispatch.

On a side note, IExpando does the same job for IDispatchEx, so a JavaScript client can add new properties which can later be accesses by managed code.

[UPDATE] Below is a prototype implementation that exposes an instance of DynamicComponent to VBScript running inside WebBrowser. It works quite well for VBScript and should do so for VBA too. Although, I doubt VBA auto-completion will work, or there is an easy way to implement such feature. AFAIU, VBA auto-completion relies upon COM type library (obtainable via IDispatch::GetTypeInfo), but I don't think .NET interop engine would generate a dynamic type library when it implements IDispatch through IReflect (I might be wrong). Also, this implementation is case-sensitive for method-by-name look-ups, which should be tweaked as VB is case-insensitive.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows.Forms;

namespace WebBrowserApp
{
    // https://stackoverflow.com/a/19067386/1768303

    public partial class MainForm : Form
    {
        WebBrowser wb;

        public MainForm()
        {
            InitializeComponent();

            this.wb = new WebBrowser();
            this.wb.Dock = DockStyle.Fill;
            this.Controls.Add(this.wb);
            this.wb.Visible = true;

            var dynamicComponent = new DynamicComponent();
            // make dynamicComponent available to VBScript
            this.wb.ObjectForScripting = dynamicComponent;

            // add a dynamic method "Convert"
            dynamicComponent.SetMethod("Convert", new Func<int, string>((a) =>
            {
                MessageBox.Show("Convert called: " + a.ToString());
                return a.ToString();
            }));

            this.Load += (s, e) =>
            {
                this.wb.DocumentText =
                    "<script type='text/vbscript'>\n" +
                    "Sub OnLoadHandler\n" +
                    "    alert window.external.Convert(42)\n" +
                    "End Sub\n" +
                    "window.onload = GetRef(\"OnLoadHandler\")\n" +
                    "</script>";
            };
        }
    }

    #region DynamicComponent
    [ComVisible(true), ClassInterface(ClassInterfaceType.None)]
    public class DynamicComponent : System.Reflection.IReflect
    {
        readonly Dictionary<string, Delegate> _methods = new Dictionary<string, Delegate>();

        public void SetMethod(string name, Delegate value)
        {
            _methods[name] = value;
        }

        static Exception NotImplemented()
        {
            var method = new StackTrace(true).GetFrame(1).GetMethod().Name;
            Debug.Assert(false, method);
            return new NotImplementedException(method);
        }

        #region IReflect
        // IReflect

        public FieldInfo GetField(string name, BindingFlags bindingAttr)
        {
            throw NotImplemented();
        }

        public FieldInfo[] GetFields(BindingFlags bindingAttr)
        {
            return new FieldInfo[0];
        }

        public MemberInfo[] GetMember(string name, BindingFlags bindingAttr)
        {
            throw NotImplemented();
        }

        public MemberInfo[] GetMembers(BindingFlags bindingAttr)
        {
            return new MemberInfo[0];
        }

        public MethodInfo GetMethod(string name, BindingFlags bindingAttr)
        {
            throw NotImplemented();
        }

        public MethodInfo GetMethod(string name, BindingFlags bindingAttr, Binder binder, Type[] types, ParameterModifier[] modifiers)
        {
            throw NotImplemented();
        }

        public MethodInfo[] GetMethods(BindingFlags bindingAttr)
        {
            return _methods.Keys.Select(name => new DynamicMethodInfo(name, _methods[name].Method)).ToArray();
        }

        public PropertyInfo[] GetProperties(BindingFlags bindingAttr)
        {
            return new PropertyInfo[0];
        }

        public PropertyInfo GetProperty(string name, BindingFlags bindingAttr, Binder binder, Type returnType, Type[] types, ParameterModifier[] modifiers)
        {
            throw NotImplemented();
        }

        public PropertyInfo GetProperty(string name, BindingFlags bindingAttr)
        {
            throw NotImplemented();
        }

        public object InvokeMember(string name, BindingFlags invokeAttr, Binder binder, object target, object[] args, ParameterModifier[] modifiers, System.Globalization.CultureInfo culture, string[] namedParameters)
        {
            if (target == this && invokeAttr.HasFlag(BindingFlags.InvokeMethod))
            {
                Delegate method;
                if (!_methods.TryGetValue(name, out method))
                    throw new MissingMethodException();
                return method.DynamicInvoke(args);
            }
            throw new ArgumentException();
        }

        public Type UnderlyingSystemType
        {
            get { throw NotImplemented(); }
        }
        #endregion

        #region DynamicMethodInfo
        // DynamicPropertyInfo

        class DynamicMethodInfo : System.Reflection.MethodInfo
        {
            string _name;
            MethodInfo _mi;

            public DynamicMethodInfo(string name, MethodInfo mi)
                : base()
            {
                _name = name;
                _mi = mi;
            }

            public override MethodInfo GetBaseDefinition()
            {
                return _mi.GetBaseDefinition();
            }

            public override ICustomAttributeProvider ReturnTypeCustomAttributes
            {
                get { return _mi.ReturnTypeCustomAttributes; }
            }

            public override MethodAttributes Attributes
            {
                get { return _mi.Attributes; }
            }

            public override MethodImplAttributes GetMethodImplementationFlags()
            {
                return _mi.GetMethodImplementationFlags();
            }

            public override ParameterInfo[] GetParameters()
            {
                return _mi.GetParameters();
            }

            public override object Invoke(object obj, BindingFlags invokeAttr, Binder binder, object[] parameters, System.Globalization.CultureInfo culture)
            {
                return _mi.Invoke(obj, invokeAttr, binder, parameters, culture);
            }

            public override RuntimeMethodHandle MethodHandle
            {
                get { return _mi.MethodHandle; }
            }

            public override Type DeclaringType
            {
                get { return _mi.DeclaringType; }
            }

            public override object[] GetCustomAttributes(Type attributeType, bool inherit)
            {
                return _mi.GetCustomAttributes(attributeType, inherit);
            }

            public override object[] GetCustomAttributes(bool inherit)
            {
                return _mi.GetCustomAttributes(inherit);
            }

            public override bool IsDefined(Type attributeType, bool inherit)
            {
                return _mi.IsDefined(attributeType, inherit);
            }

            public override string Name
            {
                get { return _name; }
            }

            public override Type ReflectedType
            {
                get { return _mi.ReflectedType; }
            }
        }

        #endregion
    }
    #endregion
}


回答2:

You can't create interfaces on the fly, but you can create methods. Look at Expando Objects in C#.

In particular, you can create a custom IDispatchEx implementation using expandos; you can use the expando to map names to IDs, and reflection to invoke the object.