Home > Mobile >  WPF event unsubscribe leak in DataTemplate (XAML) on NET Framework
WPF event unsubscribe leak in DataTemplate (XAML) on NET Framework

Time:11-06

Imagine a UserControl with a ListBox having a CheckBox in a DataTemplate. The ItemsSource for the ListBox is some global list. The CheckBox has Checked/Unchecked events attached to it.

<ListBox ItemsSource="{Binding Source={x:Static a:MainWindow.Source}}">
    <ListBox.ItemTemplate>
        <DataTemplate DataType="{x:Type a:Data}">
            <CheckBox Content="{Binding Path=Name}"
                      Checked="ToggleButton_OnChecked"
                      Unchecked="ToggleButton_OnUnchecked"
                      IsChecked="{Binding Path=IsEnabled}"
                      Padding="10"
                      VerticalContentAlignment="Center"/>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

I am logging loaded/unloaded/checked/unchecked events in the main window's log.

    private void ToggleButton_OnChecked(object sender, RoutedEventArgs e)
    {
        Log("Checked");
    }

    private void ToggleButton_OnUnchecked(object sender, RoutedEventArgs e)
    {
        Log("Unchecked");
    }

    private void UserControl1_OnLoaded(object sender, RoutedEventArgs e)
    {
        Log("Loaded");
    }

    private void UserControl1_OnUnloaded(object sender, RoutedEventArgs e)
    {
        Log("Unloaded");
    }

The main window features a dynamic list of UserControl1 instances (starting with just one). There are add/remove buttons that allow me to add more instances.

<UniformGrid Rows="2">
    <DockPanel>
        <Button DockPanel.Dock="Top" Click="Add">Add</Button>
        <Button DockPanel.Dock="Top" Click="Remove">Remove</Button>
        <ListBox x:Name="ListBox">
            <local:UserControl1 />
        </ListBox>
    </DockPanel>

    <ListBox ItemsSource="{Binding ElementName=This,Path=Log}" FontFamily="Courier New"/>
</UniformGrid>

The window's codebehind:

    private void Add(object sender, RoutedEventArgs e)
    {
        ListBox.Items.Add(new UserControl1());
    }

    private void Remove(object sender, RoutedEventArgs e)
    {
        if (ListBox.Items.Count == 0) return;
        ListBox.Items.RemoveAt(0);
    }

When I run the app there is just one UserControl1 instance. If I add one more and then immediately remove one of them, then click the one and only checkbox on the screen, I see two "Checked" events logged. If I now uncheck it, there are two "Unchecked" events (even though "Unloaded" event was previously clearly logged. The hex numbers on the left show the output of a GetHashCode() which clearly shows the events were handled by distinct UserControl1 instances.

enter image description here

So even though one of UserControl1 gets unloaded, the events don't seem to get unsubscribed automatically. I have tried upgrading to NET Framework 4.8 to no avail. I see the same behavior. If I add 10 new controls and remove them immediately, I will observe 10 "Checked" or "Unchecked" events.

I have tried searching for a similar problem but could not find it. Is there something I am missing or did I just encounter a bug? Looking for workarounds.

Full source code is available on GitHub. https://github.com/wpfwannabe/datacontext-event-leak

CodePudding user response:

In the MVVM pattern, the view is bind to the view model and doesn't survive it when the garbage collection do it's job.

In the example you provided, the view model is a static object and by definition can't be garbage collected, so the view can't neither be garbage collected.

There is no automatic unbinding since you can reuse an instance of an user control (it can be Loaded and UnLoaded multiple times).

The simplest¹ way to fix this memory leak is to do the unbinding on unload :

Wpf

// First give the ListBox a name
<ListBox x:Name="ListBox" ItemsSource="{Binding Source={x:Static a:MainWindow.Source}}">

Code behind

private void UserControl1_OnUnloaded(object sender, RoutedEventArgs e)
{
    Log("Unloaded");
    ListBox.ItemsSource = null;
}

1: The proper way is to wrap the static list in a dedicated list view model, make the view model disposable (to unbind the wrapper from the static list on dispose), dispose the view model on remove.

  • Related