F# First Class Events – Async Workflows + Events Part II
So far in this series, I’ve covered a bit about what first class events are in F# and how you might use them. In the first post, we looked at what a first class events mean and some basic combinators in order to compose events together. In the second post, we looked at how we might create events and publish them to the world through classes. And in the third post I talked about how to manage the lifetime of a subscription. In the fourth installment, I corrected my usage of the old create function and instead to use the Event class to create, trigger and publish events. Last time, we’ll look at how we can use first class events inside Async Workflows in order to do such items as tracking state. This time, let’s look at how we could use the Async Workflows together with events in order to draw on a WPF window. Before we get started, let’s get caught up to where we are today.
- Part 1 – First Class Events
- Part 2 – Creating Events
- Part 3 – Creating and Disposing Handlers
- Part 4 – Changes on Creating Events
- Part 5 – Async Workflows + Events Part I
Drawing the Easy Way
In our last post, we had a simple click tracker, which in itself is interesting, but not all that useful. This time, let’s take another approach and create a little drawing application in WPF using the async workflows and events together. In order to do so, let’s get a little infrastructure out of the way before we get to the real heart of the matter. First, we need the ability to grab the position of our mouse at any given time. In order to do so, we’ll get the position relative to our given input element.
open System.Threading open System.Windows open System.Windows.Controls open System.Windows.Input open System.Windows.Markup open System.Windows.Media open System.Windows.Shapes let getPosition (e : #IInputElement) (args : #MouseEventArgs) = let pt = args.GetPosition e (pt.X, pt.Y)
This code is rather straight forward which simply gets the position of our mouse relative to our input element. Next, we need to create an event which fires while our left mouse button is down and our mouse is moving. In order to do so, we’ll merge the MouseLeftButtonDown and MouseMove events from our given input element so that it only fires when both happen. After we define the event, we’ll create an extension method on the IInputElement interface to expose this event.
let createMouseTracker (e : #IInputElement) = e.MouseLeftButtonDown |> Event.map (fun args -> args :> MouseEventArgs) |> Event.merge e.MouseMove |> Event.filter (fun args -> args.LeftButton = MouseButtonState.Pressed) |> Event.map (getPosition e) type System.Windows.IInputElement with member this.MouseTrack = createMouseTracker this
The function we defined above first captures the MouseLeftButtonDown event, and then we want to downcast the MouseButtonEventArgs down to the MouseEventArgs so that we can merge it with the MouseMove event, due to the fact that both events must have the same type of arguments. After that, we ensure that we’re firing only when the left mouse button is down, and then finally we map our mouse event arguments to obtain only the x and y coordinates.
After defining our event and exposing it as an extension method, we need a way to draw a line from one coordinate to the next. In order to do so, we’ll simply need our from coordinates, our to coordinates and our element which holds it.
let drawLine stroke (x1, y1) (x2, y2) (e : #IAddChild) = let line = new LineGeometry(StartPoint = Point(x1, y1), EndPoint = Point(x2, y2)) let path = new Path(Stroke = stroke, StrokeThickness = 5., Data = line) e.AddChild(path)
Our draw line function from above simply creates a LineGeometry with our starting and end points, then we create a Path to store our line and brush data, and finally add it to our element. Now, to the heart of the matter. How might we track our mouse using these Async Workflows and events together? In the last post, we looked at using the recursive loops to store state, such as the number of times a button was clicked. This time, we can store the previous coordinates as our loop argument, but in order to make this work, we need to fire it one time and then initialize the loop. Let’s look at the code and go into detail below:
let trackMouse (e : #UIElement) (guiContext : SynchronizationContext) = let rec firstEvent () = async { let! args = Async.AwaitEvent e.MouseTrack return! loop args } and loop prev = async { let! current = Async.AwaitEvent e.MouseTrack do! Async.SwitchToGuiThread guiContext do drawLine Brushes.Red prev current e do! Async.SwitchToThreadPool() return! loop current } firstEvent()
As you can see, our track mouse function takes our element and a synchronization context. This context allows us to switch back and forth from the thread pool to the GUI thread at any point, which can be crucial for UI rendering. Our first function initializes the loop function by getting the first instance of the loop with the first coordinates. In the loop function, we await our instance of the event, we switch to the GUI context, draw our line, switch back to the thread pool and then recurse again to await the next instance of our mouse track event. Now, let’s tie this whole thing together.
let window = new Window(Visibility = Visibility.Visible) let canvas = new Canvas(RenderSize = window.RenderSize, Background = Brushes.AliceBlue) window.Content <- canvas let guiContext = SynchronizationContext.Current trackMouse canvas guiContext |> Async.Start
First, we create our window which holds the application and then we create a canvas which allows us to draw arbitrary shapes upon it. After getting the GUI context, we can invoke our trackMouse function with our canvas and our context and then asynchronously starting it. Our end result looks something like the following:
You might imagine that we could indeed change colors as well as part of our state. Of course there’s an issue with once we lift up our mouse to reset the previous coordinate, but that’s an issue for another post.
Conclusion
Once again, using this naive example, we’re able to see that asynchronous workflows and first class events can indeed work nicely together to do simple things such as drawing. In the next post, we’ll go after how to handle the cancel events with such things as asynchronously downloading data. After this, we’re not quite finished here as we have a lot more to cover with event-based programming with such things as the Reactive Framework (Reactive LINQ).