Question

I'm working on a piece of C# code which deals with Windows Forms controls. Here's a small example, a tiny wrapper for getting the bounding rectangle (in screen coordinates) for some control:

public class GUIObject {
    protected Control m_control;

    // [..]

    public virtual Rectangle Bounds {
        get {
            Rectangle r = m_control.Bounds;
            if ( m_control.Parent != null ) {
                return m_control.Parent.RectangleToScreen( r );
            }
            return r;
        }
    }
}

This code is compiled into a library which is the distributed as a "plugin" to be loaded into customers applications. However, it turned out that some customers used a different version of Windows Forms in their application than what my plugin was linked against. My plan was to tackle this by making the above code late-bound, so that it'll work with whatever Windows Forms version is loaded in the current application domain. With .NET 4, I could use the dynamic keyword but alas, this code should work with .NET3 applications as well. Hence, I started using the reflection API, introducing a little helper object which makes using the reflection API a little nicer:

public class LateBoundObject {
    private Object m_o;

    // [..]

    public Object GetProperty( String name ) {
        PropertyInfo pi = m_o.GetType().GetProperty( name );
        return pi == null ? null
                          : pi.GetValue( m_o, null );
    }

    public Object InvokeMethod( String name, Object[] args ) {
        MethodInfo mi = m_o.GetType().GetMethod( name );
        return mi == null ? null
                          : mi.Invoke( m_o, args );
    }
}

public class GUIObject {
    protected LateBoundObject m_control;

    // [..]

    public virtual Rectangle Bounds {
        get {
            Object r = m_control.GetProperty( "Bounds" );
            if ( r == null) {
                return new Rectangle();
            }

            Object parent = m_control.GetProperty( "Parent" );
            if ( parent != null ) {
                LateBoundObject po = new LateBoundObject( parent );
                r = po.InvokeMethod( "RectangleToScreen",
                                     new Object[] { r } );
            }
            return (Rectangle)r;
        }
    }
}

Not very pretty. A lot of casting needed on the caller side, and I suspect I have to deal with overloaded methods or properties sooner or later as well - quite a bumpy ride ahead. Ideally, the wrapper object would allow keeping the original code very much the same.

So, before I start fixing the LateBoundObject wrapper class up I wonder: does anybody else have experience with making C# code late-bound using the reflection API? If so, how did you approach it to keep the pain of using the raw reflection API to a minimum - did you also use a wrapper class along the lines of LateBoundObject or did you go a totally different route? I'm looking for the least-invasive way as far as the original code is concerned.

Was it helpful?

Solution

One idea would be to create interfaces for what you want the objects to look like then use System.Reflection.Emit to generate classes that can coerece the actual instance. You can do this by wrapping it in a dynamically generated object that proxies calls from its interface methods to the actual instance it's wrapping.

Usage would look something like this:

interface IGUIObject 
{
  Rectangle Bounds { get; }
  Rectangle RectangleToScreen(Rectangle bounds);
  IGUIObject Parent { get; }
}

var obj = GetInstance();
var proxy = Reflection.Coerce<IGUIObject>(obj);
return proxy.Parent.RectangleToScreen(proxy.Bounds);

I have a blog post here with a simple starting point for how to do dynamic coercion, including sample app: coercing types and unloading assemblies

What's interesting is that with this technique you can actually get rid of the per-call reflection, which is very expensive performance wise. Instead you do the reflection once in the proxy generator and what you generate actually calls the corresponding property/method/field directly after that. Also with this trick the dynamic assembly that is generated will be unloaded when you drop the reference to the proxy instance. You can cache type generated type to make subsequent proxy creations very fast.

Your situation is more complex than my little sample but I think you could get very far with it as a starting point.

OTHER TIPS

I don't get it. I pass .NET 4 Controls to dlls compiled against .NET 2 and they work just fine.

Use helper extensions for reflection:

 var r = m_control._P<Rectangle>("Bounds") ?? new Rectangle();
 var parent = m_control._P<Control>("Parent");
 if (parent != null)
   r = parent._M<Rectangle>("RectangleToScreen", r);



static public class ReflectionHlp2
{
  public static T _P<T>(this object item, string name)
  {
    if (item == null)
      return default(T);
    var type = item.GetType();

    var members = type.GetMembers(BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
      .Where(_member => _member.Name == name)
      .ToArray();
    if (members.Length == 0)
      return default(T);
    if (members.Length > 1)
      throw new Exception(string.Format("У объекта полей/свойств с именем '{0}' больше чем один: '{1}'", name, members.Length));
    var member = members.First();
    object result;
    if (member is FieldInfo)
      result = ((FieldInfo)member).GetValue(item);
    else
      result = ((PropertyInfo)member).GetValue(item, null);
    if (result is T)
      return (T)result;
    return default(T);
  }
  public static void _P<T>(this object item, string name, T value)
  {
    if (item == null)
      return;
    var type = item.GetType();

    var members = type.GetMembers(BindingFlags.GetField | BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
      .Where(_member => _member.Name == name)
      .ToArray();
    if (members.Length == 0)
      return;
    if (members.Length > 1)
      throw new Exception(string.Format("У объекта полей/свойств с именем '{0}' больше чем один: '{1}'", name, members.Length));
    var member = members.First();
    if (member is FieldInfo)
      ((FieldInfo)member).SetValue(item, value);
    else
      ((PropertyInfo)member).SetValue(item, value, null);
  }
  public static void _M(this object item, string name, params object[] args)
  {
    _M<object>(item, name, args);
  }
  public static T _M<T>(this object item, string name, params object[] args)
  {
    if (item == null)
      return default(T);
    var type = item.GetType();

    var methods = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
      .Where(_member => _member.Name == name)
      .ToArray();
    if (methods.Length == 0)
      return default(T);
    if (methods.Length > 1)
      throw new Exception(string.Format("Вызов перегруженных методов не поддерживается, у объекта методов с именем '{0}' больше чем один: '{1}'.", name, methods.Length));
    var method = methods.First();
    var result = method.Invoke(item, args);
    if (result is T)
      return (T)result;
    return default(T);
  }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top