diff --git a/src/Controls/src/Core/Cells/Cell.cs b/src/Controls/src/Core/Cells/Cell.cs index c63c4a94d386..3dcc71edef02 100644 --- a/src/Controls/src/Core/Cells/Cell.cs +++ b/src/Controls/src/Core/Cells/Cell.cs @@ -273,6 +273,7 @@ async void OnForceUpdateSizeRequested() // don't run more than once per 16 milliseconds await Task.Delay(TimeSpan.FromMilliseconds(16)); ForceUpdateSizeRequested?.Invoke(this, null); + Handler.Invoke("ForceUpdateSizeRequested", null); _nextCallToForceUpdateSizeQueued = false; } diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/CellRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/CellRenderer.cs index a0e390947e65..eab9e8276267 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/CellRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/CellRenderer.cs @@ -11,8 +11,6 @@ public class CellRenderer : ElementHandler, IRegisterable { static readonly BindableProperty RealCellProperty = BindableProperty.CreateAttached("RealCell", typeof(UITableViewCell), typeof(Cell), null); - EventHandler? _onForceUpdateSizeRequested; - PropertyChangedEventHandler? _onPropertyChangedEventHandler; readonly UIColor _defaultCellBgColor = (OperatingSystem.IsIOSVersionAtLeast(13) || OperatingSystem.IsTvOSVersionAtLeast(13)) ? UIColor.Clear : UIColor.White; public static PropertyMapper Mapper = @@ -22,6 +20,8 @@ public class CellRenderer : ElementHandler, IRegisterable new CommandMapper(ElementHandler.ElementCommandMapper); UITableView? _tableView; + private protected event PropertyChangedEventHandler? CellPropertyChanged; + public CellRenderer() : base(Mapper, CommandMapper) { } @@ -44,8 +44,6 @@ public virtual UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, tvc.Cell = item; - WireUpForceUpdateSizeRequested(item, tvc, tv); - if (OperatingSystem.IsIOSVersionAtLeast(14)) { var content = tvc.DefaultContentConfiguration; @@ -135,44 +133,9 @@ protected void UpdateBackground(UITableViewCell tableViewCell, Cell cell) SetBackgroundColor(tableViewCell, cell, uiBgColor); } + [Obsolete("The ForceUpdateSizeRequested event is now managed by the command mapper, so it's not necessary to invoke this event manually.")] protected void WireUpForceUpdateSizeRequested(ICellController cell, UITableViewCell platformCell, UITableView tableView) { - var inpc = cell as INotifyPropertyChanged; - cell.ForceUpdateSizeRequested -= _onForceUpdateSizeRequested; - - if (inpc != null) - inpc.PropertyChanged -= _onPropertyChangedEventHandler; - - _onForceUpdateSizeRequested = (sender, e) => - { - var index = tableView?.IndexPathForCell(platformCell); - if (index == null && sender is Cell c) - { - index = Controls.Compatibility.Platform.iOS.CellExtensions.GetIndexPath(c); - } - - if (index != null) - tableView?.ReloadRows(new[] { index }, UITableViewRowAnimation.None); - }; - - _onPropertyChangedEventHandler = (sender, e) => - { - if (e.PropertyName == "RealCell" && sender is BindableObject bo && GetRealCell(bo) == null) - { - if (sender is ICellController icc) - icc.ForceUpdateSizeRequested -= _onForceUpdateSizeRequested; - - if (sender is INotifyPropertyChanged notifyPropertyChanged) - notifyPropertyChanged.PropertyChanged -= _onPropertyChangedEventHandler; - - _onForceUpdateSizeRequested = null; - _onPropertyChangedEventHandler = null; - } - }; - - cell.ForceUpdateSizeRequested += _onForceUpdateSizeRequested; - if (inpc != null) - inpc.PropertyChanged += _onPropertyChangedEventHandler; } @@ -190,5 +153,36 @@ internal static void SetRealCell(BindableObject cell, UITableViewCell renderer) { cell.SetValue(RealCellProperty, renderer); } + + public override void UpdateValue(string property) + { + base.UpdateValue(property); + var args = new PropertyChangedEventArgs(property); + if (VirtualView is BindableObject bindableObject && + GetRealCell(bindableObject) is CellTableViewCell ctv ) + { + ctv.HandlePropertyChanged(bindableObject, args); + } + + CellPropertyChanged?.Invoke(VirtualView, args); + } + + public override void Invoke(string command, object? args) + { + base.Invoke(command, args); + + if (command == "ForceUpdateSizeRequested" && + VirtualView is BindableObject bindableObject && + GetRealCell(bindableObject) is UITableViewCell ctv) + { + var index = _tableView?.IndexPathForCell(ctv); + if (index == null && VirtualView is Cell c) + { + index = Controls.Compatibility.Platform.iOS.CellExtensions.GetIndexPath(c); + } + if (index != null) + _tableView?.ReloadRows(new[] { index }, UITableViewRowAnimation.None); + } + } } -} +} \ No newline at end of file diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/CellTableViewCell.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/CellTableViewCell.cs index 3ec0d531bc93..84ad28b6f1dd 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/CellTableViewCell.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/CellTableViewCell.cs @@ -9,7 +9,7 @@ namespace Microsoft.Maui.Controls.Handlers.Compatibility { public class CellTableViewCell : UITableViewCell, INativeElementView { - Cell _cell; + WeakReference _cell; public Action PropertyChanged; @@ -21,23 +21,28 @@ public CellTableViewCell(UITableViewCellStyle style, string key) : base(style, k public Cell Cell { - get { return _cell; } + get => _cell?.GetTargetOrDefault(); set { - if (_cell == value) - return; - - if (_cell != null) + if (_cell is not null) { - _cell.PropertyChanged -= HandlePropertyChanged; - BeginInvokeOnMainThread(_cell.SendDisappearing); + if (_cell.TryGetTarget(out var cell) && cell == value) + return; + + if (cell is not null) + { + BeginInvokeOnMainThread(cell.SendDisappearing); + } } - _cell = value; - if (_cell != null) + if (value is not null) + { + _cell = new(value); + BeginInvokeOnMainThread(value.SendAppearing); + } + else { - _cell.PropertyChanged += HandlePropertyChanged; - BeginInvokeOnMainThread(_cell.SendAppearing); + _cell = null; } } } @@ -109,12 +114,9 @@ protected override void Dispose(bool disposing) if (disposing) { - PropertyChanged = null; - - if (_cell != null) + if (Cell is Cell cell) { - _cell.PropertyChanged -= HandlePropertyChanged; - CellRenderer.SetRealCell(_cell, null); + CellRenderer.SetRealCell(cell, null); } _cell = null; } @@ -124,4 +126,4 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } } -} \ No newline at end of file +} diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/EntryCellRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/EntryCellRenderer.cs index 0a6ff189c91d..3b506c726b52 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/EntryCellRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/EntryCellRenderer.cs @@ -28,10 +28,12 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, var tvc = reusableCell as EntryCellTableViewCell; if (tvc == null) + { tvc = new EntryCellTableViewCell(item.GetType().FullName); + } else { - tvc.PropertyChanged -= HandlePropertyChanged; + CellPropertyChanged -= HandlePropertyChanged; tvc.TextFieldTextChanged -= OnTextFieldTextChanged; tvc.KeyboardDoneButtonPressed -= OnKeyBoardDoneButtonPressed; } @@ -39,12 +41,10 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, SetRealCell(item, tvc); tvc.Cell = item; - tvc.PropertyChanged += HandlePropertyChanged; + CellPropertyChanged += HandlePropertyChanged; tvc.TextFieldTextChanged += OnTextFieldTextChanged; tvc.KeyboardDoneButtonPressed += OnKeyBoardDoneButtonPressed; - WireUpForceUpdateSizeRequested(item, tvc, tv); - UpdateBackground(tvc, entryCell); UpdateLabel(tvc, entryCell); UpdateText(tvc, entryCell); diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/ImageCellRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/ImageCellRenderer.cs index 50214195fa46..428711634557 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/ImageCellRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/ImageCellRenderer.cs @@ -20,8 +20,6 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, var imageCell = (ImageCell)item; - WireUpForceUpdateSizeRequested(item, result, tv); - SetImage(imageCell, result); return result; @@ -47,12 +45,12 @@ void SetImage(ImageCell cell, CellTableViewCell target) source.LoadImage(cell.FindMauiContext(), (result) => { - var uiimage = result.Value; - if (uiimage != null) + var uiimage = result?.Value; + if (uiimage is not null) { NSRunLoop.Main.BeginInvokeOnMainThread(() => { - if (target.Cell != null) + if (target.Cell is not null) { target.ImageView.Image = uiimage; target.SetNeedsLayout(); diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/SwitchCellRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/SwitchCellRenderer.cs index fb07f837d2fe..85a8239d8869 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/SwitchCellRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/SwitchCellRenderer.cs @@ -28,7 +28,7 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, else { uiSwitch = tvc.AccessoryView as UISwitch; - tvc.PropertyChanged -= HandlePropertyChanged; + CellPropertyChanged -= HandlePropertyChanged; } SetRealCell(item, tvc); @@ -45,7 +45,7 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, _defaultOnColor = UISwitch.Appearance.OnTintColor; tvc.Cell = item; - tvc.PropertyChanged += HandlePropertyChanged; + CellPropertyChanged += HandlePropertyChanged; tvc.AccessoryView = uiSwitch; #pragma warning disable CA1416, CA1422 // TODO: 'UITableViewCell.TextLabel' is unsupported on: 'ios' 14.0 and later tvc.TextLabel.Text = boolCell.Text; @@ -53,8 +53,6 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, uiSwitch.On = boolCell.On; - WireUpForceUpdateSizeRequested(item, tvc, tv); - UpdateBackground(tvc, item); UpdateIsEnabled(tvc, boolCell); UpdateFlowDirection(tvc, boolCell); diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/TextCellRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/TextCellRenderer.cs index adc91a73b87e..82e83a4c695c 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/TextCellRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/TextCellRenderer.cs @@ -24,12 +24,12 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, if (!(reusableCell is CellTableViewCell tvc)) tvc = new CellTableViewCell(UITableViewCellStyle.Subtitle, item.GetType().FullName); else - tvc.PropertyChanged -= HandleCellPropertyChanged; + CellPropertyChanged -= HandleCellPropertyChanged; SetRealCell(item, tvc); tvc.Cell = textCell; - tvc.PropertyChanged = HandleCellPropertyChanged; + CellPropertyChanged += HandleCellPropertyChanged; #pragma warning disable CA1416, CA1422 // TODO: 'UITableViewCell.TextLabel', DetailTextLabel is unsupported on: 'ios' 14.0 and later tvc.TextLabel.Text = textCell.Text; @@ -37,8 +37,6 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, tvc.TextLabel.TextColor = (textCell.TextColor ?? DefaultTextColor).ToPlatform(); tvc.DetailTextLabel.TextColor = (textCell.DetailColor ?? DefaultDetailColor).ToPlatform(); - WireUpForceUpdateSizeRequested(item, tvc, tv); - UpdateIsEnabled(tvc, textCell); #pragma warning restore CA1416, CA1422 diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/ViewCellRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/ViewCellRenderer.cs index 3ffa18919f74..b4e43ce53683 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/ViewCellRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/iOS/ViewCellRenderer.cs @@ -31,8 +31,6 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, SetRealCell(item, cell); - WireUpForceUpdateSizeRequested(item, cell, tv); - UpdateBackground(cell, item); SetAccessibility(cell, item); @@ -43,9 +41,9 @@ public override UITableViewCell GetCell(Cell item, UITableViewCell reusableCell, internal sealed class ViewTableCell : UITableViewCell, INativeElementView { - IMauiContext MauiContext => _viewCell.FindMauiContext(); + IMauiContext MauiContext => ViewCell?.FindMauiContext(); WeakReference _rendererRef; - ViewCell _viewCell; + WeakReference _viewCell; Element INativeElementView.Element => ViewCell; internal bool SupressSeparator { get; set; } @@ -57,13 +55,15 @@ public ViewTableCell(string key) : base(UITableViewCellStyle.Default, key) public ViewCell ViewCell { - get { return _viewCell; } + get => _viewCell?.GetTargetOrDefault(); set { - if (_viewCell == value) - return; - - _viewCell?.Handler?.DisconnectHandler(); + if (_viewCell is not null) + { + if (_viewCell.TryGetTarget(out var viewCell) && viewCell == value) + return; + viewCell?.Handler?.DisconnectHandler(); + } UpdateCell(value); } } @@ -82,7 +82,7 @@ void ViewCellPropertyChanged(object sender, PropertyChangedEventArgs e) var realCell = (ViewTableCell)GetRealCell(viewCell); if (e.PropertyName == Cell.IsEnabledProperty.PropertyName) - UpdateIsEnabled(_viewCell.IsEnabled); + UpdateIsEnabled(viewCell.IsEnabled); } public override void LayoutSubviews() @@ -149,11 +149,11 @@ protected override void Dispose(bool disposing) _rendererRef = null; } - if (_viewCell != null) + if (ViewCell is ViewCell viewCell) { - _viewCell.PropertyChanged -= ViewCellPropertyChanged; - _viewCell.View.MeasureInvalidated -= OnMeasureInvalidated; - SetRealCell(_viewCell, null); + viewCell.PropertyChanged -= ViewCellPropertyChanged; + viewCell.View.MeasureInvalidated -= OnMeasureInvalidated; + SetRealCell(viewCell, null); } _viewCell = null; } @@ -165,10 +165,10 @@ protected override void Dispose(bool disposing) IPlatformViewHandler GetNewRenderer() { - if (_viewCell.View == null) - throw new InvalidOperationException($"ViewCell must have a {nameof(_viewCell.View)}"); + if (ViewCell is not ViewCell viewCell || viewCell.View == null) + throw new InvalidOperationException($"ViewCell must have a {nameof(viewCell.View)}"); - var newRenderer = _viewCell.View.ToHandler(_viewCell.View.FindMauiContext()); + var newRenderer = viewCell.View.ToHandler(viewCell.View.FindMauiContext()); _rendererRef = new WeakReference(newRenderer); ContentView.ClearSubviews(); ContentView.AddSubview(newRenderer.VirtualView.ToPlatform()); @@ -179,37 +179,37 @@ void UpdateCell(ViewCell cell) { Performance.Start(out string reference); - var oldCell = _viewCell; - if (oldCell != null) + if (ViewCell is ViewCell oldCell) { BeginInvokeOnMainThread(oldCell.SendDisappearing); oldCell.PropertyChanged -= ViewCellPropertyChanged; oldCell.View.MeasureInvalidated -= OnMeasureInvalidated; } - _viewCell = cell; - if (cell is null) { + _viewCell = null; _rendererRef = null; ContentView.ClearSubviews(); return; } - _viewCell.PropertyChanged += ViewCellPropertyChanged; - BeginInvokeOnMainThread(_viewCell.SendAppearing); + _viewCell = new(cell); + + cell.PropertyChanged += ViewCellPropertyChanged; + BeginInvokeOnMainThread(cell.SendAppearing); IPlatformViewHandler renderer; if (_rendererRef == null || !_rendererRef.TryGetTarget(out renderer)) renderer = GetNewRenderer(); else { - var viewHandlerType = MauiContext.Handlers.GetHandlerType(_viewCell.View.GetType()); + var viewHandlerType = MauiContext.Handlers.GetHandlerType(cell.View.GetType()); var reflectableType = renderer as System.Reflection.IReflectableType; var rendererType = reflectableType != null ? reflectableType.GetTypeInfo().AsType() : (renderer != null ? renderer.GetType() : typeof(System.Object)); if (rendererType == viewHandlerType/* || (renderer is Platform.DefaultRenderer && type == null)*/) - renderer.SetVirtualView(this._viewCell.View); + renderer.SetVirtualView(cell.View); else { //when cells are getting reused the element could be already set to another cell @@ -219,8 +219,8 @@ void UpdateCell(ViewCell cell) } } - UpdateIsEnabled(_viewCell.IsEnabled); - _viewCell.View.MeasureInvalidated += OnMeasureInvalidated; + UpdateIsEnabled(cell.IsEnabled); + cell.View.MeasureInvalidated += OnMeasureInvalidated; Performance.Stop(reference); } diff --git a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt index 73b19b6a1f68..ec30a83003c7 100644 --- a/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-ios/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ #nullable enable +override Microsoft.Maui.Controls.Handlers.Compatibility.CellRenderer.Invoke(string! command, object? args) -> void +override Microsoft.Maui.Controls.Handlers.Compatibility.CellRenderer.UpdateValue(string! property) -> void override Microsoft.Maui.Controls.Handlers.Compatibility.ShellRenderer.PreferredStatusBarUpdateAnimation.get -> UIKit.UIStatusBarAnimation override Microsoft.Maui.Controls.Handlers.Compatibility.ShellRenderer.PrefersStatusBarHidden() -> bool override Microsoft.Maui.Controls.GradientBrush.IsEmpty.get -> bool diff --git a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt index caa61b6d473d..c77202070fd5 100644 --- a/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-maccatalyst/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ #nullable enable +override Microsoft.Maui.Controls.Handlers.Compatibility.CellRenderer.Invoke(string! command, object? args) -> void +override Microsoft.Maui.Controls.Handlers.Compatibility.CellRenderer.UpdateValue(string! property) -> void override Microsoft.Maui.Controls.GradientBrush.IsEmpty.get -> bool override Microsoft.Maui.Controls.Label.ArrangeOverride(Microsoft.Maui.Graphics.Rect bounds) -> Microsoft.Maui.Graphics.Size override Microsoft.Maui.Controls.Handlers.Compatibility.ShellRenderer.PreferredStatusBarUpdateAnimation.get -> UIKit.UIStatusBarAnimation diff --git a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs index 31ad522ff871..ae8a6211b8fc 100644 --- a/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs +++ b/src/Controls/tests/DeviceTests/Memory/MemoryTests.cs @@ -30,6 +30,7 @@ void SetupBuilder() handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); + handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); @@ -41,6 +42,7 @@ void SetupBuilder() handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); + handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); @@ -49,11 +51,13 @@ void SetupBuilder() handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); + handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); handlers.AddHandler(); + handlers.AddHandler(); #if IOS || MACCATALYST handlers.AddHandler(); #else @@ -242,6 +246,49 @@ await navPage.Navigation.PushAsync(new ContentPage await AssertionExtensions.WaitForGC(viewReference, handlerReference); } + [Theory("Cells Do Not Leak")] + [InlineData(typeof(TextCell))] + [InlineData(typeof(EntryCell))] + [InlineData(typeof(ImageCell))] + [InlineData(typeof(SwitchCell))] + [InlineData(typeof(ViewCell))] + public async Task CellsDoNotLeak(Type type) + { + SetupBuilder(); + + WeakReference viewReference = null; + WeakReference handlerReference = null; + + var observable = new ObservableCollection { 1 }; + var navPage = new NavigationPage(new ContentPage { Title = "Page 1" }); + + await CreateHandlerAndAddToWindow(new Window(navPage), async () => + { + await navPage.Navigation.PushAsync(new ContentPage + { + Content = new ListView + { + ItemTemplate = new DataTemplate(() => + { + var cell = (Cell)Activator.CreateInstance(type); + if (cell is ViewCell viewCell) + { + viewCell.View = new Label(); + } + viewReference = new WeakReference(cell); + handlerReference = new WeakReference(cell.Handler); + return cell; + }), + ItemsSource = observable + } + }); + + await navPage.Navigation.PopAsync(); + }); + + await AssertionExtensions.WaitForGC(viewReference, handlerReference); + } + #if IOS [Fact] public async Task ResignFirstResponderTouchGestureRecognizer()