CmdPal: Clean up ListItemViewModels when we no longer need them (#41169)

_We already fixed one leak, yes, but what about second leak?_

We already clean up `ListItemViewModel`s for a page when the page is
navigated away from. However, if the page updates it's items, we would
never actually `Cleanup` the old items. We'd just lose them, and never
unregister their event handlers. The objects would just leak forever.

This builds on the work in #41166, to do two things:
* Cleanup items that were removed from our list, when we actually update
`Items`. This involved a change to `Toolkit.ListHelpers`, to let us know
which items were removed from the list during `InPlaceUpdateList`
* Cleanup items that are thrown out when we cancel a FetchItems. Those
items were constructed, and might have registered event handlers, even
if we never actually put them into `Items`.

_Targets #41166_

Closes #39837

Tested with the evil sample from #41158, and loading thousands and
thousands of items no longer causes us to leak memory like we're
Deepwater Horizon.
This commit is contained in:
Mike Griese
2025-08-19 16:02:38 -05:00
committed by GitHub
parent 917da2e07e
commit ce4d8dc11e
2 changed files with 46 additions and 4 deletions

View File

@@ -65,12 +65,32 @@ public partial class ListHelpers
public static void InPlaceUpdateList<T>(IList<T> original, IEnumerable<T> newContents)
where T : class
{
InPlaceUpdateList(original, newContents, out _);
}
/// <summary>
/// Modifies the contents of `original` in-place, to match those of
/// `newContents`. The canonical use being:
/// ```cs
/// ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(ItemsToFilter, TextToFilterOn));
/// ```
/// </summary>
/// <typeparam name="T">Any type that can be compared for equality</typeparam>
/// <param name="original">Collection to modify</param>
/// <param name="newContents">The enumerable which `original` should match</param>
/// <param name="removedItems">List of items that were removed from the original collection</param>
public static void InPlaceUpdateList<T>(IList<T> original, IEnumerable<T> newContents, out List<T> removedItems)
where T : class
{
removedItems = [];
// we're not changing newContents - stash this so we don't re-evaluate it every time
var numberOfNew = newContents.Count();
// Short circuit - new contents should just be empty
if (numberOfNew == 0)
{
removedItems.AddRange(original);
original.Clear();
return;
}
@@ -92,6 +112,7 @@ public partial class ListHelpers
for (var k = i; k < j; k++)
{
// This item from the original list was not in the new list. Remove it.
removedItems.Add(original[i]);
original.RemoveAt(i);
}
@@ -120,6 +141,7 @@ public partial class ListHelpers
while (original.Count > numberOfNew)
{
// RemoveAtEnd
removedItems.Add(original[original.Count - 1]);
original.RemoveAt(original.Count - 1);
}