How to use disposable view models in WPF?

Viewed 6419

How do I ensure view models are properly disposed of if they reference unmanaged resources or have event handlers such as handling elapsed on a dispatcher timer. In the first case, a finaliser is an option, although not ideal, but in the latter, it will never be called. How can we tell when there is no longer a view attached to the view model.

3 Answers

You should dispose the view model when the view is Unloaded or when the application is shutting down. The existing answer takes this approach, but doesn't handle the case where the view is loaded again after being unloaded. If that is a scenario you need to handle, you will also need to re-create the data context when the view is re-loaded.

public partial class CustomControl : UserControl
{
    public CustomControl()
    {
        InitializeComponent();
        
        // Initialize the view model
        SetViewModel();

        // Create/Dispose the view model as this view is loaded/unloaded
        Loaded += (_, _) => SetViewModel();
        Unloaded += (_, _) => DisposeViewModel();

        // Dispose the view model when the application is shutting down
        Dispatcher.ShutdownStarted += (_, _) => DisposeViewModel();
    }

    // Use whatever logic here that you need to create the view model
    private void SetViewModel() =>
        DataContext ??= new CustomControlViewModel();

    public void DisposeViewModel()
    {
        // Nothing to do if there is no view model to dispose
        if (DataContext is not IDisposable viewModel) return;

        // Remove and dispose the view model
        // NOTE: Make sure to set DataContext to null before disposing so 
        // that the view doesn't have a chance to access a disposed object
        DataContext = null;
        viewModel.Dispose();
    }
}

If you use this pattern a lot, it could also be easily moved into an extension method that you can call from the view constructor:

public static void UseDisposableViewModel(this FrameworkElement view, Func<IDisposable> viewModelFactory)
{
    SetViewModel();
    view.Loaded += (_, _) => SetViewModel();
    view.Unloaded += (_, _) => DisposeViewModel();
    view.Dispatcher.ShutdownStarted += (_, _) => DisposeViewModel();

    void SetViewModel() =>
        view.DataContext ??= viewModelFactory();

    void DisposeViewModel()
    {
        if (view.DataContext is not IDisposable viewModel) return;
        view.DataContext = null;
        viewModel.Dispose();
    }
}
Related