[Property Delegates]

Reading and writing properties with delegates

Friday, August 29, 2003

In .NET, you can invoke any method with a delegate, but you can't directly get or set a property this way. Ideally, this would be handled by the compiler, just like regular function/method delegates:

' VB.NET
Delegate ReadOnly Property StringPropertyDelegate() As String

// C#
delegate string StringPropertyDelegate{get;}

Instantiation should work like this:

StringPropertyDelegate d = new StringPropertyDelegate(this.Name);

Of course, such a delegate should be "bidirectional", that is, you should be able to invoke both the getter and the setter method (assuming the delegate type is declared this way) with the natural property syntax. There is one disambiguity, however: assignment to and from the delegate variable must be distinguished from assigment through the delegate. This could be done through an implicitly defined Value property:

Dim d As StringPropertyDelegate = AddressOf Me.Name
d.Value = "Gregor"
Dim s As String = d.Value

The real world

The above is just pseudo code that will probably not become reality. However, since every property is based on a get method, a set method, or both, you can use these underlying methods, and use method delegates compatible with these (of course, you'll end up with method invocation syntax, not property assignment look & feel). Another approach is to invoke the property with reflection, but that is less performant.

I'll discuss both approaches.

Using a pair of regular delegates

It's easy to define delegate types for the get and set methods of a string property:

Delegate Function StringPropertyGetter() As String
Delegate Sub StringPropertySetter(ByVal newValue As String)

Private m_Name As String

Property Name() As String
    Get
        Return m_Name
    End Get
    Set(ByVal newValue As String)
        m_Name = newValue
    End Set
End Property

Instantiating these delegates is a bit tricky, since the compiler refuses to recognize the underlying accessor methods. The only alternative is to instantiate the delegate by reflective means, using that constructor which takes an object reference (or null/Nothing if the property is static/Shared) and a function pointer. You can get the function pointer by obtaining the method handle for the method (this is the same technique as discussed in the Late Events article):

public static IntPtr GetFunctionPointer(MethodInfo mi){
    // get method handle from MethodInfo
    RuntimeMethodHandle handle = mi.MethodHandle;
    // get actual function pointer from method handle
    IntPtr ptr = handle.GetFunctionPointer();
    // return
    return ptr;
}

So here are the wrappers (they live in Gregor.Core.Reflect):

public static Delegate CreatePropertyGetter(object obj, string sPropertyName, Type tpDelegate){
    return Reflect.CreatePropertyGetterOrSetter(obj.GetType(), obj, sPropertyName,
                    BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
                    tpDelegate, false);
}
public static Delegate CreatePropertyGetter(Type tp, string sPropertyName, Type tpDelegate){
    return Reflect.CreatePropertyGetterOrSetter(tp, null, sPropertyName,
                    BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
                    tpDelegate, false);
}
public static Delegate CreatePropertySetter(object obj, string sPropertyName, Type tpDelegate){
    return Reflect.CreatePropertyGetterOrSetter(obj.GetType(), obj, sPropertyName,
                    BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
                    tpDelegate, true);
}
public static Delegate CreatePropertySetter(Type tp, string sPropertyName, Type tpDelegate){
    return Reflect.CreatePropertyGetterOrSetter(tp, null, sPropertyName,
                    BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
                    tpDelegate, true);
}
private static Delegate CreatePropertyGetterOrSetter(Type tp,
                                                     object obj,
                                                     string sPropertyName,
                                                     BindingFlags flags,
                                                     Type tpDelegate,
                                                     bool fSet){

    // get property on type and validate
    PropertyInfo pi = tp.GetProperty(sPropertyName, flags);
    if(pi == null){
        throw new ArgumentException("Cannot find property '" + sPropertyName + "'.");
    }
    if(fSet){
        if(false == pi.CanWrite){
            throw new ArgumentException("Property '" + sPropertyName + "' is read-only.");
        }
    }else{
        if(false == pi.CanRead){
            throw new ArgumentException("Property '" + sPropertyName + "' is write-only.");
        }
    }

    // get read or write method, delegate's Invoke method
    MethodInfo mi = null;
    if(fSet){
        mi = pi.GetSetMethod(true);
    }else{
        mi = pi.GetGetMethod(true);
    }
    MethodInfo miInvoke = tpDelegate.GetMethod("Invoke");

    // check method signatures (we'll soon be bypassing type checking!)
    if(false == Reflect.AreMethodsEqual(mi, miInvoke)){ // simply compares parameter types
        throw new ArgumentException("The getter of property '" + sPropertyName + "' does not match the delegate '" + tpDelegate.FullName + "'.");
    }

    // get function pointer for read or write method
    IntPtr ptr = Reflect.GetFunctionPointer(mi);

    // instantiate delegate
    object[] args = new object[]{obj, ptr};
    Delegate del = (Delegate) Activator.CreateInstance(tpDelegate, args);

    // return
    return del;
}

The downside is that you have to cast the delegate returned by this function. Specifying the delegate type in a GetType/typeof operator means you'll have to write it down a second or third time:

Dim del As System.Delegate = CreatePropertyGetter(Me, "Name", GetType(StringPropertyGetter))
Dim getter As StringPropertyGetter = DirectCast(del, StringPropertyGetter)
Dim s As String = getter()

But with generics on the horizon, this will hopefully soon be unnecessary; the System.Type parameter for the delegate type could be dropped, since the type parameter of the template itself would suffice:

StringPropertyGetter getter = CreatePropertyGetter<StringPropertyGetter>(this, "Name");

A wrapper for the reflection approach

Using System.Reflection.PropertyInfo objects, you can get or set the value of a property. For simple, parameterless properties, the following class (from Gregor.Core) wraps it up some.

public class CPropertyDelegate {

    private PropertyInfo m_Property;
    private object m_Instance;

    public CPropertyDelegate(Type tp, string sPropertyName){
        this.Init(tp, null, sPropertyName,
             BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
    }
    public CPropertyDelegate(object obj, string sPropertyName){
        this.Init(obj.GetType(), obj, sPropertyName,
             BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
    }
    private void Init(Type tp, object obj, string sPropertyName, BindingFlags flags){
        m_Property = tp.GetProperty(sPropertyName, flags);
        if(m_Property == null){
            throw new ArgumentException("Cannot find property '" + sPropertyName + "'.");
        }
        m_Instance = obj;
    }

    public object Value{
        get{
            return m_Property.GetValue(m_Instance, null);
        }
        set{
            m_Property.SetValue(m_Instance, value, null);
        }
    }

} // end class CPropertyDelegate

Maybe this class should be a base class extensible to strongly typed specializations (following the usual "Base" pattern), or maybe we'll just wait for generics. But then again, with specialization, one might define special delegate types for the accessor methods as well, and use those inside the wrapper class. That would be closer to the first approach, but result in having a single object for both reading and writing the property. For simple, non-indexable properties, a future generics feature might carry out the grunt work of defining the delegate types (but this is open to future language details).