Skip to content
23 changes: 23 additions & 0 deletions src/Controls/src/Core/InputView/InputView.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[moderate] Cross-Platform Consistency / Regression Prevention - This shared InputView override changes same-value CursorPosition/SelectionLength propagation for every Entry/Editor handler, not just WinUI. The regression and bug are Windows-specific, but Android/iOS/Tizen now receive extra mapper updates when app code re-sets the same selection values, and those handlers have platform-specific focus/timing behavior. Please either scope the forced same-value update to Windows (for example by moving this override into a Windows-specific partial/guard) or add non-Windows coverage proving same-value selection updates do not move or corrupt caret/selection state.

{
switch (property.PropertyName)
{
case nameof(CursorPosition):
Handler?.UpdateValue(nameof(CursorPosition));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[warning] Cross-platform re-entry risk This shared InputView override now forces handler mapper updates for unchanged CursorPosition / SelectionLength on every platform, but only the Windows TextBoxExtensions mapper was updated to avoid same-value write-back re-entry. Other handlers are mostly guarded, but Tizen still writes selection values to the virtual view and native Entry/Editor selection APIs unconditionally (EntryHandler.Tizen.cs selection callbacks and EntryExtensions.cs lines 90-103). Please either scope this forced same-value update to Windows or add equivalent equality/re-entry guards for the affected non-Windows mappers before enabling it in shared code.

break;
case nameof(SelectionLength):
Handler?.UpdateValue(nameof(SelectionLength));
break;
}
}
}

private protected override string GetDebuggerDisplay()
{
var debugText = DebuggerDisplayHelpers.GetDebugText(nameof(Text), Text);
Expand Down
38 changes: 38 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Issues/Issue23329.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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
15 changes: 13 additions & 2 deletions src/Core/src/Platform/Windows/TextBoxExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
Loading