Based on the logs it seems there is tremendous interest in Rx, the new .NET 4.0 interfaces that allow you to query events and asynchronous operations. I can't say I'm surprised. In my opinion Rx is the big story of .NET 4.0. I've been developing software using Rx for months now and I thought I'd share some tips and tricks that I've picked up with the community. You can get the Sivlerlight version of Rx from the binaries folder in the Silverlight Toolkit sources.
Converting Events to IObservables
One of the cumbersome things about Rx development is that events must be converted to IObservable's before they can be queried. Rx provides a static FromEvent method to help with the conversion:
var mouseMove = Observable.FromEvent<MouseEventArgs>(Application.Current.RootVisual, "MouseMover"); mouseMove.Subscribe(() => Debug.WriteLine("the mouse has been moved.");
Unfortunately putting event names (or any identifiers for that matter) in strings breaks our refactoring tools. Rx does provide another overload which accepts two actions, one that attaches the handler and another that detaches it...
class MyClass { public EventHandler<RoutedEventArgs> MyEvent; } var myClass = new MyClass(); var myClassEvent = Observable.FromEvent<RoutedEventArgs>(handler => myClass.MyEvent += handler, handler => myClass.MyEvent -= handler); mouseMove.Subscribe(() => Debug.WriteLine("the mouse has been moved.");
...but not only is this overload rather more verbose, it only works if the delegate type of your event is the generic EventHandler<T>. Unfortunately most events use custom delegate types because in .NET 1.0 generics did not exist.
**Updated: There is an overload which I missed that accepts the delegate type:
var myButton = new Button(); var myButton = Observable.FromEvent<RoutedEventHandler, RoutedEventArgs>(handler => myClass.Click += handler, handler => myClass.Click -= handler);
So what's the best way of exposing events as IObservables?
Extension Events
If you're doing Rx development in C# it's good practice to create extension methods for each event you would like to query. You can think of these methods as extension events. To demonstrate let's create a static class with extension methods that expose the events on Silverlight/WPF's UIElement class as IObservables.
internal static class UIElementExtensions { public static IObservable<Event<MouseButtonEventArgs>> GetMouseLeftButtonDown(this UIElement that) { return Observable.FromEvent<MouseButtonEventArgs>(that, "MouseLeftButtonDown"); } public static IObservable<Event<MouseButtonEventArgs>> GetMouseLeftButtonUp(this UIElement that) { return Observable.FromEvent<MouseButtonEventArgs>(that, "MouseLeftButtonUp"); } public static IObservable<Event<MouseEventArgs>> GetMouseLeave(this UIElement that) { return Observable.FromEvent<MouseEventArgs>(that, "MouseLeave"); } public static IObservable<Event<MouseEventArgs>> GetMouseEnter(this UIElement that) { return Observable.FromEvent<MouseEventArgs>(that, "MouseEnter"); } }
Unfortunately our event names are stored in strings, but at least they are in one place. Now that we've created extension methods that expose IObservables we can create more complex events by sequencing these primitive events.
Sequencing Events
The most exciting thing about Rx is that it enables you build complex events from a sequence of primitive events. Let's create an observable that fires when the sequence of keys "a", "b", "c" is pressed:
// create an event that listens for key presses and returns the // key pressed. IObservable<Key> keyPress = Observable.FromEvent<KeyEventArgs>(Application.Current.RootVisual, "KeyUp") .Select(ev => ev.EventArgs.Key); // Create a helper function for creating observables that fire // when a specific key is pressed. Func<Key, IObservable<Key>> pressedIs = key => keyPress.Where(pressedKey => pressedKey == key); // Create a helper function for creating observables that fire // when a key other than a specific key is pressed. Func<Key, IObservable<Key>> pressedIsNot = key => keyPress.Where(pressedKey => pressedKey != key); IObservable<Unit> abcPressed = // Always listen for a key press "A" because it is the start of the sequence. // Each time the "A" key is pressed we being matching the sequence again. from firstKeyPressEvent in pressedIs(Key.A) // After "A" is pressed when only want to wait for a single "B" key press. // If any other key is pressed we start at the beginning and wait for "A" from secondKeyPressEvent in pressedIs(Key.B).Take(1).Until(pressedIsNot(Key.B)) // After "B" is pressed when only want to wait for a single "C" key press. // If any other key is pressed we start at the beginning and wait for "A" from thirdKeyPressEvent in pressedIs(Key.C).Take(1).Until(pressedIsNot(Key.C)) // I could return the string "abc" here but we know what exactly what keys // were pressed because this event is so specific. I really don't have anything // to return but all queries must return something. In cases like this we return the // Unit value. It's like returning null, but it has a type and can't cause a null // reference exception. select new Unit(); abcPressed.Subscribe(()=> Debug.WriteLine("ABC was pressed."));
Just as in Linq to Objects, multiple uses of the "from" keyword are translated into nested calls to the SelectMany extension method. The abcPressed query above could also be coded this way:
IObservable<Unit> abcPressed = pressedIs(Key.A) .SelectMany( firstKeyPressEvent => pressedIs(Key.B).Take(1).Until(pressedIsNot(Key.B)) .SelectMany( secondKeyPressEvent => pressedIs(Key.C).Take(1).Until(pressedIsNot(Key.C)))) .Select(_ => new Unit());
Building a "Click" Extension Event
Now that we know how to sequence events let's use Rx to add a "Click" extension event to all instances of UIElement - a class from which all Controls inherit. The sequence of events that constitute a click event are a little more complex than one might think. In a nutshell we consider a click event on UIElement "A" has occurred if we match this sequence of events...
1. MouseLeftButtonDown over UIElement "A"
2. MouseLeftButtonUp over UIElement "A"
...or this sequence of events...
1. MouseLeftButtonDown over UIElement "A"
2. MouseLeave UIElement "A"
3. MouseEnter UIElement "A"
4. MouseLeftButtonUp over UIElement "A"
...but NOT this sequence of events:
1. MouseLeftButtonDown over UIElement "A"
2. MouseLeave UIElement "A"
3. MouseEnter UIElement "B"
3. MouseLeftButtonUp over UIElement "B"
3. MouseLeftButtonDown over UIElement "B"
2. MouseLeave UIElement "B"
3. MouseEnter UIElement "A"
4. MouseLeftButtonUp over a UIElement "A"
Let's add a GetClick extension event to the UIElementExtensions class we defined earlier.
public static IObservable<Event<MouseButtonEventArgs>> GetClick(this UIElement that) { return that // wait for any mouse left down event .GetMouseLeftButtonDown() .SelectMany( mouseLeftButtonDownEvent => // then wait for a single mouse left up event that .GetMouseLeftButtonUp() .Take(1) .Until( // We want to merge two different stop conditions... Observable.Merge( // stop listening if the mouse goes outside // the silvleright plug-in Application.Current.RootVisual.GetMouseLeave() // We return unit so that we have the // same type as the other observable // we want to merge with .Select(_ => new Unit()), // stop listening if the mouse goes outside the // element and the mouse is released. that .GetMouseLeave() .SelectMany( mouseLeaveEvent => // stop waiting for a mouse left up event // if the mouse leaves the element and the // button is released. // By listening for the event at the Root // Visual we ensure that we will get all // MouseLeftButtonUp events because this // event bubbles up. Application.Current.RootVisual .GetMouseLeftButtonUp() .Take(1) // Return unit so that we can merge .Select(_ => new Unit()) // don't cancel if the mouse enters // the element over which the mouse // was depressed. .Until(that.GetMouseEnter()))))); }
That's it! Now every single instance of UIElement has a Click event we can subscribe to:
var rectangle = new Rectangle { Width = 100, Height = 100, Fill = new SolidColorBrush(Colors.Red) }; rectangle.GetClick().Subscribe(() => Debug.WriteLine("The rectangle was clicked.");
More Reliable Code, and Less of It
Rx allows you to write complex, asynchronous code declaratively. If you master it you'll never have to explicitly unhook a handler from an event again. You also wont ever have to maintain an error-prone collection of state variables in order to ascertain whether a sequence of events has occurred in a particular order.
*Edit: Here's the code!
16 comments:
This is, as I read somewhere "plain cool".
I've tried your for the mouse button events (Down, Up and Click) but don't seem to work.
Observable.FromEvent work with MouseEnter, MouseLeave and MouseWheel but fail to Subscribe with MouseLeftButtonDown and MouseLeftButtonUp.
Any ideas what might be happening?
BTW, using SL3.
Nope. But here's the code in a Silverlight 3 project. :-)
http://cid-459b7369c1a390f3.skydrive.live.com/self.aspx/.Public/Unfold/ExtensionEvents.zip
Many thanks, Jafar,
I found out what was happenning and the thing is I was testing this in Button. As the Button control should cancel the mouse button events to prevent bubbling, these events are not fired. And Observable.FromEvent invokes event AddMethod afterwards.
All clear now.
I downloaded the code, and ran it, but I think there may be a logic problem. If I mouse down inside the rectange, then mouse up outside it, it correctly does not fire the event. However, if after that, I mouse down and up inside the rectangle, the event seems to fire twice.
Really nice article
I think you can get rid of having event names stored as strings by using expressions
for example, instead of:
Observable.FromEvent<MouseButtonEventArgs>(that, "MouseLeftButtonDown");
you can use:
Observable.FromEvent<MouseButtonEventArgs>(that, x=> x.MouseLeftButtonDown += null);
Given that FromEvent is defined as:
IObservable FromEvent(UIElement element, Expression<Action<UIElement>> event);
You can then extract the event name from the Expression and get rid of the magic string.
chrisaswain:
Good catch. When I moved this code from one project to the sample app there was a difference I missed. Unlike the original project the root visual in the sample project is transparent. This means it doesn't receive mouse events.
I've altered the code above (as well as the sample) to stop listening as soon as the mouse leaves the root visual. This is necessary because we can only detect mouse events against a particular element.
The code is more robust now. Good catch.
shady:
Nice try but no cigar :-). Expressions can't contain assignment operators. The creators of Rx looked at a lot of solutions here but I think we're stuck frankly. I hope that going forward we stop inventing custom delegates everyone uses the generic EventHandler.
Hi Jafar,
Since events on DependencyObjects are defined using a static "RoutedEvent" property, could that be used to generate the corresponding IObservable object? Something like:
var checked = checkBox1.GetObservableEvent<RoutedEventArgs>(CheckBox.CheckedEvent);
That could use the "Name" property on RoutedEvent to give you a more "strongly typed" version, but of course it would only be valid for DependencyObjects.
Good thinking mabster. Unfortunately the RoutedEvent object doesn't expose a crucial piece of information: The type of the event handler. It really would've been nice if there was a property exposing this value.
You can still use this method and just assume the prefix (minus the "Event") is the name. That will save you putting the event name in a string but it still relies on convention.
Ok I can't get this to work with buttons since buttons do not bubble events.
And I can't get it to work well for elements that bubble events since I would want to cancel that bubbling of the event once I handle it.
Is there something I am missing or is this something that will take a bit of work to get around?
I see that there is not explicit event handler += and -=, but when does it actually happen in this code?
So take the ABC example, I assume that the eventhandler is not used until Subscribe() is called on the Observable. At that point, the keypress must get registered (element.KeyDown += ...) at least once.
- Does each clause of the SelectMany end up registering? If not, how does the sharing of the even handler happen?
- When does unregistration happen? If there is no explicit unsubscribe, does the ABC example handle the AABC case? Does it handle the ABABC case? Does it handle the ABCABC case?
- Finally, how does cleanup of these happen? If I have an Observable Subscribed, then it must be registered for an event handler. What if I want to Dispose the Observable and get it to stop listening?
Thanks, I know these are basic but it's hard to get the details straight when i can't run the code.
Post a Comment