Search code examples
c#wpfmemory-leaksgarbage-collectionweakeventmanager

Avoiding WeakEventManager and memory leaks on short lived viewmodels


I have long living models with properties displayed using a view. The DataContext on my view is a ViewModel with a short lifespan.

Examples include row viewmodels in lists.

To avoid memory leaks the viewmodels subscribe to models using System.Windows.WeakEventManager.

If I were to subscribe normally the long living model would keep the viewmodel alive.

Using the WeakEventManager in about every viewmodel seems very cumbersome. The usecase looks like a standard usecase for WPF. Am I missing a fundamental concept of WPF or C# that would help me writing better code here?

Here is a minimal Example that Illustrates what I do right now.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        //building would take place in a factory method
        DataContext = new ShortLivedViewModel(new LongLivingModel());
    }
}

public class ShortLivedViewModel : INotifyPropertyChanged
{
    private string _myText;

    public ShortLivedViewModel(LongLivingModel model)
    {
        model.SomeEvent += SomeEventHandler;
        WeakEventManager<LongLivingModel, EventArgs>.AddHandler(model, nameof(LongLivingModel.SomeEvent),
            SomeEventHandler);
    }

    public string MyText
    {
        get => _myText;
        set
        {
            _myText = value;
            PropertyChanged(this, new PropertyChangedEventArgs(nameof(MyText)));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged = delegate { };

    private void SomeEventHandler(object sender, EventArgs e)
    {
        //The actual update content would come from the event args
        MyText = Guid.NewGuid().ToString("N");
    }
}

public class LongLivingModel
{
    //the event does not matter too much so I omit the implementation that causes it
    public EventHandler<EventArgs> SomeEvent = delegate { };
}

My question is if there is a less cumbersome way of subscribing to a long living object from a short living object. Or if there is some facility in WPF that I am missing for this.

It strikes me that this would be the standard case. What I played around with is adding an IDisposable interface but that just leaves me with tacking when to call dispose so I can unsubscribe.

What I am looking for may be a combination of telling the GC that subscriptions do not count for the lifetime of the viewmodel and unsubscribing on destruction - or an even better solution.


Solution

  • I think the parent viewmodel should be responsbile for getting rid of references.

    This is a simple example that pushes an update to each child viewmodel. When a child needs to be cleaned up, it tells the parent to remove its reference.

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            //building would take place in a factory method
            DataContext = new LongLivingModel();
            LongLivingModel.AddChild();
            LongLivingModel.AddChild();
        }
    }
    
    public class ShortLivedViewModel : INotifyPropertyChanged
    {
        private readonly LongLivingModel longLivingModel;
    
        public ShortLivedViewModel(LongLivingModel longLivingModel){
            this.longLivingModel = longLivingModel;
        }
    
        private string _myText;
    
        public string MyText
        {
            get => _myText;
            set
            {
                _myText = value;
                PropertyChanged(this, new PropertyChangedEventArgs(nameof(MyText)));
            }
        }
    
        public void Remove(){
            longLivingModel.Remove(this);
        }
    
        // INotifyPropertyChanged implementation
    }
    
    public class LongLivingModel
    {
        public ObservableCollection<ShortLivedViewModel> ChildViewModels { get; } = new ObservableCollection<ShortLivedViewModel>();
    
        public void AddChild(){
            ChildViewModels.Add(new ShortLivedViewModel(this));
        }
    
        public void RemoveChild(ShortLivedViewModel shortLivedViewModel) {
            ChildViewModels.Remove(shortLivedViewModel);
        }
    
        public void PushToChildren(){
            foreach(ShortLivedViewModel shortLivedViewModel in ChildViewModels){
                shortLivedViewModel.MyText = Guid.NewGuid().ToString("N");
            }
        }
    }