0

I'm using MVVM to create an application which switches between multiple views. The views are instantiated through a ContentControl like this:

<ContentControl Name="DynamicViewControl" Content="{Binding }">
    <ContentControl.Resources>
         <DataTemplate x:Key="PageOneTemplate">
             <pages:PageOne DataContext="{Binding PageOneViewModel}"/>
         </DataTemplate>
         <DataTemplate x:Key="PageTwoTemplate">
             <pages:PageTwo DataContext="{Binding PageTwoViewModel}"/>
         </DataTemplate>
         <!-- And so on... -->
    </ContentControl.Resources>
    <ContentControl.Style>
        <Style TargetType="{x:Type ContentControl}">
            <Style.Triggers>
                 <DataTrigger Binding="{Binding CurrentPage}" Value="{x:Static model:Pages.PageOne}">
                       <Setter Property="ContentTemplate" Value="{StaticResource PageOneTemplate}"/>
                 </DataTrigger>
                 <DataTrigger Binding="{Binding CurrentPage}" Value="{x:Static model:Pages.PageTwo}">
                    <Setter Property="ContentTemplate" Value="{StaticResource PageTwoTemplate}"/>
                 </DataTrigger>           
                 <!-- And so on... -->     
            </Style.Triggers>
        </Style>
    </ContentControl.Style>
</ContentControl>

Where the underlying ViewModel looks something like this:

public enum Pages {
    PageOne,
    PageTwo,
    // etc...
}

public class PageViewModel : ObservableObject {
    private Pages currentPage = Pages.PageOne;

    public PageViewModel() {
        PageOneViewModel= new PageOneViewModel(Model);
        PageTwoViewModel= new PageTwoViewModel(Model);
        // And so on...

        NavButtonCommand = new RelayCommand(NavButton);
        PreviousButtonCommand = new RelayCommand(PreviousButton);
    }

    public PageModel Model { get; } = new PageModel();

    /// <summary>Gets or sets the current page.</summary>
    public Pages CurrentPage {
        get => currentPage;
        set {
            currentPage = value;
            NotifyPropertyChanged();
        }
    }

    public DataSelectionViewModel PageOneViewModel { get; }

    public ProfileSelectionViewModel PageTwoViewModel { get; }

    public ICommand NavButtonCommand { get; }

    public ICommand PreviousButtonCommand { get; }

    // This isn't my actual page change logic, just some code with a 
    // similar effect to get my point across
    private void NavButton(object param) {
        int next = (int)CurrentPage + 1;
        if Enum.IsDefined(typeof(Pages), next) {
            CurrentPage = (Pages)next;
        }
    }

    private void PreviousButton(object param) {
        int previous = (int)CurrentPage - 1;
        if Enum.IsDefined(typeof(Pages), previous) {
            CurrentPage = (Pages)previous;
        }
    }
}

The problem is that in some of my Views, I need to subscribe to PropertyChanged notifications on their respective ViewModels so that I can change things in my View that can't be bound easily. This results in a "memory leak" (insofar as a memory leak can exist in C#) because the ContentControl creates a new View every time and it never gets cleaned up due to those event handlers still having references in the ViewModel.

I tried cleaning up all event subscribers to the ViewModel when the View changes, but quite apart from the fact that it results in view cleanup code inside the ViewModel, it also had unintended consequences, and made some of my functionality stop working.

Is there a way for me to tell my views to stop subscribing to events? Or, should I find a way to bind them (such as creating a custom control with DependencyProperties which can be bound).

Lauraducky
  • 674
  • 11
  • 25

1 Answers1

1

I found the answer, quicker than I thought. The way that most WPF controls do it turns out to be the Weak Event Pattern. This pattern allows you to subscribe to events with a weak reference. The solution was to change lines like this:

model.PropertyChanged += Model_PropertyChanged;

To something more like this:

PropertyChangedEventManager.AddHandler(model, Model_PropertyChanged, "MyProperty");

That way, even if the ViewModel has a longer lifetime than the view, any references will only be weak ones, allowing the garbage collector to come along and clean up the object even though its event subscriptions haven't been cleaned up.

Lauraducky
  • 674
  • 11
  • 25
  • Take a look to this article, maybe you will find it helpful: https://csharpvault.com/blog/weak-event-pattern/ – Arnel Jan 15 '18 at 03:56