2

I'm learning Rx-stuff and I've made a test app, which has a shared counter state and dynamically created counter widgets. Such as you can press add/remove buttons to spawn/destroy a new counter. It works fine, but I don't know how to correctly dispose removed counter widgets.

Here is my source code.

One part of a problem is at line 77:

// update each counter's label
addCounterStream.Subscribe(x => {
    counterState.Subscribe(q => x.Counter = q.ToString()); // How can I unsubscribe later?
});

And the possible solution, I can think of, is to create a Subject<IDisposable> which will collect these subscriptions and then combine this stream with removeCounterStream and do unsibscribe.

But another part of a problem is at lines 40, 41:

var incFromWidget = addCounterStream.SelectMany(x => Observable.FromEventPattern(x, "Increment").Select(_ => 1));
var decFromWidget = addCounterStream.SelectMany(x => Observable.FromEventPattern(x, "Decrement").Select(_ => -1));

It merges click events from all dynamically created widgets. What will happen if I want to remove some widget? It will disappear from the form and so won't be able to produce new click events, but it can not be GC'ed, because it still hold references to click events (because of FromEventPattern).

That is a memory leak, am I right?

So, the question is how to correctly deal with Observables of objects that can be added/removed dynamically?

saintcrawler
  • 157
  • 2
  • 8
  • can't you simply dispose it? https://msdn.microsoft.com/en-us/library/hh242977(v=vs.103).aspx – pix Oct 08 '16 at 09:35
  • Possible duplicate? http://stackoverflow.com/questions/3449834/rx-unsubscribing-from-events?rq=1 – pix Oct 08 '16 at 09:37
  • 1
    @pix I can not dispose it, because it will stop events from all widgets and I need to dispose only widgets that are about to be removed (when clicking remove counter button). I can filter (or maybe GroupBy) events by particular widget (at lines 40, 41), but how can I dispose them if I never subscribes? Because I subscribe only to `counterState` (which consists of these two). And then again, GroupBy is a buffering operation, which will lead to memory leak again. – saintcrawler Oct 08 '16 at 09:58
  • Multiple nested subscriptions is usually a bad thing. You should be able to write the query so that `Subscribe` is only called once per subscribtion AND the returned `IDisposable` actually releases all the resources when called. – supertopi Oct 11 '16 at 06:22

1 Answers1

0

While digging deeper into the problem, I found 3 possible solutions:

  1. for that specific case I can use Disposed event of the control like this:

    var incsFromWidgets = addCounterStream
            .SelectMany(x => Observable.FromEventPattern(x, "Increment")
                .Select(_ => 1)
                .TakeUntil(Observable.FromEventPattern(x, "Disposed").Take(1)))
            .Publish();
    
  2. actually it's generalization of the 1st method: I can make generic wrapper class with release semantics, like:

    class Wrapper<T> : IDisposable {
        public T Value { get; set; }
        public event EventHandler Disposed;        
    }
    
  3. manual "hoisting" of the streams:

    // declare all streams
    IObservable<int> aStream = null;
    IObservable<bool> bStream = null;
    IObservable<MyWidget> cStream = null;
    
    // then compose it
    aStream = Observable.From(...).TakeUntil(bStream.Where(...));
    bStream = Observable.From(...).CombineLatest(aStream, ...);  // cyclic dependency
    

While the 3rd method may seem awful, it gives the great possibilities to combine streams in different ways, just need to be careful and do not create infinite recursions.

EDIT: Well, actually the 3rd method is not that simple, it will throw NullReferenceException even if streams are defined below. The proper implementation would be smth like:

        IConnectableObservable<int> a = null;
        IConnectableObservable<char> b = null;
        IConnectableObservable<bool> c = null;            

        a = Observable.Defer(() => b.Select(x => (int)x)).Publish();
        b = Observable.Defer(() => Observable.Generate('A', x => x < 255, x => (char)(x + 1), x => x).TakeUntil(c)).Publish();
        c = Observable.Defer(() => a.SkipWhile(x => x < 70).Select(_ => true)).Publish();

        a.Subscribe(x => Console.WriteLine($"from a: {x}"));
        b.Subscribe(x => Console.WriteLine($"from b: {x}"));

        c.Connect();
        a.Connect();
        b.Connect();

        Console.ReadKey(true);
saintcrawler
  • 157
  • 2
  • 8