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