I'm using weak event managers such as the PropertyChangedEventManager
to subscribe to events in my WPF application.
However, I've found a possible memory leak - but I think it might not strictly be a memory leak, just things reacting to events before the garbage collector gets around to picking them up. Here's an example demonstrating my problem:
The MainWindow
class and lovely code-behind with a property Number, that I want to listen to, and a strong reference to the listening class.
It has two button events, one to add/remove the strong reference, one to change the property Number:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
// strong reference to listening class that will be added/removed
private TestReferenceClass strongReference;
// property that the listening class will listen to
private int number;
public int Number
{
get { return number; }
set { number = value; NotifyPropertyChanged(); }
}
// for UI
private bool referenceExists;
public bool ReferenceExists
{
get { return referenceExists; }
set { referenceExists = value; NotifyPropertyChanged(); }
}
//button event to change Number
private void ChangeProp(object sender, RoutedEventArgs e)
{
Number++;
}
//button event to assign/remove reference
private void ToggleReference(object sender, RoutedEventArgs e)
{
if (strongReference != null)
{
strongReference = null;
ReferenceExists = false;
}
else
{
strongReference = new TestReferenceClass(this);
ReferenceExists = true;
}
}
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
With the XAML:
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow" Height="150" Width="525">
<Window.Resources>
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
</Window.Resources>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Horizontal">
<Button Margin="10, 0" Click="ToggleReference">Toggle Reff</Button>
<Grid>
<TextBlock Text="No Refference" Background="White" VerticalAlignment="Center"/>
<TextBlock Text="Refference" Visibility="{Binding ReferenceExists, Converter={StaticResource BooleanToVisibilityConverter}}" Background="White" VerticalAlignment="Center"/>
</Grid>
<Button Margin="10, 0" Click="ChangeProp">Change property</Button>
<TextBox Text="{Binding Number}" Width="200" IsReadOnly="True"/>
</StackPanel>
</Window>
Then there's the 'referenced' class that listens to the property changes. It is simply created, and listens for a change on Number using the weak event listener PropertyChangedEventManager
:
public class TestReferenceClass
{
public TestReferenceClass(MainWindow source)
{
// add a weak event manager
PropertyChangedEventManager.AddHandler(source, Source_PropertyChanged, string.Empty);
}
private void Source_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Number")
{
Console.WriteLine("Property changed! " + (sender as MainWindow).Number);
}
}
}
The result is a little application that lets you create and destroy references, and trigger their events, leaving only the weak listener holding a reference:
It becomes clear after messing around with it a bit that you can trigger the event on an object that's only being held by the weak reference. If you spam the create/destroy button a few times you can trigger multiple events for the same click like this:
Property changed! 3
Property changed! 3
Property changed! 3
Property changed! 4
Property changed! 4
Property changed! 4
It's also triggered for quite a while after the reference is removed. However, a true leak is avoided as it does appear to be removed eventually, maybe after a round of garbage collection?
Is this expected behavior? I.e. the weak event listener doesn't actively check there's no other references before triggering the event, but simply checks that the object has been garbage collected (which CAN happen because it's only a weak reference, so just happens 'eventually')?
Normally this wouldn't be such an issue, except I've got a case where I'm doing some pretty expensive operations (getting data for a graph) because of a change, in some objects that are not easy to track (a very interactive UI where the user can drag and drop things around all over the layout). I could implement IDisposable or similar, but it would be a big refactor.