F Sharp Programming/Events

Previous: Interfaces Index Next: Modules and Namespaces
F# : Events

Events allow objects to communicate with one another through a kind of synchronous message passing. Events are simply hooks to other functions: objects register callback functions to an event, and these callbacks will be executed when (and if) the event is triggered by some object.

For example, let's say we have a clickable button which exposed an event called Click. We can register a block of code, something like fun () -> printfn "I've been clicked!", to the button's click event. When the click event is triggered, it will execute the block of code we've registered. If we wanted to, we could register an indefinite number of callback functions to the click event—the button doesn't care what code is trigged by the callbacks or how many callback functions are registered to its click event, it blindly executes whatever functions are hooked to its click event.

Event-driven programming is natural in GUI code, as GUIs tend to consist of controls which react and respond to user input. Events are, of course, useful in non-GUI applications as well. For example, if we have an object with mutable properties, we may want to notify another object when those properties change.

Defining Events

edit

Events are created and used through F#'s Event class. To create an event, use the Event constructor as follows:

type Person(name : string) =
    let mutable _name = name;
    let nameChanged = new Event<string>()
    
    member this.Name
        with get() = _name
        and set(value) = _name <- value

To allow listeners to hook onto our event, we need to expose the nameChanged field as a public member using the event's Publish property:

type Person(name : string) =
    let mutable _name = name;
    let nameChanged = new Event<unit>() (* creates event *)
    
    member this.NameChanged = nameChanged.Publish (* exposed event handler *)
    
    member this.Name
        with get() = _name
        and set(value) =
            _name <- value
            nameChanged.Trigger() (* invokes event handler *)

Now, any object can listen to the changes on the person method. By convention and Microsoft's recommendation, events are usually named Verb or VerbPhrase, as well as adding tenses like Verbed and Verbing to indicate post- and pre-events.

Adding Callbacks to Event Handlers

edit

Its very easy to add callbacks to event handlers. Each event handler has the type IEvent<'T> which exposes several methods:

val Add : event:('T -> unit) -> unit

Connect a listener function to the event. The listener will be invoked when the event is fired.

val AddHandler : 'del -> unit

Connect a handler delegate object to the event. A handler can be later removed using RemoveHandler. The listener will be invoked when the event is fired.

val RemoveHandler : 'del -> unit

Remove a listener delegate from an event listener store.

Here's an example program:

type Person(name : string) =
    let mutable _name = name;
    let nameChanged = new Event<unit>() (* creates event *)
    
    member this.NameChanged = nameChanged.Publish (* exposed event handler *)
    
    member this.Name
        with get() = _name
        and set(value) =
            _name <- value
            nameChanged.Trigger() (* invokes event handler *)
            
let p = new Person("Bob")
p.NameChanged.Add(fun () -> printfn "-- Name changed! New name: %s" p.Name)

printfn "Event handling is easy"
p.Name <- "Joe"

printfn "It handily decouples objects from one another"
p.Name <- "Moe"

p.NameChanged.Add(fun () -> printfn "-- Another handler attached to NameChanged!")

printfn "It's also causes programs behave non-deterministically."
p.Name <- "Bo"

printfn "The function NameChanged is invoked effortlessly."

This program outputs the following:

Event handling is easy
-- Name changed! New name: Joe
It handily decouples objects from one another
-- Name changed! New name: Moe
It's also causes programs behave non-deterministically.
-- Name changed! New name: Bo
-- Another handler attached to NameChanged!
The function NameChanged is invoked effortlessly.
Note: When multiple callbacks are connected to a single event, they are executed in the order they are added. However, in practice, you should not write code with the expectation that events will trigger in a particular order, as doing so can introduce complex dependencies between functions. Event-driven programming is often non-deterministic and fundamentally stateful, which can occasionally be at odds with the spirit of functional programming. Its best to write callback functions which do not modify state, and do not depend on the invocation of any prior events.

Working with EventHandlers Explicitly

edit

Adding and Removing Event Handlers

edit

The code above demonstrates how to use the IEvent<'T>.add method. However, occasionally we need to remove callbacks. To do so, we need to work with the IEvent<'T>.AddHandler and IEvent<'T>.RemoveHandler methods, as well as .NET's built-in System.Delegate type.

The function person.NameChanged.AddHandler has the type val AddHandler : Handler<'T> -> unit, where Handler<'T> inherits from System.Delegate. We can create an instance of Handler as follows:

type Person(name : string) =
    let mutable _name = name;
    let nameChanged = new Event<unit>() (* creates event *)
    
    member this.NameChanged = nameChanged.Publish (* exposed event handler *)
    
    member this.Name
        with get() = _name
        and set(value) =
            _name <- value
            nameChanged.Trigger() (* invokes event handler *)

            
let p = new Person("Bob")

let person_NameChanged =
    new Handler<unit>(fun sender eventargs -> printfn "-- Name changed! New name: %s" p.Name)

p.NameChanged.AddHandler(person_NameChanged)

printfn "Event handling is easy"
p.Name <- "Joe"

printfn "It handily decouples objects from one another"
p.Name <- "Moe"

p.NameChanged.RemoveHandler(person_NameChanged)
p.NameChanged.Add(fun () -> printfn "-- Another handler attached to NameChanged!")

printfn "It's also causes programs behave non-deterministically."
p.Name <- "Bo"

printfn "The function NameChanged is invoked effortlessly."

This program outputs the following:

Event handling is easy
-- Name changed! New name: Joe
It handily decouples objects from one another
-- Name changed! New name: Moe
It's also causes programs behave non-deterministically.
-- Another handler attached to NameChanged!
The function NameChanged is invoked effortlessly.

Defining New Delegate Types

edit

F#'s event handling model is a little different from the rest of .NET. If we want to expose F# events to different languages like C# or VB.NET, we can define a custom delegate type which compiles to a .NET delegate using the delegate keyword, for example:

type NameChangingEventArgs(oldName : string, newName : string) =
    inherit System.EventArgs()

    member this.OldName = oldName
    member this.NewName = newName
        
type NameChangingDelegate = delegate of obj * NameChangingEventArgs -> unit

The convention obj * NameChangingEventArgs corresponds to the .NET naming guidelines which recommend that all events have the type val eventName : (sender : obj * e : #EventArgs) -> unit.

Use existing .NET WPF Event and Delegate Types

edit

Try using existing .NET WPF Event and Delegate, example, ClickEvent and RoutedEventHandler. Create F# Windows Application .NET project with referring these libraries (PresentationCore PresentationFramework System.Xaml WindowsBase). The program will display a button in a window. Clicking the button will display the button's content as string.

open System.Windows
open System.Windows.Controls
open System.Windows.Input 
open System
[<EntryPoint>] [<STAThread>]              // STAThread is Single-Threading-Apartment which is required by WPF
let main argv = 
    let b = new Button(Content="Button")  // b is a Button with "Button" as content
    let f(sender:obj)(e:RoutedEventArgs) = // (#3) f is a fun going to handle the Button.ClickEvent
                                           //   f signature must be curried, not tuple as governed by Delegate-RoutedEventHandler.
                                           //   that means f(sender:obj,e:RoutedEventArgs) will not work.
        let b = sender:?>Button            // sender will have Button-type.  Convert it to Button into b.
        MessageBox.Show(b.Content:?>string) // Retrieve the content of b which is obj.  
                                            //    Convert it to string and display by <code>Messagebox.Show</code>
            |> ignore                       // ignore the return because f-signature requires: obj->RoutedEventArgs->unit
                                                      
                                                      
    let d = new RoutedEventHandler(f)       // (#2) d will have type-RoutedEventHandler, 
                                            //      RoutedEventHandler is a kind of delegate to handle Button.ClickEvent.  
                                            //      The f must have signature governed by RoutedEventHandler.
    b.AddHandler(Button.ClickEvent,d)       // (#1) attach a RountedEventHandler-d for Button.ClickEvent
    let w = new Window(Visibility=Visibility.Visible,Content=b)  // create a window-w have a Button-b 
                                                                 // which will show the content of b when clicked
    (new Application()).Run(w)      // create new Application() running the Window-w.
  • (#1) To attach a handler to a control for an event: b.AddHandler(Button.ClickEvent,d)
  • (#2) Create a delegate/handler using a function: let d = new RoutedEventHandler(f)
  • (#3) Create a function with specific signature defined by the delegate: let f(sender:obj)(e:RoutedEventArgs) = ....
  • b is the control.
  • AddHandler is attach.
  • Button.ClickEvent is the event.
  • d is delegate/handler. It is a layer to make sure the signature is correct
  • f is the real function/program provided to the delegate.
  • Rule#1: b must have this event Button.ClickEvent: b is type-Button-object. ClickEvent is a static property of type-ButtonBase which is inherited by type-Button. So Button-type will also have this static property ClickEvent.
  • Rule#2: d must be the handler of ClickEvent: ClickEvent is type-RoutedEvent. RoutedEvent's handler is RoutedEventHandler, just adding Handler at end. RoutedEventHandler is a defined delegate in .NET library. To create d, just let d = new RoutedEventHandler(f), where f is function.
  • Rule#3: f must have signature obeying delegate-d's definition: Check .NET library, RoutedEventHandler is a delegate of C#-signature: void RoutedEventHandler(object sender, RoutedEventArgs e). So f must have same signature. Present the signature in F# is (obj * RountedEventHandler) -> unit
  • Passing State To Callbacks

    edit

    Events can pass state to callbacks with minimal effort. Here is a simple program which reads a file in blocks of characters:

    open System
    
    type SuperFileReader() =
        let progressChanged = new Event<int>()
        
        member this.ProgressChanged = progressChanged.Publish
        
        member this.OpenFile (filename : string, charsPerBlock) =
            use sr = new System.IO.StreamReader(filename)
            let streamLength = int64 sr.BaseStream.Length
            let sb = new System.Text.StringBuilder(int streamLength)
            let charBuffer = Array.zeroCreate<char> charsPerBlock
            
            let mutable oldProgress = 0
            let mutable totalCharsRead = 0
            progressChanged.Trigger(0)
            while not sr.EndOfStream do
                (* sr.ReadBlock returns number of characters read from stream *)
                let charsRead = sr.ReadBlock(charBuffer, 0, charBuffer.Length)
                totalCharsRead <- totalCharsRead + charsRead
                
                (* appending chars read from buffer *)
                sb.Append(charBuffer, 0, charsRead) |> ignore
                
                let newProgress = int(decimal totalCharsRead / decimal streamLength * 100m)
                if newProgress > oldProgress then
                    progressChanged.Trigger(newProgress) // passes newProgress as state to callbacks
                    oldProgress <- newProgress
                
            sb.ToString()
            
    let fileReader = new SuperFileReader()
    fileReader.ProgressChanged.Add(fun percent ->
        printfn "%i percent done..." percent)
        
    let x = fileReader.OpenFile(@"C:\Test.txt", 50)
    printfn "%s[...]" x.[0 .. if x.Length <= 100 then x.Length - 1 else 100]
    

    This program has the following types:

    type SuperFileReader =
      class
        new : unit -> SuperFileReader
        member OpenFile : filename:string * charsToRead:int -> string
        member ProgressChanged : IEvent<int>
      end
    val fileReader : SuperFileReader
    val x : string
    

    Since our event has the type IEvent<int>, we can pass int data as state to listening callbacks. This program outputs the following:

    0 percent done...
    4 percent done...
    9 percent done...
    14 percent done...
    19 percent done...
    24 percent done...
    29 percent done...
    34 percent done...
    39 percent done...
    44 percent done...
    49 percent done...
    53 percent done...
    58 percent done...
    63 percent done...
    68 percent done...
    73 percent done...
    78 percent done...
    83 percent done...
    88 percent done...
    93 percent done...
    98 percent done...
    100 percent done...
    In computer programming, event-driven programming or event-based programming is a programming paradig{{typo help inline|reason=similar to parading|date=September 2022}}[...]

    Retrieving State from Callers

    edit

    A common idiom in event-driven programming is pre- and post-event handling, as well as the ability to cancel events. Cancellation requires two-way communication between an event handler and a listener, which we can easily accomplish through the use of ref cells or mutable members:

    type Person(name : string) =
        let mutable _name = name;
        let nameChanging = new Event<string * bool ref>()
        let nameChanged = new Event<unit>()
        
        member this.NameChanging = nameChanging.Publish
        member this.NameChanged = nameChanged.Publish
        
        member this.Name
            with get() = _name
            and set(value) =
                let cancelChange = ref false
                nameChanging.Trigger(value, cancelChange)
                
                if not !cancelChange then
                    _name <- value
                    nameChanged.Trigger()
                    
    let p = new Person("Bob")
    
    p.NameChanging.Add(fun (name, cancel) ->
        let exboyfriends = ["Steve"; "Mike"; "Jon"; "Seth"]
        if List.exists (fun forbiddenName -> forbiddenName = name) exboyfriends then
            printfn "-- No %s's allowed!" name
            cancel := true
        else
            printfn "-- Name allowed")
        
    p.NameChanged.Add(fun () ->
        printfn "-- Name changed to %s" p.Name)
        
    let tryChangeName newName =
        printfn "Attempting to change name to '%s'" newName
        p.Name <- newName
    
    tryChangeName "Joe"
    tryChangeName "Moe"
    tryChangeName "Jon"
    tryChangeName "Thor"
    

    This program has the following types:

    type Person =
      class
        new : name:string -> Person
        member Name : string
        member NameChanged : IEvent<unit>
        member NameChanging : IEvent<string * bool ref>
        member Name : string with set
      end
    val p : Person
    val tryChangeName : string -> unit
    

    This program outputs the following:

    Attempting to change name to 'Joe'
    -- Name allowed
    -- Name changed to Joe
    Attempting to change name to 'Moe'
    -- Name allowed
    -- Name changed to Moe
    Attempting to change name to 'Jon'
    -- No Jon's allowed!
    Attempting to change name to 'Thor'
    -- Name allowed
    -- Name changed to Thor

    If we need to pass a significant amount of state to listeners, then its recommended to wrap the state in an object as follows:

    type NameChangingEventArgs(newName : string) =
        inherit System.EventArgs()
        
        let mutable cancel = false
        member this.NewName = newName
        member this.Cancel
            with get() = cancel
            and set(value) = cancel <- value
    
    type Person(name : string) =
        let mutable _name = name;
        let nameChanging = new Event<NameChangingEventArgs>()
        let nameChanged = new Event<unit>()
        
        member this.NameChanging = nameChanging.Publish
        member this.NameChanged = nameChanged.Publish
        
        member this.Name
            with get() = _name
            and set(value) =
                let eventArgs = new NameChangingEventArgs(value)
                nameChanging.Trigger(eventArgs)
                
                if not eventArgs.Cancel then
                    _name <- value
                    nameChanged.Trigger()
                    
    let p = new Person("Bob")
    
    p.NameChanging.Add(fun e ->
        let exboyfriends = ["Steve"; "Mike"; "Jon"; "Seth"]
        if List.exists (fun forbiddenName -> forbiddenName = e.NewName) exboyfriends then
            printfn "-- No %s's allowed!" e.NewName
            e.Cancel <- true
        else
            printfn "-- Name allowed")
    
    (* ... rest of program ... *)
    

    By convention, custom event parameters should inherit from System.EventArgs, and should have the suffix EventArgs.

    Using the Event Module

    edit

    F# allows users to pass event handlers around as first-class values in fundamentally the same way as all other functions. The Event module has a variety of functions for working with event handlers:

    val choose : ('T -> 'U option) -> IEvent<'del,'T> -> IEvent<'U> (requires delegate and 'del :> Delegate)

    Return a new event which fires on a selection of messages from the original event. The selection function takes an original message to an optional new message.

    val filter : ('T -> bool) -> IEvent<'del,'T> -> IEvent<'T> (requires delegate and 'del :> Delegate)

    Return a new event that listens to the original event and triggers the resulting event only when the argument to the event passes the given function.

    val listen : ('T -> unit) -> IEvent<'del,'T> -> unit (requires delegate and 'del :> Delegate)

    Run the given function each time the given event is triggered.

    val map : ('T -> 'U) -> IEvent<'del,'T> -> IEvent<'U> (requires delegate and 'del :> Delegate)

    Return a new event which fires on a selection of messages from the original event. The selection function takes an original message to an optional new message.

    val merge : IEvent<'del1,'T> -> IEvent<'del2,'T> -> IEvent<'T> (requires delegate and 'del1 :> Delegate and delegate and 'del2 :> Delegate)

    Fire the output event when either of the input events fire.

    val pairwise : IEvent<'del,'T> -> IEvent<'T * 'T> (requires delegate and 'del :> Delegate)

    Return a new event that triggers on the second and subsequent triggerings of the input event. The Nth triggering of the input event passes the arguments from the N-1th and Nth triggering as a pair. The argument passed to the N-1th triggering is held in hidden internal state until the Nth triggering occurs. You should ensure that the contents of the values being sent down the event are not mutable. Note that many EventArgs types are mutable, e.g. MouseEventArgs, and each firing of an event using this argument type may reuse the same physical argument object with different values. In this case you should extract the necessary information from the argument before using this combinator.

    val partition : ('T -> bool) -> IEvent<'del,'T> -> IEvent<'T> * IEvent<'T> (requires delegate and 'del :> Delegate

    Return a new event that listens to the original event and triggers the first resulting event if the application of the predicate to the event arguments returned true, and the second event if it returned false.

    val scan : ('U -> 'T -> 'U) -> 'U -> IEvent<'del,'T> -> IEvent<'U> (requires delegate and 'del :> Delegate)

    Return a new event consisting of the results of applying the given accumulating function to successive values triggered on the input event. An item of internal state records the current value of the state parameter. The internal state is not locked during the execution of the accumulation function, so care should be taken that the input IEvent not triggered by multiple threads simultaneously.

    val split : ('T -> Choice<'U1,'U2>) -> IEvent<'del,'T> -> IEvent<'U1> * IEvent<'U2> (requires delegate and 'del :> Delegate)

    Return a new event that listens to the original event and triggers the first resulting event if the application of the function to the event arguments returned a Choice2Of1, and the second event if it returns a Choice2Of2.

    Take the following snippet:

    p.NameChanging.Add(fun (e : NameChangingEventArgs) ->
        let exboyfriends = ["Steve"; "Mike"; "Jon"; "Seth"]
        if List.exists (fun forbiddenName -> forbiddenName = e.NewName) exboyfriends then
            printfn "-- No %s's allowed!" e.NewName
            e.Cancel <- true)
    

    We can rewrite this in a more functional style as follows:

    p.NameChanging
        |> Event.filter (fun (e : NameChangingEventArgs) ->  
            let exboyfriends = ["Steve"; "Mike"; "Jon"; "Seth"]
            List.exists (fun forbiddenName -> forbiddenName = e.NewName) exboyfriends )
        |> Event.listen (fun e ->
            printfn "-- No %s's allowed!" e.NewName
            e.Cancel <- true)
    
    Previous: Interfaces Index Next: Modules and Namespaces