Search code examples
eventsf#observableweak-references

How to weakly subscribe to events/observables


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:


Solution

  • 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#.

    Implementation

    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
    

    Usage example

    See the WeakSubscriber type below.

    Important

    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.)

    Testing

    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