If you’ve grown to need really generic weak event handlers and weak delegates, then I assume you have at least tried using existing non-entirely-generic ones.
Dustin Campbell has a great article detailing the implementation of a weak event handler that can be used for any EventHandler<> parameterization, which is great for most practical purposes.
Unfortunately, there seems to be no way to create a single generic class that can act as a weak proxy for all possible delegate types. It appears to be an inherent .NET limitation –generic type parameters that are in fact delegates cannot be used for invocation or anything delegate-related. It would have been possible if you could spell it as a type constraint, e.g. class Foo<T> where T : delegate {};. Oh well, no biggie – let’s just write a separate class for each possible delegate type. However, let’s be smart about it!
By now you should’ve guessed that we’ll just use code generation – we’ll just generate an exact replica of the code in Campbell’s article, with the only difference being the delegate’s type. I’m giving you the entire code below.
/// <summary>
/// Weak delegates are useful where you don't want the lifetime of a delegate target
/// to be bound to the delegate's lifetime.
///
/// The WeakDelegateFactory is used for the the same purpose as WeakEventHandler, but
/// can work for any delegate type. Is uses Reflection.Emit to create an exact replica
/// of the WeakEventHandler class, but for the specific delegate type.
/// </summary>
public static class WeakDelegateFactory
{
private static AssemblyBuilder theDelegatesAsm;
private static ModuleBuilder theModuleBuilder;
public static TDelegate Create<TDelegate>(TDelegate targetDelegate, Action<TDelegate> unregisterDelegate) where TDelegate : class
{
EnsureBuildersCreated();
var type = targetDelegate.GetType();
if (type.GetMethod("Invoke").ReturnType != typeof(void))
throw new ArgumentException("Weak delegates can only be created for delegates with void return type.", "targetDelegate");
// create the class builder that will give birth to this weak delegate class
var className = "WeakDelg->" + GetPrettyName(type);
var classType = theModuleBuilder.GetType(className)
?? CreateWeakDelegateClass(targetDelegate, type, className);
var ctor = classType.GetConstructors()[0];
var weakDelg = ctor.Invoke(new object[] { targetDelegate, unregisterDelegate });
return (TDelegate) (object) Delegate.CreateDelegate(type, weakDelg, classType.GetMethod("Invoke"));
}
private static Type CreateWeakDelegateClass<TDelegate>(TDelegate targetDelegate, Type type, string className) where TDelegate : class
{
var classBuilder = theModuleBuilder.DefineType(className, TypeAttributes.Class | TypeAttributes.Public);
// create the class fields for the unbound delegate, the weak reference to the delegate's target and an unregister callback
var weakRefFld = classBuilder.DefineField("myWeakRef", typeof(WeakReference), FieldAttributes.Private);
MethodBuilder unboundDelgInvoke;
var unboundDelgType = CreateUnboundDelegateType(classBuilder, targetDelegate, out unboundDelgInvoke);
var unboundDelgFld = classBuilder.DefineField("myUnboundDelg", unboundDelgType, FieldAttributes.Private);
var unregisterDelgFld = classBuilder.DefineField("myUnregisterDelg", typeof(Action<TDelegate>), FieldAttributes.Private);
// create constructor
DefineConstructor<TDelegate>(type, classBuilder, unboundDelgType, weakRefFld, unboundDelgFld, unregisterDelgFld);
// create the method that implements the weak delegate calling
DefineInvokeMethod(targetDelegate, classBuilder, weakRefFld, unboundDelgFld, unregisterDelgFld, unboundDelgInvoke);
unboundDelgType.CreateType();
return classBuilder.CreateType();
}
private static void DefineConstructor<TDelegate>(Type delegateType, TypeBuilder classBuilder, TypeBuilder unboundDelgType, FieldBuilder weakRefFld, FieldBuilder unboundDelgFld, FieldBuilder unregisterDelgFld)
{
var target = delegateType.GetProperty("Target");
// create the constructor; it initializes the unbound delegate and the weak reference
var constructor = classBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.HasThis,
new[] { delegateType, typeof(Action<TDelegate>) });
var ctorIl = constructor.GetILGenerator();
// call object.ctor()
ctorIl.Emit(OpCodes.Ldarg_0);
ctorIl.Emit(OpCodes.Call, typeof(object).GetConstructor(new Type[] { }));
// store a WeakReference to the delegate's target into a field
ctorIl.Emit(OpCodes.Ldarg_0);
ctorIl.Emit(OpCodes.Ldarg_1);
ctorIl.Emit(OpCodes.Callvirt, target.GetGetMethod());
ctorIl.Emit(OpCodes.Newobj, typeof(WeakReference).GetConstructor(new[] { typeof(object) }));
ctorIl.Emit(OpCodes.Stfld, weakRefFld);
// create an unbound delegate type from the given delegate's method
ctorIl.Emit(OpCodes.Ldarg_0); // for stfld
ctorIl.Emit(OpCodes.Ldtoken, unboundDelgType);
ctorIl.Emit(OpCodes.Call, typeof(Type).GetMethod("GetTypeFromHandle", new[] { typeof(RuntimeTypeHandle) })); // first parameter; load the unbound delegate's type
ctorIl.Emit(OpCodes.Ldnull); // second parameter; no instance given, i.e. treat it as a static method
ctorIl.Emit(OpCodes.Ldarg_1); // third parameter to CreateDelegate
ctorIl.Emit(OpCodes.Callvirt, typeof(Delegate).GetProperty("Method").GetGetMethod());
ctorIl.Emit(OpCodes.Call, typeof(Delegate).GetMethod("CreateDelegate", new[] { typeof(Type), typeof(object), typeof(MethodInfo) }));
ctorIl.Emit(OpCodes.Castclass, unboundDelgType);
ctorIl.Emit(OpCodes.Stfld, unboundDelgFld);
// store the unregister callback
ctorIl.Emit(OpCodes.Ldarg_0);
ctorIl.Emit(OpCodes.Ldarg_2);
ctorIl.Emit(OpCodes.Stfld, unregisterDelgFld);
ctorIl.Emit(OpCodes.Ret);
}
private static void DefineInvokeMethod<TDelegate>(TDelegate targetDelegate, TypeBuilder classBuilder, FieldBuilder weakRefFld, FieldBuilder unboundDelgFld, FieldBuilder unregisterDelgFld, MethodBuilder unboundDelgInvoke) where TDelegate : class
{
var targetType = ((Delegate) (object) targetDelegate).Target.GetType();
var unregisterDelgType = typeof(Action<TDelegate>);
var delegateType = targetDelegate.GetType();
var delegateSignature = delegateType.GetMethod("Invoke");
var delgParamTypes = GetParameterTypes(delegateSignature);
var invoker = classBuilder.DefineMethod("Invoke", MethodAttributes.Public, delegateSignature.ReturnType, delgParamTypes.ToArray());
var invokerIl = invoker.GetILGenerator();
var targetLocal = invokerIl.DeclareLocal(targetType);
var endLabel = invokerIl.DefineLabel();
var targetIsNullLabel = invokerIl.DefineLabel();
// get Target from weak reference
invokerIl.Emit(OpCodes.Ldarg_0);
invokerIl.Emit(OpCodes.Ldfld, weakRefFld);
invokerIl.Emit(OpCodes.Callvirt, typeof(WeakReference).GetProperty("Target").GetGetMethod());
invokerIl.Emit(OpCodes.Isinst, targetType);
invokerIl.Emit(OpCodes.Castclass, targetType);
invokerIl.Emit(OpCodes.Stloc, targetLocal);
invokerIl.Emit(OpCodes.Ldloc, targetLocal);
invokerIl.Emit(OpCodes.Brfalse_S, targetIsNullLabel);
// the target is not null - call the unbound delegate
invokerIl.Emit(OpCodes.Ldarg_0);
invokerIl.Emit(OpCodes.Ldfld, unboundDelgFld);
invokerIl.Emit(OpCodes.Ldloc, targetLocal);
invokerIl.Emit(OpCodes.Ldarg_1);
invokerIl.Emit(OpCodes.Ldarg_2);
invokerIl.Emit(OpCodes.Callvirt, unboundDelgInvoke);
invokerIl.Emit(OpCodes.Ret);
invokerIl.MarkLabel(targetIsNullLabel);
// target was null, call unregister
invokerIl.Emit(OpCodes.Ldarg_0);
invokerIl.Emit(OpCodes.Ldfld, unregisterDelgFld);
invokerIl.Emit(OpCodes.Brfalse_S, endLabel); // check if unregister is null
invokerIl.Emit(OpCodes.Ldarg_0);
invokerIl.Emit(OpCodes.Ldfld, unregisterDelgFld);
invokerIl.Emit(OpCodes.Ldarg_0);
invokerIl.Emit(OpCodes.Ldftn, invoker);
invokerIl.Emit(OpCodes.Newobj, delegateType.GetConstructor(new[] { typeof(object), typeof(IntPtr) })); // create delegate from invoke
invokerIl.Emit(OpCodes.Callvirt, unregisterDelgType.GetMethod("Invoke")); // call unregister
invokerIl.Emit(OpCodes.Ldarg_0);
invokerIl.Emit(OpCodes.Ldnull);
invokerIl.Emit(OpCodes.Stfld, unregisterDelgFld); // nullify unregister
invokerIl.MarkLabel(endLabel);
invokerIl.Emit(OpCodes.Ret);
}
private static void EnsureBuildersCreated()
{
if (theDelegatesAsm == null)
{
theDelegatesAsm = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("WeakDelegates"), AssemblyBuilderAccess.Run);
theModuleBuilder = theDelegatesAsm.DefineDynamicModule("WeakDelegates");
}
}
/// <summary>
/// Creates an unbound delegate type from the type of a normal delegate
/// </summary>
private static TypeBuilder CreateUnboundDelegateType(TypeBuilder parentClass, object boundDelegate, out MethodBuilder outInvokeMethod)
{
var invokeMethod = boundDelegate.GetType().GetMethod("Invoke");
var targetType = ((Delegate) boundDelegate).Target.GetType();
return CreateDelegateType(parentClass, invokeMethod.ReturnType, new[] { targetType }.Concat(GetParameterTypes(invokeMethod)).ToArray(), out outInvokeMethod);
}
private static IEnumerable<Type> GetParameterTypes(MethodInfo mi)
{
return from p in mi.GetParameters() select p.ParameterType;
}
/// <summary>
/// Basically does what the compiler does when it has to compile a delegate statement.
/// Creates the delegate as a nested type.
/// </summary>
/// <returns>A class derived from System.MulticastDelegate that can be called with the given parameters</returns>
private static TypeBuilder CreateDelegateType(TypeBuilder parentClass, Type returnType, Type[] parameters, out MethodBuilder outInvokeMethod)
{
// from Joel Pobar's CLR weblog
// Creating delegate types via Reflection.Emit
// http://blogs.msdn.com/joelpob/archive/2004/02/15/73239.aspx
var delgTypeBuilder = parentClass.DefineNestedType("UnboundDelegate", TypeAttributes.Class | TypeAttributes.NestedPublic | TypeAttributes.Sealed | TypeAttributes.AnsiClass | TypeAttributes.AutoClass, typeof(MulticastDelegate));
var constructorBuilder = delgTypeBuilder.DefineConstructor(MethodAttributes.RTSpecialName | MethodAttributes.HideBySig | MethodAttributes.Public, CallingConventions.Standard, new[] { typeof(object), typeof(IntPtr) });
constructorBuilder.SetImplementationFlags(MethodImplAttributes.Runtime | MethodImplAttributes.Managed);
outInvokeMethod = delgTypeBuilder.DefineMethod("Invoke", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual, returnType, parameters);
outInvokeMethod.SetImplementationFlags(MethodImplAttributes.Runtime | MethodImplAttributes.Managed);
return delgTypeBuilder;
}
private static string GetPrettyName(Type t)
{
if (t.IsGenericType)
{
return t.Namespace + '.' + t.Name + '(' + String.Join(";", (from p in t.GetGenericArguments() select p.Name).ToArray()) + ')';
}
else
{
return t.FullName;
}
}
}
Note that we use Reflection.Emit to do the onerous code generation – we could’ve used CSharpCodeProvider to the same effect, so why didn’t we? Well, CSharpCodeProvider isn’t available in Silverlight, so there. This code works in Silverlight as it does in .NET. The only inherent limitation in Silverlight (as with the code in Campbell’s article) is that the bound method must be public enough – otherwise trying to create an unbound delegate from the delegate target throws a SecurityException.
The code probably works – it’s more of a proof of concept than anything else. I’ve used it in production code, so there aren’t any train-wrecking problems in it. There is probably more stuff needed to make it work in all cases, however.
This is how you use it:
public event Action<int> SomeEvent;
private void Register()
{
SomeEvent += WeakDelegateFactory.Create(new Action<int>(Sink), e => SomeEvent -= e);
}
private void Sink(int foo)
{}
It would have been great if methods were implicitly usable as delegates. You could then write (with an appropriate extension method) SomeEvent += Sink.ToWeak(e=>SomeEvent-=e);
I must admit I’d never have thought of going the reflection route. Very impressive.
I haven’t really dared to put my solution into production properly. I think I’ve just used it in one place to test the water…
Comment by Ben — May 7, 2010 @ 6:51 am
Awesome solution, it takes me a while to get it but it’s impressive.
Great job.
Comment by raffaeu — June 11, 2010 @ 8:55 pm
[...] Generic implementation for a weak event handling http://puremsil.wordpress.com/2010/05/03/generic-weak-event-handlers/ [...]
Pingback by Customizable On Screen Keyboard (OSK) for WPF (it work for WebBrowser control) « Hmadrigal's Blog — December 27, 2011 @ 3:41 am