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)
form.Click.Add(ShowMessageOnClickHandler)
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
Let's 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 three 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")
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")
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
delegatePropertyChangedEventArgs
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
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:
open System
open System.Diagnostics
open System.ComponentModel
[<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
()
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()
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
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