I have a static-like (publisher lifetime = application lifetime) event I need to subscribe to from views. I have no way of reliably determining when the view is navigated away from (navbar back button pressed in a Xamarin.Forms NavigationPage being one example), so I can't determine when the view should unsubscribe from the observable. (I know it's possible to subscribe/unsubscribe in OnAppearing/OnDisappearing, but that carries its own set of problems I won't go into detail about here.)
Thus, I find myself in need of having the view subscribe weakly to the event, i.e. allow the view to be garbage collected without having to unsubscribe from the event. Ideally I'd like something that can be used along the lines of myObj.myEvent |> Observable.AsWeak |> Observable.Subscribe ...
, or myObj.myEvent |> Observable.SubscribeWeakly ...
, or simply myObj.myEvent.SubscribeWeakly ...
.
Unfortunately I have no idea how to implement this. I have heard of the System.WeakReference class, but this is all very new to me and I have no idea how to properly use it - most examples I've seen seem overly complicated for what I'm trying to do, which means that either I want something different, or there's many more pitfalls beneath the surface than I suspect.
How can I subscribe to events/observables in F# while allowing the subscriber to be garbage collected without unsubscribing?
Similar but not duplicate questions:
I have arrived at a relatively simple function that seems to work correctly, though I don't really know what I'm doing, so I've put this up at Code Review SE. It's based on information from Samuel Jack's Weak Events in .Net, the easy way as well as solution 4 in CodeProject's Weak Events in C#.
module Observable =
open System
// ('a -> 'b -> unit) -> 'a -> IObservable<'b>
let subscribeWeakly callback target source =
let mutable sub:IDisposable = null
let mutable disposed = false
let wr = new WeakReference<_>(target)
let dispose() =
lock (sub) (fun () ->
if not disposed then sub.Dispose(); disposed <- true)
let callback' x =
let isAlive, target = wr.TryGetTarget()
if isAlive then callback target x else dispose()
sub <- Observable.subscribe callback' source
sub
See the WeakSubscriber
type below.
You have to use the callback's me
parameter to invoke the relevant method. If you use this
inside the callback, you'll still end up with a strong reference for reasons described in the aforementioned articles. For the same reason (I guess?), you can't invoke a "plain" function in the class defined using let
. (You can, however, define the method as private
.)
Helper classes:
type Publisher() =
let myEvent = new Event<_>()
[<CLIEvent>] member this.MyEvent = myEvent.Publish
member this.Trigger(x) = myEvent.Trigger(x)
type StrongSubscriber() =
member this.MyMethod x =
printfn "Strong: method received %A" x
member this.Subscribe(publisher:Publisher) =
publisher.MyEvent |> Observable.subscribe this.MyMethod
publisher.MyEvent |> Observable.subscribe
(fun x -> printfn "Strong: lambda received %A" x)
type WeakSubscriber() =
member this.MyMethod x =
printfn "Weak: method received %A" x
member this.Subscribe(publisher:Publisher) =
publisher.MyEvent |> Observable.subscribeWeakly
(fun (me:WeakSubscriber) x -> me.MyMethod x) this
publisher.MyEvent |> Observable.subscribeWeakly
(fun _ x -> printfn "Weak: lambda received %A" x) this
The actual test:
[<EntryPoint>]
let main argv =
let pub = Publisher()
let doGc() =
System.GC.Collect()
System.GC.WaitForPendingFinalizers()
System.GC.Collect()
printfn "\nGC completed\n"
let someScope() =
let strong = StrongSubscriber()
let weak = WeakSubscriber()
strong.Subscribe(pub)
weak.Subscribe(pub)
doGc() // should not remove weak subscription since it's still in scope
printfn "All subscribers should still be triggered:"
pub.Trigger(1)
someScope()
doGc() // should remove weak subscriptions
printfn "Weak subscribers should not be triggered:"
pub.Trigger(2)
System.Console.ReadKey() |> ignore
0
Output:
GC completed
All subscribers should still be triggered:
Strong: method received 1
Strong: lambda received 1
Weak: method received 1
Weak: lambda received 1
GC completed
Weak subscribers should not be triggered:
Strong: method received 2
Strong: lambda received 2
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With