From de3720ce77d181cab10be89f2f022c335b666dff Mon Sep 17 00:00:00 2001 From: ahmedmohammedfawzy <42243982+ahmedmohammedfawzy@users.noreply.github.com> Date: Thu, 9 Jun 2022 20:53:26 +0200 Subject: [PATCH 01/10] Update TextBox.cs Added the option to determine whether to ignore changes while the user is inputting or not --- src/Avalonia.Controls/TextBox.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 7652b23162..52e5da95b3 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -53,6 +53,9 @@ namespace Avalonia.Controls public static readonly StyledProperty PasswordCharProperty = AvaloniaProperty.Register(nameof(PasswordChar)); + + public static readonly StyledProperty IgnoreChangesWhileEditingProperty = + AvaloniaProperty.Register(nameof(IgnoreChangesWhileEditing)); public static readonly StyledProperty SelectionBrushProperty = AvaloniaProperty.Register(nameof(SelectionBrush)); @@ -276,6 +279,12 @@ namespace Avalonia.Controls get => GetValue(IsReadOnlyProperty); set => SetValue(IsReadOnlyProperty, value); } + + public bool IgnoreChangesWhileEditing + { + get => GetValue(IgnoreChangesWhileEditingProperty); + set => SetValue(IgnoreChangesWhileEditingProperty, value); + } public char PasswordChar { @@ -1501,7 +1510,9 @@ namespace Avalonia.Controls { try { - _ignoreTextChanges = true; + if (IgnoreChangesWhileEditing == true) + _ignoreTextChanges = true; + SetAndRaise(TextProperty, ref _text, value); } finally From 0868442ec92aa4d427a8e750e9b1b0232c9662ce Mon Sep 17 00:00:00 2001 From: ahmedmohammedfawzy <42243982+ahmedmohammedfawzy@users.noreply.github.com> Date: Thu, 9 Jun 2022 21:04:12 +0200 Subject: [PATCH 02/10] Update TextBox.cs --- src/Avalonia.Controls/TextBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 52e5da95b3..77be6bd9ee 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -55,7 +55,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(PasswordChar)); public static readonly StyledProperty IgnoreChangesWhileEditingProperty = - AvaloniaProperty.Register(nameof(IgnoreChangesWhileEditing)); + AvaloniaProperty.Register(nameof(IgnoreChangesWhileEditing), true); public static readonly StyledProperty SelectionBrushProperty = AvaloniaProperty.Register(nameof(SelectionBrush)); From 5ebba8e68c960016c7054e80073ff952a0bc77c1 Mon Sep 17 00:00:00 2001 From: ahmedmohammedfawzy <42243982+ahmedmohammedfawzy@users.noreply.github.com> Date: Fri, 10 Jun 2022 08:55:08 +0200 Subject: [PATCH 03/10] Update TextBox.cs --- src/Avalonia.Controls/TextBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 77be6bd9ee..52e5da95b3 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -55,7 +55,7 @@ namespace Avalonia.Controls AvaloniaProperty.Register(nameof(PasswordChar)); public static readonly StyledProperty IgnoreChangesWhileEditingProperty = - AvaloniaProperty.Register(nameof(IgnoreChangesWhileEditing), true); + AvaloniaProperty.Register(nameof(IgnoreChangesWhileEditing)); public static readonly StyledProperty SelectionBrushProperty = AvaloniaProperty.Register(nameof(SelectionBrush)); From 32e2043ec0f37327eb31c3db42dea9ca7ed661a1 Mon Sep 17 00:00:00 2001 From: Ahmed Fawzy Date: Fri, 10 Jun 2022 18:23:24 +0200 Subject: [PATCH 04/10] removed the _ignoreTextChanges field from the TextBox.cs --- src/Avalonia.Controls/TextBox.cs | 81 ++++++++++---------------------- 1 file changed, 24 insertions(+), 57 deletions(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 52e5da95b3..9531f719b9 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -54,9 +54,6 @@ namespace Avalonia.Controls public static readonly StyledProperty PasswordCharProperty = AvaloniaProperty.Register(nameof(PasswordChar)); - public static readonly StyledProperty IgnoreChangesWhileEditingProperty = - AvaloniaProperty.Register(nameof(IgnoreChangesWhileEditing)); - public static readonly StyledProperty SelectionBrushProperty = AvaloniaProperty.Register(nameof(SelectionBrush)); @@ -199,7 +196,6 @@ namespace Avalonia.Controls private TextBoxTextInputMethodClient _imClient = new TextBoxTextInputMethodClient(); private UndoRedoHelper _undoRedoHelper; private bool _isUndoingRedoing; - private bool _ignoreTextChanges; private bool _canCut; private bool _canCopy; private bool _canPaste; @@ -280,12 +276,6 @@ namespace Avalonia.Controls set => SetValue(IsReadOnlyProperty, value); } - public bool IgnoreChangesWhileEditing - { - get => GetValue(IgnoreChangesWhileEditingProperty); - set => SetValue(IgnoreChangesWhileEditingProperty, value); - } - public char PasswordChar { get => GetValue(PasswordCharProperty); @@ -377,21 +367,17 @@ namespace Avalonia.Controls get => _text; set { - if (!_ignoreTextChanges) - { - var caretIndex = CaretIndex; - var selectionStart = SelectionStart; - var selectionEnd = SelectionEnd; + var caretIndex = CaretIndex; + var selectionStart = SelectionStart; + var selectionEnd = SelectionEnd; - CaretIndex = CoerceCaretIndex(caretIndex, value); - SelectionStart = CoerceCaretIndex(selectionStart, value); - SelectionEnd = CoerceCaretIndex(selectionEnd, value); - - if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing) - { - _undoRedoHelper.Clear(); - SnapshotUndoRedo(); // so we always have an initial state - } + CaretIndex = CoerceCaretIndex(caretIndex, value); + SelectionStart = CoerceCaretIndex(selectionStart, value); + SelectionEnd = CoerceCaretIndex(selectionEnd, value); + if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing) + { + _undoRedoHelper.Clear(); + SnapshotUndoRedo(); // so we always have an initial state } } } @@ -745,32 +731,23 @@ namespace Avalonia.Controls { var oldText = _text; - _ignoreTextChanges = true; - - try - { - DeleteSelection(false); - var caretIndex = CaretIndex; - text = Text ?? string.Empty; - SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); - ClearSelection(); - - if (IsUndoEnabled) - { - _undoRedoHelper.DiscardRedo(); - } - - if (_text != oldText) - { - RaisePropertyChanged(TextProperty, oldText, _text); - } + DeleteSelection(false); + var caretIndex = CaretIndex; + text = Text ?? string.Empty; + SetTextInternal(text.Substring(0, caretIndex) + input + text.Substring(caretIndex)); + ClearSelection(); - CaretIndex = caretIndex + input.Length; + if (IsUndoEnabled) + { + _undoRedoHelper.DiscardRedo(); } - finally + + if (_text != oldText) { - _ignoreTextChanges = false; + RaisePropertyChanged(TextProperty, oldText, _text); } + + CaretIndex = caretIndex + input.Length; } } @@ -1508,17 +1485,7 @@ namespace Avalonia.Controls { if (raiseTextChanged) { - try - { - if (IgnoreChangesWhileEditing == true) - _ignoreTextChanges = true; - - SetAndRaise(TextProperty, ref _text, value); - } - finally - { - _ignoreTextChanges = false; - } + SetAndRaise(TextProperty, ref _text, value); } else { From 7698505770347044562072fba43e9d73bcbc28cf Mon Sep 17 00:00:00 2001 From: Ahmed Fawzy Date: Fri, 10 Jun 2022 20:43:14 +0200 Subject: [PATCH 05/10] Removed tests which collided with removing the _ignoreTextChanges field --- .../MaskedTextBoxTests.cs | 28 ------------------- .../TextBoxTests.cs | 28 ------------------- 2 files changed, 56 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs index af54be61f7..d1fa522206 100644 --- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs @@ -179,34 +179,6 @@ namespace Avalonia.Controls.UnitTests } } - [Fact] - public void Typing_Beginning_With_0_Should_Not_Modify_Text_When_Bound_To_Int() - { - using (Start()) - { - var source = new Class1(); - var target = new MaskedTextBox - { - DataContext = source, - Template = CreateTemplate(), - }; - - target.ApplyTemplate(); - target.Bind(TextBox.TextProperty, new Binding(nameof(Class1.Foo), BindingMode.TwoWay)); - - Assert.Equal("0", target.Text); - - target.CaretIndex = 1; - target.RaiseEvent(new TextInputEventArgs - { - RoutedEvent = InputElement.TextInputEvent, - Text = "2", - }); - - Assert.Equal("02", target.Text); - } - } - [Fact] public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_Is_No_Selection() { diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index f15da8e0c5..23a330c96f 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -180,34 +180,6 @@ namespace Avalonia.Controls.UnitTests } } - [Fact] - public void Typing_Beginning_With_0_Should_Not_Modify_Text_When_Bound_To_Int() - { - using (UnitTestApplication.Start(Services)) - { - var source = new Class1(); - var target = new TextBox - { - DataContext = source, - Template = CreateTemplate(), - }; - - target.ApplyTemplate(); - target.Bind(TextBox.TextProperty, new Binding(nameof(Class1.Foo), BindingMode.TwoWay)); - - Assert.Equal("0", target.Text); - - target.CaretIndex = 1; - target.RaiseEvent(new TextInputEventArgs - { - RoutedEvent = InputElement.TextInputEvent, - Text = "2", - }); - - Assert.Equal("02", target.Text); - } - } - [Fact] public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_Is_No_Selection() { From 6a5e0055393dd61dede522d533a537be8d6f67d1 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Wed, 22 Jun 2022 00:05:28 -0400 Subject: [PATCH 06/10] Add trim_trailing_whitespace to editorconfig --- .editorconfig | 1 + 1 file changed, 1 insertion(+) diff --git a/.editorconfig b/.editorconfig index 25e0135725..cb589a5ce1 100644 --- a/.editorconfig +++ b/.editorconfig @@ -21,6 +21,7 @@ csharp_new_line_before_finally = true csharp_new_line_before_members_in_object_initializers = true csharp_new_line_before_members_in_anonymous_types = true csharp_new_line_between_query_expression_clauses = true +trim_trailing_whitespace = true # Indentation preferences csharp_indent_block_contents = true From 64518efc5184d67208a7d0c3b7df9c66a364a562 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Jun 2022 12:59:40 +0200 Subject: [PATCH 07/10] Add failing test for #8372. --- .../AvaloniaObjectTests_SetValue.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs index 954a609315..72162a4d8e 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs @@ -17,6 +17,21 @@ namespace Avalonia.Base.UnitTests Assert.Equal("foodefault", target.GetValue(Class1.FooProperty)); } + [Fact] + public void ClearValue_Resets_Value_To_Style_value() + { + Class1 target = new Class1(); + + target.SetValue(Class1.FooProperty, "style", BindingPriority.Style); + target.SetValue(Class1.FooProperty, "local"); + + Assert.Equal("local", target.GetValue(Class1.FooProperty)); + + target.ClearValue(Class1.FooProperty); + + Assert.Equal("style", target.GetValue(Class1.FooProperty)); + } + [Fact] public void ClearValue_Raises_PropertyChanged() { From f33d4e881f3b4fb4db9a13279ad8bf1c648b3151 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Jun 2022 13:01:37 +0200 Subject: [PATCH 08/10] Correctly clear local value in PriorityValue. --- src/Avalonia.Base/PropertyStore/PriorityValue.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Base/PropertyStore/PriorityValue.cs b/src/Avalonia.Base/PropertyStore/PriorityValue.cs index 112cf6619f..182b2638c4 100644 --- a/src/Avalonia.Base/PropertyStore/PriorityValue.cs +++ b/src/Avalonia.Base/PropertyStore/PriorityValue.cs @@ -121,6 +121,7 @@ namespace Avalonia.PropertyStore public void ClearLocalValue() { + _localValue = default; UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs( _owner, Property, From c9e10f0d2f88346caeb461b900199b6fb571653d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Jun 2022 15:53:16 +0200 Subject: [PATCH 09/10] Added additional failing test. Exposed by the previous fix for #8372: re-entrancy in `PropertySetterInstance.Dispose()` is causing detaching a style to call `ClearValue` on the property. Previously this wasn't a problem as `ClearValue` didn't work, but now it is. (Also added one passing test which tests the same scenario in `PropertySetterBindingInstance` for future coverage) --- .../Styling/SetterTests.cs | 42 ++++++++++++++++--- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs b/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs index ed4c78aa3e..c684466200 100644 --- a/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs +++ b/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs @@ -150,13 +150,43 @@ namespace Avalonia.Base.UnitTests.Styling Assert.Equal(BindingPriority.StyleTrigger, control.GetDiagnostic(TextBlock.TagProperty).Priority); } - private IBinding CreateMockBinding(AvaloniaProperty property) + [Fact] + public void Disposing_Setter_Should_Preserve_LocalValue() { - var subject = new Subject(); - var descriptor = InstancedBinding.OneWay(subject); - var binding = Mock.Of(x => - x.Initiate(It.IsAny(), property, null, false) == descriptor); - return binding; + var control = new Canvas(); + var setter = new Setter(TextBlock.TagProperty, "foo"); + + var instance = setter.Instance(control); + instance.Start(true); + instance.Activate(); + + control.Tag = "bar"; + + instance.Dispose(); + + Assert.Equal("bar", control.Tag); + } + + [Fact] + public void Disposing_Binding_Setter_Should_Preserve_LocalValue() + { + var control = new Canvas(); + var source = new { Foo = "foo" }; + var setter = new Setter(TextBlock.TagProperty, new Binding + { + Source = source, + Path = nameof(source.Foo), + }); + + var instance = setter.Instance(control); + instance.Start(true); + instance.Activate(); + + control.Tag = "bar"; + + instance.Dispose(); + + Assert.Equal("bar", control.Tag); } private class TestConverter : IValueConverter From 857bfb5bd2b863825c09fbfb780dc379fba4d345 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 22 Jun 2022 16:00:29 +0200 Subject: [PATCH 10/10] Prevent re-entrancy in PropertySetterInstance.Dispose. The call to `_subscription.Dispose()` causes `BindingEntry.Dispose()` to call `_subscription.Dispose()`, but in this case the `BindingEntry._subscription` instance is the `PropertySetterInstance`! Except now `PropertySetterInstance._subscription` is null, and so `PropertySetterInstance.Dispose` called `ClearValue`, which is obviously wrong. --- .../Styling/PropertySetterInstance.cs | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Base/Styling/PropertySetterInstance.cs b/src/Avalonia.Base/Styling/PropertySetterInstance.cs index c4e8f47e67..9028224cc1 100644 --- a/src/Avalonia.Base/Styling/PropertySetterInstance.cs +++ b/src/Avalonia.Base/Styling/PropertySetterInstance.cs @@ -18,7 +18,7 @@ namespace Avalonia.Styling private readonly DirectPropertyBase? _directProperty; private readonly T _value; private IDisposable? _subscription; - private bool _isActive; + private State _state; public PropertySetterInstance( IStyleable target, @@ -40,6 +40,8 @@ namespace Avalonia.Styling _value = value; } + private bool IsActive => _state == State.Active; + public void Start(bool hasActivator) { if (hasActivator) @@ -70,31 +72,35 @@ namespace Avalonia.Styling public void Activate() { - if (!_isActive) + if (!IsActive) { - _isActive = true; + _state = State.Active; PublishNext(); } } public void Deactivate() { - if (_isActive) + if (IsActive) { - _isActive = false; + _state = State.Inactive; PublishNext(); } } public override void Dispose() { + if (_state == State.Disposed) + return; + _state = State.Disposed; + if (_subscription is object) { var sub = _subscription; _subscription = null; sub.Dispose(); } - else if (_isActive) + else if (IsActive) { if (_styledProperty is object) { @@ -114,7 +120,14 @@ namespace Avalonia.Styling private void PublishNext() { - PublishNext(_isActive ? new BindingValue(_value) : default); + PublishNext(IsActive ? new BindingValue(_value) : default); + } + + private enum State + { + Inactive, + Active, + Disposed, } } }