Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/Controls/src/Core/ListView/ListView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ public class ListView : ItemsView<Cell>, IListViewController, IElementConfigurat
int _previousGroupSelected = -1;
int _previousRowSelected = -1;

WeakCommandSubscription _refreshCommandSubscription;

/// <summary>
/// Controls whether anything happens in BeginRefresh(), is set based on RefreshCommand.CanExecute
/// </summary>
Expand Down Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions src/Controls/tests/Core.UnitTests/ListViewTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down
Loading