diff --git a/src/Controls/src/Core/InputView/InputView.cs b/src/Controls/src/Core/InputView/InputView.cs index c0d9863bd6d1..51cd1ff9c711 100644 --- a/src/Controls/src/Core/InputView/InputView.cs +++ b/src/Controls/src/Core/InputView/InputView.cs @@ -315,6 +315,29 @@ string ITextInput.Text set => SetValue(TextProperty, value, SetterSpecificity.FromHandler); } + private protected override void OnBindablePropertySet(BindableProperty property, object original, object value, bool changed, bool willFirePropertyChanged) + { + base.OnBindablePropertySet(property, original, value, changed, willFirePropertyChanged); + + // When the same CursorPosition or SelectionLength value is re-set, the bindable property + // system detects no change (changed=false) and does not fire PropertyChanged, so the + // handler mapper is never invoked. We force a handler update here so the native control + // always receives the selection state. This is required on WinUI where the native TextBox + // resets the caret position on each focus event. + if (!changed) + { + switch (property.PropertyName) + { + case nameof(CursorPosition): + Handler?.UpdateValue(nameof(CursorPosition)); + break; + case nameof(SelectionLength): + Handler?.UpdateValue(nameof(SelectionLength)); + break; + } + } + } + private protected override string GetDebuggerDisplay() { var debugText = DebuggerDisplayHelpers.GetDebugText(nameof(Text), Text); diff --git a/src/Controls/tests/TestCases.HostApp/Issues/Issue23329.cs b/src/Controls/tests/TestCases.HostApp/Issues/Issue23329.cs new file mode 100644 index 000000000000..d1cfc267e465 --- /dev/null +++ b/src/Controls/tests/TestCases.HostApp/Issues/Issue23329.cs @@ -0,0 +1,38 @@ +namespace Maui.Controls.Sample.Issues; + +[Issue(IssueTracker.Github, 23329, "Entry select all text on refocus does not work on WinUI", PlatformAffected.UWP)] +public class Issue23329 : ContentPage +{ + readonly Entry _textBox; + + public Issue23329() + { + _textBox = new Entry + { + AutomationId = "TextBox", + WidthRequest = 300 + }; + _textBox.Focused += OnTextBoxFocused; + + var otherElement = new Button + { + AutomationId = "OtherElement", + Text = "Other", + WidthRequest = 128 + }; + + Content = new VerticalStackLayout + { + Children = { _textBox, otherElement } + }; + } + + void OnTextBoxFocused(object sender, FocusEventArgs e) + { + if (_textBox.Text != null) + { + _textBox.CursorPosition = 0; + _textBox.SelectionLength = _textBox.Text.Length; + } + } +} diff --git a/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue23329.cs b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue23329.cs new file mode 100644 index 000000000000..ca876a7245c6 --- /dev/null +++ b/src/Controls/tests/TestCases.Shared.Tests/Tests/Issues/Issue23329.cs @@ -0,0 +1,44 @@ +#if WINDOWS +using NUnit.Framework; +using UITest.Appium; +using UITest.Core; + +namespace Microsoft.Maui.TestCases.Tests.Issues; + +public class Issue23329 : _IssuesUITest +{ + public Issue23329(TestDevice device) + : base(device) + { } + + public override string Issue => "Entry select all text on refocus does not work on WinUI"; + + [Test] + [Category(UITestCategories.Entry)] + public void EntrySelectAllOnRefocusReplacesText() + { + App.WaitForElement("TextBox"); + App.Click("TextBox"); + App.EnterText("TextBox", "Hello"); + + // Blur and refocus twice. The second refocus is where the bug manifests: + // the Focused handler re-applies the same CursorPosition / SelectionLength + // values, so the bindable property change tracking would skip propagation + // to the native TextBox unless the InputView / TextBoxExtensions fix forces + // a handler update for unchanged values. + App.Click("OtherElement"); + App.Click("TextBox"); + App.Click("OtherElement"); + App.Click("TextBox"); + + // If select-all worked on the second refocus, typing 'X' replaces the + // entire selected text, so the Entry ends up containing just "X". If the + // bug is still present, 'X' is inserted at the native caret position + // (wherever the click landed in "Hello"), producing something like "HelloX". + App.EnterText("TextBox", "X"); + + var text = App.FindElement("TextBox").GetText(); + Assert.That(text, Is.EqualTo("X")); + } +} +#endif diff --git a/src/Core/src/Platform/Windows/TextBoxExtensions.cs b/src/Core/src/Platform/Windows/TextBoxExtensions.cs index 1f3e20620a18..f57fb5b97701 100644 --- a/src/Core/src/Platform/Windows/TextBoxExtensions.cs +++ b/src/Core/src/Platform/Windows/TextBoxExtensions.cs @@ -224,7 +224,13 @@ public static void UpdateVerticalTextAlignment(this TextBox textBox, ITextAlignm public static void UpdateCursorPosition(this TextBox textBox, ITextInput entry) { // It seems that the TextBox does not limit the CursorPosition to the Text.Length natively - entry.CursorPosition = Math.Min(entry.CursorPosition, textBox.Text.Length); + var clampedPos = Math.Min(entry.CursorPosition, textBox.Text.Length); + + // Only write back the clamped value when it actually differs. An unconditional write + // would re-enter InputView.OnBindablePropertySet → Handler.UpdateValue in an infinite loop + // because that override now forces handler updates even when the value hasn't changed. + if (entry.CursorPosition != clampedPos) + entry.CursorPosition = clampedPos; if (textBox.SelectionStart != entry.CursorPosition) textBox.SelectionStart = entry.CursorPosition; @@ -233,7 +239,12 @@ public static void UpdateCursorPosition(this TextBox textBox, ITextInput entry) public static void UpdateSelectionLength(this TextBox textBox, ITextInput entry) { // It seems that the TextBox does not limit the SelectionLength to the Text.Length natively - entry.SelectionLength = Math.Min(entry.SelectionLength, textBox.Text.Length - textBox.SelectionStart); + var clampedLen = Math.Min(entry.SelectionLength, textBox.Text.Length - textBox.SelectionStart); + + // Only write back the clamped value when it actually differs — see UpdateCursorPosition + // comment above for the re-entry rationale. + if (entry.SelectionLength != clampedLen) + entry.SelectionLength = clampedLen; if (textBox.SelectionLength != entry.SelectionLength) textBox.SelectionLength = entry.SelectionLength;