4

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.

Pragmateek
  • 13,174
  • 9
  • 74
  • 108
  • 1
    You've got it all backwards, the *dynamic* keyword was added in C# version 4 to make it easier to late-bind to a COM server in C# code. Creating your own COM server in C# with [ComVisible] that supports late binding was always possible, going back to C# version 1. – Hans Passant Sep 27 '13 at 16:52
  • 1
    Thanks for your comment *Hans*. When I use the word "dynamic" I don't mean the **dynamic** type added to C#, sorry for the confusion. And this is what I expected that C# has always supported the **IDispatch** COM interface. But what I'm not sure about is if the whole workflow from VBA has any chance to work. So I'm searching for any resource with a minimal sample of a dynamic COM interface based on **IDispatch** implemented in C#. With that I'll be able to test it into Excel/VBA and have a definitive answer. – Pragmateek Sep 27 '13 at 17:33
  • Use *any* example of a [ComVisible] class. The CLR implements IDispatch without you explicitly inheriting that interface, just don't use [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]. If you want auto-completion support then use InterfaceIsDual. – Hans Passant Sep 27 '13 at 17:48
  • Thanks for the input *Hans*. But how do I let the client discover an interface created **on the fly**? Or more exactly, as in .Net the CLR acts as a proxy between the client and the .Net class, how do I say to the CLR "here is the interface I want you to present to any COM client"? – Pragmateek Sep 27 '13 at 17:56

2 Answers2

5

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
}
Community
  • 1
  • 1
noseratio
  • 59,932
  • 34
  • 208
  • 486
0

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.

Eric Brown
  • 13,774
  • 7
  • 30
  • 71
  • Thanks *Eric* for looking at the issue. I know about *ExpandoObject* but here I'd like to benefit from the "dynamic" capabilities of COM. And the interfaces I'm interested in are COM interfaces. It would be really great if COM interop was integrated with the DLR: inheriting from *DynamicObject* and implementing *TryInvoke* would do the job. :) But AFAIK this is not that simple, and implementing this kind of dynamism is at best cumbersome, but I hope I'm wrong... – Pragmateek Sep 27 '13 at 22:51
  • 1
    @Pragmateek Given the spec you outlined in the question, ExpandoObjects seem like a perfect fit. In particular, it seems like a "simple matter of programming" to write an adapter from IDispatchEx to Expandos. – Eric Brown Sep 27 '13 at 23:24
  • Thanks for pointing out **IDispatchEx**, I've never heard about it before, and after some research seems like it has been created for exactly this kind of usage: http://blogs.msdn.com/b/ericlippert/archive/2004/10/07/239289.aspx. It may be a perfect fit but: first this interface does not exist in C# (not really an issue I've found some port), secondly seems like nobody has cared of implementing it in C# (and even in C++ there is few resources), and lastly I fear the COM interop management of the CLR will get in the way... :/ – Pragmateek Sep 28 '13 at 10:50