Introduction
In part 1 of the AttachedPropertyEvent pattern article, we described how to create an attached property and publish an IEventAggregator
event from the property. The reason for this was to avoid any code-behind in our views, and also to make sure there aren't any dependencies between our views and viewmodels.
One drawback with the implementation described in part 1 is that you need to implement an attached property for each and every event in your project. We wanted to do this in a generic way, and that's what we're going to describe here.
With our implementation, there are two ways to publish events. One is to publish an event (e.g., KeyDown
), while the other is to publish an event when some property changes.
Current limitations
With the current implementation, it is only possible to publish one event per control. This is because it's only possible to set a value to an attached property once in a control. We're working on a way to solve this. If you have a suggestion, shoot!
Implementation
First, we've implemented a class named
AttachedPropertyEvent
. This class is in our Infrastructure project so that it can be referenced by whomever. Here, we have three attached properties:
The first attached property is named "CompositeEventName
". This is the string representation of a composite event which has to be attached to each and every event that will be published. We have to have this property in order to publish the correct event. This attached property doesn't need a callback method.
public static DependencyProperty CompositeEventNameProperty =
DependencyProperty.RegisterAttached("CompositeEventName",
typeof(string),
typeof(AttachedPropertyEvent));
The next attached property is named "ListenForEvent
". Using this property, you can publish events, by specifying the name of the event.
public static DependencyProperty ListenForEventProperty =
DependencyProperty.RegisterAttached("ListenForEvent",
typeof (string),
typeof (AttachedPropertyEvent),
new PropertyMetadata(RaiseListenForEventCallback));
The callback method for this event is slightly complicated using Intermediate Language, but have a look at MSDN for a more detailed explanation: http://msdn.microsoft.com/en-us/library/system.reflection.eventinfo.addeventhandler.aspx.
We've created a method which we call from IL, instead of writing all the code in IL, named "HandleEvent
". HandleEvent
takes the composite event name and an object. It then gets the current event aggregator and uses Reflection to publish the event. We need to use Reflection here for "MakeGenericMethod
" since we only have the string representation of the composite event and not a reference to the type itself.
public static void HandleEvent(string compositeEventName, object eventArgs)
{
string compositeEventString =
string.Format("_10100.Infrastructure.Events." + compositeEventName);
Type compositeEventType = Type.GetType(compositeEventString);
IEventAggregator eventAggregator =
ServiceLocator.Current.GetInstance<IEventAggregator>();
Type eventAggregatorType = eventAggregator.GetType();
MethodInfo getEvent = eventAggregatorType.GetMethod("GetEvent");
MethodInfo method = getEvent.MakeGenericMethod(new[] { compositeEventType });
object compositeEventAggregator = method.Invoke(eventAggregator, null);
foreach (MethodInfo methodInfo in compositeEventAggregator.GetType().GetMethods())
{
Console.WriteLine(methodInfo.Name);
}
MethodInfo publishMethod = compositeEventAggregator.GetType().GetMethod("Publish");
publishMethod.Invoke(compositeEventAggregator, new[] {eventArgs});
}
Back to the callback method of the ListenForEvent
attached property. As said above, look at the MSDN link in order to understand fully what's going on here, but what we've added is the call to "HandleEvent
".
The reason for creating an assembly during runtime is because we need to call EventInfo.AddEventHandler
. This method takes a delegate, and to create this delegate, we need to know something about the event. So, by doing it this way, the delegate is being dynamically created, and we don't need to worry about what kind of event it is, what kind of EventArg
it has, and how many parameters there are.
private static void RaiseListenForEventCallback(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == null) return;
string listenForEvent = (string) GetListenForEvent(d);
string compositeEventName = (string) GetCompositeEventName(d);
Type control = d.GetType();
EventInfo eventInfo = control.GetEvent(listenForEvent);
Type eventHandlerType = eventInfo.EventHandlerType;
MethodInfo invokeMethod = eventHandlerType.GetMethod("Invoke");
ParameterInfo[] parms = invokeMethod.GetParameters();
Type[] parmTypes = new Type[parms.Length];
for (int i = 0; i < parms.Length; i++)
{
parmTypes[i] = parms[i].ParameterType;
}
AssemblyName aName = new AssemblyName { Name = "DynamicTypes" };
AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly(aName,
AssemblyBuilderAccess.Run);
ModuleBuilder mb = ab.DefineDynamicModule(aName.Name);
TypeBuilder tb = mb.DefineType("Handler",
TypeAttributes.Class | TypeAttributes.Public);
MethodBuilder handler =
tb.DefineMethod("DynamicHandler",
MethodAttributes.Public | MethodAttributes.Static,
invokeMethod.ReturnType, parmTypes);
ILGenerator il = handler.GetILGenerator();
il.EmitWriteLine(string.Format("{0} event was raised!",
eventHandlerType.Name));
Type apeMethodsType = typeof(AttachedPropertyEventMethods);
MethodInfo handleEventMethodInfo = apeMethodsType.GetMethod("HandleEvent");
il.DeclareLocal(typeof (string));
il.Emit(OpCodes.Ldstr, compositeEventName);
il.Emit(OpCodes.Stloc_0);
il.Emit(OpCodes.Ldloc_0);
il.Emit(OpCodes.Ldarg_1);
il.EmitCall(OpCodes.Call, handleEventMethodInfo,
new[] { typeof(string), typeof(object) });
il.Emit(OpCodes.Ret);
Type finished = tb.CreateType();
MethodInfo eventHandler = finished.GetMethod("DynamicHandler");
Delegate del = Delegate.CreateDelegate(eventHandlerType, eventHandler);
eventInfo.AddEventHandler(d, del);
}
The last attached property is "ListenForProperty
". Using this attached property, you can publish events when a property changes.
public static DependencyProperty ListenForPropertyProperty =
DependencyProperty.RegisterAttached("ListenForProperty",
typeof (object),
typeof (AttachedPropertyEvent),
new PropertyMetadata(
RaiseListenForPropertyCallback));
The callback method for "ListenForProperty
" uses the HandleEvent
method which is described above, giving it the composite event name and the new value of the control's property you're listening to.
private static void RaiseListenForPropertyCallback(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
if (e.NewValue == null) return;
AttachedPropertyEventMethods.HandleEvent(
(string) GetCompositeEventName(d), e.NewValue);
}
How to publish an event
After the above code is implemented, you're ready to publish events and subscribe to them. There's two things you need to do:
- Create a composite event.
- Add two lines in your XAML code.
We're creating our composite events in our namespace _10100.Infrastructure.Events
. Right now, it's hardcoded in the "HandleEvent
" method; that's where our composite events are. The only thing you need to do to create a composite event is:
public class MyCompositeEvent : CompositePresentationEvent<KeyEventArgs> {}
Now, to publish this event from XAML, do the following in a control:
<Button x:Name="btnSomeButton"
Content="My Button"
Events:AttachedPropertyEvent.CompositeEventName="MyCompositeEvent"
Events:AttachedPropertyEvent.ListenForEvent="KeyDown"
/>
Do the same when you want to listen to some property, but instead of using the attached property "ListenForEvent
", use "ListenForProperty
".
Note, you need to always have the "CompositeEventName
" before any of the two others!
Conclusion
That's it for now. It works, but it has a major drawback that you can only have one attached property per control. So, if you have any suggestions on how to get around this issue, please don't hesitate to contact us.
Another things is that we haven't done any performance testing on this yet. It seems to be running fine, but there're no guarantees. Note also that exception handling is removed from the code in this article.
Again, please give us feedback, ideas, criticism, etc... to our way of handling this. We really wish to improve it as much as we can.