F# First Class Events – Creating and Disposing Handlers

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.  This time, let’s look at how we might manage the lifetime of a given event subscription.  Before we begin, let’s get caught up to where we are today:

Subscribing and Unsubscribing From Events

In the first post in the series, we looked at how we might add handlers through either the Event module listen function or through the Add instance function on the IEvent interface.  Below are some examples of adding event handlers through an IEvent:

open System.Net

let wc = new WebClient()

// IEvent.Add
wc.DownloadProgressChanged.Add(
  fun args -> printfn "Percent complete %d" args.ProgressPercentage)

// Event.listen
wc.DownloadStringCompleted
  |> Event.filter(fun args -> args.Error = null && not args.Cancelled)
  |> Event.map(fun args -> args.Result)
  |> Event.listen(printfn "%s")

As you can see, if we do nothing more than add a handler without any transforms like maps or filters, we can simply call the Add method on the IEvent interface.  Conversely, if we should need some transforms through the use of the Event module, then we would subscribe through the listen function.  Now that we see how to add, what about actually removing an event handler?

In order to support the ability to both add and remove event handler listeners, we discussed the IDelegateEvent usage much like the following:

let handler = 
  new UploadStringCompletedEventHandler(
    fun sender args -> printfn "%s" args.Result)

// Add and remove handler
wc.UploadStringCompleted.AddHandler(handler)
wc.UploadStringCompleted.RemoveHandler(handler)

This allows us to add and remove our handlers quite easily, although there is a bit of syntax overhead as compared to the simple elegance of the listen and Add from above.  Instead, why don’t we combine the behavior of the AddHandler and RemoveHandler to support simple lambdas as we have above in our listen and Add solution?  Taking a cue from the Reactive Framework, let’s implement a wrapper around the AddHandler and RemoveHandler to return an IDisposable which on Dispose, will simple remove the handler.  Through the use of object expressions, creating such a solution is rather easy as I have below:

open System

type IDelegateEvent<'Del when 'Del :> Delegate> with
  member this.Subscribe handler =
    do this.AddHandler(handler)
    { new IDisposable with 
        member x.Dispose() =
          this.RemoveHandler(handler) }

What we’re doing is creating an extension method to the IDelegateEvent interface called Subscribe which takes a simple lambda expression with the sender and EventArgs derived class.  Because it is a member, F# treats this with some syntactic sugar which allows us to simply use a lambda expression much like the following:

open System.ComponentModel

let worker = new BackgroundWorker(WorkerReportsProgress = true)
let reportProgress =
  worker.ProgressChanged.Subscribe(fun _ args ->
    printfn "Percent complete %d" args.ProgressPercentage)
    
// Do stuff and then clean up
reportProgress.Dispose()

In this instance, I subscribed to the ProgressChanged event on the BackgroundWorker class which then allows me at a later time to unsubscribe rather easily through the Dispose method.  If we were doing it the old way, we’d have to remember what the handler’s target was and then pass that in to the RemoveHandler.  What we’re doing here allows for the easier removal of events and allows us to avoid these “leaks” for our long lived objects.

Conclusion

Taking a nod from the Reactive Framework, an approach of instead of using the standard Add or listen, or even the AddHandler and RemoveHandler, we’re able to both start and stop listening to events through the use of an IDisposable.  By encapsulating both the adding and removing of handlers, we can make more explicit how we handle events and treat them indeed as first class citizens.  This series is not yet complete with more adventures in adding more combinators as well as looking at the Reactive Framework itself.

No Comments