Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / F#

F# 21 : Events

5.00/5 (1 vote)
22 May 2014CPOL8 min read 19.3K  
We continue our OO journey, and this time we look at events within classes.

We continue our OO journey, and this time we look at events within classes.

Events are a good way for no related classes t communicate, you can think of events as a kind of publish-subscribe arrangement, where the source of the event (the class that contains the event) is the publisher, whilst a consumer of the event is the subscriber.

According to MSDN (http://msdn.microsoft.com/en-us/library/dd233189.aspx)

F# events are represented by the F# Event class, which implements the IEvent interface. IEvent is itself an interface that combines the functionality of two other interfaces, IObservable<T> and IDelegateEvent. Therefore, Events have the equivalent functionality of delegates in other languages, plus the additional functionality from IObservable, which means that F# events support event filtering and using F# first-class functions and lambda expressions as event handlers.

This is actually pretty cool, as we can use the full gamut of Observable functions with F# events straight out of the box

Adding Events To A Class

Here is some code that shows how to add custom events to a custom class. There are several key points here:

  • We need to declare a private let binding which may or may not be a generic new Event type (depending on how you want your event to work more on this later)
  • We need to use a CLIEvent attribute on the member itself. This instructs the compiler to emit a CLR type event signature with the standard Add/Remove event handler methods
  • We need to use the .Publish from the private event value when exposing the member
  • We can raise the event using the Trigger(..) function of the private event. The Trigger function call will need to match the type of the Event arguments you chose to use, if you chose to let it be anything using the “Event<_>”, then it should take the form of this, args, which is the sender(ie this class), and the event args you want to use
C#
type MyCustomEventArgs<'a>(value : string) =
    inherit System.EventArgs()
 
    member this.Value = value
 
type MyCustomDelegate<'a> = delegate of obj * MyCustomEventArgs<'a> -> unit
 
type MyClassWithCLIEvent<'a when 'a : comparison>() =
 
    let customEventArgsEvent = new Event<string>()
    let genericEventArgsEvent = new Event<_>()
    let standardDotNetEventArgsEvent = new Event<EventHandler, EventArgs>()
    let customEventHandlerEvent = new Event<MyCustomDelegate<string>, MyCustomEventArgs<string>>()
 
    [<CLIEvent>]
    member this.CustomEventArgsEvent = customEventArgsEvent.Publish
 
    [<CLIEvent>]
    member this.GenericEventArgsEvent = genericEventArgsEvent.Publish
 
    [<CLIEvent>]
    member this.StandardDotNetEventArgsEvent = standardDotNetEventArgsEvent.Publish
 
    [<CLIEvent>]
    member this.CustomEventHandlerEvent = customEventHandlerEvent.Publish
 
    member this.TestCustomEventArgsEvent(arg : string) =
        customEventArgsEvent.Trigger(arg)
 
    member this.TestGenericEventArgsEvent(args : string) =
        genericEventArgsEvent.Trigger(this, args)
 
    member this.TestStandardDotNetEventArgsEvent() =
        standardDotNetEventArgsEvent.Trigger(this, EventArgs.Empty)
 
    member this.TestCustomEventHandlerEvent(x) =
        customEventHandlerEvent.Trigger(this, new MyCustomEventArgs<_>(x))

Raising Events

We just saw this above where we use the Trigger(..) function of the Event that is being exposed. This would tyically be done is a function that could be used internally, though you may want to expose the facility to raise the event outside the class that has the event, but in my opinion that would should be quite a rare case.

Subscribing To Events

Suppose we had this VERY simple win forms F# application with this code. This code actually shows you how to subscribe to 2 exists events, namely

  • Form.MouseMove : Which expects to have a event handler which takes System.Windows.Forms.MouseEventArgs, so in the handler we setup we make sure that the event arguments are just that.
  • Form.Click : Which expects to have System.EventArgs

When subscribing to events you have 2 choices:

  1. You may use the Add which has the following signature callback: (’T –> unit) –> unit
  2. You may also use the AddHandler

It is really just a question of creating your correct event handlers and then Adding them to the events invocation list using the Add(..) function

C#
open System.Windows.Forms
 
[<EntryPoint>]
let main argv = 
 
    let ShowMessageOnClickHandler evArgs =
        MessageBox.Show("Clicked") |> ignore
 
    let form = new Form(Text = "F# Windows Form",
                        Visible = true,
                        TopMost = true)
 
    let MouseMoveEventHandler (evArgs : System.Windows.Forms.MouseEventArgs) =
        form.Text <- System.String.Format("{0},{1}", evArgs.X, evArgs.Y)
 
    //adds the Click handler delegate (function in F#)
    form.Click.Add(ShowMessageOnClickHandler)
 
    //adds the MouseMove handler delegate (function in F#)
    form.MouseMove.Add(MouseMoveEventHandler)
    Application.Run(form)
 
    0

If you run this code, and click anywhere on the form you will get a MessageBox shown

image

Let's revisit our custom class that had it own events where we had this code:

C#
type MyCustomEventArgs<'a>(value : string) =
    inherit System.EventArgs()

    member this.Value = value

type MyCustomDelegate<'a> = delegate of obj * MyCustomEventArgs<'a> -> unit

type MyClassWithCLIEvent<'a when 'a : comparison>() =

    let customEventArgsEvent = new Event<string>()
    let genericEventArgsEvent = new Event<_>()
    let standardDotNetEventArgsEvent = new Event<EventHandler, EventArgs>()
    let customEventHandlerEvent = new Event<MyCustomDelegate<string>, MyCustomEventArgs<string>>()

    [<CLIEvent>]
    member this.CustomEventArgsEvent = customEventArgsEvent.Publish

    [<CLIEvent>]
    member this.GenericEventArgsEvent = genericEventArgsEvent.Publish

    [<CLIEvent>]
    member this.StandardDotNetEventArgsEvent = standardDotNetEventArgsEvent.Publish

    [<CLIEvent>]
    member this.CustomEventHandlerEvent = customEventHandlerEvent.Publish

    member this.TestCustomEventArgsEvent(arg : string) =
        customEventArgsEvent.Trigger(arg)

    member this.TestGenericEventArgsEvent(args : string) =
        genericEventArgsEvent.Trigger(this, args)

    member this.TestStandardDotNetEventArgsEvent() =
        standardDotNetEventArgsEvent.Trigger(this, EventArgs.Empty)

    member this.TestCustomEventHandlerEvent(x) =
        customEventHandlerEvent.Trigger(this, new MyCustomEventArgs<_>(x))

When you declare your Event<’T>, one of the things you need to decide is what type of event handler you want to expose/allow users to use, this will dictate what type of Event<’T> you will declare. The above code demonstrates three possible flavours of Event<’T>, which are:

  1. Custom event argument type
  2. Generic event arguments
  3. Standard Handler/EventArgs which will be of the type sender EventArgs (this is the standard in .NET), you could obviously use any EventHandler and EventArgs you like
  4. Custom EventHandler delegate and EventArgs

Custom EventArgs

As can be seen from the code that when we choose to use a custom object type for the event we end up with this sort of code in the event source class

C#
type MyClassWithCLIEvent<'a when 'a : comparison>() =
 
    let customEventArgsEvent = new Event<string>()
 
    [<CLIEvent>]
    member this.CustomEventArgsEvent = customEventArgsEvent.Publish
 
    member this.TestCustomEventArgsEvent(arg : string) =
        customEventArgsEvent.Trigger(arg)

Where we can subscribe using this sort of code when subscribing

let classWithEvent = new MyClassWithCLIEvent<string>()

let handler = new Handler<string>(fun sender args -> printfn "CustomEventArgsEvent AddHandler with new Handler<string> : %s" args)
classWithEvent.CustomEventArgsEvent.AddHandler(handler)

classWithEvent.TestCustomEventArgsEvent("I Am TestCustomEventArgsEvent")
classWithEvent.TestCustomEventArgsEvent("I Am TestCustomEventArgsEvent")
classWithEvent.TestCustomEventArgsEvent("I Am TestCustomEventArgsEvent")

//removing handler for CustomEventArgsEvent, so it should not fire any more
classWithEvent.CustomEventArgsEvent.RemoveHandler(handler)
classWithEvent.TestCustomEventArgsEvent("I Am event1")
classWithEvent.TestCustomEventArgsEvent("I Am event1")
classWithEvent.TestCustomEventArgsEvent("I Am event1")

Generic Event Arguments

As can be seen from the code that when we choose to use a generic Event<..> we end up with this sort of code in the event source class

C#
type MyClassWithCLIEvent<'a when 'a : comparison>() =

    let genericEventArgsEvent = new Event<_>()

    [<CLIEvent>]
    member this.GenericEventArgsEvent = genericEventArgsEvent.Publish

    member this.TestGenericEventArgsEvent(args : string) =
        genericEventArgsEvent.Trigger(this, args)

As can be seen from the code that when we choose to use a generic Event<..> type for the event we end up with this sort of code in the event source class, where we must use sender args when we subscribe. We need to match the args with the correct type when we raise the event as the raising method expects a string value in this example

C#
let classWithEvent = new MyClassWithCLIEvent<string>()

classWithEvent.GenericEventArgsEvent.Add(fun (sender, arg) ->
        printfn "Event1 occurred! Object data: %s" arg)

classWithEvent.TestGenericEventArgsEvent("I Am TestGenericEventArgsEvent")

Using Standard EventHandler/EventArgs

As can be seen from the code that when we choose to use a generic Event<EventHandler/EventArgs> we end up with this sort of code in the event source class

C#
type MyClassWithCLIEvent<'a when 'a : comparison>() = 

    let standardDotNetEventArgsEvent = new Event<EventHandler, EventArgs>() 

    [<CLIEvent>]
    member this.StandardDotNetEventArgsEvent = standardDotNetEventArgsEvent.Publish

    member this.TestStandardDotNetEventArgsEvent() =
        standardDotNetEventArgsEvent.Trigger(this, EventArgs.Empty)

As can be seen from the code that when we choose to use a generic Event<EventHandler/EventArgs> type for the event we need to create a new EventHandler when we subscribe

C#
let classWithEvent = new MyClassWithCLIEvent<string>()

classWithEvent.StandardDotNetEventArgsEvent.AddHandler(
    EventHandler(fun _ _ -> printfn "StandardDotNetEventArgsEvent.AddHandler"))

classWithEvent.TestStandardDotNetEventArgsEvent()
classWithEvent.TestStandardDotNetEventArgsEvent()

Using Custom Delegate And Custom EventArgs

We can also create our own custom EventHandler delegate and create completely custom EventArgs, this is shown below:

C#
type MyClassWithCLIEvent<'a when 'a : comparison>() =

    let customEventHandlerEvent = new Event<MyCustomDelegate<string>, MyCustomEventArgs<string>>()

    [<CLIEvent>]
    member this.CustomEventHandlerEvent = customEventHandlerEvent.Publish

    member this.TestCustomEventHandlerEvent(x) =
        customEventHandlerEvent.Trigger(this, new MyCustomEventArgs<_>(x))

As can be seen from the code that when we choose to use a custom EventHandler delegate, we need to use an instance of the custom EventHandler delegate when we subscribe

C#
let classWithEvent = new MyClassWithCLIEvent<string>()

let delegateHandler = new MyCustomDelegate<string>(fun sender args -> printfn "CustomEventArgsEvent AddHandler new MyCustomDelegate<string> : %s" args.Value)
classWithEvent.CustomEventHandlerEvent.AddHandler(delegateHandler)

classWithEvent.TestCustomEventHandlerEvent("I Am TestCustomEventHandlerEvent")

If you use events you will undoubtedly know about the problem where the subscriber may outlive the source of an event, and if we do not unhook an event handler we will get a memory leak, so it will come as no surprise that we are also able to Remove events subscriptions using RemoveHandler.

Here is the full demo code:

C#
let classWithEvent = new MyClassWithCLIEvent<string>()

let handler = new Handler<string>(fun sender args -> printfn "CustomEventArgsEvent AddHandler with new Handler<string> : %s" args)
classWithEvent.CustomEventArgsEvent.AddHandler(handler)

classWithEvent.GenericEventArgsEvent.Add(fun (sender, arg) ->
        printfn "Event1 occurred! Object data: %s" arg)

classWithEvent.StandardDotNetEventArgsEvent.AddHandler(
    EventHandler(fun _ _ -> printfn "StandardDotNetEventArgsEvent.AddHandler"))

let delegateHandler = new MyCustomDelegate<string>(fun sender args -> printfn "CustomEventArgsEvent AddHandler new MyCustomDelegate<string> : %s" args.Value)
classWithEvent.CustomEventHandlerEvent.AddHandler(delegateHandler)

classWithEvent.TestCustomEventArgsEvent("I Am TestCustomEventArgsEvent")
classWithEvent.TestCustomEventArgsEvent("I Am TestCustomEventArgsEvent")
classWithEvent.TestCustomEventArgsEvent("I Am TestCustomEventArgsEvent")
classWithEvent.TestGenericEventArgsEvent("I Am TestGenericEventArgsEvent")
classWithEvent.TestStandardDotNetEventArgsEvent()
classWithEvent.TestStandardDotNetEventArgsEvent()
classWithEvent.TestCustomEventHandlerEvent("I Am TestCustomEventHandlerEvent")

//removing handler for CustomEventArgsEvent, so it should not fire any more
classWithEvent.CustomEventArgsEvent.RemoveHandler(handler)
classWithEvent.TestCustomEventArgsEvent("I Am event1")
classWithEvent.TestCustomEventArgsEvent("I Am event1")
classWithEvent.TestCustomEventArgsEvent("I Am event1")

Which shows adding event subscriptions, and removing them too.

This produces the following output:

image

Implementing Interfaces That Contain Events

Occasionally you will get an interface or abstract class that has an event on it, and you need to implement the event in your type. A canonical example is the System.ComponentModel.INotifyPropertyChanged interface, which has the following attributes:

  1. Single PropertyChanged event
  2. PropertyChangedEventHandler delegate
  3. PropertyChangedEventArgs

So to implement this in your own code, you would simply do something like this:

C#
open System
open System.Collections.Generic
open System.ComponentModel

type INPCObject() =
    let mutable total = 0.0
    let mutable name = ""

    let propertyChanged = Event<_, _>()
    interface INotifyPropertyChanged with
        [<CLIEvent>]
        member x.PropertyChanged = propertyChanged.Publish

    member this.Total
        with get() = total
        and  set(v) =
            total <- v
            propertyChanged.Trigger(this, new PropertyChangedEventArgs("Total"))

    member this.Name
        with get() = name
        and  set(v) =
            name <- v
            propertyChanged.Trigger(this, new PropertyChangedEventArgs("Name"))

Where you would hook up a subscription to this event, like this:

C#
open System.ComponentModel
.....
.....
let classWithEvent = new INPCObject()

let inpc = (classWithEvent :> INotifyPropertyChanged)
inpc.PropertyChanged.AddHandler(
    (fun sender args -> printfn "PropertyChanged was : %s " args.PropertyName))

classWithEvent.Total <- 23.6;
classWithEvent.Name <- "yo";
classWithEvent.Total <- 11.2;

Which when run will give the following results:

image

MailBoxProcessor

Although not directly related to the subject at hand, I just want to mention a rather cool F# class called “MailBoxProcessor”, which is a message processing agent. It is quite cool, and could be used to send messages between objects with a little bit of love (think Mediator pattern here hint hint)

Here is a link to the class, have a read : http://msdn.microsoft.com/en-us/library/ee370357.aspx

Weak Events / Observable Module

Although slightly off topic for this brief introduction into F#, I thought it may be useful to point out some further resources that may help when working with events in general as well as Rx:

Further Reading I

Lovely Weak Event Article

This is a very in depth article about weak events, and presents several solutions to the problem, though the demos are in C#, it is still an excellent read, and one that everyone should read

Further Reading II

Paul Stovells Weak Event Proxy

A simple easy to use proxy to allow event subscription to be made weakly, which allows event source to be garbage collected even if a subscriber outlives the source life time.

I actually found a bit of time today to have a stab at translating Pauls Weak Event proxy into F#, which is shown below. Now this is not a generic version which Pauls original post was, but it was done with the purpose of demonstrating how to handle weak events, so if any of you want a generic version that is capable of dealing with any EventHandler delegate and EventArgs derived class it should not be that hard to change.

So here is the code, the crucial bit t note is that we hook the original source event up to a weak event (thanks to WeakReference) proxy, which will only raise the event if the event source is still alive. More crucially because we use a WeakReference we allow the original source object to be garbage collected, which is demonstrated when we set it to a default value and attempt to change one of its properties before we assign a new event source object.

Anyway enough waffle here is the code for the relevant types:

C#
open System
open System.Diagnostics
open System.ComponentModel
 
//WeakPropertyChangedEventHandler for PropertyChangedEventHandler
[<DebuggerNonUserCode>]
type public WeakPropertyChangedEventHandler(callback : PropertyChangedEventHandler) =
    let _method = callback.Method
    let _targetReference  = new WeakReference(callback.Target, true);
 
    [<DebuggerNonUserCode>]
    member public this.Handler(sender, e : PropertyChangedEventArgs) =
        let target = _targetReference.Target;
        if (target <> null) then
            let callback = Delegate.CreateDelegate(typedefof<PropertyChangedEventHandler>, target, _method, true) :?> PropertyChangedEventHandler
            if (callback <> null) then
                callback.Invoke(sender, e);
            ()
        else
            ()
 
//Event Consumer
type SomeClassWithStoredEventObject(inpcObject) as this =
    let mutable eventSourceObject = inpcObject
    do this.HookEvent()
 
    member this.HookEvent() =
        let inpc = eventSourceObject :> INotifyPropertyChanged
        let inpcHandler = new PropertyChangedEventHandler(
            fun obj args -> printfn "Property Changed!")
        let WeakPropertyChangedEventHandler = new WeakPropertyChangedEventHandler(inpcHandler)
        let weakInpcHandler = new PropertyChangedEventHandler(
            fun obj args -> WeakPropertyChangedEventHandler.Handler(obj, args))
        do inpc.PropertyChanged.AddHandler(weakInpcHandler) |> ignore
 
    member this.EventSourceObject
        with get() = eventSourceObject
        and set(v) =
            eventSourceObject <- v
            this.HookEvent()
 
//Event producer
type INPCObject() =
    let mutable age = 0.0
 
    let propertyChanged = Event<_,_>()
    interface INotifyPropertyChanged with
        [<CLIEvent>]
        member x.PropertyChanged = propertyChanged.Publish
 
    member this.Age
        with get() = age
        and set(v) =
            age <- v
            propertyChanged.Trigger(this, new PropertyChangedEventArgs("Age"))

Where we have this consuming code:

C#
let myObject = new INPCObject()
let someClassWithStoredEventObject = new SomeClassWithStoredEventObject(myObject)
myObject.Age <- 12.0
myObject.Age <- 24.0
let myObject = Unchecked.defaultof<INPCObject>
GC.Collect();
GC.WaitForPendingFinalizers();

let testItsNull x =
    if myObject = Unchecked.defaultof<INPCObject> then
        printfn "Yes it null now, so setting value should be an issue"
    try
        myObject.Age <- 35.0
    with
    | :? System.NullReferenceException as ex -> printfn "NullReferenceException! %s " (ex.Message); 

testItsNull myObject

let myObject2 = new INPCObject()
printfn "Assigning new Event Source object"
someClassWithStoredEventObject.EventSourceObject <- myObject2
myObject2.Age <- 35.0
myObject2.Age <- 89.0

Which when run gives the following output, which as you can see allows the original event source to be garbage collected:

image

Further Reading III

http://weblogs.asp.net/podwysocki/archive/2009/08/21/f-first-class-events-creating-and-disposing-handlers.aspx

F# blog posts which borrows ideas from Rx, to allow event subscriptions to return an IDisposable, such that they can be disposed using the standard .Dispose() method

Further Reading IV

Observable module in F#,

Some of Rx in F#, we will be covering this is a later post

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)