From 08d0388fb48ce318b1152b703e549440c69da78c Mon Sep 17 00:00:00 2001 From: Shane Neuville Date: Wed, 7 Feb 2024 17:59:51 -0600 Subject: [PATCH] Bring back some aspect of ConvertView on TableView and avoid AT_MOST Measure (#20130) * Bring back some aspect of convertView on TableView * Update TableViewRenderer.cs * - add more tests * - add comments fix double create of views * - add extra disconnect code to GetCell * - fix events --- .../src/Issues.Shared/Issue5555.cs | 3 + .../Issues/Issue5555.cs | 123 ++++++++++++++++++ .../Issues/Issue5924.xaml | 18 +++ .../Issues/Issue5924.xaml.cs | 18 +++ .../Utils/GarbageCollectionHelper.cs | 54 ++++++++ .../Handlers/ListView/Android/CellFactory.cs | 9 +- .../Handlers/ListView/Android/CellRenderer.cs | 46 ++++--- .../ListView/Android/EntryCellRenderer.cs | 24 ++-- .../ListView/Android/SwitchCellRenderer.cs | 7 - .../ListView/Android/TextCellRenderer.cs | 7 - .../ListView/Android/ViewCellRenderer.cs | 31 +++-- .../Android/TableViewModelRenderer.cs | 5 + .../TableView/Android/TableViewRenderer.cs | 49 ++++++- .../net-android/PublicAPI.Unshipped.txt | 2 + .../tests/UITests/Tests/Issues/Issue5555.cs | 35 +++++ .../tests/UITests/Tests/Issues/Issue5924.cs | 29 +++++ 16 files changed, 404 insertions(+), 56 deletions(-) create mode 100644 src/Controls/samples/Controls.Sample.UITests/Issues/Issue5555.cs create mode 100644 src/Controls/samples/Controls.Sample.UITests/Issues/Issue5924.xaml create mode 100644 src/Controls/samples/Controls.Sample.UITests/Issues/Issue5924.xaml.cs create mode 100644 src/Controls/samples/Controls.Sample.UITests/Utils/GarbageCollectionHelper.cs create mode 100644 src/Controls/tests/UITests/Tests/Issues/Issue5555.cs create mode 100644 src/Controls/tests/UITests/Tests/Issues/Issue5924.cs diff --git a/src/Compatibility/ControlGallery/src/Issues.Shared/Issue5555.cs b/src/Compatibility/ControlGallery/src/Issues.Shared/Issue5555.cs index 32023010c5c8..647e5d840778 100644 --- a/src/Compatibility/ControlGallery/src/Issues.Shared/Issue5555.cs +++ b/src/Compatibility/ControlGallery/src/Issues.Shared/Issue5555.cs @@ -102,6 +102,9 @@ protected override void Init() #if UITEST [Test] [Compatibility.UITests.FailsOnMauiIOS] +#if ANDROID + [Compatibility.UITests.MovedToAppium] +#endif public void Issue5555Test() { RunningApp.Tap(q => q.Marked("Push page")); diff --git a/src/Controls/samples/Controls.Sample.UITests/Issues/Issue5555.cs b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue5555.cs new file mode 100644 index 000000000000..113238799009 --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue5555.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Maui.Controls; + +namespace Maui.Controls.Sample.Issues +{ + [Issue(IssueTracker.None, 5555, "Memory leak when SwitchCell or EntryCell", PlatformAffected.iOS)] + public class Issue5555 : TestContentPage + { + public static Label DestructorCount = new Label() { Text = "0" }; + protected override void Init() + { + var instructions = new Label + { + FontSize = 16, + Text = "Click 'Push page' twice" + }; + + var result = new Label + { + Text = "Success", + AutomationId = "SuccessLabel", + IsVisible = false + }; + + var list = new List(); + + var checkButton = new Button + { + Text = "Check Result", + AutomationId = "CheckResult", + IsVisible = false, + Command = new Command(async () => + { + if (list.Count < 2) + { + instructions.Text = "Click 'Push page' again"; + return; + } + + try + { + await GarbageCollectionHelper.WaitForGC(2500, list.ToArray()); + result.Text = "Success"; + result.IsVisible = true; + instructions.Text = ""; + } + catch (Exception) + { + instructions.Text = "Failed"; + result.IsVisible = false; + return; + } + }) + }; + + Content = new StackLayout + { + Children = { + DestructorCount, + instructions, + result, + new Button + { + Text = "Push page", + AutomationId = "PushPage", + Command = new Command(async() => { + if (list.Count >= 2) + list.Clear(); + + var wref = new WeakReference(new LeakPage()); + + await Navigation.PushAsync(wref.Target as Page); + await (wref.Target as Page).Navigation.PopAsync(); + + list.Add(wref); + if (list.Count > 1) + { + checkButton.IsVisible = true; + instructions.Text = "You can check result"; + } + else + { + instructions.Text = "Again"; + } + }) + }, + checkButton + } + }; + } + + class LeakPage : ContentPage + { + public LeakPage() + { + Content = new StackLayout + { + Children = { + new Entry { Text = "LeakPage" }, + new TableView + { + Root = new TableRoot + { + new TableSection + { + new SwitchCell { Text = "switch cell", On = true }, + new EntryCell { Text = "entry cell" } + } + } + } + } + }; + } + + ~LeakPage() + { + System.Diagnostics.Debug.WriteLine("LeakPage Finalized"); + } + } + } +} \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample.UITests/Issues/Issue5924.xaml b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue5924.xaml new file mode 100644 index 000000000000..8946f04450f9 --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue5924.xaml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample.UITests/Issues/Issue5924.xaml.cs b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue5924.xaml.cs new file mode 100644 index 000000000000..3142f0c7280f --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Issues/Issue5924.xaml.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Microsoft.Maui.Controls; +using Microsoft.Maui.Controls.Xaml; + +namespace Maui.Controls.Sample.Issues +{ + [XamlCompilation(XamlCompilationOptions.Compile)] + [Issue(IssueTracker.Github, 5924, "TableView ViewCell vanishes after content is updated", PlatformAffected.Android)] + public partial class Issue5924 : ContentPage + { + public Issue5924() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/src/Controls/samples/Controls.Sample.UITests/Utils/GarbageCollectionHelper.cs b/src/Controls/samples/Controls.Sample.UITests/Utils/GarbageCollectionHelper.cs new file mode 100644 index 000000000000..d366cc4b37f3 --- /dev/null +++ b/src/Controls/samples/Controls.Sample.UITests/Utils/GarbageCollectionHelper.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Maui.Controls.Sample +{ + public static class GarbageCollectionHelper + { + public static async Task WaitForGC(params WeakReference[] references) => await WaitForGC(5000, references); + + public static async Task WaitForGC(int timeout, params WeakReference[] references) + { + bool referencesCollected() + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + + foreach (var reference in references) + { + if (reference.IsAlive) + { + return false; + } + } + + return true; + } + + await AssertEventually(referencesCollected, timeout); + } + + public static async Task AssertEventually(this Func assertion, int timeout = 1000, int interval = 100, string message = "Assertion timed out") + { + do + { + if (assertion()) + { + return; + } + + await Task.Delay(interval); + timeout -= interval; + + } + while (timeout >= 0); + + if (!assertion()) + { + throw new Exception(message); + } + } + } +} diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/CellFactory.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/CellFactory.cs index 104e18bdc2c3..f7706972bb01 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/CellFactory.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/CellFactory.cs @@ -12,13 +12,20 @@ public static class CellFactory { public static AView GetCell(Cell item, AView convertView, ViewGroup parent, Context context, View view) { + // If the convert view coming in is null that means this cell is going to need a new view generated for it + // This should probably be copied over to ListView once all sets of these TableView changes are propagated There + if (item.Handler is IElementHandler handler && convertView is null && view is TableView) + { + handler.DisconnectHandler(); + } + CellRenderer renderer = CellRenderer.GetRenderer(item); if (renderer == null) { var mauiContext = view.FindMauiContext() ?? item.FindMauiContext(); item.ConvertView = convertView; - _ = item.ToPlatform(mauiContext); + convertView = item.ToPlatform(mauiContext); item.ConvertView = null; renderer = CellRenderer.GetRenderer(item); diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/CellRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/CellRenderer.cs index 3a4ea454c69c..bc559007bc7c 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/CellRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/CellRenderer.cs @@ -15,8 +15,6 @@ public class CellRenderer : ElementHandler, IRegisterable { static readonly PropertyChangedEventHandler PropertyChangedHandler = OnGlobalCellPropertyChanged; - EventHandler _onForceUpdateSizeRequested; - public static PropertyMapper Mapper = new PropertyMapper(ElementHandler.ElementMapper); @@ -48,10 +46,13 @@ public AView GetCell(Cell item, AView convertView, ViewGroup parent, Context con Performance.Start(out string reference); + if (Cell is ICellController cellController) + cellController.ForceUpdateSizeRequested -= OnForceUpdateSizeRequested; + Cell = item; Cell.PropertyChanged -= PropertyChangedHandler; - if (convertView != null) + if (convertView is not null) { Object tag = convertView.Tag; CellRenderer renderer = (tag as RendererHolder)?.Renderer; @@ -112,20 +113,35 @@ protected virtual void OnCellPropertyChanged(object sender, PropertyChangedEvent protected void WireUpForceUpdateSizeRequested(Cell cell, AView platformCell) { ICellController cellController = cell; - cellController.ForceUpdateSizeRequested -= _onForceUpdateSizeRequested; + cellController.ForceUpdateSizeRequested -= OnForceUpdateSizeRequested; + cellController.ForceUpdateSizeRequested += OnForceUpdateSizeRequested; + } + + protected override void DisconnectHandler(AView platformView) + { + if (Cell is ICellController cellController) + cellController.ForceUpdateSizeRequested -= OnForceUpdateSizeRequested; + + base.DisconnectHandler(platformView); + } - _onForceUpdateSizeRequested = (sender, e) => + static void OnForceUpdateSizeRequested(object sender, EventArgs e) + { + if (sender is not Cell cellInner) + return; + + if (cellInner.Handler is not IElementHandler elementHandler || + elementHandler.PlatformView is not AView pCell || + !pCell.IsAlive()) { - if (platformCell.Handle == IntPtr.Zero) - return; - // RenderHeight may not be changed, but that's okay, since we - // don't actually use the height argument in the OnMeasure override. - platformCell.Measure(platformCell.Width, (int)cell.RenderHeight); - platformCell.SetMinimumHeight(platformCell.MeasuredHeight); - platformCell.SetMinimumWidth(platformCell.MeasuredWidth); - }; - - cellController.ForceUpdateSizeRequested += _onForceUpdateSizeRequested; + return; + } + + // RenderHeight may not be changed, but that's okay, since we + // don't actually use the height argument in the OnMeasure override. + pCell.Measure(pCell.Width, (int)cellInner.RenderHeight); + pCell.SetMinimumHeight(pCell.MeasuredHeight); + pCell.SetMinimumWidth(pCell.MeasuredWidth); } internal static CellRenderer GetRenderer(Cell cell) diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/EntryCellRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/EntryCellRenderer.cs index e7e753035942..3a8d5794a201 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/EntryCellRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/EntryCellRenderer.cs @@ -13,20 +13,10 @@ public class EntryCellRenderer : CellRenderer protected override global::Android.Views.View GetCellCore(Cell item, global::Android.Views.View convertView, ViewGroup parent, Context context) { - if (item?.Parent is TableView && item.Handler?.PlatformView is EntryCellView entryCellView) - { - // TableView doesn't use convertView - _view = entryCellView; - return _view; - } - + Disconnect(); if ((_view = convertView as EntryCellView) == null) - _view = new EntryCellView(context, item); - else { - _view.TextChanged = null; - _view.FocusChanged = null; - _view.EditingCompleted = null; + _view = new EntryCellView(context, item); } UpdateLabel(); @@ -166,5 +156,15 @@ void UpdateText() _view.EditText.Text = entryCell.Text; } + + void Disconnect() + { + if (_view is null) + return; + + _view.TextChanged = null; + _view.FocusChanged = null; + _view.EditingCompleted = null; + } } } diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/SwitchCellRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/SwitchCellRenderer.cs index 20439df13713..1cfb23cddbd7 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/SwitchCellRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/SwitchCellRenderer.cs @@ -19,13 +19,6 @@ protected override AView GetCellCore(Cell item, AView convertView, ViewGroup par { var cell = (SwitchCell)Cell; - if (item?.Parent is TableView && item.Handler?.PlatformView is SwitchCellView switchCellView) - { - // TableView doesn't use convertView - _view = switchCellView; - return _view; - } - if ((_view = convertView as SwitchCellView) == null) _view = new SwitchCellView(context, item); diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/TextCellRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/TextCellRenderer.cs index 7f5a2b405b3f..c9226d6ae8f7 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/TextCellRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/TextCellRenderer.cs @@ -13,13 +13,6 @@ public class TextCellRenderer : CellRenderer protected override AView GetCellCore(Cell item, AView convertView, ViewGroup parent, Context context) { - if (item?.Parent is TableView && item.Handler?.PlatformView is TextCellView textCellView) - { - // TableView doesn't use convertView - View = textCellView; - return View; - } - if ((View = convertView as TextCellView) == null) View = new TextCellView(context, item); diff --git a/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/ViewCellRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/ViewCellRenderer.cs index 2473a22e68ad..b1fbcfeac652 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/ViewCellRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/ListView/Android/ViewCellRenderer.cs @@ -20,7 +20,7 @@ protected override AView GetCellCore(Cell item, AView convertView, ViewGroup par var cell = (ViewCell)item; var container = convertView as ViewCellContainer; - if (container != null) + if (container is not null) { container.Update(cell); Performance.Stop(reference, "GetCellCore"); @@ -47,16 +47,25 @@ protected override AView GetCellCore(Cell item, AView convertView, ViewGroup par var view = (IPlatformViewHandler)cell.View.ToHandler(cell.FindMauiContext()); cell.View.IsPlatformEnabled = true; - ViewCellContainer c = view.PlatformView.GetParentOfType(); + // If the convertView is null we don't want to return the same view, we need to return a new one. + // We should probably do this for ListView as well + if (ParentView is TableView) + { + view.ToPlatform().RemoveFromParent(); + } + else + { + ViewCellContainer c = view.ToPlatform().GetParentOfType(); - if (c != null) - return c; + if (c != null) + return c; + } - c = new ViewCellContainer(context, (IPlatformViewHandler)cell.View.Handler, cell, ParentView, unevenRows, rowHeight); + var newContainer = new ViewCellContainer(context, (IPlatformViewHandler)cell.View.Handler, cell, ParentView, unevenRows, rowHeight); Performance.Stop(reference, "GetCellCore"); - return c; + return newContainer; } protected override void DisconnectHandler(AView platformView) @@ -142,7 +151,7 @@ public ViewCellContainer(Context context, IPlatformViewHandler view, ViewCell vi _unevenRows = unevenRows; _rowHeight = rowHeight; _viewCell = viewCell; - AddView(view.PlatformView); + AddView(view.ToPlatform()); UpdateIsEnabled(); UpdateWatchForLongPress(); } @@ -185,7 +194,7 @@ public override bool DispatchTouchEvent(MotionEvent e) public void Update(ViewCell cell) { - // This cell could have a handler that was used for the measure pass for the Listview height calculations + // This cell could have a handler that was used for the measure pass for the ListView height calculations //cell.View.Handler.DisconnectHandler(); Performance.Start(out string reference); @@ -250,7 +259,7 @@ public void UpdateIsEnabled() public void DisconnectHandler() { - var oldView = _currentView ?? _viewHandler.PlatformView; + var oldView = _currentView ?? _viewHandler.ToPlatform(); if (oldView != null) RemoveView(oldView); @@ -274,7 +283,7 @@ public override void AddView(AView child) protected override void OnLayout(bool changed, int l, int t, int r, int b) { - if (_viewHandler.PlatformView == null || Context == null) + if (_viewHandler.PlatformView is null || Context is null) { return; } @@ -291,7 +300,7 @@ protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) if (ParentHasUnevenRows) { - if (_viewHandler.PlatformView == null) + if (_viewHandler.PlatformView is null) { SetMeasuredDimension(0, 0); return; diff --git a/src/Controls/src/Core/Compatibility/Handlers/TableView/Android/TableViewModelRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/TableView/Android/TableViewModelRenderer.cs index 6df17a9a6854..901c72dda3bf 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/TableView/Android/TableViewModelRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/TableView/Android/TableViewModelRenderer.cs @@ -139,6 +139,11 @@ public override AView GetView(int position, AView convertView, ViewGroup parent) return new AView(Context); } + if (convertView is ConditionalFocusLayout cfl && cfl.ChildCount > 0) + { + convertView = cfl.GetChildAt(0); + } + AView nativeCellContent = CellFactory.GetCell(item, convertView, parent, Context, _view); // The cell content we get back might already be in a ConditionalFocusLayout; if it is, diff --git a/src/Controls/src/Core/Compatibility/Handlers/TableView/Android/TableViewRenderer.cs b/src/Controls/src/Core/Compatibility/Handlers/TableView/Android/TableViewRenderer.cs index 5d573090c401..9395c646e5b5 100644 --- a/src/Controls/src/Core/Compatibility/Handlers/TableView/Android/TableViewRenderer.cs +++ b/src/Controls/src/Core/Compatibility/Handlers/TableView/Android/TableViewRenderer.cs @@ -4,6 +4,7 @@ using AndroidX.Core.Widget; using Microsoft.Maui.Controls.Platform; using Microsoft.Maui.Graphics; +using Microsoft.Maui.Primitives; using AListView = Android.Widget.ListView; using AView = Android.Views.View; @@ -86,6 +87,36 @@ protected override void OnAttachedToWindow() } } + protected override void OnMeasure(int widthMeasureSpec, int heightMeasureSpec) + { + // If you perform an AtMost (or Unspecified) height measure of ListView, Android will essentially create + // a scrap copy of all the ListView cells to calculate the height. + // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/widget/ListView.java;l=1314-1322?q=ListView + // This causes issues because if a TextCell already has a view that's attached to the visual tree, then `OnMeasure(AT_MOST)` + // will call "GetView" without a convert view. Android basically creates an in memory copy of the table to calculate the measure. + // + // Our problem is that we don't have a way of knowing if a view we are returning from getView will be the one we + // should track against our TextCellHandler or not. + // This all worked fine in XF because in XF we didn't really block against just creating as many renderers against a single + // VirtualView as you wanted. This led to a whole different set of hard-to-track issues. + // Fundamentally, the ListView control on Android is an old control and the TableView should really be converted to + // a BindableLayout or just generating cross-platform views against a VerticalStackLayout. + // + // We handle the Unspecified path inside "GetDesiredSize" by calculating the height of the cells ourselves and requesting an exact measure. + // Because another quirk of ListView is that if you give it an unspecified measure, it'll just size itself to the first row + // https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/android/widget/ListView.java;l=1289-1304?q=ListView + // + // There is a path here where we could make our structures play friendly with the ListView and then just let ListView do its scrapview thing + // But, for how we use TableView, converting to an Exactly measure seems good enough for us. + if (heightMeasureSpec.GetMode() == MeasureSpecMode.AtMost) + { + var size = MeasureSpec.GetSize(heightMeasureSpec); + heightMeasureSpec = MeasureSpec.MakeMeasureSpec(size, MeasureSpecMode.Exactly); + } + + base.OnMeasure(widthMeasureSpec, heightMeasureSpec); + } + public override SizeRequest GetDesiredSize(double widthConstraint, double heightConstraint) { if (double.IsInfinity(heightConstraint)) @@ -94,7 +125,11 @@ public override SizeRequest GetDesiredSize(double widthConstraint, double height { heightConstraint = (int)(_adapter.Count * Element.RowHeight); } - else if (_adapter != null) + else if (this is IViewHandler vh && vh.VirtualView is not null && Dimension.IsExplicitSet(vh.VirtualView.Height) && Context is not null) + { + heightConstraint = MeasureSpec.MakeMeasureSpec((int)Context.ToPixels(vh.VirtualView.Height), MeasureSpecMode.Exactly); + } + else if (_adapter is not null) { double totalHeight = 0; int adapterCount = _adapter.Count; @@ -107,7 +142,15 @@ public override SizeRequest GetDesiredSize(double widthConstraint, double height continue; } - AView listItem = _adapter.GetView(i, null, Control); + // We aren't using ToPlatform because we only want the platform view if it's been created + // Also the cells only implement `IElementHandler` so they don't have ContainerViews + var platformView = cell.Handler?.PlatformView as AView; + + // If the view has a parent that means it's already been added to the `ConditionalFocusLayout` + // That's going to be the actual `convertView` + var convertView = platformView?.Parent as AView; + + AView listItem = _adapter.GetView(i, convertView, Control); int widthSpec; if (double.IsInfinity(widthConstraint)) @@ -154,4 +197,4 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } } -} \ No newline at end of file +} diff --git a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt index c2084bb89b88..4d96e859eb7f 100644 --- a/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt +++ b/src/Controls/src/Core/PublicAPI/net-android/PublicAPI.Unshipped.txt @@ -64,6 +64,7 @@ Microsoft.Maui.Controls.PointerGestureRecognizer.PointerReleasedCommand.set -> v Microsoft.Maui.Controls.PointerGestureRecognizer.PointerReleasedCommandParameter.get -> object! Microsoft.Maui.Controls.PointerGestureRecognizer.PointerReleasedCommandParameter.set -> void override Microsoft.Maui.Controls.Handlers.Compatibility.TableViewModelRenderer.GetItemViewType(int position) -> int +override Microsoft.Maui.Controls.Handlers.Compatibility.TableViewRenderer.OnMeasure(int widthMeasureSpec, int heightMeasureSpec) -> void override Microsoft.Maui.Controls.Handlers.Items.MauiCarouselRecyclerView.OnAttachedToWindow() -> void override Microsoft.Maui.Controls.Handlers.Items.MauiCarouselRecyclerView.OnDetachedFromWindow() -> void static readonly Microsoft.Maui.Controls.KeyboardAccelerator.KeyProperty -> Microsoft.Maui.Controls.BindableProperty! @@ -128,6 +129,7 @@ override Microsoft.Maui.Controls.SearchBar.IsEnabledCore.get -> bool ~Microsoft.Maui.Controls.InputView.FontFamily.set -> void ~Microsoft.Maui.Controls.WebView.UserAgent.get -> string ~Microsoft.Maui.Controls.WebView.UserAgent.set -> void +~override Microsoft.Maui.Controls.Handlers.Compatibility.CellRenderer.DisconnectHandler(Android.Views.View platformView) -> void ~override Microsoft.Maui.Controls.Handlers.Items.ItemsViewHandler.DisconnectHandler(AndroidX.RecyclerView.Widget.RecyclerView platformView) -> void ~override Microsoft.Maui.Controls.ImageButton.OnPropertyChanged(string propertyName = null) -> void ~override Microsoft.Maui.Controls.LayoutOptions.Equals(object obj) -> bool diff --git a/src/Controls/tests/UITests/Tests/Issues/Issue5555.cs b/src/Controls/tests/UITests/Tests/Issues/Issue5555.cs new file mode 100644 index 000000000000..50b721c3bfa4 --- /dev/null +++ b/src/Controls/tests/UITests/Tests/Issues/Issue5555.cs @@ -0,0 +1,35 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.AppiumTests.Issues +{ + public class Issue5555 : _IssuesUITest + { + public override string Issue => "Memory leak when SwitchCell or EntryCell"; + public Issue5555(TestDevice device) : base(device) + { + } + + [Test] + public void TableViewMemoryLeakWhenUsingSwitchCellOrEntryCell() + { + this.IgnoreIfPlatforms(new[] + { + TestDevice.Mac, + TestDevice.iOS, + }); + + App.WaitForElement("PushPage"); + App.Click("PushPage"); + App.WaitForElement("PushPage"); + App.Click("PushPage"); + App.WaitForElement("PushPage"); + + App.WaitForElement("CheckResult"); + App.Click("CheckResult"); + + App.WaitForElement("SuccessLabel"); + } + } +} \ No newline at end of file diff --git a/src/Controls/tests/UITests/Tests/Issues/Issue5924.cs b/src/Controls/tests/UITests/Tests/Issues/Issue5924.cs new file mode 100644 index 000000000000..2cacca030eb2 --- /dev/null +++ b/src/Controls/tests/UITests/Tests/Issues/Issue5924.cs @@ -0,0 +1,29 @@ +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.AppiumTests.Issues +{ + public class Issue5924 : _IssuesUITest + { + public override string Issue => "TableView ViewCell vanishes after content is updated"; + public Issue5924(TestDevice device) : base(device) + { + } + + [Test] + public void TableViewViewCellVanishesAfterContentIsUpdated() + { + App.WaitForElement("entry"); + App.EnterText("entry", "I haven't disappeared"); + + var entry = App.WaitForElement("entry").GetRect(); + var label = App.WaitForElement("label").GetRect(); + + Assert.Greater(entry.Height, 0); + Assert.Greater(entry.Width, 0); + Assert.Greater(label.Height, 0); + Assert.Greater(label.Width, 0); + } + } +} \ No newline at end of file