From 78866d54ed1ce78df089eee8fe1387c0ef0c989a Mon Sep 17 00:00:00 2001 From: "all.owing" Date: Wed, 21 Aug 2019 22:51:59 +0500 Subject: [PATCH 1/5] Allowed pass object as validation error --- .../Core/Plugins/IndeiValidationPlugin.cs | 11 +++++---- .../Data/DataValidationException.cs | 21 ++++++++++++++++ src/Avalonia.Controls/DataValidationErrors.cs | 24 ++++++++++++------- 3 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 src/Avalonia.Base/Data/DataValidationException.cs diff --git a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs index 2e83e0c25e..7abbcab245 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs @@ -85,8 +85,9 @@ namespace Avalonia.Data.Core.Plugins if (target != null) { var errors = target.GetErrors(_name)? - .Cast() - .Where(x => x != null).ToList(); + .Cast() + .Where(x => x != null) + .ToList(); if (errors?.Count > 0) { @@ -100,16 +101,16 @@ namespace Avalonia.Data.Core.Plugins return new BindingNotification(value); } - private Exception GenerateException(IList errors) + private Exception GenerateException(IList errors) { if (errors.Count == 1) { - return new Exception(errors[0]); + return new DataValidationException(errors[0]); } else { return new AggregateException( - errors.Select(x => new Exception(x))); + errors.Select(x => new DataValidationException(x))); } } } diff --git a/src/Avalonia.Base/Data/DataValidationException.cs b/src/Avalonia.Base/Data/DataValidationException.cs new file mode 100644 index 0000000000..e38e6c30ad --- /dev/null +++ b/src/Avalonia.Base/Data/DataValidationException.cs @@ -0,0 +1,21 @@ +using System; + +namespace Avalonia.Data +{ + /// + /// Exception, which wrap validation errors. + /// + public class DataValidationException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// Data of validation error. + public DataValidationException(object errorData) : base(errorData?.ToString()) + { + ErrorData = errorData; + } + + public object ErrorData { get; } + } +} diff --git a/src/Avalonia.Controls/DataValidationErrors.cs b/src/Avalonia.Controls/DataValidationErrors.cs index f0d7f8257e..50b387e636 100644 --- a/src/Avalonia.Controls/DataValidationErrors.cs +++ b/src/Avalonia.Controls/DataValidationErrors.cs @@ -22,8 +22,8 @@ namespace Avalonia.Controls /// /// Defines the DataValidationErrors.Errors attached property. /// - public static readonly AttachedProperty> ErrorsProperty = - AvaloniaProperty.RegisterAttached>("Errors"); + public static readonly AttachedProperty> ErrorsProperty = + AvaloniaProperty.RegisterAttached>("Errors"); /// /// Defines the DataValidationErrors.HasErrors attached property. @@ -76,7 +76,7 @@ namespace Avalonia.Controls private static void ErrorsChanged(AvaloniaPropertyChangedEventArgs e) { var control = (Control)e.Sender; - var errors = (IEnumerable)e.NewValue; + var errors = (IEnumerable)e.NewValue; var hasErrors = false; if (errors != null && errors.Any()) @@ -91,11 +91,11 @@ namespace Avalonia.Controls classes.Set(":error", (bool)e.NewValue); } - public static IEnumerable GetErrors(Control control) + public static IEnumerable GetErrors(Control control) { return control.GetValue(ErrorsProperty); } - public static void SetErrors(Control control, IEnumerable errors) + public static void SetErrors(Control control, IEnumerable errors) { control.SetValue(ErrorsProperty, errors); } @@ -112,14 +112,14 @@ namespace Avalonia.Controls return control.GetValue(HasErrorsProperty); } - private static IEnumerable UnpackException(Exception exception) + private static IEnumerable UnpackException(Exception exception) { if (exception != null) { var aggregate = exception as AggregateException; var exceptions = aggregate == null ? - (IEnumerable)new[] { exception } : - aggregate.InnerExceptions; + new[] { GetExceptionData(exception) } : + aggregate.InnerExceptions.Select(GetExceptionData).ToArray(); var filtered = exceptions.Where(x => !(x is BindingChainException)).ToList(); if (filtered.Count > 0) @@ -130,5 +130,13 @@ namespace Avalonia.Controls return null; } + + private static object GetExceptionData(Exception exception) + { + if (exception is DataValidationException dataValidationException) + return dataValidationException.ErrorData; + + return exception; + } } } From a6b65a39de30f4f8d0740f647976261bc98c518e Mon Sep 17 00:00:00 2001 From: "all.owing" Date: Wed, 21 Aug 2019 23:32:16 +0500 Subject: [PATCH 2/5] Test fix --- .../Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs index 4aaf0ab5b7..0f4a0759aa 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs @@ -58,7 +58,7 @@ namespace Avalonia.Controls.UnitTests Assert.Null(DataValidationErrors.GetErrors(target)); target.Text = "20"; - IEnumerable errors = DataValidationErrors.GetErrors(target); + IEnumerable errors = DataValidationErrors.GetErrors(target); Assert.Single(errors); Assert.IsType(errors.Single()); target.Text = "1"; From 6dbe617ed6baddac4e90614a23a99b7e2c5fbdf1 Mon Sep 17 00:00:00 2001 From: "all.owing" Date: Thu, 22 Aug 2019 09:37:50 +0500 Subject: [PATCH 3/5] One more tests fix --- .../Data/Core/ExpressionObserverTests_DataValidation.cs | 4 ++-- .../Data/Core/Plugins/IndeiValidationPluginTests.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs index b66dd610dd..c472fffb38 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs @@ -100,7 +100,7 @@ namespace Avalonia.Base.UnitTests.Data.Core // Value is first signalled without an error as validation hasn't been updated. new BindingNotification(-5), - new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, -5), + new BindingNotification(new DataValidationException("Must be positive"), BindingErrorType.DataValidationError, -5), // Exception is thrown by trying to set value to "foo". new BindingNotification( @@ -108,7 +108,7 @@ namespace Avalonia.Base.UnitTests.Data.Core BindingErrorType.DataValidationError), // Value is set then validation is updated. - new BindingNotification(new Exception("Must be positive"), BindingErrorType.DataValidationError, 5), + new BindingNotification(new DataValidationException("Must be positive"), BindingErrorType.DataValidationError, 5), new BindingNotification(5), }, result); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs index 383030cb6c..2423900c7a 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs @@ -37,13 +37,13 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins new BindingNotification(6), // Then the ErrorsChanged event is fired. - new BindingNotification(new Exception("Must be less than Maximum"), BindingErrorType.DataValidationError, 6), + new BindingNotification(new DataValidationException("Must be less than Maximum"), BindingErrorType.DataValidationError, 6), // Maximum is changed to 10 so value is now valid. new BindingNotification(6), // And Maximum is changed back to 5. - new BindingNotification(new Exception("Must be less than Maximum"), BindingErrorType.DataValidationError, 6), + new BindingNotification(new DataValidationException("Must be less than Maximum"), BindingErrorType.DataValidationError, 6), }, result); } From 5afc0f5395231476769b1a59fb764adb2f9af6a5 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 25 Aug 2019 23:56:53 +0200 Subject: [PATCH 4/5] Unify AddRange and InsertRange. Fix ranged versions causing multiple enumerations. Get rid of unnecessary allocations (notifications, enumerators). --- src/Avalonia.Base/Collections/AvaloniaList.cs | 229 +++++++++++++----- .../Collections/AvaloniaListTests.cs | 17 ++ 2 files changed, 187 insertions(+), 59 deletions(-) diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index 4d4a561b08..3c8d4ca7e6 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -55,15 +55,15 @@ namespace Avalonia.Collections /// public class AvaloniaList : IAvaloniaList, IList, INotifyCollectionChangedDebug { - private List _inner; + private readonly List _inner; private NotifyCollectionChangedEventHandler _collectionChanged; /// /// Initializes a new instance of the class. /// public AvaloniaList() - : this(Enumerable.Empty()) { + _inner = new List(); } /// @@ -89,8 +89,8 @@ namespace Avalonia.Collections /// public event NotifyCollectionChangedEventHandler CollectionChanged { - add { _collectionChanged += value; } - remove { _collectionChanged -= value; } + add => _collectionChanged += value; + remove => _collectionChanged -= value; } /// @@ -150,7 +150,7 @@ namespace Avalonia.Collections T old = _inner[index]; - if (!object.Equals(old, value)) + if (!EqualityComparer.Default.Equals(old, value)) { _inner[index] = value; @@ -187,45 +187,38 @@ namespace Avalonia.Collections Validate?.Invoke(item); int index = _inner.Count; _inner.Add(item); - NotifyAdd(new[] { item }, index); + NotifyAdd(item, index); } /// /// Adds multiple items to the collection. /// /// The items. - public virtual void AddRange(IEnumerable items) - { - Contract.Requires(items != null); - - var list = (items as IList) ?? items.ToList(); - - if (list.Count > 0) - { - if (Validate != null) - { - foreach (var item in list) - { - Validate((T)item); - } - } - - int index = _inner.Count; - _inner.AddRange(items); - NotifyAdd(list, index); - } - } + public virtual void AddRange(IEnumerable items) => InsertRange(_inner.Count, items); /// /// Removes all items from the collection. /// public virtual void Clear() { - if (this.Count > 0) + if (Count > 0) { - var old = _inner; - _inner = new List(); - NotifyReset(old); + if (_collectionChanged != null) + { + var e = ResetBehavior == ResetBehavior.Reset ? + EventArgsCache.ResetCollectionChanged : + new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, _inner.ToList(), 0); + + _inner.Clear(); + + _collectionChanged(this, e); + } + else + { + _inner.Clear(); + } + + NotifyCountChanged(); } } @@ -253,9 +246,20 @@ namespace Avalonia.Collections /// Returns an enumerator that enumerates the items in the collection. /// /// An . - public IEnumerator GetEnumerator() + IEnumerator IEnumerable.GetEnumerator() { - return _inner.GetEnumerator(); + return new Enumerator(_inner); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return new Enumerator(_inner); + } + + public Enumerator GetEnumerator() + { + return new Enumerator(_inner); } /// @@ -289,7 +293,7 @@ namespace Avalonia.Collections { Validate?.Invoke(item); _inner.Insert(index, item); - NotifyAdd(new[] { item }, index); + NotifyAdd(item, index); } /// @@ -301,20 +305,83 @@ namespace Avalonia.Collections { Contract.Requires(items != null); - var list = (items as IList) ?? items.ToList(); + bool willRaiseCollectionChanged = _collectionChanged != null; + bool hasValidation = Validate != null; - if (list.Count > 0) + if (items is IList list) { - if (Validate != null) + if (list.Count > 0) { - foreach (var item in list) + if (list is ICollection collection) { - Validate((T)item); + if (hasValidation) + { + foreach (T item in collection) + { + Validate(item); + } + } + + _inner.InsertRange(index, collection); + NotifyAdd(list, index); + } + else + { + using (IEnumerator en = items.GetEnumerator()) + { + int insertIndex = index; + + while (en.MoveNext()) + { + T item = en.Current; + + if (hasValidation) + { + Validate(item); + } + + _inner.Insert(insertIndex++, item); + } + } + + NotifyAdd(list, index); } } + } + else + { + using (IEnumerator en = items.GetEnumerator()) + { + if (en.MoveNext()) + { + // Avoid allocating list for collection notification if there is no event subscriptions. + List notificationItems = willRaiseCollectionChanged ? + new List() : + null; + + int insertIndex = index; + + do + { + T item = en.Current; + + if (hasValidation) + { + Validate(item); + } - _inner.InsertRange(index, items); - NotifyAdd((items as IList) ?? items.ToList(), index); + _inner.Insert(insertIndex++, item); + + if (willRaiseCollectionChanged) + { + notificationItems.Add(item); + } + + } while (en.MoveNext()); + + NotifyAdd(notificationItems, index); + } + } } } @@ -382,7 +449,7 @@ namespace Avalonia.Collections if (index != -1) { _inner.RemoveAt(index); - NotifyRemove(new[] { item }, index); + NotifyRemove(item , index); return true; } @@ -412,7 +479,7 @@ namespace Avalonia.Collections { T item = _inner[index]; _inner.RemoveAt(index); - NotifyRemove(new[] { item }, index); + NotifyRemove(item , index); } /// @@ -480,12 +547,6 @@ namespace Avalonia.Collections _inner.CopyTo((T[])array, index); } - /// - IEnumerator IEnumerable.GetEnumerator() - { - return _inner.GetEnumerator(); - } - /// Delegate[] INotifyCollectionChangedDebug.GetCollectionChangedSubscribers() => _collectionChanged?.GetInvocationList(); @@ -505,13 +566,29 @@ namespace Avalonia.Collections NotifyCountChanged(); } + /// + /// Raises the event with a add action. + /// + /// The item that was added. + /// The starting index. + private void NotifyAdd(T item, int index) + { + if (_collectionChanged != null) + { + var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new[] { item }, index); + _collectionChanged(this, e); + } + + NotifyCountChanged(); + } + /// /// Raises the event when the property /// changes. /// private void NotifyCountChanged() { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count))); + PropertyChanged?.Invoke(this, EventArgsCache.CountPropertyChanged); } /// @@ -531,23 +608,57 @@ namespace Avalonia.Collections } /// - /// Raises the event with a reset action. + /// Raises the event with a remove action. /// - /// The items that were removed. - private void NotifyReset(IList t) + /// The item that was removed. + /// The starting index. + private void NotifyRemove(T item, int index) { if (_collectionChanged != null) { - NotifyCollectionChangedEventArgs e; - - e = ResetBehavior == ResetBehavior.Reset ? - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset) : - new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, t, 0); - + var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { item }, index); _collectionChanged(this, e); } NotifyCountChanged(); } + + /// + /// Enumerates the elements of a . + /// + public struct Enumerator : IEnumerator + { + private List.Enumerator _innerEnumerator; + + public Enumerator(List inner) + { + _innerEnumerator = inner.GetEnumerator(); + } + + public bool MoveNext() + { + return _innerEnumerator.MoveNext(); + } + + void IEnumerator.Reset() + { + ((IEnumerator)_innerEnumerator).Reset(); + } + + public T Current => _innerEnumerator.Current; + + object IEnumerator.Current => Current; + + public void Dispose() + { + _innerEnumerator.Dispose(); + } + } + } + + internal static class EventArgsCache + { + internal static readonly PropertyChangedEventArgs CountPropertyChanged = new PropertyChangedEventArgs(nameof(AvaloniaList.Count)); + internal static readonly NotifyCollectionChangedEventArgs ResetCollectionChanged = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); } } diff --git a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs index 8a38a00493..5c01e6a588 100644 --- a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs +++ b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs @@ -148,6 +148,23 @@ namespace Avalonia.Base.UnitTests.Collections Assert.True(raised); } + [Fact] + public void AddRange_Items_Should_Raise_Correct_CollectionChanged() + { + var target = new AvaloniaList(); + + var eventItems = new List(); + + target.CollectionChanged += (sender, args) => + { + eventItems.AddRange(args.NewItems.Cast()); + }; + + target.AddRange(Enumerable.Range(0,10).Select(i => new object())); + + Assert.Equal(eventItems, target); + } + [Fact] public void Replacing_Item_Should_Raise_CollectionChanged() { From 0486de87a6b596a27ac643d55213439835509803 Mon Sep 17 00:00:00 2001 From: Ben Carman Date: Mon, 26 Aug 2019 14:48:12 -0500 Subject: [PATCH 5/5] MaxLength Property for TextBox --- src/Avalonia.Controls/TextBox.cs | 11 ++++++++++- .../TextBoxTests.cs | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 6e26d1b1ff..fdc9d153e2 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -59,6 +59,9 @@ namespace Avalonia.Controls o => o.SelectionEnd, (o, v) => o.SelectionEnd = v); + public static readonly StyledProperty MaxLengthProperty = + AvaloniaProperty.Register(nameof(MaxLength), defaultValue: 0); + public static readonly DirectProperty TextProperty = TextBlock.TextProperty.AddOwner( o => o.Text, @@ -232,6 +235,12 @@ namespace Avalonia.Controls } } + public int MaxLength + { + get { return GetValue(MaxLengthProperty); } + set { SetValue(MaxLengthProperty, value); } + } + [Content] public string Text { @@ -345,7 +354,7 @@ namespace Avalonia.Controls input = RemoveInvalidCharacters(input); string text = Text ?? string.Empty; int caretIndex = CaretIndex; - if (!string.IsNullOrEmpty(input)) + if (!string.IsNullOrEmpty(input) && (MaxLength == 0 || input.Length + text.Length - (Math.Abs(SelectionStart - SelectionEnd)) <= MaxLength)) { DeleteSelection(); caretIndex = CaretIndex; diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index febc1de5f9..225eca17b2 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -460,6 +460,24 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Text_Box_MaxLength_Work_Properly() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "abc", + MaxLength = 3, + }; + + RaiseKeyEvent(target, Key.D, KeyModifiers.None); + + Assert.Equal("abc", target.Text); + } + } + private static TestServices FocusServices => TestServices.MockThreadingInterface.With( focusManager: new FocusManager(), keyboardDevice: () => new KeyboardDevice(),