Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ebf3822
Enable MemoryAnalyzers on Controls.Core + sample fix (GroupableItemsV…
Copilot Jul 2, 2026
4ee8758
Resolve MemoryAnalyzers findings on modern Controls.Core handlers
Copilot Jul 2, 2026
3e12ee3
Remove .editorconfig severity downgrade; enforce MemoryAnalyzers at e…
Copilot Jul 3, 2026
036bbbf
Resolve MemoryAnalyzers findings: Navigation/FlyoutPage/Tabbed + iOS …
Copilot Jul 3, 2026
4b04c0e
Resolve MemoryAnalyzers findings: ListView/TableView + Shell support …
Copilot Jul 3, 2026
7fd07b6
Resolve MemoryAnalyzers findings: Shell section/flyout/item renderers
Copilot Jul 3, 2026
5a3f7ec
[Tests] Verify Shell renderer does not leak after navigation
Copilot Jul 7, 2026
78e53ff
Merge remote-tracking branch 'origin/main' into revive/memoryanalyzer…
Copilot Jul 7, 2026
edddbf5
Handle new MemoryAnalyzers findings from main merge
Copilot Jul 7, 2026
508cc38
Fix MemoryAnalyzers lifecycle issues
Copilot Jul 21, 2026
264aa27
Fix remaining Apple lifecycle leaks
Copilot Jul 22, 2026
417883b
Guard Shell transitions during teardown
Copilot Jul 22, 2026
45c9a2b
Dispose stale Shell flyout images
Copilot Jul 22, 2026
e759ebc
Fix late iOS lifecycle callbacks after disposal
Copilot Jul 22, 2026
cda4ac9
Fix remaining Apple lifecycle teardown
Copilot Jul 22, 2026
b657c38
Dispose Shell section root native resources
Copilot Jul 22, 2026
e7f32c3
Track secondary toolbar content ownership
Copilot Jul 22, 2026
2228762
Fix Shell transition teardown review findings
Copilot Jul 22, 2026
96731ea
Fix Shell flyout late lifecycle callbacks
Copilot Jul 22, 2026
7ebf934
Cancel Shell item transitions during teardown
Copilot Jul 23, 2026
436e010
Fix delayed image test cancellation race
Copilot Jul 23, 2026
6a3f854
Guard secondary toolbar updates after custom view replacement
Copilot Jul 23, 2026
54a1b7f
Make Shell flyout disposal idempotent
Copilot Jul 23, 2026
c49b7e8
Disconnect Shell flyout container handlers
Copilot Jul 23, 2026
5ef9015
Clear Shell table source subscribers on disposal
Copilot Jul 23, 2026
d54ad27
Fix Shell title view handler reparenting
Copilot Jul 23, 2026
a6a6559
Fix Shell teardown review findings
Copilot Jul 23, 2026
5f07efd
Merge branch 'inflight/current' into revive/memoryanalyzers-controls
kubaflo Jul 24, 2026
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#nullable disable
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using CoreGraphics;
using Microsoft.Maui.Controls.Internals;
Expand All @@ -15,16 +16,21 @@ namespace Microsoft.Maui.Controls.Handlers.Compatibility
{
public class PhoneFlyoutPageRenderer : UIViewController, IPlatformViewHandler
{
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "The click-off view is owned by the renderer and disposed in Dispose.")]
UIView _clickOffView;
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "The detail child controller is owned by the renderer while its containers are packed and emptied in Dispose.")]
UIViewController _detailController;
WeakReference<VisualElement> _element;
bool _disposed;

[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "The flyout child controller is owned by the renderer while its containers are packed and emptied in Dispose.")]
UIViewController _flyoutController;

[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "The pan gesture recognizer is removed from the view and disposed in Dispose.")]
UIPanGestureRecognizer _panGesture;

bool _presented;
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "The tap gesture recognizer is removed from the click-off view and disposed in Dispose.")]
UIGestureRecognizer _tapGesture;

bool _applyShadow;
Expand All @@ -34,6 +40,7 @@ public class PhoneFlyoutPageRenderer : UIViewController, IPlatformViewHandler

Page Page => Element as Page;
IFlyoutPageController FlyoutPageController => FlyoutPage;
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "The Maui context is required for the compatibility renderer lifetime and is not exposed outside the handler.")]
IMauiContext _mauiContext;
IMauiContext MauiContext => _mauiContext;

Expand All @@ -58,6 +65,7 @@ public bool FlyoutOverlapsDetailsInPopoverMode

bool IsRTL => (Element as IVisualElementController)?.EffectiveFlowDirection.IsRightToLeft() == true;

[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "The mapper is static shared handler metadata and is not retained by renderer instances.")]
public static IPropertyMapper<FlyoutPage, PhoneFlyoutPageRenderer> Mapper = new PropertyMapper<FlyoutPage, PhoneFlyoutPageRenderer>(ViewHandler.ViewMapper);
public static CommandMapper<FlyoutPage, PhoneFlyoutPageRenderer> CommandMapper = new CommandMapper<FlyoutPage, PhoneFlyoutPageRenderer>(ViewHandler.ViewCommandMapper);
ViewHandlerDelegator<FlyoutPage> _viewHandlerWrapper;
Expand All @@ -77,6 +85,7 @@ bool Presented

public VisualElement Element => _viewHandlerWrapper.Element ?? _element?.GetTargetOrDefault();

[UnconditionalSuppressMessage("Memory", "MEM0001", Justification = "ElementChanged is a legacy public compatibility renderer event kept for API compatibility.")]
public event EventHandler<VisualElementChangedEventArgs> ElementChanged;

public Size GetDesiredSize(double widthConstraint, double heightConstraint)
Expand All @@ -89,6 +98,7 @@ public UIView NativeView
get { return View; }
}

[UnconditionalSuppressMessage("Memory", "MEM0003", Justification = "The Element SizeChanged subscription is removed in Dispose.")]
public void SetElement(VisualElement element)
{

Expand Down Expand Up @@ -163,6 +173,7 @@ void SetInitialPresented()
UpdateLeftBarButton();
}

[UnconditionalSuppressMessage("Memory", "MEM0003", Justification = "The FlyoutPage PropertyChanged subscription is removed in Dispose.")]
public override void ViewDidLoad()
{
base.ViewDidLoad();
Expand Down Expand Up @@ -242,6 +253,8 @@ protected override void Dispose(bool disposing)
{
Element.SizeChanged -= PageOnSizeChanged;
Element.PropertyChanged -= HandlePropertyChanged;
if (Element is FlyoutPage flyoutPage)
flyoutPage.Flyout.PropertyChanged -= HandleFlyoutPropertyChanged;

if (_tapGesture != null)
{
Expand Down Expand Up @@ -603,6 +616,7 @@ void UpdateBackground()
});
}

[UnconditionalSuppressMessage("Memory", "MEM0003", Justification = "The Flyout page PropertyChanged subscription is removed when containers update and in Dispose.")]
void UpdateFlyoutPageContainers()
{
((FlyoutPage)Element).Flyout.PropertyChanged -= HandleFlyoutPropertyChanged;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#nullable disable
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Maui.Controls.Compatibility;
using ObjCRuntime;
using UIKit;
Expand All @@ -13,6 +14,7 @@ public class CellTableViewCell : UITableViewCell, INativeElementView
WeakReference<Cell> _cell;
#pragma warning restore CS0618 // Type or member is obsolete

[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Callback is cleared in Dispose(bool) before the cell is released.")]
public Action<object, PropertyChangedEventArgs> PropertyChanged;
Comment thread
kubaflo marked this conversation as resolved.

Comment thread
kubaflo marked this conversation as resolved.
bool _disposed;
Expand Down Expand Up @@ -46,6 +48,7 @@ public Cell Cell
}
else
{
PropertyChanged = null;
_cell = null;
}
}
Expand Down Expand Up @@ -119,6 +122,8 @@ protected override void Dispose(bool disposing)

if (disposing)
{
PropertyChanged = null;

#pragma warning disable CS0618 // Type or member is obsolete
if (Cell is Cell cell)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#nullable disable
using System;
using System.Diagnostics.CodeAnalysis;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
Expand Down Expand Up @@ -27,8 +28,11 @@ internal sealed class ContextActionsCell : UITableViewCell, INativeElementView
#pragma warning disable CS0618 // Type or member is obsolete
Cell _cell;
#pragma warning restore CS0618 // Type or member is obsolete
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Button is owned for the context-actions cell lifetime and disposed in Dispose(bool).")]
UIButton _moreButton;
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Scroll view is owned for the context-actions cell lifetime and disposed in Dispose(bool).")]
UIScrollView _scroller;
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Table view reference is cleared in Dispose(bool) when the cell is released.")]
UITableView _tableView;
bool _isDiposed;

Expand All @@ -55,6 +59,7 @@ public ContextActionsCell(string templateId) : base(UITableViewCellStyle.Default
{
}

[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Content cell is replaced during updates and retained after disposal because UIKit can issue late layout and reuse callbacks.")]
public UITableViewCell ContentCell { get; private set; }

public bool IsOpen => ScrollDelegate.IsOpen;
Expand Down Expand Up @@ -131,6 +136,7 @@ public override void RemoveFromSuperview()
}

#pragma warning disable CS0618 // Type or member is obsolete
[UnconditionalSuppressMessage("Memory", "MEM0003", Justification = "Cell and context-action subscriptions are removed in Update and Dispose(bool).")]
Comment thread
kubaflo marked this conversation as resolved.
public void Update(UITableView tableView, Cell cell, UITableViewCell nativeCell)
#pragma warning restore CS0618 // Type or member is obsolete
{
Expand Down Expand Up @@ -297,6 +303,7 @@ protected override void Dispose(bool disposing)

if (_cell != null)
{
_cell.PropertyChanged -= OnCellPropertyChanged;
if (_cell.HasContextActions)
((INotifyCollectionChanged)_cell.ContextActions).CollectionChanged -= OnContextItemsChanged;
_cell = null;
Expand Down Expand Up @@ -520,6 +527,7 @@ void ReloadRowCore()
}
}

[UnconditionalSuppressMessage("Memory", "MEM0003", Justification = "Button handlers are attached to buttons owned by this cell and the buttons are disposed in Update and Dispose(bool).")]
UIView SetupButtons(nfloat width, nfloat height)
{
MenuItem destructive = null;
Expand Down Expand Up @@ -641,6 +649,7 @@ internal static void SetupSelection(UITableView table)

private sealed class SelectGestureRecognizer : UITapGestureRecognizer
{
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Gesture recognizer stores only the last touched index path while attached to the table view.")]
NSIndexPath _lastPath;

public SelectGestureRecognizer() : base(Tapped)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using ObjCRuntime;
using UIKit;
using NSAction = System.Action;
Expand Down Expand Up @@ -42,14 +43,19 @@ public override void LayoutSubviews()
internal sealed class ContextScrollViewDelegate : UIScrollViewDelegate
{
readonly nfloat _finalButtonSize;
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Background view reference is cleared in Dispose(bool) or after RestoreHighlight reinserts it.")]
UIView _backgroundView;
List<UIButton> _buttons;
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Tap recognizer is removed and disposed by ClearCloserRecognizer or Dispose(bool).")]
UITapGestureRecognizer _closer;
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Container view reference is cleared in Dispose(bool) when the delegate is released.")]
UIView _container;
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Global close recognizer is removed from the table and disposed by ClearCloserRecognizer or Dispose(bool).")]
Controls.Compatibility.Platform.iOS.GlobalCloseContextGestureRecognizer _globalCloser;

bool _isDisposed;
static WeakReference<UIScrollView> s_scrollViewBeingScrolled;
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Table view reference is cleared in Dispose(bool) and when the global close recognizer is removed.")]
UITableView _table;

public ContextScrollViewDelegate(UIView container, List<UIButton> buttons, bool isOpen)
Expand All @@ -70,6 +76,7 @@ public ContextScrollViewDelegate(UIView container, List<UIButton> buttons, bool

public nfloat ButtonsWidth { get; }

[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Callback is cleared in Dispose(bool) when the delegate is released.")]
public Action ClosedCallback { get; set; }

public bool IsOpen { get; private set; }
Expand Down Expand Up @@ -216,6 +223,20 @@ protected override void Dispose(bool disposing)
{
ClosedCallback = null;

if (_closer != null)
Comment thread
kubaflo marked this conversation as resolved.
{
_closer.View?.RemoveGestureRecognizer(_closer);
_closer.Dispose();
Comment thread
kubaflo marked this conversation as resolved.
_closer = null;
}

if (_globalCloser != null)
{
_table?.RemoveGestureRecognizer(_globalCloser);
_globalCloser.Dispose();
_globalCloser = null;
}

s_scrollViewBeingScrolled = null;
_table = null;
_backgroundView = null;
Expand All @@ -232,11 +253,14 @@ void ClearCloserRecognizer(ContextActionsCell cell)
if (_globalCloser == null || _globalCloser.State == UIGestureRecognizerState.Cancelled)
return;

cell?.ContentCell?.RemoveGestureRecognizer(_closer);
_closer.Dispose();
_closer = null;
if (_closer is not null)
{
cell?.ContentCell?.RemoveGestureRecognizer(_closer);
_closer.Dispose();
_closer = null;
}

_table.RemoveGestureRecognizer(_globalCloser);
_table?.RemoveGestureRecognizer(_globalCloser);
_table = null;
_globalCloser.Dispose();
_globalCloser = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#nullable disable
using System;
using System.Diagnostics.CodeAnalysis;
using System.ComponentModel;
using System.Runtime.Versioning;
using Foundation;
Expand Down Expand Up @@ -196,8 +197,10 @@ public EntryCellTableViewCell(string cellName) : base(UITableViewCellStyle.Value
ContentView.AddSubview(TextField);
}

[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Text field is owned as a UIKit subview for the entry-cell lifetime.")]
public UITextField TextField { get; }

[UnconditionalSuppressMessage("Memory", "MEM0001", Justification = "Event is cleared in Dispose(bool) and renderer subscriptions are removed when cells are reused.")]
public event EventHandler KeyboardDoneButtonPressed;

public override void LayoutSubviews()
Expand All @@ -219,8 +222,22 @@ public override void LayoutSubviews()
TextField.VerticalAlignment = UIControlContentVerticalAlignment.Center;
}

[UnconditionalSuppressMessage("Memory", "MEM0001", Justification = "Event is cleared in Dispose(bool) and renderer subscriptions are removed when cells are reused.")]
public event EventHandler TextFieldTextChanged;

protected override void Dispose(bool disposing)
{
if (disposing)
{
TextField.EditingChanged -= TextFieldOnEditingChanged;
TextField.ShouldReturn = null;
KeyboardDoneButtonPressed = null;
TextFieldTextChanged = null;
}

base.Dispose(disposing);
}

static bool OnShouldReturn(UITextField view)
{
var realCell = GetRealCell<EntryCellTableViewCell>(view);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#nullable disable
using System;
using System.Diagnostics.CodeAnalysis;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
Expand Down Expand Up @@ -40,14 +41,19 @@ public class ListViewRenderer : ViewRenderer<ListView, UITableView>

const int DefaultRowHeight = 44;

[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Background view is owned for the renderer lifetime and disposed in CleanUpResources.")]
UIView _backgroundUIView;
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Data source is owned for the renderer lifetime and disposed in CleanUpResources.")]
ListViewDataSource _dataSource;
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Header handler is owned for the renderer lifetime and cleared in CleanUpResources.")]
IPlatformViewHandler _headerRenderer;
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Footer handler is owned for the renderer lifetime and cleared in CleanUpResources.")]
IPlatformViewHandler _footerRenderer;

RectangleF _previousFrame;
ScrollToRequestedEventArgs _requestedScroll;

[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Table view controller is owned for the renderer lifetime and disposed in CleanUpResources.")]
FormsUITableViewController _tableViewController;
#pragma warning disable CS0618 // Type or member is obsolete
ListView ListView => Element;
Expand Down Expand Up @@ -201,6 +207,7 @@ void CleanUpResources()
Control?.TableFooterView?.Dispose();
}
#pragma warning disable CS0618 // Type or member is obsolete
[UnconditionalSuppressMessage("Memory", "MEM0003", Justification = "ListView and templated-items subscriptions are removed for the old element and in CleanUpResources.")]
protected override void OnElementChanged(ElementChangedEventArgs<ListView> e)
#pragma warning restore CS0618 // Type or member is obsolete
{
Expand Down Expand Up @@ -485,6 +492,7 @@ void OnScrollToRequested(object sender, ScrollToRequestedEventArgs e)
}
}

[UnconditionalSuppressMessage("Memory", "MEM0003", Justification = "Footer MeasureInvalidated subscription is removed when the footer changes and in CleanUpResources.")]
void UpdateFooter()
{
var footer = ListView.FooterElement;
Expand Down Expand Up @@ -524,6 +532,7 @@ void UpdateFooter()
}
}

[UnconditionalSuppressMessage("Memory", "MEM0003", Justification = "Header MeasureInvalidated subscription is removed when the header changes and in CleanUpResources.")]
void UpdateHeader()
{
var header = ListView.HeaderElement;
Expand Down Expand Up @@ -838,6 +847,7 @@ void UpdateHorizontalScrollBarVisibility()

internal sealed class UnevenListViewDataSource : ListViewDataSource
{
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Prototype handler is disconnected and cleared in ClearPrototype from Dispose(bool).")]
IPlatformViewHandler _prototype;
bool _disposed;
#pragma warning disable CS0618 // Type or member is obsolete
Expand Down Expand Up @@ -1068,6 +1078,7 @@ public ListViewDataSource(ListViewDataSource source)
}

#pragma warning disable CS0618 // Type or member is obsolete
[UnconditionalSuppressMessage("Memory", "MEM0003", Justification = "ListView ItemSelected subscription is removed in ListViewDataSource.Dispose(bool).")]
public ListViewDataSource(ListView list, FormsUITableViewController uiTableViewController)
#pragma warning restore CS0618 // Type or member is obsolete
{
Expand Down Expand Up @@ -1541,6 +1552,7 @@ void UpdateShortNameListener()
WatchShortNameCollection(list.IsGroupingEnabled);
}

[UnconditionalSuppressMessage("Memory", "MEM0003", Justification = "ShortNames CollectionChanged subscription is removed by WatchShortNameCollection(false) in Dispose(bool).")]
void WatchShortNameCollection(bool watch)
{
if (!_list.TryGetTarget(out var list))
Expand Down Expand Up @@ -1630,6 +1642,7 @@ public HeaderWrapperView(string reuseIdentifier) : base((NSString)reuseIdentifie
{
}

[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Header/footer wrapper owns the table view cell as a UIKit subview for the wrapper lifetime.")]
UITableViewCell _tableViewCell;

#pragma warning disable CS0618 // Type or member is obsolete
Expand Down Expand Up @@ -1658,6 +1671,7 @@ internal sealed class FormsUITableViewController : UITableViewController
#pragma warning disable CS0618 // Type or member is obsolete
readonly WeakReference<ListView> _list;
#pragma warning restore CS0618 // Type or member is obsolete
[UnconditionalSuppressMessage("Memory", "MEM0002", Justification = "Refresh control is owned by the table controller and disposed in Dispose(bool).")]
UIRefreshControl _refresh;

bool _refreshAdded;
Expand All @@ -1667,6 +1681,7 @@ internal sealed class FormsUITableViewController : UITableViewController
bool _isStartRefreshingPending;

#pragma warning disable CS0618 // Type or member is obsolete
[UnconditionalSuppressMessage("Memory", "MEM0003", Justification = "Refresh control ValueChanged subscription is removed in FormsUITableViewController.Dispose(bool).")]
public FormsUITableViewController(ListView element, bool usingLargeTitles)
: base(element.OnThisPlatform().GetGroupHeaderStyle() == GroupHeaderStyle.Plain
? UITableViewStyle.Plain
Expand Down
Loading
Loading