Been doing a little coding tonight and I found myself once again needing to refer to references to figure out how to declare a WPF RoutedEvent. Getting this done is a four step process in your code, each of the steps being fairly boiler plate (and hence I hope they eventually roll this into the language / CLR along with dependency properties):
- Declare a static RoutedEvent field in your class.
- Register the event during the execution of a static constructor for your class.
- Declare and event Property to allow clients to connect / disconnect from the event
- Raise the event at some point(s) in your code.
Step 1 declares the member that defines the event any by which other methods (such as the RaiseEvent method) can refer to the event that you are declaring. This has the following syntax:
private static readonly RoutedEvent _NodeCreatedEvent;This member is static and read only as there only needs to be one instance and its value should not be changed. Basically, this is initialized in step 2 and not changed by your code at any other point in time.
Step 2 initializes the RoutedEvent field. This is done within a static constructor in the class containing the RoutedEvent field:
static ForceDirectedLayoutControl()
{
_NodeCreatedEvent =
EventManager.RegisterRoutedEvent(
"NodeCreated",
RoutingStrategy.Bubble,
typeof(RoutedEventHandler),
typeof(ForceDirectedLayoutControl));
}A static constructor is executed once and only once by the runtime prior to the class being used in any capacity by other objects to initialize static members to non-default values. This is actually a quite nice feature of C# as this was particularly troublesome in C++ programming prior to .NET.
The code in this constructor utilizes the EventManager in WPF to inform it that we want to declare a routed event to be processed by WPF. We give it a string giving the name of the event (which will also show up in XAML intellisense), the routing technique for the event (which I've only used bubble), the type of handler needed for the event (basically defining the signature of the event handling method - which you can see auto generated by the XAML designer), the the type of the class enclosing the event. Once this method is executed, the runtime will then know how to respond to a RaiseEvent method call on a RoutedEvent object in this type of class.
Step 3 is to declare a property that allows the clients to connect / disconnect from the event. Basically, this allows them to become a "observer" as in the observer/subject GoF design pattern:
public event RoutedEventHandler NodeCreated
{
add { AddHandler(_NodeCreatedEvent, value); }
remove { RemoveHandler(_NodeCreatedEvent, value); }
}A bit on naming. I like to name by event field <EventName>Event, and register it as "<EventName>", and provide a Property with the name <EventName>.
With this in place, you now can raise an event in your class. This is done as follows:
RaiseEvent(new RoutedEventArgs(Button.ClickEvent, this));
When this is called, if there are any observers the WPF runtime will bubble up this event args object.
A variation on this pattern is if you want to pass extra data to the observers as the RoutedEventArgs class only has default fields and no custom ones for your use. To pass data up you need just subclass the RoutedEventArgs class, provide your own fields in that class, and replace the new in the RaiseEvent class with an instance of your subclass.
As an example, in this code I actually want anyone interested to be able to pass back to the code raising the event (through the event args object) a UI element to be displayed by this code. To get this done I declare a class as follows:
public class NodeCreatedEventArgs : RoutedEventArgs
{
public INode Node { get; set; }
public UIElement Element { get; set; }
public NodeCreatedEventArgs(RoutedEvent routedEvent) : base(routedEvent)
{
}
}This is a fairly simple extension to the RoutedEventArgs object as it just declares two properties, the Node field (which is a domain object passed to the observer), and the Element field that is passed back from the observer. You need to pass in the routed event and pass it to the base class as this is how the runtime determines which event to route this to.
Note that this assumes that there is only ONE observer, which works in this model. If there is a need for more than one then we would remove the Element field and make it the responsibility of an observer to call back into the subject to pass the Element to it, in which case it would also need the Node so that the subject object could match up the call. And also note that in this case the actual reply should probably only be done by a single observer (it's fine if multiple listen). The point is, this event is intended to be handled by only one observer, and that observer should assign true to the Handled property of the RoutedEventArgs to prevent it from bubbling further.
Now, the RaiseEvent call becomes:
NodeCreatedEventArgs args = new NodeCreatedEventArgs(_NodeCreatedEvent)
{
Node = node
};
RaiseEvent(args);
Notice the use of the new C# object initialize syntax. Those are so nice! Also note the passing in on the constructor of the previously created routed event object. This is how the runtime will know which event to route as you should notice that RaiseEvent does not actually have an event parameter as it is passed in through the event arguments object.
To handle the event, there are ways to do this in XAML and in C#. In this case we'll use XAML:
<l:ForceDirectedLayoutControl x:Name="fdlc" NodeCreated="fdlc_NodeCreated" />Notice now that since we declared that property the XAML picks up on it and we can declare a method to handle it (actually it's generated by the designer automatically) as follows:
private void fdlc_NodeCreated(object sender, RoutedEventArgs e)
{
}
Obviously this method doesn't do a lot for us, so we need to add some code to it:
private void fdlc_NodeCreated(object sender, RoutedEventArgs e)
{
ForceDirectedLayoutControl.NodeCreatedEventArgs ncea =
e as ForceDirectedLayoutControl.NodeCreatedEventArgs;
Ellipse ellipse = new Ellipse();
ellipse.Width = 20;
ellipse.Height = 20;
ellipse.Fill = Brushes.Green;
ncea.Element = ellipse;
e.Handled = true;
}Here I cast the incoming RoutedEventArgs object to the type that is actually being passed so that the new data that is passed can be accessed (Element and Node fields). I create an ellipse that will be displayed by the classes and pass it back in the element field. Lastly, I stop the bubbling of the event. Note that at this point I don't actually use the Node field, but this will in the future be used to make a dynamic decision on what type of UIElement to create.