diff --git a/src/Controls/src/Core/ListView/ListView.cs b/src/Controls/src/Core/ListView/ListView.cs index 0b380d22773d..f1bdc94febfb 100644 --- a/src/Controls/src/Core/ListView/ListView.cs +++ b/src/Controls/src/Core/ListView/ListView.cs @@ -97,6 +97,8 @@ public class ListView : ItemsView, IListViewController, IElementConfigurat int _previousGroupSelected = -1; int _previousRowSelected = -1; + WeakCommandSubscription _refreshCommandSubscription; + /// /// Controls whether anything happens in BeginRefresh(), is set based on RefreshCommand.CanExecute /// @@ -763,12 +765,13 @@ void OnRefreshCommandChanged(ICommand oldCommand, ICommand newCommand) { if (oldCommand != null) { - oldCommand.CanExecuteChanged -= OnCommandCanExecuteChanged; + _refreshCommandSubscription?.Dispose(); + _refreshCommandSubscription = null; } if (newCommand != null) { - newCommand.CanExecuteChanged += OnCommandCanExecuteChanged; + _refreshCommandSubscription = new WeakCommandSubscription(this, newCommand, OnCommandCanExecuteChanged); RefreshAllowed = newCommand.CanExecute(null); } else diff --git a/src/Controls/tests/Core.UnitTests/ListViewTests.cs b/src/Controls/tests/Core.UnitTests/ListViewTests.cs index 8b0f905646c5..4a8024e2dab0 100644 --- a/src/Controls/tests/Core.UnitTests/ListViewTests.cs +++ b/src/Controls/tests/Core.UnitTests/ListViewTests.cs @@ -35,6 +35,43 @@ public void TestConstructor() Assert.Equal(LayoutOptions.FillAndExpand, listView.VerticalOptions); } + [Fact] + public async Task RefreshCommandDoesNotLeakListView() + { + // A long-lived command whose CanExecuteChanged is a *strong* .NET event, as with a + // custom ICommand or CommunityToolkit's RelayCommand. (Microsoft.Maui.Controls.Command + // routes CanExecuteChanged through a WeakEventManager and would not reproduce the leak.) + ICommand command = new StrongCanExecuteCommand(); + + // Create the ListView in a separate method so no local in this frame roots it. + WeakReference CreateReference() + { + var listView = new ListView { RefreshCommand = command }; + return new WeakReference(listView); + } + + WeakReference reference = CreateReference(); + + await TestHelpers.Collect(); + + // The command out-lives the ListView; subscribing to its CanExecuteChanged must not + // keep the ListView (and its cells, templates and binding context) alive. + Assert.False(await reference.WaitForCollect(), "ListView should not be alive!"); + + GC.KeepAlive(command); + } + + sealed class StrongCanExecuteCommand : ICommand + { + public event EventHandler CanExecuteChanged; + + public bool CanExecute(object parameter) => true; + + public void Execute(object parameter) { } + + public void ChangeCanExecute() => CanExecuteChanged?.Invoke(this, EventArgs.Empty); + } + internal class ListItem { public string Name { get; set; }