Skip to content

Commit 98d4bbf

Browse files
[ios] fix memory leak in Entry (#16349)
Context: #16346 This addresses the memory leak discovered by: src/Core/src/Platform/iOS/MauiTextField.cs(69,32): error MA0001: Event 'SelectionChanged' could cause memory leaks in an NSObject subclass. Remove the event or add the [UnconditionalSuppressMessage("Memory", "MA0001")] attribute with a justification as to why the event will not leak. src/Core/src/Platform/iOS/MauiTextField.cs(68,30): error MA0001: Event 'TextPropertySet' could cause memory leaks in an NSObject subclass. Remove the event or add the [UnconditionalSuppressMessage("Memory", "MA0001")] attribute with a justification as to why the event will not leak. I could reproduce a leak in a test such as: await InvokeOnMainThreadAsync(() => { var layout = new Grid(); var entry = new Entry(); layout.Add(entry); var handler = CreateHandler<LayoutHandler>(layout); viewReference = new WeakReference(entry); handlerReference = new WeakReference(entry.Handler); platformViewReference = new WeakReference(entry.Handler.PlatformView); }); await AssertionExtensions.WaitForGC(viewReference, handlerReference, platformViewReference); Assert.False(viewReference.IsAlive, "Entry should not be alive!"); Assert.False(handlerReference.IsAlive, "Handler should not be alive!"); Assert.False(platformViewReference.IsAlive, "PlatformView should not be alive!");
1 parent 13cfac6 commit 98d4bbf

File tree

3 files changed

+136
-55
lines changed

3 files changed

+136
-55
lines changed

src/Controls/tests/DeviceTests/Elements/Entry/EntryTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ namespace Microsoft.Maui.DeviceTests
1212
[Category(TestCategory.Entry)]
1313
public partial class EntryTests : ControlsHandlerTestBase
1414
{
15+
void SetupBuilder()
16+
{
17+
EnsureHandlerCreated(builder =>
18+
{
19+
builder.ConfigureMauiHandlers(handlers =>
20+
{
21+
handlers.AddHandler<Entry, EntryHandler>();
22+
});
23+
});
24+
}
25+
1526
[Fact]
1627
public async Task MaxLengthTrims()
1728
{
@@ -70,6 +81,32 @@ await InvokeOnMainThreadAsync(() =>
7081
});
7182
}
7283

84+
[Fact(DisplayName = "Does Not Leak")]
85+
public async Task DoesNotLeak()
86+
{
87+
SetupBuilder();
88+
89+
WeakReference viewReference = null;
90+
WeakReference platformViewReference = null;
91+
WeakReference handlerReference = null;
92+
93+
await InvokeOnMainThreadAsync(() =>
94+
{
95+
var layout = new Grid();
96+
var entry = new Entry();
97+
layout.Add(entry);
98+
var handler = CreateHandler<LayoutHandler>(layout);
99+
viewReference = new WeakReference(entry);
100+
handlerReference = new WeakReference(entry.Handler);
101+
platformViewReference = new WeakReference(entry.Handler.PlatformView);
102+
});
103+
104+
await AssertionExtensions.WaitForGC(viewReference, handlerReference, platformViewReference);
105+
Assert.False(viewReference.IsAlive, "Entry should not be alive!");
106+
Assert.False(handlerReference.IsAlive, "Handler should not be alive!");
107+
Assert.False(platformViewReference.IsAlive, "PlatformView should not be alive!");
108+
}
109+
73110
#if WINDOWS
74111
// Only Windows needs the IsReadOnly workaround for MaxLength==0 to prevent text from being entered
75112
[Fact]

src/Core/src/Handlers/Entry/EntryHandler.iOS.cs

Lines changed: 96 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Microsoft.Maui.Handlers
99
{
1010
public partial class EntryHandler : ViewHandler<IEntry, MauiTextField>
1111
{
12-
bool _set;
12+
readonly MauiTextFieldProxy _proxy = new();
1313

1414
protected override MauiTextField CreatePlatformView() =>
1515
new MauiTextField
@@ -22,35 +22,17 @@ public override void SetVirtualView(IView view)
2222
{
2323
base.SetVirtualView(view);
2424

25-
if (!_set)
26-
PlatformView.SelectionChanged += OnSelectionChanged;
27-
28-
_set = true;
25+
_proxy.SetVirtualView(PlatformView);
2926
}
3027

3128
protected override void ConnectHandler(MauiTextField platformView)
3229
{
33-
platformView.ShouldReturn += OnShouldReturn;
34-
platformView.EditingDidBegin += OnEditingBegan;
35-
platformView.EditingChanged += OnEditingChanged;
36-
platformView.EditingDidEnd += OnEditingEnded;
37-
platformView.TextPropertySet += OnTextPropertySet;
38-
platformView.ShouldChangeCharacters += OnShouldChangeCharacters;
30+
_proxy.Connect(VirtualView, platformView);
3931
}
4032

4133
protected override void DisconnectHandler(MauiTextField platformView)
4234
{
43-
platformView.ShouldReturn -= OnShouldReturn;
44-
platformView.EditingDidBegin -= OnEditingBegan;
45-
platformView.EditingChanged -= OnEditingChanged;
46-
platformView.EditingDidEnd -= OnEditingEnded;
47-
platformView.TextPropertySet -= OnTextPropertySet;
48-
platformView.ShouldChangeCharacters -= OnShouldChangeCharacters;
49-
50-
if (_set)
51-
platformView.SelectionChanged -= OnSelectionChanged;
52-
53-
_set = false;
35+
_proxy.Disconnect(platformView);
5436
}
5537

5638
public static void MapText(IEntryHandler handler, IEntry entry)
@@ -124,53 +106,112 @@ public static void MapFormatting(IEntryHandler handler, IEntry entry)
124106
handler.PlatformView?.UpdateHorizontalTextAlignment(entry);
125107
}
126108

127-
protected virtual bool OnShouldReturn(UITextField view)
109+
protected virtual bool OnShouldReturn(UITextField view) =>
110+
_proxy.OnShouldReturn(view);
111+
112+
class MauiTextFieldProxy
128113
{
129-
KeyboardAutoManager.GoToNextResponderOrResign(view);
114+
bool _set;
115+
WeakReference<IEntry>? _virtualView;
130116

131-
VirtualView?.Completed();
117+
IEntry? VirtualView => _virtualView is not null && _virtualView.TryGetTarget(out var v) ? v : null;
132118

133-
return false;
134-
}
119+
public void Connect(IEntry virtualView, MauiTextField platformView)
120+
{
121+
_virtualView = new(virtualView);
135122

136-
void OnEditingBegan(object? sender, EventArgs e)
137-
{
138-
if (VirtualView == null || PlatformView == null)
139-
return;
123+
platformView.ShouldReturn += OnShouldReturn;
124+
platformView.EditingDidBegin += OnEditingBegan;
125+
platformView.EditingChanged += OnEditingChanged;
126+
platformView.EditingDidEnd += OnEditingEnded;
127+
platformView.TextPropertySet += OnTextPropertySet;
128+
platformView.ShouldChangeCharacters += OnShouldChangeCharacters;
129+
}
140130

141-
PlatformView?.UpdateSelectionLength(VirtualView);
131+
public void Disconnect(MauiTextField platformView)
132+
{
133+
_virtualView = null;
142134

143-
VirtualView.IsFocused = true;
144-
}
135+
platformView.ShouldReturn -= OnShouldReturn;
136+
platformView.EditingDidBegin -= OnEditingBegan;
137+
platformView.EditingChanged -= OnEditingChanged;
138+
platformView.EditingDidEnd -= OnEditingEnded;
139+
platformView.TextPropertySet -= OnTextPropertySet;
140+
platformView.ShouldChangeCharacters -= OnShouldChangeCharacters;
145141

146-
void OnEditingChanged(object? sender, EventArgs e) =>
147-
VirtualView.UpdateText(PlatformView.Text);
142+
if (_set)
143+
platformView.SelectionChanged -= OnSelectionChanged;
148144

149-
void OnEditingEnded(object? sender, EventArgs e)
150-
{
151-
if (VirtualView == null || PlatformView == null)
152-
return;
145+
_set = false;
146+
}
153147

154-
VirtualView.UpdateText(PlatformView.Text);
155-
VirtualView.IsFocused = false;
156-
}
148+
public void SetVirtualView(MauiTextField platformView)
149+
{
150+
if (!_set)
151+
platformView.SelectionChanged += OnSelectionChanged;
152+
_set = true;
153+
}
157154

158-
void OnTextPropertySet(object? sender, EventArgs e) =>
159-
VirtualView.UpdateText(PlatformView.Text);
155+
public bool OnShouldReturn(UITextField view)
156+
{
157+
KeyboardAutoManager.GoToNextResponderOrResign(view);
160158

161-
bool OnShouldChangeCharacters(UITextField textField, NSRange range, string replacementString) =>
162-
VirtualView.TextWithinMaxLength(textField.Text, range, replacementString);
159+
VirtualView?.Completed();
163160

164-
private void OnSelectionChanged(object? sender, EventArgs e)
165-
{
166-
var cursorPosition = PlatformView.GetCursorPosition();
167-
var selectedTextLength = PlatformView.GetSelectedTextLength();
161+
return false;
162+
}
163+
164+
void OnEditingBegan(object? sender, EventArgs e)
165+
{
166+
if (sender is MauiTextField platformView && VirtualView is IEntry virtualView)
167+
{
168+
platformView.UpdateSelectionLength(virtualView);
169+
virtualView.IsFocused = true;
170+
}
171+
}
172+
173+
void OnEditingChanged(object? sender, EventArgs e)
174+
{
175+
if (sender is MauiTextField platformView)
176+
{
177+
VirtualView?.UpdateText(platformView.Text);
178+
}
179+
}
168180

169-
if (VirtualView.CursorPosition != cursorPosition)
170-
VirtualView.CursorPosition = cursorPosition;
181+
void OnEditingEnded(object? sender, EventArgs e)
182+
{
183+
if (sender is MauiTextField platformView && VirtualView is IEntry virtualView)
184+
{
185+
virtualView.UpdateText(platformView.Text);
186+
virtualView.IsFocused = false;
187+
}
188+
}
189+
190+
void OnTextPropertySet(object? sender, EventArgs e)
191+
{
192+
if (sender is MauiTextField platformView)
193+
{
194+
VirtualView?.UpdateText(platformView.Text);
195+
}
196+
}
171197

172-
if (VirtualView.SelectionLength != selectedTextLength)
173-
VirtualView.SelectionLength = selectedTextLength;
198+
bool OnShouldChangeCharacters(UITextField textField, NSRange range, string replacementString) =>
199+
VirtualView?.TextWithinMaxLength(textField.Text, range, replacementString) ?? false;
200+
201+
void OnSelectionChanged(object? sender, EventArgs e)
202+
{
203+
if (sender is MauiTextField platformView && VirtualView is IEntry virtualView)
204+
{
205+
var cursorPosition = platformView.GetCursorPosition();
206+
var selectedTextLength = platformView.GetSelectedTextLength();
207+
208+
if (virtualView.CursorPosition != cursorPosition)
209+
virtualView.CursorPosition = cursorPosition;
210+
211+
if (virtualView.SelectionLength != selectedTextLength)
212+
virtualView.SelectionLength = selectedTextLength;
213+
}
214+
}
174215
}
175216
}
176217
}

src/Core/src/Platform/iOS/MauiTextField.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Diagnostics.CodeAnalysis;
23
using CoreGraphics;
34
using Foundation;
45
using ObjCRuntime;
@@ -65,7 +66,9 @@ public override UITextRange? SelectedTextRange
6566
}
6667
}
6768

69+
[UnconditionalSuppressMessage("Memory", "MA0001", Justification = "Proven safe in test: EntryTests.DoesNotLeak")]
6870
public event EventHandler? TextPropertySet;
71+
[UnconditionalSuppressMessage("Memory", "MA0001", Justification = "Proven safe in test: EntryTests.DoesNotLeak")]
6972
internal event EventHandler? SelectionChanged;
7073
}
7174
}

0 commit comments

Comments
 (0)