From 0617dff7aaa5d96879edfcbfd79c78f2902a2f8d Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Mon, 8 Mar 2021 22:15:03 +0200 Subject: [PATCH 1/2] [Entry] Enable support for text events --- .../src/Android/Renderers/EntryRenderer.cs | 2 + .../Core/src/iOS/Renderers/EntryRenderer.cs | 3 ++ .../samples/Controls.Sample/Pages/MainPage.cs | 7 ++- src/Core/src/Core/IEntry.cs | 2 +- src/Core/src/Core/ITextInput.cs | 9 +--- .../Handlers/Entry/EntryHandler.Android.cs | 46 +++++++++++++++++++ .../src/Handlers/Entry/EntryHandler.iOS.cs | 44 ++++++++++++++++-- .../src/Platform/Android/EntryExtensions.cs | 6 ++- src/Core/src/Platform/iOS/MauiTextField.cs | 30 ++++++++++++ .../Entry/EntryHandlerTests.Android.cs | 3 ++ .../Handlers/Entry/EntryHandlerTests.cs | 44 ++++++++++++++++++ .../Handlers/Entry/EntryHandlerTests.iOS.cs | 3 ++ .../DeviceTests/Handlers/HandlerTestBase.cs | 9 ++++ src/Core/tests/DeviceTests/Stubs/EntryStub.cs | 15 +++++- src/Core/tests/DeviceTests/Stubs/StubBase.cs | 5 +- .../Stubs/StubPropertyChangedEventArgs.cs | 17 +++++++ 16 files changed, 225 insertions(+), 20 deletions(-) create mode 100644 src/Core/src/Platform/iOS/MauiTextField.cs create mode 100644 src/Core/tests/DeviceTests/Stubs/StubPropertyChangedEventArgs.cs diff --git a/src/Compatibility/Core/src/Android/Renderers/EntryRenderer.cs b/src/Compatibility/Core/src/Android/Renderers/EntryRenderer.cs index 9fa47d234889..78deb8331b30 100644 --- a/src/Compatibility/Core/src/Android/Renderers/EntryRenderer.cs +++ b/src/Compatibility/Core/src/Android/Renderers/EntryRenderer.cs @@ -115,6 +115,7 @@ void ITextWatcher.BeforeTextChanged(ICharSequence s, int start, int count, int a { } + [PortHandler] void ITextWatcher.OnTextChanged(ICharSequence s, int start, int before, int count) { Internals.TextTransformUtilites.SetPlainText(Element, s?.ToString()); @@ -524,6 +525,7 @@ protected virtual void UpdateIsReadOnly() EditText.Focusable = isReadOnly; } + [PortHandler("Ported Text setter")] void UpdateText() { var text = Element.UpdateFormsText(Element.Text, Element.TextTransform); diff --git a/src/Compatibility/Core/src/iOS/Renderers/EntryRenderer.cs b/src/Compatibility/Core/src/iOS/Renderers/EntryRenderer.cs index 14aa0d096f8e..07dbe9adb382 100644 --- a/src/Compatibility/Core/src/iOS/Renderers/EntryRenderer.cs +++ b/src/Compatibility/Core/src/iOS/Renderers/EntryRenderer.cs @@ -220,12 +220,14 @@ void OnEditingBegan(object sender, EventArgs e) ElementController.SetValueFromRenderer(VisualElement.IsFocusedPropertyKey, true); } + [PortHandler("Ported Text setter")] void OnEditingChanged(object sender, EventArgs eventArgs) { ElementController.SetValueFromRenderer(Entry.TextProperty, Control.Text); UpdateCursorFromControl(null); } + [PortHandler("Ported Text setter")] void OnEditingEnded(object sender, EventArgs e) { // Typing aid changes don't always raise EditingChanged event @@ -361,6 +363,7 @@ protected virtual void UpdatePlaceholder() protected virtual void UpdateAttributedPlaceholder(NSAttributedString nsAttributedString) => Control.AttributedPlaceholder = nsAttributedString; + [PortHandler] void UpdateText() { var text = Element.UpdateFormsText(Element.Text, Element.TextTransform); diff --git a/src/Controls/samples/Controls.Sample/Pages/MainPage.cs b/src/Controls/samples/Controls.Sample/Pages/MainPage.cs index a6c95a769123..c0eced14a5a1 100644 --- a/src/Controls/samples/Controls.Sample/Pages/MainPage.cs +++ b/src/Controls/samples/Controls.Sample/Pages/MainPage.cs @@ -51,7 +51,12 @@ void SetupMauiLayout() verticalStack.Add(horizontalStack); - verticalStack.Add(new Entry()); + var entry = new Entry(); + entry.TextChanged += (sender, e) => + { + System.Console.WriteLine($"Text Changed from '{e.OldTextValue}' to '{e.NewTextValue}'"); + }; + verticalStack.Add(entry); verticalStack.Add(new Entry { Text = "Entry", TextColor = Color.DarkRed }); verticalStack.Add(new Entry { IsPassword = true, TextColor = Color.Black }); verticalStack.Add(new Entry { IsTextPredictionEnabled = false }); diff --git a/src/Core/src/Core/IEntry.cs b/src/Core/src/Core/IEntry.cs index 46ff1989548a..9263d3d2d2b3 100644 --- a/src/Core/src/Core/IEntry.cs +++ b/src/Core/src/Core/IEntry.cs @@ -1,6 +1,6 @@ namespace Microsoft.Maui { - public interface IEntry : IView, IText + public interface IEntry : IView, IText, ITextInput { bool IsPassword { get; } bool IsTextPredictionEnabled { get; } diff --git a/src/Core/src/Core/ITextInput.cs b/src/Core/src/Core/ITextInput.cs index 417b8df3bfc8..134d5bc7de0f 100644 --- a/src/Core/src/Core/ITextInput.cs +++ b/src/Core/src/Core/ITextInput.cs @@ -1,14 +1,7 @@ -using Microsoft.Maui; - namespace Microsoft.Maui { public interface ITextInput : IText { - Keyboard Keyboard { get; } - bool IsSpellCheckEnabled { get; } - int MaxLength { get; } - string Placeholder { get; } - Color PlaceholderColor { get; } - bool IsReadOnly { get; } + new string Text { get; set; } } } \ No newline at end of file diff --git a/src/Core/src/Handlers/Entry/EntryHandler.Android.cs b/src/Core/src/Handlers/Entry/EntryHandler.Android.cs index 0fe804dc180a..6f47472de1f0 100644 --- a/src/Core/src/Handlers/Entry/EntryHandler.Android.cs +++ b/src/Core/src/Handlers/Entry/EntryHandler.Android.cs @@ -1,10 +1,13 @@ using Android.Content.Res; +using Android.Text; using Android.Widget; namespace Microsoft.Maui.Handlers { public partial class EntryHandler : AbstractViewHandler { + TextWatcher Watcher { get; } = new TextWatcher(); + static ColorStateList? DefaultTextColors { get; set; } protected override EditText CreateNativeView() @@ -12,6 +15,18 @@ protected override EditText CreateNativeView() return new EditText(Context); } + protected override void ConnectHandler(EditText nativeView) + { + Watcher.Handler = this; + nativeView.AddTextChangedListener(Watcher); + } + + protected override void DisconnectHandler(EditText nativeView) + { + nativeView.RemoveTextChangedListener(Watcher); + Watcher.Handler = null; + } + protected override void SetupDefaults(EditText nativeView) { base.SetupDefaults(nativeView); @@ -37,5 +52,36 @@ public static void MapIsTextPredictionEnabled(EntryHandler handler, IEntry entry { handler.TypedNativeView?.UpdateIsTextPredictionEnabled(entry); } + + void OnTextChanged(string? text) + { + if (VirtualView == null || TypedNativeView == null) + return; + + // Even though is technically different to "", it has no + // functional difference to apps. Thus, hide it. + var mauiText = VirtualView.Text ?? string.Empty; + var nativeText = text ?? string.Empty; + if (mauiText != nativeText) + VirtualView.Text = nativeText; + } + + class TextWatcher : Java.Lang.Object, ITextWatcher + { + public EntryHandler? Handler { get; set; } + + void ITextWatcher.AfterTextChanged(IEditable? s) + { + } + + void ITextWatcher.BeforeTextChanged(Java.Lang.ICharSequence? s, int start, int count, int after) + { + } + + void ITextWatcher.OnTextChanged(Java.Lang.ICharSequence? s, int start, int before, int count) + { + Handler?.OnTextChanged(s?.ToString()); + } + } } } \ No newline at end of file diff --git a/src/Core/src/Handlers/Entry/EntryHandler.iOS.cs b/src/Core/src/Handlers/Entry/EntryHandler.iOS.cs index 0046d44ae140..214527550436 100644 --- a/src/Core/src/Handlers/Entry/EntryHandler.iOS.cs +++ b/src/Core/src/Handlers/Entry/EntryHandler.iOS.cs @@ -1,24 +1,39 @@ -using CoreGraphics; +using System; +using Microsoft.Maui.Platform.iOS; using UIKit; namespace Microsoft.Maui.Handlers { - public partial class EntryHandler : AbstractViewHandler + public partial class EntryHandler : AbstractViewHandler { static readonly int BaseHeight = 30; static UIColor? DefaultTextColor; - protected override UITextField CreateNativeView() + protected override MauiTextField CreateNativeView() { - return new UITextField(CGRect.Empty) + return new MauiTextField { BorderStyle = UITextBorderStyle.RoundedRect, ClipsToBounds = true }; } - protected override void SetupDefaults(UITextField nativeView) + protected override void ConnectHandler(MauiTextField nativeView) + { + nativeView.EditingChanged += OnEditingChanged; + nativeView.EditingDidEnd += OnEditingEnded; + nativeView.TextPropertySet += OnTextPropertySet; + } + + protected override void DisconnectHandler(MauiTextField nativeView) + { + nativeView.EditingChanged -= OnEditingChanged; + nativeView.EditingDidEnd -= OnEditingEnded; + nativeView.TextPropertySet -= OnTextPropertySet; + } + + protected override void SetupDefaults(MauiTextField nativeView) { DefaultTextColor = nativeView.TextColor; } @@ -45,5 +60,24 @@ public static void MapIsTextPredictionEnabled(EntryHandler handler, IEntry entry { handler.TypedNativeView?.UpdateIsTextPredictionEnabled(entry); } + + void OnEditingChanged(object? sender, EventArgs e) => OnTextChanged(); + + void OnEditingEnded(object? sender, EventArgs e) => OnTextChanged(); + + void OnTextPropertySet(object sender, EventArgs e) => OnTextChanged(); + + void OnTextChanged() + { + if (VirtualView == null || TypedNativeView == null) + return; + + // Even though is technically different to "", it has no + // functional difference to apps. Thus, hide it. + var mauiText = VirtualView.Text ?? string.Empty; + var nativeText = TypedNativeView.Text ?? string.Empty; + if (mauiText != nativeText) + VirtualView.Text = nativeText; + } } } \ No newline at end of file diff --git a/src/Core/src/Platform/Android/EntryExtensions.cs b/src/Core/src/Platform/Android/EntryExtensions.cs index cf53a3e48737..3111a347314b 100644 --- a/src/Core/src/Platform/Android/EntryExtensions.cs +++ b/src/Core/src/Platform/Android/EntryExtensions.cs @@ -13,7 +13,11 @@ public static class EntryExtensions public static void UpdateText(this EditText editText, IEntry entry) { - editText.Text = entry.Text; + var newText = entry.Text ?? string.Empty; + var oldText = editText.Text ?? string.Empty; + + if (oldText != newText) + editText.Text = newText; } public static void UpdateTextColor(this EditText editText, IEntry entry, ColorStateList? defaultColor) diff --git a/src/Core/src/Platform/iOS/MauiTextField.cs b/src/Core/src/Platform/iOS/MauiTextField.cs new file mode 100644 index 000000000000..d78a96a6efdc --- /dev/null +++ b/src/Core/src/Platform/iOS/MauiTextField.cs @@ -0,0 +1,30 @@ +using System; +using CoreGraphics; +using UIKit; + +namespace Microsoft.Maui.Platform.iOS +{ + public class MauiTextField : UITextField + { + public MauiTextField(CGRect frame) + : base(frame) + { + } + + public MauiTextField() + { + } + + public override string? Text + { + get => base.Text; + set + { + base.Text = value; + TextPropertySet?.Invoke(this,EventArgs.Empty); + } + } + + public event EventHandler? TextPropertySet; + } +} \ No newline at end of file diff --git a/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.Android.cs b/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.Android.cs index 69338ae9f87e..44c0b3617e9b 100644 --- a/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.Android.cs +++ b/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.Android.cs @@ -13,6 +13,9 @@ EditText GetNativeEntry(EntryHandler entryHandler) => string GetNativeText(EntryHandler entryHandler) => GetNativeEntry(entryHandler).Text; + void SetNativeText(EntryHandler entryHandler, string text) => + GetNativeEntry(entryHandler).Text = text; + Color GetNativeTextColor(EntryHandler entryHandler) { int currentTextColorInt = GetNativeEntry(entryHandler).CurrentTextColor; diff --git a/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.cs b/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.cs index c30401116545..9f044f384e9a 100644 --- a/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.cs +++ b/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.cs @@ -135,5 +135,49 @@ await ValidatePropertyUpdatesValue( setValue, unsetValue); } + + [Theory(DisplayName = "Text Changed Events Fire Correctly")] + // null/empty + [InlineData(null, null, false)] + [InlineData(null, "", false)] + [InlineData("", null, false)] + [InlineData("", "", false)] + // whitespace + [InlineData(null, " ", true)] + [InlineData("", " ", true)] + [InlineData(" ", null, true)] + [InlineData(" ", "", true)] + [InlineData(" ", " ", false)] + // text + [InlineData(null, "Hello", true)] + [InlineData("", "Hello", true)] + [InlineData(" ", "Hello", true)] + [InlineData("Hello", null, true)] + [InlineData("Hello", "", true)] + [InlineData("Hello", " ", true)] + [InlineData("Hello", "Goodbye", true)] + public async Task TextChangeEventsFireCorrectly(string initialText, string newText, bool eventExpected) + { + var entry = new EntryStub + { + Text = initialText, + }; + + var eventFiredCount = 0; + entry.TextChanged += (sender, e) => + { + eventFiredCount++; + + Assert.Equal(initialText, e.OldValue); + Assert.Equal(newText ?? string.Empty, e.NewValue); + }; + + await SetValueAsync(entry, newText, SetNativeText); + + if (eventExpected) + Assert.Equal(1, eventFiredCount); + else + Assert.Equal(0, eventFiredCount); + } } } diff --git a/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.iOS.cs b/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.iOS.cs index bd968421e8fd..143f12bd891e 100644 --- a/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.iOS.cs +++ b/src/Core/tests/DeviceTests/Handlers/Entry/EntryHandlerTests.iOS.cs @@ -11,6 +11,9 @@ UITextField GetNativeEntry(EntryHandler entryHandler) => string GetNativeText(EntryHandler entryHandler) => GetNativeEntry(entryHandler).Text; + void SetNativeText(EntryHandler entryHandler, string text) => + GetNativeEntry(entryHandler).Text = text; + Color GetNativeTextColor(EntryHandler entryHandler) => GetNativeEntry(entryHandler).TextColor.ToColor(); diff --git a/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.cs b/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.cs index acbb790d76f8..32cee23e94be 100644 --- a/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.cs +++ b/src/Core/tests/DeviceTests/Handlers/HandlerTestBase.cs @@ -47,6 +47,15 @@ protected Task GetValueAsync(IView view, Func }); } + protected Task SetValueAsync(IView view, TValue value, Action func) + { + return InvokeOnMainThreadAsync(() => + { + var handler = CreateHandler(view); + func(handler, value); + }); + } + async protected Task ValidatePropertyInitValue( IView view, Func GetValue, diff --git a/src/Core/tests/DeviceTests/Stubs/EntryStub.cs b/src/Core/tests/DeviceTests/Stubs/EntryStub.cs index 659e5f4314f7..86b468d615d6 100644 --- a/src/Core/tests/DeviceTests/Stubs/EntryStub.cs +++ b/src/Core/tests/DeviceTests/Stubs/EntryStub.cs @@ -1,15 +1,26 @@ -namespace Microsoft.Maui.DeviceTests.Stubs +using System; + +namespace Microsoft.Maui.DeviceTests.Stubs { public partial class EntryStub : StubBase, IEntry { private string _text; - public string Text { get => _text; set => this.SetProperty(ref _text, value); } + public string Text + { + get => _text; + set => SetProperty(ref _text, value, onChanged: OnTextChanged); + } public Color TextColor { get; set; } public bool IsPassword { get; set; } public bool IsTextPredictionEnabled { get; set; } + + public event EventHandler> TextChanged; + + void OnTextChanged(string oldValue, string newValue) => + TextChanged?.Invoke(this, new StubPropertyChangedEventArgs(oldValue, newValue)); } } \ No newline at end of file diff --git a/src/Core/tests/DeviceTests/Stubs/StubBase.cs b/src/Core/tests/DeviceTests/Stubs/StubBase.cs index c2f87b1f8c66..5de2f95119e5 100644 --- a/src/Core/tests/DeviceTests/Stubs/StubBase.cs +++ b/src/Core/tests/DeviceTests/Stubs/StubBase.cs @@ -36,14 +36,15 @@ public void Arrange(Rectangle bounds) protected bool SetProperty(ref T backingStore, T value, [CallerMemberName] string propertyName = "", - Action onChanged = null) + Action onChanged = null) { if (EqualityComparer.Default.Equals(backingStore, value)) return false; + var oldValue = backingStore; backingStore = value; - onChanged?.Invoke(); Handler?.UpdateValue(propertyName); + onChanged?.Invoke(oldValue, value); return true; } diff --git a/src/Core/tests/DeviceTests/Stubs/StubPropertyChangedEventArgs.cs b/src/Core/tests/DeviceTests/Stubs/StubPropertyChangedEventArgs.cs new file mode 100644 index 000000000000..bb42a585e7c7 --- /dev/null +++ b/src/Core/tests/DeviceTests/Stubs/StubPropertyChangedEventArgs.cs @@ -0,0 +1,17 @@ +using System; + +namespace Microsoft.Maui.DeviceTests.Stubs +{ + public class StubPropertyChangedEventArgs : EventArgs + { + public StubPropertyChangedEventArgs(T oldValue, T newValue) + { + OldValue = oldValue; + NewValue = newValue; + } + + public T OldValue { get; } + + public T NewValue { get; } + } +} \ No newline at end of file From 7e82dfbe7c7370a704726642b67397b63ed40988 Mon Sep 17 00:00:00 2001 From: Matthew Leibowitz Date: Tue, 9 Mar 2021 01:33:09 +0200 Subject: [PATCH 2/2] Nice! --- src/Core/src/Handlers/Entry/EntryHandler.iOS.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/src/Handlers/Entry/EntryHandler.iOS.cs b/src/Core/src/Handlers/Entry/EntryHandler.iOS.cs index 214527550436..6647a9027ae3 100644 --- a/src/Core/src/Handlers/Entry/EntryHandler.iOS.cs +++ b/src/Core/src/Handlers/Entry/EntryHandler.iOS.cs @@ -65,7 +65,7 @@ public static void MapIsTextPredictionEnabled(EntryHandler handler, IEntry entry void OnEditingEnded(object? sender, EventArgs e) => OnTextChanged(); - void OnTextPropertySet(object sender, EventArgs e) => OnTextChanged(); + void OnTextPropertySet(object? sender, EventArgs e) => OnTextChanged(); void OnTextChanged() {