Click here to Skip to main content
14,984,060 members
Articles / Desktop Programming / WPF
Tip/Trick
Posted 16 May 2021

Stats

3.2K views
20 downloads
1 bookmarked

WPF and RX - Dispose vs Complete

Rate me:
Please Sign up or sign in to vote.
5.00/5 (1 vote)
16 May 2021CPOL3 min read
This tip shows two different approaches for disposing view models that use Reactive Extension
When implementing ViewModels that are using ReactiveExtension, it introduces reference retentions from your Observable to your ViewModels. Once you don't need your screen anymore, let's say after the user decides to close your view tab, you will have to carefully Dispose your ViewModels to make them collectable by the GarbageCollector.

Introduction

While developing a WPF screen that uses Reactive Extensions, the same question will arise again and again: how should I implement IDisposable when my screen is not needed anymore.

You will have (at least) two different ways to implement it:

  • The first approach is the "classic" disposing of subscriptions.
  • The second way is completing the source observable.

Project

The source code is available on Github here.

The UI looks like:

Image 1

We have a TabControl where we can add or remove "Completing" or "Disposing" tabs.

Completing tab is bound to a ParentViewModelCompleting, whereas Disposing tab is bound to a ParentViewModelDisposing.

Each ParentViewModel is composed of multiple ChildViewModels.

Each ParentViewModel is populated from a Rx source:

C#
Observable.Interval(TimeSpan.FromMilliseconds(300))
                .GroupBy(t => t % 10)
                .Select(t => new ChildViewModel
                 (t.Key, t.Select(t2 => new Random().NextDouble())))

The main goal of this article is to show how we can implement closing a tab so our ViewModels will be eligible for garbage collection.

Memory Profiling

Let's first look at the memory state when a tab is open.

Here is a Memory Profiling capture for reference retentions of an open tab using DotMemory.

We show ParentViewModelDisposing here, captures will be similar for ParentViewModelCompleting and for ChildViewModels.

Image 2

The first branch is the reference from our ObservableCollection to our ViewModel.
The second and third branches are WPF references between our View and our ViewModel.
The fourth one is a reference from our Observable source to our ViewModel.

Cleaning the ObservableCollection and WPF References Entries

For our first three branches to be cleaned, we just bind our TabItem close button to a command to remove the ParentViewModel from the ObservableCollection

XML
<TabControl ItemsSource="{Binding ViewModels}"  MinHeight="250">
    <TabControl.ItemTemplate>
        <DataTemplate>
            <StackPanel Orientation="Horizontal">
                <TextBlock>
                    ...
                </TextBlock>
                <Button Width="10" Content="X"
                        Command="{Binding RelativeSource=
                        {RelativeSource FindAncestor,AncestorType={x:Type Window}},
                        Path=DataContext.RemoveViewModelCommand}"
                        CommandParameter="{Binding .}"
                        BorderBrush="Transparent" />
            </StackPanel>
        </DataTemplate>
    </TabControl.ItemTemplate>
</TabControl>

Here is MainWindowViewModel.RemoveViewModelCommand code:

C#
RemoveViewModelCommand = new DelegateCommand<object>(t =>
{
    _viewModels.Remove(t);
    (t as IDisposable)?.Dispose();
});

When we click the tab header close button, we remove our ParentViewModel from our ObservableCollection. It will clean the first three paths of our memory profiling key retention capture.

To clean the Observable reference path, we need to look at ParentViewModel.Dispose methods.

We will see two different approaches to implement it:

  • The first approach where our Dispose method will make all Parent and Children ViewModels unsubscribe from the Observable.
  • The second approach where we will complete our Observable.

First Approach

The classic way is disposing subscriptions. I call it the classic way because as a (good?) software developer, we can hardly resist the temptation to call Dispose on a IDisposable object. In Rx, the Subscribe method will return one of this IDisposable beauty, what should we do?

Let's dispose it.

C#
public class ParentViewModelDisposing : IDisposable, INotifyPropertyChanged
{
    private readonly ObservableCollection<ChildViewModelDisposing> _children = 
                     new ObservableCollection<ChildViewModelDisposing>();
    private readonly IDisposable _subscription;

    public ParentViewModelDisposing()
    {
        _subscription = Observable.Interval(TimeSpan.FromMilliseconds(300))
            .GroupBy(t => t % 10)
            .Select(t => new ChildViewModelDisposing
                         (t.Key, t.Select(t2 => new Random().NextDouble())))
            .ObserveOn(DispatcherScheduler.Current)
            .Subscribe(t => _children.Add(t));
    }

    public IEnumerable<ChildViewModelDisposing> Children => _children;

    public void Dispose()
    {
        _subscription.Dispose();

        foreach (var childViewModel in _children) childViewModel.Dispose();
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

And here is ChildViewModelDisposing:

C#
public class ChildViewModelDisposing : INotifyPropertyChanged, IDisposable
{
    private readonly IDisposable _subscription;

    public ChildViewModelDisposing(long key, IObservable<double> observable)
    {
        Key = key;
        _subscription = observable
            .Subscribe(t =>
                {
                    Value = t;
                    PropertyChanged?.Invoke
                    (this, new PropertyChangedEventArgs(nameof(Value)));
                }
            );
    }

    public long Key { get; }
    public double Value { get; private set; }

    public void Dispose()
    {
        _subscription?.Dispose();
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

In this scenario, the Dispose of ParentViewModelDisposing will dispose its RX subscription and dispose its children.

Each child will clean its subscription.

On Dispose, Observable source won't have any reference on our ViewModels.

Second Approach

In this second approach, we state that the Observable is responsible for the lifetime of our screen. We can complete it and our screen will be ready for cleaning.

Let's look at the code:

C#
public class ParentViewModelCompleting : IDisposable, INotifyPropertyChanged
{
    private readonly ObservableCollection<ChildViewModelCompleting> _children = 
                                 new ObservableCollection<ChildViewModelCompleting>();
    private readonly Subject<Unit> _dispose = new Subject<Unit>();

    public ParentViewModelCompleting()
    {
        Observable.Interval(TimeSpan.FromMilliseconds(300))
            .TakeUntil(_dispose)
            .GroupBy(t => t % 10)
            .Select(t => new ChildViewModelCompleting(t.Key, 
                             t.Select(t2 => new Random().NextDouble())))
            .ObserveOn(DispatcherScheduler.Current)
            .Subscribe(t => _children.Add(t));
    }

    public IEnumerable<ChildViewModelCompleting> Children => _children;

    public void Dispose()
    {
        if (_dispose.IsDisposed)
            return;

        _dispose.OnNext(Unit.Default);
        _dispose.Dispose();
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

We complete the feeding Observable with the method TakeUntil passing it our subject that will be in charge of completing it.

Here is ChildViewModelCompleting:

C#
public class ChildViewModelCompleting : INotifyPropertyChanged
{
    public ChildViewModelCompleting(long key, IObservable<double> observable)
    {
        Key = key;
        // we rely on observable completing
        observable
            .Subscribe(t =>
                {
                    Value = t;
                    PropertyChanged?.Invoke
                            (this, new PropertyChangedEventArgs(nameof(Value)));
                }
            );
    }

    public long Key { get; }
    public double Value { get; private set; }

    public event PropertyChangedEventHandler PropertyChanged;
}

And how surprising, the Child does not implement IDisposable.

When ParentViewModelCompleting completes the Observable, the parent and all the children subscriptions end gracefully.

On Dispose, Observable source won't have any reference retention on our ViewModels.

In this approach, a child cannot be cleaned independently. In our scenario, that is totally fine.

Post Profiling

To double check our ViewModel is garbage collected, we can close all of our tabs and re run our favourite memory profiler.

Job done, when we close our tabs, our ViewModels are well garbage collected.

Points of Interest

We have seen two different ways of cleaning ViewModel consuming RX, we can dispose all of the subscriptions or we can choose to complete the Observable.

Completing the observable is less code and more of a functional programming approach.

You can read this article to go further.

History

  • 17th May 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

G.TR
Software Developer
France France
No Biography provided

Comments and Discussions

 
QuestionA great topic but missing a lot Pin
PureNsanity2-Jun-21 15:14
professionalPureNsanity2-Jun-21 15:14 
AnswerRe: A great topic but missing a lot Pin
G.TR2-Jun-21 23:11
MemberG.TR2-Jun-21 23:11 
QuestionYour advice is incomplete Pin
Pete O'Hanlon16-May-21 20:10
mvePete O'Hanlon16-May-21 20:10 
AnswerRe: Your advice is incomplete Pin
G.TR16-May-21 23:07
MemberG.TR16-May-21 23:07 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.