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.

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:

image

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).

3 Comments

  • You wrote:
    ... 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. ...

    But F# 1.9.6.16 manual says,
    merge: Fire the output event when either of the input events fire

  • @SooHyoung

    Correct, which is why I had to do a filter to ensure that the left button was down.

    Matt

  • Hi Matt.
    Really interested in Async for front-end GUI interactive manipulation ... Your article seems to be one of a very few that address GUI, most seem to address other Async cases. Tomas' more recent articles are of interest too.
    When I try to run your example I get an error on "Async.SwitchToGuiThread." My perception is that things are in transition in the Async .NET world? I'm metastable wondering what is the best Async GUI practice. Any help, links, ptrs appreciated. Thanks. Art

Comments have been disabled for this content.