So the Reactive Extensions (Rx) demonstrate some sweet syntax for handling CLR events. How about doing the same thing for loosely coupled events exposed through an Event Broker like Prism’s IEventAggregator. For example if you wanted to publish an event from any window that could be observed by any other window without them all having intimate knowledge of each other.
The following is a simple F# event broker coded on the train this morning (Note: the interface ISubscribe<’a> could be replaced by IObservable<’a>):
type ITrigger<'a> =
abstract member Trigger : 'a -> unit
type ISubscribe<'a> =
abstract member Subscribe : ('a -> unit) -> unit
type IHappen<'a> =
inherit ITrigger<'a>
inherit ISubscribe<'a>
type IHappenings =
abstract member ObtainHappening<'a> : unit -> IHappen<'a>
type Happenings () =
let happenings = System.Collections.Generic.Dictionary<System.Type,_>()
interface IHappenings with
member this.ObtainHappening<'a> () =
let CreateHappening () =
let subscribers = ref []
{ new IHappen<'a> with
member this.Subscribe f =
lock(this) (fun _ ->
subscribers := f::!subscribers)
member this.Trigger (x) =
!subscribers |> List.iter (fun f -> f x)
}
lock (this) (fun _ ->
match happenings.TryGetValue(typeof<'a>) with
| true, happen -> unbox happen
| false, _ ->
let happen = CreateHappening ()
happenings.Add(typeof<'a>,box happen)
happen
)
end
Then, in Starbucks for a coffee and some higher-order functions :
module Happening =
open System
open System.Windows.Threading
let Subscribe<'a> f (happening:ISubscribe<'a>) =
happening.Subscribe f
let OnDispatcher<'a> (happening:ISubscribe<'a>) =
let dispatcher = Dispatcher.CurrentDispatcher
{ new ISubscribe<'a> with
member this.Subscribe f =
happening.Subscribe (fun x ->
dispatcher.Invoke(Action(fun _ -> f x)) |> ignore
)
}
let Filter<'a> filterF (happening:ISubscribe<'a>) =
{ new ISubscribe<'a> with
member this.Subscribe f =
happening.Subscribe (fun x -> if filterF x then f x)
}
let Map<'a,'b> mapF (happening:ISubscribe<'a>) =
{ new ISubscribe<'b> with
member this.Subscribe f =
happening.Subscribe (fun x -> let y = mapF x in f y)
}
let Delay<'a> milliseconds (happening:ISubscribe<'a>) =
{ new ISubscribe<'a> with
member this.Subscribe f =
happening.Subscribe (fun x ->
async {
do! Async.Sleep(milliseconds)
f x
} |> Async.Start )
}
Finally, here is Matthew Podwysocki’s cool Time Flies Like An Arrow sample rewritten to use more loosely-coupled events:
module Test =
open System
open System.Windows
open System.Windows.Controls
open System.Windows.Input
open System.Windows.Media
let getPosition (element : #UIElement) (args : MouseEventArgs) =
let point = args.GetPosition(element)
(point.X, point.Y)
type TimeFliesWindow(happenings:IHappenings) as this =
inherit Window()
do this.Title <- "Time files like an arrow"
let canvas =
Canvas(Width=800.0, Height=400.0, Background = Brushes.White)
do this.Content <- canvas
let happen = happenings.ObtainHappening<MouseEventArgs>()
do this.MouseMove
|> Observable.subscribe happen.Trigger |> ignore
do "F# can react to first class events!"
|> Seq.iteri(fun i c ->
let s = TextBlock(Width=20.0,
Height=30.0,
FontSize=20.0,
Text=string c,
Foreground=Brushes.Black,
Background=Brushes.White)
canvas.Children.Add(s) |> ignore
happen
|> Happening.Map (getPosition canvas)
|> Happening.Delay (i * 100)
|> Happening.OnDispatcher
|> Happening.Subscribe (fun (x, y) ->
Canvas.SetTop(s, y)
Canvas.SetLeft(s, x + float ( i * 10)))
)
let happenings = new Happenings()
let win = TimeFliesWindow(happenings)
[<STAThread>]
do (new Application()).Run(win) |> ignore