Browse Source

Use coercion for MaskedTextBox.Text (#17143)

Julien Lebosquain 1 year ago
parent
commit
db10780e15

+ 18 - 19
src/Avalonia.Controls/MaskedTextBox.cs

@@ -4,10 +4,7 @@ using System.ComponentModel;
 using System.Globalization;
 using System.Linq;
 using Avalonia.Input;
-using Avalonia.Input.Platform;
 using Avalonia.Interactivity;
-using Avalonia.Styling;
-using Avalonia.VisualTree;
 
 namespace Avalonia.Controls
 {
@@ -82,8 +79,8 @@ namespace Avalonia.Controls
         /// <summary>
         ///  Constructs the MaskedTextBox with the specified MaskedTextProvider object.
         /// </summary>
-        [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty", 
-            "AVP1012:An AvaloniaObject should use SetCurrentValue when assigning its own StyledProperty or AttachedProperty values", 
+        [System.Diagnostics.CodeAnalysis.SuppressMessage("AvaloniaProperty",
+            "AVP1012:An AvaloniaObject should use SetCurrentValue when assigning its own StyledProperty or AttachedProperty values",
             Justification = "These values are being explicitly provided by a constructor parameter.")]
         public MaskedTextBox(MaskedTextProvider maskedTextProvider)
         {
@@ -305,20 +302,7 @@ namespace Avalonia.Controls
                 }
                 RefreshText(MaskProvider, 0);
             }
-            if (change.Property == TextProperty && MaskProvider != null && _ignoreTextChanges == false)
-            {
-                if (string.IsNullOrEmpty(Text))
-                {
-                    MaskProvider.Clear();
-                    RefreshText(MaskProvider, CaretIndex);
-                    base.OnPropertyChanged(change);
-                    return;
-                }
-
-                MaskProvider.Set(Text);
-                RefreshText(MaskProvider, CaretIndex);
-            }
-            else if (change.Property == MaskProperty)
+            if (change.Property == MaskProperty)
             {
                 UpdateMaskProvider();
 
@@ -445,5 +429,20 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <inheritdoc />
+        protected override string? CoerceText(string? text)
+        {
+            if (!_ignoreTextChanges && MaskProvider is { } maskProvider)
+            {
+                if (string.IsNullOrEmpty(text))
+                    maskProvider.Clear();
+                else
+                    maskProvider.Set(text);
+
+                text = maskProvider.ToDisplayString();
+            }
+
+            return base.CoerceText(text);
+        }
     }
 }

+ 15 - 5
src/Avalonia.Controls/TextBox.cs

@@ -568,19 +568,29 @@ namespace Avalonia.Controls
         }
 
         private static string? CoerceText(AvaloniaObject sender, string? value)
-        {
-            var textBox = (TextBox)sender;
+            => ((TextBox)sender).CoerceText(value);
 
+        /// <summary>
+        /// Coerces the current text.
+        /// </summary>
+        /// <param name="value">The initial text.</param>
+        /// <returns>A coerced text.</returns>
+        /// <remarks>
+        /// This method also manages the internal undo/redo state whenever the text changes:
+        /// if overridden, ensure that the base is called or undo/redo won't work correctly.
+        /// </remarks>
+        protected virtual string? CoerceText(string? value)
+        {
             // Before #9490, snapshot here was done AFTER text change - this doesn't make sense
-            // since intial state would never be no text and you'd always have to make a text 
+            // since initial state would never be no text and you'd always have to make a text
             // change before undo would be available
             // The undo/redo stacks were also cleared at this point, which also doesn't make sense
             // as it is still valid to want to undo a programmatic text set
             // So we snapshot text now BEFORE the change so we can always revert
             // Also don't need to check IsUndoEnabled here, that's done in SnapshotUndoRedo
-            if (!textBox._isUndoingRedoing)
+            if (!_isUndoingRedoing)
             {
-                textBox.SnapshotUndoRedo();
+                SnapshotUndoRedo();
             }
 
             return value;

+ 36 - 1
tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs

@@ -884,7 +884,42 @@ namespace Avalonia.Controls.UnitTests
 
                 RaiseKeyEvent(target, key, modifiers);
                 RaiseKeyEvent(target, Key.Z, KeyModifiers.Control); // undo
-                Assert.True(target.Text == "0123");
+                Assert.Equal("0123", target.Text);
+            }
+        }
+
+        [Fact]
+        public void Invalid_Text_Is_Coerced_Without_Raising_Intermediate_Change()
+        {
+            using (Start())
+            {
+                var target = new MaskedTextBox
+                {
+                    Template = CreateTemplate()
+                };
+
+                var impl = CreateMockTopLevelImpl();
+                var topLevel = new TestTopLevel(impl.Object) {
+                    Template = CreateTopLevelTemplate(),
+                    Content = target
+                };
+                topLevel.ApplyTemplate();
+                topLevel.LayoutManager.ExecuteInitialLayoutPass();
+
+                var texts = new List<string>();
+
+                target.PropertyChanged += (_, e) =>
+                {
+                    if (e.Property == TextBox.TextProperty)
+                        texts.Add(e.GetNewValue<string>());
+                };
+
+                target.Mask = "000";
+
+                target.Text = "123";
+                target.Text = "abc";
+
+                Assert.Equal(["___", "123"], texts);
             }
         }