Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Achieve Loose Coupling with the WPF/Silverlight EventBroker

0.00/5 (No votes)
8 Jun 2010 1  
Using attached properties in WPF or Silverlight to loosely couple UI element events to arbitrary code.

Introduction

Are you using the MVVM pattern? Are you binding ICommands to your button-based controls and handling the events in your view model? Have you seen the RelayCommand? It's pretty cool, and I like that it lets you enable/disable your buttons by implementing a CanExecute predicate. Although I'm not trying to discourage you from using the ICommand approach to loose-coupling, you may however want to use (or maybe just learn about) another option.

This article is my first contribution to the community. I've created what I think is a nifty class and I'd like to share it with everyone. Perhaps someone will find it useful.

Note

The EventBroker class I'm going to talk about makes use of attached properties. If you're not familiar with these or just need a refresher, you can read about them on MSDN here.

Using the Code

First, we will see how to use the EventBroker. It's really simple. Afterwards, for those who stick around, I will show what the code looks like.

So let's get started...

  • Create a new WPF or Silverlight application and include EventBroker.cs in the project.
  • If you chose WPF, open your MainWindow.xaml file. If you chose Silverlight, open your MainPage.xaml file.
  • At the top of the XAML file, add a reference to the EventBroker's namespace.

    Bobeck_EventBroker_Image1.png

  • Edit the XAML to include a rectangle. We are going to handle the rectangle's MouseLeftButtonDown event.
  • To the rectangle, add the MouseLeftButtonDown attached property. The value can be whatever string you want (more on that later).

    Bobeck_EventBroker_Image2.png

  • Find a class that you would like to receive event notifications. I will choose the MainWindow class for this example. (Of course, putting the EventBroker handler in the view's code-behind doesn't buy you anything since you could always do that. The beauty of the EventBroker is that you can handle the events in any arbitrary code!)
  • Include the EventBroker's namespace at the top of the file.
    using Bobeck.Windows.Events;
  • In the constructor for the MainWindow class (or other appropriate location), attach to the one-and-only event published by the EventBroker.
    EventBroker.EventHandler += 
    	new EventBroker.EventHandlerDelegate(EventBroker_EventHandler);
  • To handle the event, we must provide an event handler whose signature matches the EventBroker's delegate...
    void EventManager_EventHandler(object sender, EventArgs e, 
    	EventBroker.EventType eventType, string eventKey)
    {
        MessageBox.Show("eventKey = " + eventKey);
    }

    Bobeck_EventBroker_Image3.png

And that's it! Pretty simple, right! As this application runs, whenever you click inside the red rectangle, the above event handler will execute. Notice the arguments. The eventType will indicate the type of event (in this case EventBroker.EventType.MouseLeftButtonDown). The eventKey argument will be the string you assigned to the attached property in XAML. (in this case "Click!"). You can use either (or both) of these values for filtering. Unless you are using the EventBroker for something like event logging, you would likely want to qualify the eventKey before you react, as there may be many other controls that fire the same MouseLeftButtonDown event and you will probably want to react to each one differently.

So that brings up a point I want to make... When you attach to the EventBroker's EventHandler in the above fashion, you are essentially subscribing to a broadcast channel. All events processed by the EventBroker are broadcasted to all clients that have attached.

But, the EventBroker also provides an API that allows clients to "Register" for events. This puts the burden on the EventBroker to route events to the appropriate handlers. To register for an event, the code looks like this...

EventBroker.Register(EventBroker.EventType.MouseLeftButtonDown, 
	"Click!", EventBroker_EventHandler);

And when you're done listening to the event, you can "Unregister" (make sure each parameter is the same as those used to register):

EventBroker.Unregister(EventBroker.EventType.MouseLeftButtonDown, 
	"Click!", EventBroker_EventHandler);

So, broadcasting or routing... You can choose the implementation that best fits your needs. The following list shows the events that are supported as of this writing.

  • KeyUp (FrameworkElement)
  • KeyDown (FrameworkElement)
  • MouseEnter (FrameworkElement)
  • MouseLeave (FrameworkElement)
  • MouseLeftButtonUp (FrameworkElement)
  • MouseLeftButtonDown (FrameworkElement)
  • MouseRightButtonUp (FrameworkElement)
  • MouseRightButtonDown (FrameworkElement)
  • MouseMove (FrameworkElement)
  • MouseWheel (FrameworkElement)
  • SelectionChanged (Selector & TextBox)
  • Click (ButtonBase)
  • CheckedChanged (ToggleButton)
  • ValueChanged (RangeBase)

Under the Hood

The EventBroker is a static class that implements a number of attached properties -- one for each event actually. The plumbing for these looks pretty typical. Here is the implementation for the MouseLeftButtonDown attached property. We also must provide CLR get() & set() methods so we can use these properties in XAML.

    public static readonly DependencyProperty MouseLeftButtonDownProperty =
        DependencyProperty.RegisterAttached("MouseLeftButtonDown",
        typeof(string),
        typeof(EventBroker),
        new PropertyMetadata(String.Empty, OnMouseLeftButtonDownChanged)
    );

    public static string GetMouseLeftButtonDown(DependencyObject obj)
    {
        return (string)obj.GetValue(MouseLeftButtonDownProperty);
    }

    public static void SetMouseLeftButtonDown(DependencyObject obj, string value)
    {
        obj.SetValue(MouseLeftButtonDownProperty, value);
    }

For each attached property definition, we provide a callback to give us a chance to hook up to the framework element's actual event interface. That code looks like this...

    private static void OnMouseLeftButtonDownChanged(DependencyObject obj, 
	DependencyPropertyChangedEventArgs e)
    {
        FrameworkElement target = obj as FrameworkElement;
        if (target == null) return;

        string newValue = e.NewValue as string;
        string oldValue = e.OldValue as string;

        if (!String.IsNullOrEmpty(newValue) && String.IsNullOrEmpty(oldValue))
        {
            target.MouseLeftButtonDown += 
		new MouseButtonEventHandler(Proxy_MouseLeftButtonDown);
            target.Unloaded += new RoutedEventHandler(Proxy_UnLoaded);
        }
        else if (String.IsNullOrEmpty(newValue) && !String.IsNullOrEmpty(oldValue))
        {
            target.MouseLeftButtonDown -= 
		new MouseButtonEventHandler(Proxy_MouseLeftButtonDown);
            target.Unloaded -= new RoutedEventHandler(Proxy_UnLoaded);
        }
    }

First we coerce the DependencyObject obj into a FrameworkElement, which is the lowest common denominator control class that supports the mouse events. If the attached property is being set, then we attach to the control's MouseLeftButtonDown event. If the attached property is being cleared, then we detach from the control's MouseLeftButtonDown event. We also take this opportunity to attach/detach to/from the control's Unloaded event. This is monitored so we can detach our handlers from the control when the control gets unloaded. If we didn't do this, then the control would not get garbage collected. Yikes!

As you can see from the code above, whenever a MouseLeftButtonDown event occurs, the EventBroker's Proxy_MouseLeftButtonDown handler will execute. Each event type has its own Proxy_XXX handler. Here is an example of one...

    private static void Proxy_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        string eventKey = GetMouseLeftButtonDown(sender as DependencyObject);
        Fire_EventHandler(sender, e, EventType.MouseLeftButtonDown, eventKey);
    }

Each Proxy_XXX handler then calls a common method named Fire_EventHandler, passing the eventType and eventKey.

    private static void Fire_EventHandler
	(object sender, EventArgs e, EventType eventType, string eventKey)
    {
        // Step 1: Broadcast to all those connected to our EventHandler
        if (EventHandler != null)
        {
            EventHandler(sender, e, eventType, eventKey);
        }

        // Step 2: Send to matching registered handlers
        List contracts = _Contracts.Where(c => (c.EventType == eventType) &&
                                                      (c.EventKey == eventKey)).ToList();
        foreach (EventContract contract in contracts)
        {
            if (contract.EventHandler != null)
            {
                contract.EventHandler(sender, e, eventType, eventKey);
            }
        }                                                     
    }

The Fire_EventHandler method is responsible for broadcasting the event data to all clients/listeners, and to route the event data only to those interested (registered) parties. Internally, the EventBroker maintains a list of contracts that map the registered client handlers to event types and event keys (those strings set in XAML).

Note: At the time of this writing, the SelectionChanged event only works with ListBox and ComboBox controls, which are common to both WPF and Silverlight, and not TabControls, ListViews or DataGrids.

Well, I think that covers just about everything. I hope you find the EventBroker useful. And if not, maybe you found an educational nugget in the article somewhere. Too-da-loo for now and happy coding!

History

  • June 6, 2010 Created the article - DAB
  • June 8, 2010 Updated source code to handle RangeBase and ToggleButton - DAB

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here