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
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:
- You may use the Add which has the following signature callback: (’T –> unit) –> unit
- 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
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
Lets revisit our custom class that had it own events where we had this code:
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 3 possible flavours of Event<’T>, which are:
- Custom event argument type
- Generic event arguments
- 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
- 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
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
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
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
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
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:
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
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:
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:
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:
- Single PropertyChanged event
- PropertyChangedEventHandler delegate
- PropertyChangedEventArgs
So to implement this in your own code, you would simply do something like this:
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:
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:
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
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:
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:
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:
Further Reading III
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
Some of Rx in F#, we will be covering this is a later post
A reader left the following comment elsewhere which I thought was quite useful to have here
I tried to post the following on your articleF# 21 Events, but it said “comment could not be posted”. Can you move it there?
Note that it is also possible to have events at module level. This may come in handy if you want one-time initialization and allow a listener or responder for the initialization.
Another use of module events is that they are not bound to a created object and therefore not to their lifetime. If you have events that can happen all over your code and from different contexts (say, for instance, a LicenseValidation event for a certain feature of your product), this can be very helpful. It can be set from any .NET language.
Yet another use I found was for solving forward references, where initialization requires an instance of an interface, but your current project cannot have a forward reference to it (a well known limitation of F#), or would otherwise result in a circular reference (a limitation of any .NET language). There are other ways around this, but a module event is an easy, secure and lightweight method.
(disclaimer: I am the owner of Exselt, an XSLT 3.0 processor written in F#, and use this pattern in some places)