Browse Source

Merge branch 'master' into animations-alloc

pull/2895/head
Jumar Macato 7 years ago
committed by GitHub
parent
commit
8bf17ea5a6
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 229
      src/Avalonia.Base/Collections/AvaloniaList.cs
  2. 11
      src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs
  3. 21
      src/Avalonia.Base/Data/DataValidationException.cs
  4. 24
      src/Avalonia.Controls/DataValidationErrors.cs
  5. 11
      src/Avalonia.Controls/TextBox.cs
  6. 17
      tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs
  7. 4
      tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_DataValidation.cs
  8. 4
      tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs
  9. 18
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  10. 2
      tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs

229
src/Avalonia.Base/Collections/AvaloniaList.cs

@ -55,15 +55,15 @@ namespace Avalonia.Collections
/// </remarks>
public class AvaloniaList<T> : IAvaloniaList<T>, IList, INotifyCollectionChangedDebug
{
private List<T> _inner;
private readonly List<T> _inner;
private NotifyCollectionChangedEventHandler _collectionChanged;
/// <summary>
/// Initializes a new instance of the <see cref="AvaloniaList{T}"/> class.
/// </summary>
public AvaloniaList()
: this(Enumerable.Empty<T>())
{
_inner = new List<T>();
}
/// <summary>
@ -89,8 +89,8 @@ namespace Avalonia.Collections
/// </summary>
public event NotifyCollectionChangedEventHandler CollectionChanged
{
add { _collectionChanged += value; }
remove { _collectionChanged -= value; }
add => _collectionChanged += value;
remove => _collectionChanged -= value;
}
/// <summary>
@ -150,7 +150,7 @@ namespace Avalonia.Collections
T old = _inner[index];
if (!object.Equals(old, value))
if (!EqualityComparer<T>.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);
}
/// <summary>
/// Adds multiple items to the collection.
/// </summary>
/// <param name="items">The items.</param>
public virtual void AddRange(IEnumerable<T> items)
{
Contract.Requires<ArgumentNullException>(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<T> items) => InsertRange(_inner.Count, items);
/// <summary>
/// Removes all items from the collection.
/// </summary>
public virtual void Clear()
{
if (this.Count > 0)
if (Count > 0)
{
var old = _inner;
_inner = new List<T>();
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.
/// </summary>
/// <returns>An <see cref="IEnumerator{T}"/>.</returns>
public IEnumerator<T> GetEnumerator()
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
return _inner.GetEnumerator();
return new Enumerator(_inner);
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return new Enumerator(_inner);
}
public Enumerator GetEnumerator()
{
return new Enumerator(_inner);
}
/// <summary>
@ -289,7 +293,7 @@ namespace Avalonia.Collections
{
Validate?.Invoke(item);
_inner.Insert(index, item);
NotifyAdd(new[] { item }, index);
NotifyAdd(item, index);
}
/// <summary>
@ -301,20 +305,83 @@ namespace Avalonia.Collections
{
Contract.Requires<ArgumentNullException>(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<T> collection)
{
Validate((T)item);
if (hasValidation)
{
foreach (T item in collection)
{
Validate(item);
}
}
_inner.InsertRange(index, collection);
NotifyAdd(list, index);
}
else
{
using (IEnumerator<T> 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<T> en = items.GetEnumerator())
{
if (en.MoveNext())
{
// Avoid allocating list for collection notification if there is no event subscriptions.
List<T> notificationItems = willRaiseCollectionChanged ?
new List<T>() :
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);
}
/// <summary>
@ -480,12 +547,6 @@ namespace Avalonia.Collections
_inner.CopyTo((T[])array, index);
}
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
{
return _inner.GetEnumerator();
}
/// <inheritdoc/>
Delegate[] INotifyCollectionChangedDebug.GetCollectionChangedSubscribers() => _collectionChanged?.GetInvocationList();
@ -505,13 +566,29 @@ namespace Avalonia.Collections
NotifyCountChanged();
}
/// <summary>
/// Raises the <see cref="CollectionChanged"/> event with a add action.
/// </summary>
/// <param name="item">The item that was added.</param>
/// <param name="index">The starting index.</param>
private void NotifyAdd(T item, int index)
{
if (_collectionChanged != null)
{
var e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new[] { item }, index);
_collectionChanged(this, e);
}
NotifyCountChanged();
}
/// <summary>
/// Raises the <see cref="PropertyChanged"/> event when the <see cref="Count"/> property
/// changes.
/// </summary>
private void NotifyCountChanged()
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Count)));
PropertyChanged?.Invoke(this, EventArgsCache.CountPropertyChanged);
}
/// <summary>
@ -531,23 +608,57 @@ namespace Avalonia.Collections
}
/// <summary>
/// Raises the <see cref="CollectionChanged"/> event with a reset action.
/// Raises the <see cref="CollectionChanged"/> event with a remove action.
/// </summary>
/// <param name="t">The items that were removed.</param>
private void NotifyReset(IList t)
/// <param name="item">The item that was removed.</param>
/// <param name="index">The starting index.</param>
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();
}
/// <summary>
/// Enumerates the elements of a <see cref="AvaloniaList{T}"/>.
/// </summary>
public struct Enumerator : IEnumerator<T>
{
private List<T>.Enumerator _innerEnumerator;
public Enumerator(List<T> 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<object>.Count));
internal static readonly NotifyCollectionChangedEventArgs ResetCollectionChanged = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
}
}

11
src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs

@ -90,8 +90,9 @@ namespace Avalonia.Data.Core.Plugins
if (target != null)
{
var errors = target.GetErrors(_name)?
.Cast<String>()
.Where(x => x != null).ToList();
.Cast<object>()
.Where(x => x != null)
.ToList();
if (errors?.Count > 0)
{
@ -112,16 +113,16 @@ namespace Avalonia.Data.Core.Plugins
return target;
}
private Exception GenerateException(IList<string> errors)
private Exception GenerateException(IList<object> 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)));
}
}
}

21
src/Avalonia.Base/Data/DataValidationException.cs

@ -0,0 +1,21 @@
using System;
namespace Avalonia.Data
{
/// <summary>
/// Exception, which wrap validation errors.
/// </summary>
public class DataValidationException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="DataValidationException"/> class.
/// </summary>
/// <param name="errorData">Data of validation error.</param>
public DataValidationException(object errorData) : base(errorData?.ToString())
{
ErrorData = errorData;
}
public object ErrorData { get; }
}
}

24
src/Avalonia.Controls/DataValidationErrors.cs

@ -22,8 +22,8 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the DataValidationErrors.Errors attached property.
/// </summary>
public static readonly AttachedProperty<IEnumerable<Exception>> ErrorsProperty =
AvaloniaProperty.RegisterAttached<DataValidationErrors, Control, IEnumerable<Exception>>("Errors");
public static readonly AttachedProperty<IEnumerable<object>> ErrorsProperty =
AvaloniaProperty.RegisterAttached<DataValidationErrors, Control, IEnumerable<object>>("Errors");
/// <summary>
/// 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<Exception>)e.NewValue;
var errors = (IEnumerable<object>)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<Exception> GetErrors(Control control)
public static IEnumerable<object> GetErrors(Control control)
{
return control.GetValue(ErrorsProperty);
}
public static void SetErrors(Control control, IEnumerable<Exception> errors)
public static void SetErrors(Control control, IEnumerable<object> errors)
{
control.SetValue(ErrorsProperty, errors);
}
@ -112,14 +112,14 @@ namespace Avalonia.Controls
return control.GetValue(HasErrorsProperty);
}
private static IEnumerable<Exception> UnpackException(Exception exception)
private static IEnumerable<object> UnpackException(Exception exception)
{
if (exception != null)
{
var aggregate = exception as AggregateException;
var exceptions = aggregate == null ?
(IEnumerable<Exception>)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;
}
}
}

11
src/Avalonia.Controls/TextBox.cs

@ -59,6 +59,9 @@ namespace Avalonia.Controls
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
public static readonly StyledProperty<int> MaxLengthProperty =
AvaloniaProperty.Register<TextBox, int>(nameof(MaxLength), defaultValue: 0);
public static readonly DirectProperty<TextBox, string> TextProperty =
TextBlock.TextProperty.AddOwner<TextBox>(
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;

17
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<object>();
var eventItems = new List<object>();
target.CollectionChanged += (sender, args) =>
{
eventItems.AddRange(args.NewItems.Cast<object>());
};
target.AddRange(Enumerable.Range(0,10).Select(i => new object()));
Assert.Equal(eventItems, target);
}
[Fact]
public void Replacing_Item_Should_Raise_CollectionChanged()
{

4
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);

4
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);
}

18
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(),

2
tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs

@ -58,7 +58,7 @@ namespace Avalonia.Controls.UnitTests
Assert.Null(DataValidationErrors.GetErrors(target));
target.Text = "20";
IEnumerable<Exception> errors = DataValidationErrors.GetErrors(target);
IEnumerable<object> errors = DataValidationErrors.GetErrors(target);
Assert.Single(errors);
Assert.IsType<InvalidOperationException>(errors.Single());
target.Text = "1";

Loading…
Cancel
Save