From ce92286112282e5cead7fd5f9fc40590c4bb46cc Mon Sep 17 00:00:00 2001 From: JaggerJo Date: Mon, 22 Jul 2019 00:21:43 +0200 Subject: [PATCH 01/68] add basic operations as static methods and use them in the declared operators. - Dot - Cross - Normalize - Divide - Multiply - Add - Subtract - Negate --- src/Avalonia.Visuals/Vector.cs | 115 +++++++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 2f1690184d..f185682dc0 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -65,9 +65,7 @@ namespace Avalonia /// Second vector /// The dot product public static double operator *(Vector a, Vector b) - { - return a.X * b.X + a.Y * b.Y; - } + => Dot(a, b); /// /// Scales a vector. @@ -76,9 +74,7 @@ namespace Avalonia /// The scaling factor. /// The scaled vector. public static Vector operator *(Vector vector, double scale) - { - return new Vector(vector._x * scale, vector._y * scale); - } + => Multiply(vector, scale); /// /// Scales a vector. @@ -87,9 +83,7 @@ namespace Avalonia /// The divisor. /// The scaled vector. public static Vector operator /(Vector vector, double scale) - { - return new Vector(vector._x / scale, vector._y / scale); - } + => Divide(vector, scale); /// /// Length of the vector @@ -102,9 +96,7 @@ namespace Avalonia /// The vector. /// The negated vector. public static Vector operator -(Vector a) - { - return new Vector(-a._x, -a._y); - } + => Negate(a); /// /// Adds two vectors. @@ -113,9 +105,7 @@ namespace Avalonia /// The second vector. /// A vector that is the result of the addition. public static Vector operator +(Vector a, Vector b) - { - return new Vector(a._x + b._x, a._y + b._y); - } + => Add(a, b); /// /// Subtracts two vectors. @@ -124,9 +114,7 @@ namespace Avalonia /// The second vector. /// A vector that is the result of the subtraction. public static Vector operator -(Vector a, Vector b) - { - return new Vector(a._x - b._x, a._y - b._y); - } + => Subtract(a, b); /// /// Check if two vectors are equal (bitwise). @@ -155,7 +143,8 @@ namespace Avalonia public override bool Equals(object obj) { - if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(null, obj)) + return false; return obj is Vector vector && Equals(vector); } @@ -206,5 +195,93 @@ namespace Avalonia { return new Vector(_x, y); } + + /// + /// Returns the dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product. + public static double Dot(Vector a, Vector b) + => a._x * b._x + a._y * b._y; + + /// + /// Returns the cross product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The cross product. + public static double Cross(Vector a, Vector b) + => a._x * b._y - a._y * b._x; + + /// + /// Normalizes the given vector. + /// + /// The vector + /// The normalized vector. + public static Vector Normalize(Vector vector) + => Divide(vector, vector.Length); + + /// + /// Divides the first vector by the second. + /// + /// The first vector. + /// The second vector. + /// The scaled vector. + public static Vector Divide(Vector a, Vector b) + => new Vector(a._x / b._x, a._y / b._y); + + /// + /// Divides the vector by the given scalar. + /// + /// The vector + /// The scalar value + /// The scaled vector. + public static Vector Divide(Vector vector, double scalar) + => new Vector(vector._x / scalar, vector._y / scalar); + + /// + /// Multiplies the first vector by the second. + /// + /// The first vector. + /// The second vector. + /// The scaled vector. + public static Vector Multiply(Vector a, Vector b) + => new Vector(a._x * b._x, a._y * b._y); + + /// + /// Multiplies the vector by the given scalar. + /// + /// The vector + /// The scalar value + /// The scaled vector. + public static Vector Multiply(Vector vector, double scalar) + => new Vector(vector._x * scalar, vector._y * scalar); + + /// + /// Adds the second to the first vector + /// + /// The first vector. + /// The second vector. + /// The summed vector. + public static Vector Add(Vector a, Vector b) + => new Vector(a._x + b._x, a._y + b._y); + + /// + /// Subtracts the second from the first vector + /// + /// The first vector. + /// The second vector. + /// The difference vector. + public static Vector Subtract(Vector a, Vector b) + => new Vector(a._x - b._x, a._y - b._y); + + /// + /// Negates the vector + /// + /// The vector to negate. + /// The scaled vector. + public static Vector Negate(Vector vector) + => new Vector(-vector._x, -vector._y); } } From c0a595f48e8601296ffbf667e0192dc1961d988e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 28 Jul 2019 18:32:37 +0200 Subject: [PATCH 02/68] Fix scrolling to selected item in TreeView. - Define a `PART_Header` in the `TreeViewItem` template which represents the header to be scrolled into view when a `TreeViewItem` is selected. Make this header only take up the minimum of required space. - Catch the `RequestBringIntoViewEventArgs` in `TreeViewItem` and update the target rect to that of `PART_Header` --- src/Avalonia.Controls/TreeViewItem.cs | 23 +++++++++++++++++++ src/Avalonia.Themes.Default/TreeViewItem.xaml | 4 +++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 4d229390cb..e185bc227b 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -42,6 +42,7 @@ namespace Avalonia.Controls new FuncTemplate(() => new StackPanel()); private TreeView _treeView; + private IControl _header; private bool _isExpanded; private int _level; @@ -53,6 +54,7 @@ namespace Avalonia.Controls SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(true); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); + RequestBringIntoViewEvent.AddClassHandler(x => x.OnRequestBringIntoView); } /// @@ -120,6 +122,21 @@ namespace Avalonia.Controls ItemContainerGenerator.Clear(); } + protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e) + { + if (e.TargetObject == this && _header != null) + { + var m = _header.TransformToVisual(this); + + if (m.HasValue) + { + var bounds = new Rect(_header.Bounds.Size); + var rect = bounds.TransformToAABB(m.Value); + e.TargetRect = rect; + } + } + } + /// protected override void OnKeyDown(KeyEventArgs e) { @@ -146,6 +163,12 @@ namespace Avalonia.Controls // Don't call base.OnKeyDown - let events bubble up to containing TreeView. } + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + base.OnTemplateApplied(e); + _header = e.NameScope.Find("PART_Header"); + } + private static int CalculateDistanceFromLogicalParent(ILogical logical, int @default = -1) where T : class { var result = 0; diff --git a/src/Avalonia.Themes.Default/TreeViewItem.xaml b/src/Avalonia.Themes.Default/TreeViewItem.xaml index 5dd082cf7a..0d826806d0 100644 --- a/src/Avalonia.Themes.Default/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Default/TreeViewItem.xaml @@ -16,7 +16,9 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" TemplatedControl.IsTemplateFocusTarget="True"> - Date: Thu, 8 Aug 2019 14:11:44 +0200 Subject: [PATCH 03/68] Add failing unit test for TwoWay binding issue. --- .../Data/BindingTests.cs | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index d19accb0ad..0ba06980af 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -60,6 +60,80 @@ namespace Avalonia.Markup.UnitTests.Data Assert.Equal("baz", source.Foo); } + [Fact] + public void TwoWay_Binding_Should_Be_Set_Up_GC_Collect() + { + var source = new WeakRefSource { Foo = null }; + var target = new TestControl { DataContext = source }; + + var binding = new Binding + { + Path = "Foo", + Mode = BindingMode.TwoWay + }; + + target.Bind(TestControl.ValueProperty, binding); + + var ref1 = AssignValue(target, "ref1"); + + Assert.Equal(ref1.Target, source.Foo); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + + var ref2 = AssignValue(target, "ref2"); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + + target.Value = null; + + Assert.Null(source.Foo); + } + + private class DummyObject : ICloneable + { + private readonly string _val; + + public DummyObject(string val) + { + _val = val; + } + + public object Clone() + { + return new DummyObject(_val); + } + + protected bool Equals(DummyObject other) + { + return string.Equals(_val, other._val); + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((DummyObject) obj); + } + + public override int GetHashCode() + { + return (_val != null ? _val.GetHashCode() : 0); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private WeakReference AssignValue(TestControl source, string val) + { + var obj = new DummyObject(val); + + source.Value = obj; + + return new WeakReference(obj); + } + [Fact] public void OneTime_Binding_Should_Be_Set_Up() { @@ -568,12 +642,70 @@ namespace Avalonia.Markup.UnitTests.Data } } + public class WeakRefSource : INotifyPropertyChanged + { + private WeakReference _foo; + + public object Foo + { + get + { + if (_foo == null) + { + return null; + } + + if (_foo.TryGetTarget(out object target)) + { + if (target is ICloneable cloneable) + { + return cloneable.Clone(); + } + + return target; + } + + return null; + } + set + { + _foo = new WeakReference(value); + + RaisePropertyChanged(); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + private void RaisePropertyChanged([CallerMemberName] string prop = "") + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop)); + } + } + private class OldDataContextViewModel { public int Foo { get; set; } = 1; public int Bar { get; set; } = 2; } + private class TestControl : Control + { + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect( + nameof(Value), + o => o.Value, + (o, v) => o.Value = v); + + private object _value; + + public object Value + { + get => _value; + set => SetAndRaise(ValueProperty, ref _value, value); + } + } + private class OldDataContextTest : Control { public static readonly StyledProperty FooProperty = From 868b5ea840094c6f701fc6295e9fd44e0f606196 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 8 Aug 2019 19:18:08 +0200 Subject: [PATCH 04/68] Update WeakReference usages to use the generic one. --- .../Data/Core/AvaloniaPropertyAccessorNode.cs | 6 ++-- src/Avalonia.Base/Data/Core/ExpressionNode.cs | 26 +++++++++-------- .../Data/Core/ExpressionObserver.cs | 8 +++--- .../Data/Core/IndexerExpressionNode.cs | 11 ++++++-- .../Data/Core/IndexerNodeBase.cs | 5 ++-- .../Plugins/AvaloniaPropertyAccessorPlugin.cs | 4 +-- .../DataAnnotationsValidationPlugin.cs | 23 ++++++++------- .../Core/Plugins/ExceptionValidationPlugin.cs | 8 +++--- .../Core/Plugins/IDataValidationPlugin.cs | 5 ++-- .../Core/Plugins/IPropertyAccessorPlugin.cs | 3 +- .../Data/Core/Plugins/IStreamPlugin.cs | 4 +-- .../Core/Plugins/IndeiValidationPlugin.cs | 28 +++++++++++++------ .../Plugins/InpcPropertyAccessorPlugin.cs | 23 +++++++++------ .../Data/Core/Plugins/MethodAccessorPlugin.cs | 21 ++++++++++---- .../Core/Plugins/ObservableStreamPlugin.cs | 12 ++++---- .../Data/Core/Plugins/TaskStreamPlugin.cs | 13 ++++++--- .../Data/Core/PropertyAccessorNode.cs | 6 ++-- src/Avalonia.Base/Data/Core/SettableNode.cs | 18 ++++++++++-- src/Avalonia.Base/Data/Core/StreamNode.cs | 2 +- src/Avalonia.Input/Gestures.cs | 12 ++++---- .../Markup/Parsers/Nodes/ElementNameNode.cs | 2 +- .../Markup/Parsers/Nodes/FindAncestorNode.cs | 4 +-- .../Markup/Parsers/Nodes/StringIndexerNode.cs | 26 +++++++++++------ .../Core/ExpressionObserverTests_Property.cs | 2 +- .../DataAnnotationsValidationPluginTests.cs | 14 +++++----- .../Plugins/ExceptionValidationPluginTests.cs | 4 +-- .../Plugins/IndeiValidationPluginTests.cs | 8 +++--- 27 files changed, 183 insertions(+), 115 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs index 28c0dce518..0a33eeb2c1 100644 --- a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs @@ -24,7 +24,7 @@ namespace Avalonia.Data.Core { try { - if (Target.IsAlive && Target.Target is IAvaloniaObject obj) + if (Target.TryGetTarget(out object target) && target is IAvaloniaObject obj) { obj.SetValue(_property, value, priority); return true; @@ -37,9 +37,9 @@ namespace Avalonia.Data.Core } } - protected override void StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { - if (reference.Target is IAvaloniaObject obj) + if (reference.TryGetTarget(out object target) && target is IAvaloniaObject obj) { _subscription = new AvaloniaPropertyObservable(obj, _property).Subscribe(ValueChanged); } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index 8a2dd46b86..ce40b3e517 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -8,27 +8,27 @@ namespace Avalonia.Data.Core public abstract class ExpressionNode { private static readonly object CacheInvalid = new object(); - protected static readonly WeakReference UnsetReference = - new WeakReference(AvaloniaProperty.UnsetValue); + protected static readonly WeakReference UnsetReference = + new WeakReference(AvaloniaProperty.UnsetValue); - private WeakReference _target = UnsetReference; + private WeakReference _target = UnsetReference; private Action _subscriber; private bool _listening; - protected WeakReference LastValue { get; private set; } + protected WeakReference LastValue { get; private set; } public abstract string Description { get; } public ExpressionNode Next { get; set; } - public WeakReference Target + public WeakReference Target { get { return _target; } set { Contract.Requires(value != null); - var oldTarget = _target?.Target; - var newTarget = value.Target; + _target.TryGetTarget(out var oldTarget); + value.TryGetTarget(out object newTarget); if (!ReferenceEquals(oldTarget, newTarget)) { @@ -72,9 +72,11 @@ namespace Avalonia.Data.Core _subscriber = null; } - protected virtual void StartListeningCore(WeakReference reference) + protected virtual void StartListeningCore(WeakReference reference) { - ValueChanged(reference.Target); + reference.TryGetTarget(out object target); + + ValueChanged(target); } protected virtual void StopListeningCore() @@ -96,7 +98,7 @@ namespace Avalonia.Data.Core if (notification == null) { - LastValue = new WeakReference(value); + LastValue = new WeakReference(value); if (Next != null) { @@ -109,7 +111,7 @@ namespace Avalonia.Data.Core } else { - LastValue = new WeakReference(notification.Value); + LastValue = new WeakReference(notification.Value); if (Next != null) { @@ -125,7 +127,7 @@ namespace Avalonia.Data.Core private void StartListening() { - var target = _target.Target; + _target.TryGetTarget(out object target); if (target == null) { diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 65f26df011..7060fd3451 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -78,7 +78,7 @@ namespace Avalonia.Data.Core _node = node; Description = description; - _root = new WeakReference(root); + _root = new WeakReference(root); } /// @@ -120,7 +120,7 @@ namespace Avalonia.Data.Core Contract.Requires(update != null); Description = description; _node = node; - _node.Target = new WeakReference(rootGetter()); + _node.Target = new WeakReference(rootGetter()); _root = update.Select(x => rootGetter()); } @@ -285,13 +285,13 @@ namespace Avalonia.Data.Core if (_root is IObservable observable) { _rootSubscription = observable.Subscribe( - x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null), + x => _node.Target = new WeakReference(x != AvaloniaProperty.UnsetValue ? x : null), x => PublishCompleted(), () => PublishCompleted()); } else { - _node.Target = (WeakReference)_root; + _node.Target = (WeakReference)_root; } } diff --git a/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs index 4206a99e3d..a3852cc371 100644 --- a/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/IndexerExpressionNode.cs @@ -36,7 +36,9 @@ namespace Avalonia.Data.Core { try { - _setDelegate.DynamicInvoke(Target.Target, value); + Target.TryGetTarget(out object target); + + _setDelegate.DynamicInvoke(target, value); return true; } catch (Exception) @@ -64,6 +66,11 @@ namespace Avalonia.Data.Core return _expression.Indexer == null || _expression.Indexer.Name == e.PropertyName; } - protected override int? TryGetFirstArgumentAsInt() => _firstArgumentDelegate.DynamicInvoke(Target.Target) as int?; + protected override int? TryGetFirstArgumentAsInt() + { + Target.TryGetTarget(out object target); + + return _firstArgumentDelegate.DynamicInvoke(target) as int?; + } } } diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index 5e09bbcc2f..47d5147ac2 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -13,9 +13,10 @@ namespace Avalonia.Data.Core { private IDisposable _subscription; - protected override void StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { - var target = reference.Target; + reference.TryGetTarget(out object target); + var incc = target as INotifyCollectionChanged; var inpc = target as INotifyPropertyChanged; var inputs = new List>(); diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index 8d2ed905ee..ab4a109cc2 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -31,12 +31,12 @@ namespace Avalonia.Data.Core.Plugins /// An interface through which future interactions with the /// property will be made. /// - public IPropertyAccessor Start(WeakReference reference, string propertyName) + public IPropertyAccessor Start(WeakReference reference, string propertyName) { Contract.Requires(reference != null); Contract.Requires(propertyName != null); - var instance = reference.Target; + reference.TryGetTarget(out object instance); var o = (AvaloniaObject)instance; var p = LookupProperty(o, propertyName); diff --git a/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs index 33c40abea8..f5b545d2ff 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs @@ -15,9 +15,11 @@ namespace Avalonia.Data.Core.Plugins public class DataAnnotationsValidationPlugin : IDataValidationPlugin { /// - public bool Match(WeakReference reference, string memberName) + public bool Match(WeakReference reference, string memberName) { - return reference.Target? + reference.TryGetTarget(out object target); + + return target? .GetType() .GetRuntimeProperty(memberName)? .GetCustomAttributes() @@ -25,25 +27,22 @@ namespace Avalonia.Data.Core.Plugins } /// - public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner) + public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner) { return new Accessor(reference, name, inner); } - private class Accessor : DataValidationBase + private sealed class Accessor : DataValidationBase { - private ValidationContext _context; + private readonly ValidationContext _context; - public Accessor(WeakReference reference, string name, IPropertyAccessor inner) + public Accessor(WeakReference reference, string name, IPropertyAccessor inner) : base(inner) { - _context = new ValidationContext(reference.Target); - _context.MemberName = name; - } + reference.TryGetTarget(out object target); - public override bool SetValue(object value, BindingPriority priority) - { - return base.SetValue(value, priority); + _context = new ValidationContext(target); + _context.MemberName = name; } protected override void InnerValueChanged(object value) diff --git a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs index eabfa31d4b..f305912fe1 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs @@ -12,17 +12,17 @@ namespace Avalonia.Data.Core.Plugins public class ExceptionValidationPlugin : IDataValidationPlugin { /// - public bool Match(WeakReference reference, string memberName) => true; + public bool Match(WeakReference reference, string memberName) => true; /// - public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner) + public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor inner) { return new Validator(reference, name, inner); } - private class Validator : DataValidationBase + private sealed class Validator : DataValidationBase { - public Validator(WeakReference reference, string name, IPropertyAccessor inner) + public Validator(WeakReference reference, string name, IPropertyAccessor inner) : base(inner) { } diff --git a/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs index 2c3a9a53b4..5b1af22f14 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IDataValidationPlugin.cs @@ -16,7 +16,7 @@ namespace Avalonia.Data.Core.Plugins /// A weak reference to the object. /// The name of the member to validate. /// True if the plugin can handle the object; otherwise false. - bool Match(WeakReference reference, string memberName); + bool Match(WeakReference reference, string memberName); /// /// Starts monitoring the data validation state of a property on an object. @@ -28,8 +28,7 @@ namespace Avalonia.Data.Core.Plugins /// An interface through which future interactions with the /// property will be made. /// - IPropertyAccessor Start( - WeakReference reference, + IPropertyAccessor Start(WeakReference reference, string propertyName, IPropertyAccessor inner); } diff --git a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs index 539f518083..a0021fa4d4 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IPropertyAccessorPlugin.cs @@ -28,8 +28,7 @@ namespace Avalonia.Data.Core.Plugins /// An interface through which future interactions with the /// property will be made. /// - IPropertyAccessor Start( - WeakReference reference, + IPropertyAccessor Start(WeakReference reference, string propertyName); } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs index b80d9d75c8..3df578d25b 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IStreamPlugin.cs @@ -15,7 +15,7 @@ namespace Avalonia.Data.Core.Plugins /// /// A weak reference to the value. /// True if the plugin can handle the value; otherwise false. - bool Match(WeakReference reference); + bool Match(WeakReference reference); /// /// Starts producing output based on the specified value. @@ -24,6 +24,6 @@ namespace Avalonia.Data.Core.Plugins /// /// An observable that produces the output for the value. /// - IObservable Start(WeakReference reference); + IObservable Start(WeakReference reference); } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs index 2e83e0c25e..e1f6d1590d 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs @@ -15,20 +15,25 @@ namespace Avalonia.Data.Core.Plugins public class IndeiValidationPlugin : IDataValidationPlugin { /// - public bool Match(WeakReference reference, string memberName) => reference.Target is INotifyDataErrorInfo; + public bool Match(WeakReference reference, string memberName) + { + reference.TryGetTarget(out object target); + + return target is INotifyDataErrorInfo; + } /// - public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor) + public IPropertyAccessor Start(WeakReference reference, string name, IPropertyAccessor accessor) { return new Validator(reference, name, accessor); } private class Validator : DataValidationBase, IWeakSubscriber { - WeakReference _reference; - string _name; + private readonly WeakReference _reference; + private readonly string _name; - public Validator(WeakReference reference, string name, IPropertyAccessor inner) + public Validator(WeakReference reference, string name, IPropertyAccessor inner) : base(inner) { _reference = reference; @@ -45,7 +50,7 @@ namespace Avalonia.Data.Core.Plugins protected override void SubscribeCore() { - var target = _reference.Target as INotifyDataErrorInfo; + var target = GetReferenceTarget() as INotifyDataErrorInfo; if (target != null) { @@ -60,7 +65,7 @@ namespace Avalonia.Data.Core.Plugins protected override void UnsubscribeCore() { - var target = _reference.Target as INotifyDataErrorInfo; + var target = GetReferenceTarget() as INotifyDataErrorInfo; if (target != null) { @@ -80,7 +85,7 @@ namespace Avalonia.Data.Core.Plugins private BindingNotification CreateBindingNotification(object value) { - var target = (INotifyDataErrorInfo)_reference.Target; + var target = (INotifyDataErrorInfo)GetReferenceTarget(); if (target != null) { @@ -100,6 +105,13 @@ namespace Avalonia.Data.Core.Plugins return new BindingNotification(value); } + private object GetReferenceTarget() + { + _reference.TryGetTarget(out object target); + + return target; + } + private Exception GenerateException(IList errors) { if (errors.Count == 1) diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index 4047489ccc..4716b45340 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -28,12 +28,12 @@ namespace Avalonia.Data.Core.Plugins /// An interface through which future interactions with the /// property will be made. /// - public IPropertyAccessor Start(WeakReference reference, string propertyName) + public IPropertyAccessor Start(WeakReference reference, string propertyName) { Contract.Requires(reference != null); Contract.Requires(propertyName != null); - var instance = reference.Target; + reference.TryGetTarget(out object instance); var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(x => x.Name == propertyName); if (p != null) @@ -50,11 +50,11 @@ namespace Avalonia.Data.Core.Plugins private class Accessor : PropertyAccessorBase, IWeakSubscriber { - private readonly WeakReference _reference; + private readonly WeakReference _reference; private readonly PropertyInfo _property; private bool _eventRaised; - public Accessor(WeakReference reference, PropertyInfo property) + public Accessor(WeakReference reference, PropertyInfo property) { Contract.Requires(reference != null); Contract.Requires(property != null); @@ -69,7 +69,7 @@ namespace Avalonia.Data.Core.Plugins { get { - var o = _reference.Target; + var o = GetReferenceTarget(); return (o != null) ? _property.GetValue(o) : null; } } @@ -79,7 +79,7 @@ namespace Avalonia.Data.Core.Plugins if (_property.CanWrite) { _eventRaised = false; - _property.SetValue(_reference.Target, value); + _property.SetValue(GetReferenceTarget(), value); if (!_eventRaised) { @@ -109,7 +109,7 @@ namespace Avalonia.Data.Core.Plugins protected override void UnsubscribeCore() { - var inpc = _reference.Target as INotifyPropertyChanged; + var inpc = GetReferenceTarget() as INotifyPropertyChanged; if (inpc != null) { @@ -120,6 +120,13 @@ namespace Avalonia.Data.Core.Plugins } } + private object GetReferenceTarget() + { + _reference.TryGetTarget(out object target); + + return target; + } + private void SendCurrentValue() { try @@ -132,7 +139,7 @@ namespace Avalonia.Data.Core.Plugins private void SubscribeToChanges() { - var inpc = _reference.Target as INotifyPropertyChanged; + var inpc = GetReferenceTarget() as INotifyPropertyChanged; if (inpc != null) { diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index e48c671a13..c19ee8dba7 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs @@ -9,12 +9,12 @@ namespace Avalonia.Data.Core.Plugins public bool Match(object obj, string methodName) => obj.GetType().GetRuntimeMethods().Any(x => x.Name == methodName); - public IPropertyAccessor Start(WeakReference reference, string methodName) + public IPropertyAccessor Start(WeakReference reference, string methodName) { Contract.Requires(reference != null); Contract.Requires(methodName != null); - var instance = reference.Target; + reference.TryGetTarget(out object instance); var method = instance.GetType().GetRuntimeMethods().FirstOrDefault(x => x.Name == methodName); if (method != null) @@ -35,9 +35,9 @@ namespace Avalonia.Data.Core.Plugins } } - private class Accessor : PropertyAccessorBase + private sealed class Accessor : PropertyAccessorBase { - public Accessor(WeakReference reference, MethodInfo method) + public Accessor(WeakReference reference, MethodInfo method) { Contract.Requires(reference != null); Contract.Requires(method != null); @@ -61,8 +61,17 @@ namespace Avalonia.Data.Core.Plugins var genericTypeParameters = paramTypes.Concat(new[] { returnType }).ToArray(); PropertyType = Type.GetType($"System.Func`{genericTypeParameters.Length}").MakeGenericType(genericTypeParameters); } - - Value = method.IsStatic ? method.CreateDelegate(PropertyType) : method.CreateDelegate(PropertyType, reference.Target); + + if (method.IsStatic) + { + Value = method.CreateDelegate(PropertyType); + } + else + { + reference.TryGetTarget(out object target); + + Value = method.CreateDelegate(PropertyType, target); + } } public override Type PropertyType { get; } diff --git a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs index c41097c274..ef5ce05821 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs @@ -20,9 +20,11 @@ namespace Avalonia.Data.Core.Plugins /// /// A weak reference to the value. /// True if the plugin can handle the value; otherwise false. - public virtual bool Match(WeakReference reference) + public virtual bool Match(WeakReference reference) { - return reference.Target.GetType().GetInterfaces().Any(x => + reference.TryGetTarget(out object target); + + return target != null && target.GetType().GetInterfaces().Any(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IObservable<>)); } @@ -34,9 +36,9 @@ namespace Avalonia.Data.Core.Plugins /// /// An observable that produces the output for the value. /// - public virtual IObservable Start(WeakReference reference) + public virtual IObservable Start(WeakReference reference) { - var target = reference.Target; + reference.TryGetTarget(out object target); // If the observable returns a reference type then we can cast it. if (target is IObservable result) @@ -46,7 +48,7 @@ namespace Avalonia.Data.Core.Plugins // If the observable returns a value type then we need to call Observable.Select on it. // First get the type of T in `IObservable`. - var sourceType = reference.Target.GetType().GetInterfaces().First(x => + var sourceType = target.GetType().GetInterfaces().First(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IObservable<>)).GetGenericArguments()[0]; diff --git a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs index 16862f576d..a3d2714747 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs @@ -19,7 +19,12 @@ namespace Avalonia.Data.Core.Plugins /// /// A weak reference to the value. /// True if the plugin can handle the value; otherwise false. - public virtual bool Match(WeakReference reference) => reference.Target is Task; + public virtual bool Match(WeakReference reference) + { + reference.TryGetTarget(out object target); + + return target is Task; + } /// /// Starts producing output based on the specified value. @@ -28,11 +33,11 @@ namespace Avalonia.Data.Core.Plugins /// /// An observable that produces the output for the value. /// - public virtual IObservable Start(WeakReference reference) + public virtual IObservable Start(WeakReference reference) { - var task = reference.Target as Task; + reference.TryGetTarget(out object target); - if (task != null) + if (target is Task task) { var resultProperty = task.GetType().GetRuntimeProperty("Result"); diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index df8f46a7d7..70f53b8b88 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -37,9 +37,11 @@ namespace Avalonia.Data.Core return false; } - protected override void StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { - var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(reference.Target, PropertyName)); + reference.TryGetTarget(out object target); + + var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(target, PropertyName)); var accessor = plugin?.Start(reference, PropertyName); if (_enableValidation && Next == null) diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs index 7c839acb78..eb98b9e8d6 100644 --- a/src/Avalonia.Base/Data/Core/SettableNode.cs +++ b/src/Avalonia.Base/Data/Core/SettableNode.cs @@ -19,11 +19,25 @@ namespace Avalonia.Data.Core { return false; } + + if (LastValue == null) + { + return false; + } + + bool isLastValueAlive = LastValue.TryGetTarget(out object lastValue); + + if (!isLastValueAlive) + { + return false; + } + if (PropertyType.IsValueType) { - return LastValue?.Target != null && LastValue.Target.Equals(value); + return lastValue.Equals(value); } - return LastValue != null && Object.ReferenceEquals(LastValue?.Target, value); + + return ReferenceEquals(lastValue, value); } protected abstract bool SetTargetValueCore(object value, BindingPriority priority); diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index 6fc178e7f8..183e0662aa 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/StreamNode.cs @@ -12,7 +12,7 @@ namespace Avalonia.Data.Core public override string Description => "^"; - protected override void StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { foreach (var plugin in ExpressionObserver.StreamHandlers) { diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index 02dda45e99..bb8c8b8c40 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -31,7 +31,7 @@ namespace Avalonia.Input RoutedEvent.Register( "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); - private static WeakReference s_lastPress; + private static WeakReference s_lastPress; static Gestures() { @@ -47,11 +47,11 @@ namespace Avalonia.Input if (e.ClickCount <= 1) { - s_lastPress = new WeakReference(e.Source); + s_lastPress = new WeakReference(e.Source); } - else if (s_lastPress?.IsAlive == true && e.ClickCount == 2 && s_lastPress.Target == e.Source) + else if (s_lastPress != null && e.ClickCount == 2 && e.MouseButton != MouseButton.Right) { - if (e.MouseButton != MouseButton.Right) + if (s_lastPress.TryGetTarget(out var target) && target == e.Source) { e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent)); } @@ -65,10 +65,10 @@ namespace Avalonia.Input { var e = (PointerReleasedEventArgs)ev; - if (s_lastPress?.IsAlive == true && s_lastPress.Target == e.Source) + if (s_lastPress.TryGetTarget(out var target) && target == e.Source) { var et = e.MouseButton != MouseButton.Right ? TappedEvent : RightTappedEvent; - ((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(et)); + e.Source.RaiseEvent(new RoutedEventArgs(et)); } } } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs index 981e93c534..7eec80fc00 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Markup.Parsers.Nodes public override string Description => $"#{_name}"; - protected override void StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { if (_nameScope.TryGetTarget(out var scope)) _subscription = NameScopeLocator.Track(scope, _name).Subscribe(ValueChanged); diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs index 221df44327..321a85c1d7 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs @@ -31,9 +31,9 @@ namespace Avalonia.Markup.Parsers.Nodes } } - protected override void StartListeningCore(WeakReference reference) + protected override void StartListeningCore(WeakReference reference) { - if (reference.Target is ILogical logical) + if (reference.TryGetTarget(out object target) && target is ILogical logical) { _subscription = ControlLocator.Track(logical, _level, _ancestorType).Subscribe(ValueChanged); } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs index ea847bde11..a11879238b 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/StringIndexerNode.cs @@ -26,9 +26,11 @@ namespace Avalonia.Markup.Parsers.Nodes protected override bool SetTargetValueCore(object value, BindingPriority priority) { - var typeInfo = Target.Target.GetType().GetTypeInfo(); - var list = Target.Target as IList; - var dictionary = Target.Target as IDictionary; + Target.TryGetTarget(out object target); + + var typeInfo = target.GetType().GetTypeInfo(); + var list = target as IList; + var dictionary = target as IDictionary; var indexerProperty = GetIndexer(typeInfo); var indexerParameters = indexerProperty?.GetIndexParameters(); @@ -53,7 +55,7 @@ namespace Avalonia.Markup.Parsers.Nodes // Try special cases where we can validate indices if (typeInfo.IsArray) { - return SetValueInArray((Array)Target.Target, intArgs, value); + return SetValueInArray((Array)target, intArgs, value); } else if (Arguments.Count == 1) { @@ -83,14 +85,14 @@ namespace Avalonia.Markup.Parsers.Nodes else { // Fallback to unchecked access - indexerProperty.SetValue(Target.Target, value, convertedObjectArray); + indexerProperty.SetValue(target, value, convertedObjectArray); return true; } } else { // Fallback to unchecked access - indexerProperty.SetValue(Target.Target, value, convertedObjectArray); + indexerProperty.SetValue(target, value, convertedObjectArray); return true; } } @@ -98,7 +100,7 @@ namespace Avalonia.Markup.Parsers.Nodes // multidimensional indexer, which doesn't take the same number of arguments else if (typeInfo.IsArray) { - SetValueInArray((Array)Target.Target, value); + SetValueInArray((Array)target, value); return true; } return false; @@ -126,7 +128,15 @@ namespace Avalonia.Markup.Parsers.Nodes public IList Arguments { get; } - public override Type PropertyType => GetIndexer(Target.Target.GetType().GetTypeInfo())?.PropertyType; + public override Type PropertyType + { + get + { + Target.TryGetTarget(out object target); + + return GetIndexer(target.GetType().GetTypeInfo())?.PropertyType; + } + } protected override object GetValue(object target) { diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs index b56afa33a4..a2ef8eedad 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Property.cs @@ -574,7 +574,7 @@ namespace Avalonia.Base.UnitTests.Data.Core var source = new Class1 { Foo = "foo" }; var target = new PropertyAccessorNode("Foo", false); Assert.NotNull(target); - target.Target = new WeakReference(source); + target.Target = new WeakReference(source); target.Subscribe(_ => { }); target.Unsubscribe(); target.Unsubscribe(); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/DataAnnotationsValidationPluginTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/DataAnnotationsValidationPluginTests.cs index 378c225e23..435ead0b80 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/DataAnnotationsValidationPluginTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/DataAnnotationsValidationPluginTests.cs @@ -20,7 +20,7 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins var target = new DataAnnotationsValidationPlugin(); var data = new Data(); - Assert.True(target.Match(new WeakReference(data), nameof(Data.Between5And10))); + Assert.True(target.Match(new WeakReference(data), nameof(Data.Between5And10))); } [Fact] @@ -29,7 +29,7 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins var target = new DataAnnotationsValidationPlugin(); var data = new Data(); - Assert.True(target.Match(new WeakReference(data), nameof(Data.PhoneNumber))); + Assert.True(target.Match(new WeakReference(data), nameof(Data.PhoneNumber))); } [Fact] @@ -38,7 +38,7 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins var target = new DataAnnotationsValidationPlugin(); var data = new Data(); - Assert.False(target.Match(new WeakReference(data), nameof(Data.Unvalidated))); + Assert.False(target.Match(new WeakReference(data), nameof(Data.Unvalidated))); } [Fact] @@ -47,8 +47,8 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); var validatorPlugin = new DataAnnotationsValidationPlugin(); var data = new Data(); - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Between5And10)); - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Between5And10), accessor); + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Between5And10)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Between5And10), accessor); var result = new List(); var errmsg = new RangeAttribute(5, 10).FormatErrorMessage(nameof(Data.Between5And10)); @@ -79,8 +79,8 @@ namespace Avalonia.Markup.UnitTests.Data.Plugins var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); var validatorPlugin = new DataAnnotationsValidationPlugin(); var data = new Data(); - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber)); - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber), accessor); + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.PhoneNumber), accessor); var result = new List(); validator.Subscribe(x => result.Add(x)); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/ExceptionValidationPluginTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/ExceptionValidationPluginTests.cs index 2a307f9a61..6bd5fe5093 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/ExceptionValidationPluginTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/ExceptionValidationPluginTests.cs @@ -19,8 +19,8 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); var validatorPlugin = new ExceptionValidationPlugin(); var data = new Data(); - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive)); - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor); + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.MustBePositive), accessor); var result = new List(); validator.Subscribe(x => result.Add(x)); diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs index 383030cb6c..db0f5b0c77 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/Plugins/IndeiValidationPluginTests.cs @@ -18,8 +18,8 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); var validatorPlugin = new IndeiValidationPlugin(); var data = new Data { Maximum = 5 }; - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value)); - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor); + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor); var result = new List(); validator.Subscribe(x => result.Add(x)); @@ -53,8 +53,8 @@ namespace Avalonia.Base.UnitTests.Data.Core.Plugins var inpcAccessorPlugin = new InpcPropertyAccessorPlugin(); var validatorPlugin = new IndeiValidationPlugin(); var data = new Data { Maximum = 5 }; - var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value)); - var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor); + var accessor = inpcAccessorPlugin.Start(new WeakReference(data), nameof(data.Value)); + var validator = validatorPlugin.Start(new WeakReference(data), nameof(data.Value), accessor); Assert.Equal(0, data.ErrorsChangedSubscriptionCount); validator.Subscribe(_ => { }); From 7d96244e3c2480d63782245e0e52b2744d0dee5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josua=20J=C3=A4ger?= Date: Thu, 15 Aug 2019 20:17:58 +0200 Subject: [PATCH 05/68] add squared length --- src/Avalonia.Visuals/Vector.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index f185682dc0..78ed688295 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -90,6 +90,11 @@ namespace Avalonia /// public double Length => Math.Sqrt(X * X + Y * Y); + /// + /// Squared Length of the vector + /// + public double SquaredLength => Math.Sqrt(Length); + /// /// Negates a vector. /// From 22ac34d89b912cef0b5de1b5660201e27916e011 Mon Sep 17 00:00:00 2001 From: JaggerJo Date: Thu, 15 Aug 2019 20:28:42 +0200 Subject: [PATCH 06/68] add - Zero - One - UnitX - UnitY --- src/Avalonia.Visuals/Vector.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index f185682dc0..aede9f3905 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -283,5 +283,29 @@ namespace Avalonia /// The scaled vector. public static Vector Negate(Vector vector) => new Vector(-vector._x, -vector._y); + + /// + /// Returnes the vector (0.0, 0.0) + /// + public static Vector Zero + => new Vector(0, 0); + + /// + /// Returnes the vector (1.0, 1.0) + /// + public static Vector One + => new Vector(1, 1); + + /// + /// Returnes the vector (1.0, 0.0) + /// + public static Vector UnitX + => new Vector(1, 0); + + /// + /// Returnes the vector (0.0, 1.0) + /// + public static Vector UnitY + => new Vector(0, 1); } } From b475acab1e677c3d570e83fe791fdffac54f0e0b Mon Sep 17 00:00:00 2001 From: JaggerJo Date: Thu, 15 Aug 2019 21:17:38 +0200 Subject: [PATCH 07/68] - add tests - add instance methods for normalize & negate - make length squared simpler --- src/Avalonia.Visuals/Vector.cs | 18 ++- .../Avalonia.Visuals.UnitTests/VectorTests.cs | 112 ++++++++++++++++++ 2 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 tests/Avalonia.Visuals.UnitTests/VectorTests.cs diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 2c02c00d00..782a917c62 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -88,12 +88,12 @@ namespace Avalonia /// /// Length of the vector /// - public double Length => Math.Sqrt(X * X + Y * Y); + public double Length => Math.Sqrt(_x * _x + _y * _y); /// /// Squared Length of the vector /// - public double SquaredLength => Math.Sqrt(Length); + public double SquaredLength => _x * _x + _y * _y; /// /// Negates a vector. @@ -201,6 +201,20 @@ namespace Avalonia return new Vector(_x, y); } + /// + /// Returns a normalized version of this vector. + /// + /// The normalized vector. + public Vector Normalize() + => Normalize(this); + + /// + /// Returns a negated version of this vector. + /// + /// The negated vector. + public Vector Negate() + => Negate(this); + /// /// Returns the dot product of two vectors. /// diff --git a/tests/Avalonia.Visuals.UnitTests/VectorTests.cs b/tests/Avalonia.Visuals.UnitTests/VectorTests.cs new file mode 100644 index 0000000000..1bcc165aef --- /dev/null +++ b/tests/Avalonia.Visuals.UnitTests/VectorTests.cs @@ -0,0 +1,112 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Xunit; +using Avalonia; +using System; + +namespace Avalonia.Visuals.UnitTests +{ + public class VectorTests + { + [Fact] + public void Length_Should_Return_Correct_Length_Of_Vector() + { + var vector = new Vector(2, 4); + var length = Math.Sqrt(2 * 2 + 4 * 4); + + Assert.Equal(length, vector.Length); + } + + [Fact] + public void Length_Squared_Should_Return_Correct_Length_Of_Vector() + { + var vectorA = new Vector(2, 4); + var squaredLengthA = 2 * 2 + 4 * 4; + + Assert.Equal(squaredLengthA, vectorA.SquaredLength); + } + + [Fact] + public void Normalize_Should_Return_Normalized_Vector() + { + // the length of a normalized vector must be 1 + + var vectorA = new Vector(13, 84); + var vectorB = new Vector(-34, 345); + var vectorC = new Vector(-34, -84); + + Assert.Equal(1.0, vectorA.Normalize().Length); + Assert.Equal(1.0, vectorB.Normalize().Length); + Assert.Equal(1.0, vectorC.Normalize().Length); + } + + [Fact] + public void Negate_Should_Return_Negated_Vector() + { + var vector = new Vector(2, 4); + var negated = new Vector(-2, -4); + + Assert.Equal(negated, vector.Negate()); + } + + [Fact] + public void Dot_Should_Return_Correct_Value() + { + var a = new Vector(-6, 8.0); + var b = new Vector(5, 12.0); + + Assert.Equal(66.0, Vector.Dot(a, b)); + } + + [Fact] + public void Cross_Should_Return_Correct_Value() + { + var a = new Vector(-6, 8.0); + var b = new Vector(5, 12.0); + + Assert.Equal(-112.0, Vector.Cross(a, b)); + } + + [Fact] + public void Divied_By_Vector_Should_Return_Correct_Value() + { + var a = new Vector(10, 2); + var b = new Vector(5, 2); + + var expected = new Vector(2, 1); + + Assert.Equal(expected, Vector.Divide(a, b)); + } + + [Fact] + public void Divied_Should_Return_Correct_Value() + { + var vector = new Vector(10, 2); + var expected = new Vector(5, 1); + + Assert.Equal(expected, Vector.Divide(vector, 2)); + } + + [Fact] + public void Multiply_By_Vector_Should_Return_Correct_Value() + { + var a = new Vector(10, 2); + var b = new Vector(2, 2); + + var expected = new Vector(20, 4); + + Assert.Equal(expected, Vector.Multiply(a, b)); + } + + [Fact] + public void Multiply_Should_Return_Correct_Value() + { + var vector = new Vector(10, 2); + + var expected = new Vector(20, 4); + + Assert.Equal(expected, Vector.Multiply(vector, 2)); + } + } +} From ba873bf33936234ad533c56da161a2890954c958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josua=20J=C3=A4ger?= Date: Sat, 17 Aug 2019 17:48:01 +0200 Subject: [PATCH 08/68] use field directly instead of property Co-Authored-By: Benedikt Schroeder --- src/Avalonia.Visuals/Vector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Vector.cs b/src/Avalonia.Visuals/Vector.cs index 782a917c62..11bda8b00e 100644 --- a/src/Avalonia.Visuals/Vector.cs +++ b/src/Avalonia.Visuals/Vector.cs @@ -88,7 +88,7 @@ namespace Avalonia /// /// Length of the vector /// - public double Length => Math.Sqrt(_x * _x + _y * _y); + public double Length => Math.Sqrt(SquaredLength); /// /// Squared Length of the vector From f3dfddc112739ac8c1602fbf9516004b803d54c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Wed, 21 Aug 2019 01:15:55 +0100 Subject: [PATCH 09/68] Fixed Binding.DoNothing for MultiBinding. --- .../Avalonia.Markup/Data/MultiBinding.cs | 12 +-- .../Converters/MultiValueConverterTests.cs | 76 +++++++++++++++++++ 2 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 19f92149ec..29945e25c3 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -76,7 +76,12 @@ namespace Avalonia.Data } var children = Bindings.Select(x => x.Initiate(target, null)); - var input = children.Select(x => x.Observable).CombineLatest().Select(x => ConvertValue(x, targetType, converter)); + + var input = children.Select(x => x.Observable) + .CombineLatest() + .Select(x => ConvertValue(x, targetType, converter)) + .Where(x => x != BindingOperations.DoNothing); + var mode = Mode == BindingMode.Default ? targetProperty?.GetMetadata(target.GetType()).DefaultBindingMode : Mode; @@ -97,11 +102,6 @@ namespace Avalonia.Data var culture = CultureInfo.CurrentCulture; var converted = converter.Convert(values, targetType, ConverterParameter, culture); - if (converted == BindingOperations.DoNothing) - { - return converted; - } - if (converted == AvaloniaProperty.UnsetValue) { converted = FallbackValue; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs new file mode 100644 index 0000000000..a77723afe1 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Data.Converters; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Converters +{ + public class MultiValueConverterTests : XamlTestBase + { + [Fact] + public void MultiValueConverter_Special_Values_Work() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + window.ApplyTemplate(); + + window.DataContext = Tuple.Create(2, 2); + Assert.Equal("foo", textBlock.Text); + + window.DataContext = Tuple.Create(-3, 3); + Assert.Equal("foo", textBlock.Text); + + window.DataContext = Tuple.Create(0, 2); + Assert.Equal("bar", textBlock.Text); + } + } + } + + public class TestMultiValueConverter : IMultiValueConverter + { + public static readonly TestMultiValueConverter Instance = new TestMultiValueConverter(); + + public object Convert(IList values, Type targetType, object parameter, CultureInfo culture) + { + if (values[0] is int i && values[1] is int j) + { + var p = i * j; + + if (p > 0) + { + return "foo"; + } + + if (p == 0) + { + return AvaloniaProperty.UnsetValue; + } + + return BindingOperations.DoNothing; + } + + return "(default)"; + } + } +} From 78866d54ed1ce78df089eee8fe1387c0ef0c989a Mon Sep 17 00:00:00 2001 From: "all.owing" Date: Wed, 21 Aug 2019 22:51:59 +0500 Subject: [PATCH 10/68] 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 11/68] 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 12/68] 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 13/68] 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 a7fec3afa64272250a20a23ce3146337681aa2e2 Mon Sep 17 00:00:00 2001 From: Yatao Li Date: Mon, 26 Aug 2019 17:00:38 +0800 Subject: [PATCH 14/68] add X11 keyboard symbol mappings --- src/Avalonia.X11/X11KeyTransform.cs | 57 ++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.X11/X11KeyTransform.cs b/src/Avalonia.X11/X11KeyTransform.cs index c68cb04733..87a4174c06 100644 --- a/src/Avalonia.X11/X11KeyTransform.cs +++ b/src/Avalonia.X11/X11KeyTransform.cs @@ -52,16 +52,6 @@ namespace Avalonia.X11 {X11Key.Delete, Key.Delete}, {X11Key.KP_Delete, Key.Delete}, {X11Key.Help, Key.Help}, - {X11Key.XK_0, Key.D0}, - {X11Key.XK_1, Key.D1}, - {X11Key.XK_2, Key.D2}, - {X11Key.XK_3, Key.D3}, - {X11Key.XK_4, Key.D4}, - {X11Key.XK_5, Key.D5}, - {X11Key.XK_6, Key.D6}, - {X11Key.XK_7, Key.D7}, - {X11Key.XK_8, Key.D8}, - {X11Key.XK_9, Key.D9}, {X11Key.A, Key.A}, {X11Key.B, Key.B}, {X11Key.C, Key.C}, @@ -114,8 +104,8 @@ namespace Avalonia.X11 {X11Key.x, Key.X}, {X11Key.y, Key.Y}, {X11Key.z, Key.Z}, - //{ X11Key.?, Key.LWin } - //{ X11Key.?, Key.RWin } + {X11Key.Meta_L, Key.LWin }, + {X11Key.Meta_R, Key.RWin }, {X11Key.Menu, Key.Apps}, //{ X11Key.?, Key.Sleep } {X11Key.KP_0, Key.NumPad0}, @@ -185,20 +175,51 @@ namespace Avalonia.X11 //{ X11Key.?, Key.SelectMedia } //{ X11Key.?, Key.LaunchApplication1 } //{ X11Key.?, Key.LaunchApplication2 } - {X11Key.semicolon, Key.OemSemicolon}, + {X11Key.minus, Key.OemMinus}, + {X11Key.underscore, Key.OemMinus}, {X11Key.plus, Key.OemPlus}, {X11Key.equal, Key.OemPlus}, + {X11Key.bracketleft, Key.OemOpenBrackets}, + {X11Key.braceleft, Key.OemOpenBrackets}, + {X11Key.bracketright, Key.OemCloseBrackets}, + {X11Key.braceright, Key.OemCloseBrackets}, + {X11Key.backslash, Key.OemPipe}, + {X11Key.bar, Key.OemPipe}, + {X11Key.semicolon, Key.OemSemicolon}, + {X11Key.colon, Key.OemSemicolon}, + {X11Key.apostrophe, Key.OemQuotes}, + {X11Key.quotedbl, Key.OemQuotes}, {X11Key.comma, Key.OemComma}, - {X11Key.minus, Key.OemMinus}, + {X11Key.less, Key.OemComma}, {X11Key.period, Key.OemPeriod}, + {X11Key.greater, Key.OemPeriod}, {X11Key.slash, Key.Oem2}, + {X11Key.question, Key.Oem2}, {X11Key.grave, Key.OemTilde}, + {X11Key.asciitilde, Key.OemTilde}, + {X11Key.XK_1, Key.D1}, + {X11Key.exclam, Key.D1}, + {X11Key.XK_2, Key.D2}, + {X11Key.at, Key.D2}, + {X11Key.XK_3, Key.D3}, + {X11Key.numbersign, Key.D3}, + {X11Key.XK_4, Key.D4}, + {X11Key.dollar, Key.D4}, + {X11Key.XK_5, Key.D5}, + {X11Key.percent, Key.D5}, + {X11Key.XK_6, Key.D6}, + {X11Key.asciicircum, Key.D6}, + {X11Key.XK_7, Key.D7}, + {X11Key.ampersand, Key.D7}, + {X11Key.XK_8, Key.D8}, + {X11Key.asterisk, Key.D8}, + {X11Key.XK_9, Key.D9}, + {X11Key.parenleft, Key.D9}, + {X11Key.XK_0, Key.D0}, + {X11Key.parenright, Key.D0}, + //{ X11Key.?, Key.AbntC1 } //{ X11Key.?, Key.AbntC2 } - {X11Key.bracketleft, Key.OemOpenBrackets}, - {X11Key.backslash, Key.OemPipe}, - {X11Key.bracketright, Key.OemCloseBrackets}, - {X11Key.apostrophe, Key.OemQuotes}, //{ X11Key.?, Key.Oem8 } //{ X11Key.?, Key.Oem102 } //{ X11Key.?, Key.ImeProcessed } From d720d32631d77815a3e9061f32749f0eecbaa15f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 26 Aug 2019 19:20:37 +0300 Subject: [PATCH 15/68] Android build workaround --- build/AndroidWorkarounds.props | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build/AndroidWorkarounds.props b/build/AndroidWorkarounds.props index 8a5c18e1ae..76334ecca5 100644 --- a/build/AndroidWorkarounds.props +++ b/build/AndroidWorkarounds.props @@ -5,4 +5,9 @@ + + + + + From 01e728fb42f267bc89548fd14d22da4b90891e7a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 26 Aug 2019 19:39:57 +0300 Subject: [PATCH 16/68] Another android workaround --- build/AndroidWorkarounds.props | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/AndroidWorkarounds.props b/build/AndroidWorkarounds.props index 76334ecca5..67947296b3 100644 --- a/build/AndroidWorkarounds.props +++ b/build/AndroidWorkarounds.props @@ -10,4 +10,7 @@ + + false + From 3335a06420c10701221ac61251ca76481775f94d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 26 Aug 2019 21:49:19 +0300 Subject: [PATCH 17/68] Updated XamlIl: [Flags] and TypeConverterAttribute for properties --- .../AvaloniaXamlIlTransformInstanceAttachedProperties.cs | 2 ++ src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs index bbacef43dd..548f0161d6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs @@ -164,6 +164,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public bool IsStatic => true; public string Name { get; protected set; } public IXamlIlType DeclaringType { get; } + public IXamlIlMethod MakeGenericMethod(IReadOnlyList typeArguments) + => throw new System.NotSupportedException(); public bool Equals(IXamlIlMethod other) => diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index c2ec091f79..d3b1f5bafb 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit c2ec091f79fb4e1eea629bc823c9c24da7050022 +Subproject commit d3b1f5bafbea93fb70343c76ca53271905976db8 From 0486de87a6b596a27ac643d55213439835509803 Mon Sep 17 00:00:00 2001 From: Ben Carman Date: Mon, 26 Aug 2019 14:48:12 -0500 Subject: [PATCH 18/68] 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(), From 0c94291f5e79077005d82a4aef4b5bef460a4f67 Mon Sep 17 00:00:00 2001 From: wieslawsoltes Date: Tue, 27 Aug 2019 13:18:49 +0000 Subject: [PATCH 19/68] Make Avalonia.Dialogs packable --- src/Avalonia.Dialogs/Avalonia.Dialogs.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj index 8b48b4a92c..3bfb254601 100644 --- a/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj +++ b/src/Avalonia.Dialogs/Avalonia.Dialogs.csproj @@ -1,7 +1,6 @@  netstandard2.0 - false From aa8773b394be283b1509f5f7284bca5240382123 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 27 Aug 2019 16:32:21 +0300 Subject: [PATCH 20/68] Updated XamlIl: MeScanner from System.Xaml --- src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index d3b1f5bafb..2bfb0ccadd 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit d3b1f5bafbea93fb70343c76ca53271905976db8 +Subproject commit 2bfb0ccaddcd311bc8948eec475d1a8744a8d645 From af4c1f06ad8f1be9ca2770d256478e28e1817cc8 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 27 Aug 2019 16:56:01 +0300 Subject: [PATCH 21/68] Extra tests for markup extensions --- .../Xaml/BindingTests.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index 7281542bc1..8681722e57 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs @@ -309,8 +309,12 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } - [Fact] - public void Binding_To_TextBlock_Text_With_StringConverter_Works() + [Theory, + InlineData(@"Hello \{0\}"), + InlineData(@"'Hello {0}'"), + InlineData(@"Hello {0}")] + + public void Binding_To_TextBlock_Text_With_StringConverter_Works(string fmt) { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -318,8 +322,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml - -"; + +"; var loader = new AvaloniaXamlLoader(); var window = (Window)loader.Load(xaml); var textBlock = window.FindControl("textBlock"); @@ -331,7 +335,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } - [Fact(Skip="Issue #2592")] + [Fact] public void MultiBinding_To_TextBlock_Text_With_StringConverter_Works() { using (UnitTestApplication.Start(TestServices.StyledWindow)) @@ -342,7 +346,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'> - + From 766a4d69c85b5937451fee97fa22afc126f34924 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 27 Aug 2019 17:00:46 +0300 Subject: [PATCH 22/68] Fixed escaped attribute values (e. g. StringFormat="\{0\}!") --- src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github | 2 +- tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index 2bfb0ccadd..c7155c5f6c 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit 2bfb0ccaddcd311bc8948eec475d1a8744a8d645 +Subproject commit c7155c5f6c1a5153ee2d8cd78e5d1524dd6744cf diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index 8681722e57..b1abc9ea54 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs @@ -335,8 +335,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } - [Fact] - public void MultiBinding_To_TextBlock_Text_With_StringConverter_Works() + [Theory, + InlineData("{}{0} {1}!"), + InlineData(@"\{0\} \{1\}!")] + public void MultiBinding_To_TextBlock_Text_With_StringConverter_Works(string fmt) { using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -346,7 +348,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'> - + From c64cc02ec68d5d5bdecc64e9001d2c09691cc43b Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Wed, 28 Aug 2019 14:06:00 +0800 Subject: [PATCH 23/68] Move transitions initializer to setter. --- src/Avalonia.Animation/Animatable.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 3a3d00b94a..89bcd2efa5 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -44,6 +44,10 @@ namespace Avalonia.Animation public Transitions Transitions { get + { + return _transitions; + } + set { if (_transitions == null) _transitions = new Transitions(); @@ -51,10 +55,6 @@ namespace Avalonia.Animation if (_previousTransitions == null) _previousTransitions = new Dictionary(); - return _transitions; - } - set - { SetAndRaise(TransitionsProperty, ref _transitions, value); } } From 87245c90b46684a01c0c9283a40af4180002eae4 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Wed, 28 Aug 2019 14:20:43 +0800 Subject: [PATCH 24/68] Fixes: part 2 of n. --- src/Avalonia.Animation/Animatable.cs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 89bcd2efa5..09991a5990 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -49,10 +49,10 @@ namespace Avalonia.Animation } set { - if (_transitions == null) - _transitions = new Transitions(); + if (value is null) + return; - if (_previousTransitions == null) + if (_previousTransitions is null) _previousTransitions = new Dictionary(); SetAndRaise(TransitionsProperty, ref _transitions, value); @@ -66,19 +66,18 @@ namespace Avalonia.Animation /// The event args. protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) { - if (e.Priority != BindingPriority.Animation && Transitions != null && _previousTransitions != null) - { - var match = Transitions.FirstOrDefault(x => x.Property == e.Property); + if (Transitions is null || e.Priority == BindingPriority.Animation) return; + + var match = Transitions.FirstOrDefault(x => x.Property == e.Property); - if (match != null) - { - if (_previousTransitions.TryGetValue(e.Property, out var dispose)) - dispose.Dispose(); + if (match != null) + { + if (_previousTransitions.TryGetValue(e.Property, out var dispose)) + dispose.Dispose(); - var instance = match.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue); + var instance = match.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue); - _previousTransitions[e.Property] = instance; - } + _previousTransitions[e.Property] = instance; } } } From b376313acff353872ce610d39997c9c553180ed4 Mon Sep 17 00:00:00 2001 From: ahopper Date: Wed, 28 Aug 2019 16:28:17 +0100 Subject: [PATCH 25/68] speed up GetValue and GetDefaultValue --- src/Avalonia.Base/AvaloniaObject.cs | 49 +++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 0e2f0feada..2fee277f21 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -208,20 +208,9 @@ namespace Avalonia { return ((IDirectPropertyAccessor)GetRegistered(property)).GetValue(this); } - else if (_values != null) - { - var result = Values.GetValue(property); - - if (result == AvaloniaProperty.UnsetValue) - { - result = GetDefaultValue(property); - } - - return result; - } else { - return GetDefaultValue(property); + return GetValueOrDefault(property); } } @@ -598,10 +587,44 @@ namespace Avalonia private object GetDefaultValue(AvaloniaProperty property) { if (property.Inherits && InheritanceParent is AvaloniaObject aobj) - return aobj.GetValue(property); + return aobj.GetValueOrDefault(property); return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType()); } + /// + /// Gets the value or default value for a property. + /// + /// The property. + /// The default value. + private object GetValueOrDefault(AvaloniaProperty property) + { + var aobj = this; + if (aobj.Values != null) + { + var result = aobj.Values.GetValue(property); + if (result != AvaloniaProperty.UnsetValue) + { + return result; + } + } + if (property.Inherits) + { + while(aobj.InheritanceParent is AvaloniaObject parent) + { + aobj = parent; + if (aobj.Values != null) + { + var result = aobj.Values.GetValue(property); + if (result != AvaloniaProperty.UnsetValue) + { + return result; + } + } + } + } + return ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); + } + /// /// Sets the value of a direct property. /// From ebccccb8e4c8a6138274eeb1e51769f93919c857 Mon Sep 17 00:00:00 2001 From: ahopper Date: Wed, 28 Aug 2019 17:37:01 +0100 Subject: [PATCH 26/68] add UnChecked suffix --- src/Avalonia.Base/AvaloniaObject.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 2fee277f21..5fa6dab29e 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -210,7 +210,7 @@ namespace Avalonia } else { - return GetValueOrDefault(property); + return GetValueOrDefaultUnChecked(property); } } @@ -596,7 +596,7 @@ namespace Avalonia /// /// The property. /// The default value. - private object GetValueOrDefault(AvaloniaProperty property) + private object GetValueOrDefaultUnChecked(AvaloniaProperty property) { var aobj = this; if (aobj.Values != null) From e8ba46160d3aad056a121b23b82f5290d8d360e6 Mon Sep 17 00:00:00 2001 From: ahopper Date: Wed, 28 Aug 2019 17:39:59 +0100 Subject: [PATCH 27/68] UnChecked suffix fixed --- src/Avalonia.Base/AvaloniaObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 5fa6dab29e..f8efb36562 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -587,7 +587,7 @@ namespace Avalonia private object GetDefaultValue(AvaloniaProperty property) { if (property.Inherits && InheritanceParent is AvaloniaObject aobj) - return aobj.GetValueOrDefault(property); + return aobj.GetValueOrDefaultUnChecked(property); return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType()); } From d176b1d7dc10e22137a21b7e2cf1836ec4cd2e27 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Thu, 29 Aug 2019 12:34:03 +0800 Subject: [PATCH 28/68] Fix unit tests. --- src/Avalonia.Animation/Animatable.cs | 4 ++-- tests/Avalonia.Animation.UnitTests/TransitionsTests.cs | 4 ++-- tests/Avalonia.LeakTests/TransitionTests.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 09991a5990..5ff3b17fb5 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -35,7 +35,7 @@ namespace Avalonia.Animation (o, v) => o.Transitions = v); private Transitions _transitions; - + private bool _isTransitionsSet = false; private Dictionary _previousTransitions; /// @@ -66,7 +66,7 @@ namespace Avalonia.Animation /// The event args. protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs e) { - if (Transitions is null || e.Priority == BindingPriority.Animation) return; + if (_transitions is null || _previousTransitions is null || e.Priority == BindingPriority.Animation) return; var match = Transitions.FirstOrDefault(x => x.Property == e.Property); diff --git a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs index f1b4b0d071..a8efc2c8ae 100644 --- a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs +++ b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs @@ -23,7 +23,7 @@ namespace Avalonia.Animation.UnitTests { var border = new Border { - Transitions = + Transitions = new Transitions { new DoubleTransition { @@ -51,7 +51,7 @@ namespace Avalonia.Animation.UnitTests { var border = new Border { - Transitions = + Transitions = new Transitions { new DoubleTransition { diff --git a/tests/Avalonia.LeakTests/TransitionTests.cs b/tests/Avalonia.LeakTests/TransitionTests.cs index c7add1fe11..699dec7229 100644 --- a/tests/Avalonia.LeakTests/TransitionTests.cs +++ b/tests/Avalonia.LeakTests/TransitionTests.cs @@ -27,7 +27,7 @@ namespace Avalonia.LeakTests { var border = new Border { - Transitions = + Transitions = new Transitions { new DoubleTransition { From 7ac47b7437749a00352f72503f56bd805aecf875 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 29 Aug 2019 23:44:04 +0200 Subject: [PATCH 29/68] Fix drag and drop not working on .net core 2.2+ --- samples/ControlCatalog.NetCore/Program.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 5aef0b5520..d683092edf 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -4,22 +4,16 @@ using System.Globalization; using System.Linq; using System.Threading; using Avalonia; -using Avalonia.Controls; -using Avalonia.LinuxFramebuffer.Output; -using Avalonia.Skia; using Avalonia.ReactiveUI; using Avalonia.Dialogs; -using System.Collections.Generic; -using System.Threading.Tasks; namespace ControlCatalog.NetCore { static class Program { - + [STAThread] static int Main(string[] args) { - Thread.CurrentThread.TrySetApartmentState(ApartmentState.STA); if (args.Contains("--wait-for-attach")) { Console.WriteLine("Attach debugger and use 'Set next statement'"); From addc1ddce2e4a2200953277517f54b92f941b3d0 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Fri, 30 Aug 2019 01:13:19 +0200 Subject: [PATCH 30/68] Avoid initializing properties if there is no observer. Optimize access. --- src/Avalonia.Base/AvaloniaProperty.cs | 5 ++ src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 74 ++++++++++++++----- .../AvaloniaObjectInitializationBenchmark.cs | 15 ++++ 3 files changed, 76 insertions(+), 18 deletions(-) create mode 100644 tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 1de5cb06c6..56ad241187 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -492,6 +492,11 @@ namespace Avalonia return Name; } + /// + /// True if has any observers. + /// + internal bool HasNotifyInitializedObservers => _initialized.HasObservers; + /// /// Notifies the observable. /// diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 037e0dd72e..88b0201fcb 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -24,8 +24,8 @@ namespace Avalonia new Dictionary>(); private readonly Dictionary> _attachedCache = new Dictionary>(); - private readonly Dictionary>> _initializedCache = - new Dictionary>>(); + private readonly Dictionary> _initializedCache = + new Dictionary>(); /// /// Gets the instance @@ -286,35 +286,73 @@ namespace Avalonia property.NotifyInitialized(e); } - if (!_initializedCache.TryGetValue(type, out var items)) + if (!_initializedCache.TryGetValue(type, out var initializationData)) { - var build = new Dictionary(); + var visited = new HashSet(); - foreach (var property in GetRegistered(type)) + initializationData = new List(); + + foreach (AvaloniaProperty property in GetRegistered(type)) { - var value = !property.IsDirect ? - ((IStyledPropertyAccessor)property).GetDefaultValue(type) : - null; - build.Add(property, value); + if (property.IsDirect) + { + initializationData.Add(new PropertyInitializationData(property, (IDirectPropertyAccessor)property)); + } + else + { + initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type)); + } + + visited.Add(property); } - foreach (var property in GetRegisteredAttached(type)) + foreach (AvaloniaProperty property in GetRegisteredAttached(type)) { - if (!build.ContainsKey(property)) + if (!visited.Contains(property)) { - var value = ((IStyledPropertyAccessor)property).GetDefaultValue(type); - build.Add(property, value); + initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type)); + + visited.Add(property); } } - items = build.ToList(); - _initializedCache.Add(type, items); + _initializedCache.Add(type, initializationData); + } + + foreach (PropertyInitializationData data in initializationData) + { + if (!data.Property.HasNotifyInitializedObservers) + { + continue; + } + + object value = data.IsDirect ? data.DirectAccessor.GetValue(o) : data.Value; + + Notify(data.Property, value); + } + } + + private readonly struct PropertyInitializationData + { + public AvaloniaProperty Property { get; } + public object Value { get; } + public bool IsDirect { get; } + public IDirectPropertyAccessor DirectAccessor { get; } + + public PropertyInitializationData(AvaloniaProperty property, IDirectPropertyAccessor directAccessor) + { + Property = property; + Value = null; + IsDirect = true; + DirectAccessor = directAccessor; } - foreach (var i in items) + public PropertyInitializationData(AvaloniaProperty property, IStyledPropertyAccessor styledAccessor, Type type) { - var value = i.Key.IsDirect ? o.GetValue(i.Key) : i.Value; - Notify(i.Key, value); + Property = property; + Value = styledAccessor.GetDefaultValue(type); + IsDirect = false; + DirectAccessor = null; } } } diff --git a/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs b/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs new file mode 100644 index 0000000000..06716f7102 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs @@ -0,0 +1,15 @@ +using Avalonia.Controls; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Base +{ + [MemoryDiagnoser] + public class AvaloniaObjectInitializationBenchmark + { + [Benchmark(OperationsPerInvoke = 1000)] + public Button InitializeButton() + { + return new Button(); + } + } +} From 77d9ae1cacfee35930c0543643c34a167ffe3b87 Mon Sep 17 00:00:00 2001 From: ahopper Date: Fri, 30 Aug 2019 10:18:30 +0100 Subject: [PATCH 31/68] change UnChecked suffix to Unchecked --- src/Avalonia.Base/AvaloniaObject.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index f8efb36562..48a222d5b4 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -210,7 +210,7 @@ namespace Avalonia } else { - return GetValueOrDefaultUnChecked(property); + return GetValueOrDefaultUnchecked(property); } } @@ -587,7 +587,7 @@ namespace Avalonia private object GetDefaultValue(AvaloniaProperty property) { if (property.Inherits && InheritanceParent is AvaloniaObject aobj) - return aobj.GetValueOrDefaultUnChecked(property); + return aobj.GetValueOrDefaultUnchecked(property); return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType()); } @@ -596,7 +596,7 @@ namespace Avalonia /// /// The property. /// The default value. - private object GetValueOrDefaultUnChecked(AvaloniaProperty property) + private object GetValueOrDefaultUnchecked(AvaloniaProperty property) { var aobj = this; if (aobj.Values != null) From 4d8973226d10ed188181367d5b522f0e2fd99bc7 Mon Sep 17 00:00:00 2001 From: ahopper Date: Fri, 30 Aug 2019 10:35:24 +0100 Subject: [PATCH 32/68] unbreak lazy initialization of Values --- src/Avalonia.Base/AvaloniaObject.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 48a222d5b4..035e0056e0 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -599,9 +599,9 @@ namespace Avalonia private object GetValueOrDefaultUnchecked(AvaloniaProperty property) { var aobj = this; - if (aobj.Values != null) + if (aobj._values != null) { - var result = aobj.Values.GetValue(property); + var result = aobj._values.GetValue(property); if (result != AvaloniaProperty.UnsetValue) { return result; @@ -612,9 +612,9 @@ namespace Avalonia while(aobj.InheritanceParent is AvaloniaObject parent) { aobj = parent; - if (aobj.Values != null) + if (aobj._values != null) { - var result = aobj.Values.GetValue(property); + var result = aobj._values.GetValue(property); if (result != AvaloniaProperty.UnsetValue) { return result; From a1ee8d98eb4231ac29449939a028bd813225bd21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Komosi=C5=84ski?= Date: Fri, 30 Aug 2019 15:10:53 +0200 Subject: [PATCH 33/68] "Fix" TreeView SelectedItems causing side effects and resetting selection. --- src/Avalonia.Controls/TreeView.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 4514109e12..eb68ba647e 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -105,11 +105,13 @@ namespace Avalonia.Controls get => _selectedItem; set { + var selectedItems = SelectedItems; + SetAndRaise(SelectedItemProperty, ref _selectedItem, value); if (value != null) { - if (SelectedItems.Count != 1 || SelectedItems[0] != value) + if (selectedItems.Count != 1 || selectedItems[0] != value) { _syncingSelectedItems = true; SelectSingleItem(value); From 02ebded6a227481bde5347d5fc49ae1e2b6cde0d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 31 Aug 2019 18:25:23 +0200 Subject: [PATCH 34/68] Don't bring hidden controls into view. --- src/Avalonia.Controls/ControlExtensions.cs | 15 +++++++++------ .../Presenters/ScrollContentPresenter.cs | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/ControlExtensions.cs b/src/Avalonia.Controls/ControlExtensions.cs index 2fccb15acc..3bd222d132 100644 --- a/src/Avalonia.Controls/ControlExtensions.cs +++ b/src/Avalonia.Controls/ControlExtensions.cs @@ -34,14 +34,17 @@ namespace Avalonia.Controls { Contract.Requires(control != null); - var ev = new RequestBringIntoViewEventArgs + if (control.IsEffectivelyVisible) { - RoutedEvent = Control.RequestBringIntoViewEvent, - TargetObject = control, - TargetRect = rect, - }; + var ev = new RequestBringIntoViewEventArgs + { + RoutedEvent = Control.RequestBringIntoViewEvent, + TargetObject = control, + TargetRect = rect, + }; - control.RaiseEvent(ev); + control.RaiseEvent(ev); + } } /// diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index e7d8018a42..ec6a228421 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -141,7 +141,7 @@ namespace Avalonia.Controls.Presenters /// True if the scroll offset was changed; otherwise false. public bool BringDescendantIntoView(IVisual target, Rect targetRect) { - if (Child == null) + if (Child?.IsEffectivelyVisible != true) { return false; } From 394b8f77b70a7a0d4b92d0e961a0fc53f7b7ff33 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 31 Aug 2019 22:37:22 +0200 Subject: [PATCH 35/68] Get rid of allocations in Rect.TransformToAABB. --- src/Avalonia.Visuals/Rect.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Visuals/Rect.cs b/src/Avalonia.Visuals/Rect.cs index 49d4724b9a..8f08f7f51f 100644 --- a/src/Avalonia.Visuals/Rect.cs +++ b/src/Avalonia.Visuals/Rect.cs @@ -371,12 +371,12 @@ namespace Avalonia /// The bounding box public Rect TransformToAABB(Matrix matrix) { - var points = new[] + ReadOnlySpan points = stackalloc Point[4] { TopLeft.Transform(matrix), TopRight.Transform(matrix), BottomRight.Transform(matrix), - BottomLeft.Transform(matrix), + BottomLeft.Transform(matrix) }; var left = double.MaxValue; From a9da85e4aed730e286c3b07a6288f525e8556204 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 1 Sep 2019 12:49:30 +0200 Subject: [PATCH 36/68] Avoid per-frame allocations in renderer lock. --- .../Rendering/ManagedDeferredRendererLock.cs | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs index 2d4a39e026..1295961a1b 100644 --- a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs +++ b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs @@ -1,5 +1,4 @@ using System; -using System.Reactive.Disposables; using System.Threading; namespace Avalonia.Rendering @@ -7,7 +6,13 @@ namespace Avalonia.Rendering public class ManagedDeferredRendererLock : IDeferredRendererLock { private readonly object _lock = new object(); - + private readonly LockDisposable _lockDisposable; + + public ManagedDeferredRendererLock() + { + _lockDisposable = new LockDisposable(_lock); + } + /// /// Tries to lock the target surface or window /// @@ -15,7 +20,7 @@ namespace Avalonia.Rendering public IDisposable TryLock() { if (Monitor.TryEnter(_lock)) - return Disposable.Create(() => Monitor.Exit(_lock)); + return _lockDisposable; return null; } @@ -25,7 +30,22 @@ namespace Avalonia.Rendering public IDisposable Lock() { Monitor.Enter(_lock); - return Disposable.Create(() => Monitor.Exit(_lock)); + return _lockDisposable; + } + + private class LockDisposable : IDisposable + { + private readonly object _lock; + + public LockDisposable(object @lock) + { + _lock = @lock; + } + + public void Dispose() + { + Monitor.Exit(_lock); + } } } } From 26b1320971d3e7b8f75e0a7f5b4a2f84f71d9fb0 Mon Sep 17 00:00:00 2001 From: ahopper Date: Sun, 1 Sep 2019 19:39:18 +0100 Subject: [PATCH 37/68] reduce repeated field access --- src/Avalonia.Base/AvaloniaObject.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 035e0056e0..c619d80e23 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -599,9 +599,10 @@ namespace Avalonia private object GetValueOrDefaultUnchecked(AvaloniaProperty property) { var aobj = this; - if (aobj._values != null) + var valuestore = aobj._values; + if (valuestore != null) { - var result = aobj._values.GetValue(property); + var result = valuestore.GetValue(property); if (result != AvaloniaProperty.UnsetValue) { return result; @@ -612,9 +613,10 @@ namespace Avalonia while(aobj.InheritanceParent is AvaloniaObject parent) { aobj = parent; - if (aobj._values != null) + valuestore = aobj._values; + if (valuestore != null) { - var result = aobj._values.GetValue(property); + var result = valuestore.GetValue(property); if (result != AvaloniaProperty.UnsetValue) { return result; From 112484d7e40c1b85ab41d77ae21d866d2dfdac7c Mon Sep 17 00:00:00 2001 From: ahopper Date: Mon, 2 Sep 2019 11:45:15 +0100 Subject: [PATCH 38/68] reduce allocation, cache Inherited props,remove duplicate events --- src/Avalonia.Base/AvaloniaObject.cs | 37 ++++++++++------ src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 44 +++++++++++++++++++ 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index c619d80e23..34b209473b 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -82,6 +82,7 @@ namespace Avalonia set { + VerifyAccess(); if (_inheritanceParent != value) { if (_inheritanceParent != null) @@ -89,25 +90,33 @@ namespace Avalonia _inheritanceParent.InheritablePropertyChanged -= ParentPropertyChanged; } - var properties = AvaloniaPropertyRegistry.Instance.GetRegistered(this) - .Concat(AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(this.GetType())); - var inherited = (from property in properties - where property.Inherits - select new - { - Property = property, - Value = GetValue(property), - }).ToList(); - + var oldInheritanceParent = _inheritanceParent; _inheritanceParent = value; + var valuestore = _values; - foreach (var i in inherited) + foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType())) { - object newValue = GetValue(i.Property); + if (valuestore != null && valuestore.GetValue(property) != AvaloniaProperty.UnsetValue) + { + // if local value set there can be no change + continue; + } + // get the value as it would have been with the previous InheritanceParent + object oldValue; + if (oldInheritanceParent is AvaloniaObject aobj) + { + oldValue = aobj.GetValueOrDefaultUnchecked(property); + } + else + { + oldValue = ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); + } + + object newValue = GetDefaultValue(property); - if (!Equals(i.Value, newValue)) + if (!Equals(oldValue, newValue)) { - RaisePropertyChanged(i.Property, i.Value, newValue, BindingPriority.LocalValue); + RaisePropertyChanged(property, oldValue, newValue, BindingPriority.LocalValue); } } diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 88b0201fcb..d718f5917c 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -26,6 +26,8 @@ namespace Avalonia new Dictionary>(); private readonly Dictionary> _initializedCache = new Dictionary>(); + private readonly Dictionary> _inheritedCache = + new Dictionary>(); /// /// Gets the instance @@ -103,6 +105,46 @@ namespace Avalonia return result; } + /// + /// Gets all inherited s registered on a type. + /// + /// The type. + /// A collection of definitions. + public IEnumerable GetRegisteredInherited(Type type) + { + Contract.Requires(type != null); + + if (_inheritedCache.TryGetValue(type, out var result)) + { + return result; + } + + result = new List(); + var visited = new HashSet(); + + foreach (var property in GetRegistered(type)) + { + if (property.Inherits) + { + result.Add(property); + visited.Add(property); + } + } + foreach (var property in GetRegisteredAttached(type)) + { + if (property.Inherits) + { + if (!visited.Contains(property)) + { + result.Add(property); + } + } + } + + _inheritedCache.Add(type, result); + return result; + } + /// /// Gets all s registered on a object. /// @@ -230,6 +272,7 @@ namespace Avalonia _registeredCache.Clear(); _initializedCache.Clear(); + _inheritedCache.Clear(); } /// @@ -266,6 +309,7 @@ namespace Avalonia _attachedCache.Clear(); _initializedCache.Clear(); + _inheritedCache.Clear(); } internal void NotifyInitialized(AvaloniaObject o) From 8ebe1c1288544bbf31426ba7b253b5c79c4acc7b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 3 Sep 2019 14:49:14 +0200 Subject: [PATCH 39/68] Added failing test for #2901. After the selected item is removed from a `ListBox` with `AlwaysSelected == true`, the container for the newly selected item does not get `:selected` applied. --- .../SelectingItemsControlTests_AutoSelect.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs index 72f2b8022f..a7010c521b 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs @@ -102,6 +102,25 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Null(target.SelectedItem); } + [Fact] + public void Removing_Selected_First_Item_Should_Select_Next_Item() + { + var items = new AvaloniaList(new[] { "foo", "bar" }); + var target = new TestSelector + { + Items = items, + Template = Template(), + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + items.RemoveAt(0); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal("bar", target.SelectedItem); + Assert.Equal(new[] { ":selected" }, target.Presenter.Panel.Children[0].Classes); + } + private FuncControlTemplate Template() { return new FuncControlTemplate((control, scope) => From 5de0bf5fecbc52cb2c8b56323df9b2a7113e568d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 3 Sep 2019 14:54:08 +0200 Subject: [PATCH 40/68] Fix AlwaysSelected selected state. Fixes #2901. Two things needed to be done here: - When an item is removed, causing indexes to be reassigned, raise `Recycled` so that `SelectingItemsControl` knows to update the selection state - Update selection state in `SelectingItemsControl` when the selected item changes, but the selected index does not (due to an item being remove) --- src/Avalonia.Controls/Generators/ItemContainerGenerator.cs | 6 ++++++ src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index 4fd6f4135c..8d1d69db1c 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -128,6 +128,12 @@ namespace Avalonia.Controls.Generators } Dematerialized?.Invoke(this, new ItemContainerEventArgs(startingIndex, result)); + + if (toMove.Count > 0) + { + var containers = toMove.Select(x => x.Value).ToList(); + Recycled?.Invoke(this, new ItemContainerEventArgs(containers[0].Index, containers)); + } } return result; diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index c8c15bc079..44ae89fdbc 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -979,13 +979,14 @@ namespace Avalonia.Controls.Primitives } var item = ElementAt(Items, index); + var itemChanged = !Equals(item, oldItem); var added = -1; HashSet removed = null; _selectedIndex = index; _selectedItem = item; - if (oldIndex != index || _selection.HasMultiple) + if (oldIndex != index || itemChanged || _selection.HasMultiple) { if (clear) { @@ -1022,7 +1023,7 @@ namespace Avalonia.Controls.Primitives index); } - if (!Equals(item, oldItem)) + if (itemChanged) { RaisePropertyChanged( SelectedItemProperty, From bd354143caba5dca2ad0878989803edac2caa5c2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 3 Sep 2019 18:04:48 +0200 Subject: [PATCH 41/68] Notify MouseDevice when TopLevel closed. So it can remove pointerover state. --- src/Avalonia.Controls/TopLevel.cs | 2 +- src/Avalonia.Input/IMouseDevice.cs | 2 ++ src/Avalonia.Input/MouseDevice.cs | 5 +++++ .../TopLevelTests.cs | 18 ++++++++++++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 87100ceeb0..e0acab1133 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -269,8 +269,8 @@ namespace Avalonia.Controls /// protected virtual void HandleClosed() { + (this as IInputRoot).MouseDevice?.TopLevelClosed(this); PlatformImpl = null; - Closed?.Invoke(this, EventArgs.Empty); Renderer?.Dispose(); Renderer = null; diff --git a/src/Avalonia.Input/IMouseDevice.cs b/src/Avalonia.Input/IMouseDevice.cs index 7e6bf657ae..a641544f7a 100644 --- a/src/Avalonia.Input/IMouseDevice.cs +++ b/src/Avalonia.Input/IMouseDevice.cs @@ -16,6 +16,8 @@ namespace Avalonia.Input [Obsolete("Use PointerEventArgs.GetPosition")] PixelPoint Position { get; } + void TopLevelClosed(IInputRoot root); + void SceneInvalidated(IInputRoot root, Rect rect); } } diff --git a/src/Avalonia.Input/MouseDevice.cs b/src/Avalonia.Input/MouseDevice.cs index d5152f58d5..0d5471f790 100644 --- a/src/Avalonia.Input/MouseDevice.cs +++ b/src/Avalonia.Input/MouseDevice.cs @@ -86,6 +86,11 @@ namespace Avalonia.Input ProcessRawEvent(margs); } + public void TopLevelClosed(IInputRoot root) + { + ClearPointerOver(this, 0, root, PointerPointProperties.None, KeyModifiers.None); + } + public void SceneInvalidated(IInputRoot root, Rect rect) { var clientPoint = root.PointToClient(Position); diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index c744543f99..901d780f16 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -224,6 +224,24 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Close_Should_Notify_MouseDevice() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var impl = new Mock(); + var mouseDevice = new Mock(); + impl.SetupAllProperties(); + impl.Setup(x => x.MouseDevice).Returns(mouseDevice.Object); + + var target = new TestTopLevel(impl.Object); + + impl.Object.Closed(); + + mouseDevice.Verify(x => x.TopLevelClosed(target)); + } + } + private FuncControlTemplate CreateTemplate() { return new FuncControlTemplate((x, scope) => From c619d1f0d4ceb92cb8bfe6d6bad2f4bceb8954a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Komosi=C5=84ski?= Date: Wed, 4 Sep 2019 13:55:33 +0200 Subject: [PATCH 42/68] Cache delegate used for hit testing. --- src/Avalonia.Input/InputExtensions.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Input/InputExtensions.cs b/src/Avalonia.Input/InputExtensions.cs index c1d0729560..f83c41e266 100644 --- a/src/Avalonia.Input/InputExtensions.cs +++ b/src/Avalonia.Input/InputExtensions.cs @@ -13,6 +13,8 @@ namespace Avalonia.Input /// public static class InputExtensions { + private static readonly Func s_hitTestDelegate = IsHitTestVisible; + /// /// Returns the active input elements at a point on an . /// @@ -25,7 +27,7 @@ namespace Avalonia.Input { Contract.Requires(element != null); - return element.GetVisualsAt(p, IsHitTestVisible).Cast(); + return element.GetVisualsAt(p, s_hitTestDelegate).Cast(); } /// From 8cb5eedcda7e06f76bc0be81dd4cd17f154033a8 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 5 Sep 2019 16:14:13 +0300 Subject: [PATCH 43/68] Use [NSApp run] instead of a custom run loop --- native/Avalonia.Native/inc/avalonia-native.h | 2 +- native/Avalonia.Native/src/OSX/app.mm | 24 ++++++++ native/Avalonia.Native/src/OSX/common.h | 2 +- native/Avalonia.Native/src/OSX/main.mm | 3 +- .../src/OSX/platformthreading.mm | 59 +++++++++++-------- 5 files changed, 61 insertions(+), 29 deletions(-) create mode 100644 native/Avalonia.Native/src/OSX/app.mm diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index a35f4f3eeb..36e16c24d1 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -280,7 +280,7 @@ AVNCOM(IAvnPlatformThreadingInterface, 0b) : IUnknown virtual bool GetCurrentThreadIsLoopThread() = 0; virtual void SetSignaledCallback(IAvnSignaledCallback* cb) = 0; virtual IAvnLoopCancellation* CreateLoopCancellation() = 0; - virtual void RunLoop(IAvnLoopCancellation* cancel) = 0; + virtual HRESULT RunLoop(IAvnLoopCancellation* cancel) = 0; // Can't pass int* to sharpgentools for some reason virtual void Signal(int priority) = 0; virtual IUnknown* StartTimer(int priority, int ms, IAvnActionCallback* callback) = 0; diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm new file mode 100644 index 0000000000..7d45720a8b --- /dev/null +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -0,0 +1,24 @@ +#include "common.h" +@interface AvnAppDelegate : NSObject +@end + +@implementation AvnAppDelegate +- (void)applicationWillFinishLaunching:(NSNotification *)notification +{ + +} + +- (void)applicationDidFinishLaunching:(NSNotification *)notification +{ + [NSApp activateIgnoringOtherApps:true]; +} + +@end + +extern void InitializeAvnApp() +{ + NSApplication* app = [NSApplication sharedApplication]; + id delegate = [AvnAppDelegate new]; + [app setDelegate:delegate]; + +} diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index f748dd59ad..a716af1e2c 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -19,7 +19,7 @@ extern IAvnClipboard* CreateClipboard(); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlFeature* GetGlFeature(); extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* view); - +extern void InitializeAvnApp(); extern NSPoint ToNSPoint (AvnPoint p); extern AvnPoint ToAvnPoint (NSPoint p); extern AvnPoint ConvertPointY (AvnPoint p); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 5043246c53..fec5b9b662 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -64,8 +64,9 @@ public: { @autoreleasepool{ [[ThreadingInitializer new] do]; - return S_OK; } + InitializeAvnApp(); + return S_OK; }; virtual IAvnMacOptions* GetMacOptions() override diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index 0c814b2088..297097584a 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -57,16 +57,36 @@ class PlatformThreadingInterface : public ComSingleObject { public: FORWARD_IUNKNOWN() - bool Cancelled = 0; - virtual void Cancel() override + bool Running = false; + bool Cancelled = false; + virtual void Cancel() { - Cancelled = 1; + Cancelled = true; + if(Running) + { + Running = false; + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSApplication sharedApplication] stop:nil]; + NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined + location:NSMakePoint(0, 0) + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + subtype:0 + data1:0 + data2:0]; + [NSApp postEvent:event atStart:YES]; + }); + } } + }; public: @@ -99,30 +119,17 @@ public: return new LoopCancellation(); } - virtual void RunLoop(IAvnLoopCancellation* cancel) override + virtual HRESULT RunLoop(IAvnLoopCancellation* cancel) override { - @autoreleasepool { - auto can = dynamic_cast(cancel); - [[NSApplication sharedApplication] activateIgnoringOtherApps:true]; - while(true) - { - @autoreleasepool - { - if(can != NULL && can->Cancelled) - return; - NSEvent* ev = [[NSApplication sharedApplication] - nextEventMatchingMask:NSEventMaskAny - untilDate: [NSDate dateWithTimeIntervalSinceNow:1] - inMode:NSDefaultRunLoopMode - dequeue:true]; - if(can != NULL && can->Cancelled) - return; - if(ev != NULL) - [[NSApplication sharedApplication] sendEvent:ev]; - } - } - NSDebugLog(@"RunLoop exited"); - } + auto can = dynamic_cast(cancel); + if(can->Cancelled) + return S_OK; + if(_wasRunningAtLeastOnce) + return E_FAIL; + can->Running = true; + _wasRunningAtLeastOnce = true; + [NSApp run]; + return S_OK; } virtual void Signal(int priority) override From 349ffa16002016e346f6a966b170314e78486353 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 5 Sep 2019 17:01:11 +0300 Subject: [PATCH 44/68] Set activation policy from applicationWillFinishLaunching --- .../OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj | 4 ++++ native/Avalonia.Native/src/OSX/app.mm | 4 ++-- native/Avalonia.Native/src/OSX/common.h | 2 +- native/Avalonia.Native/src/OSX/main.mm | 11 ++--------- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj index cc74d5669f..1870ef7ab3 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 1A002B9E232135EE00021753 /* app.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A002B9D232135EE00021753 /* app.mm */; }; 37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; }; 37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; }; 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37DDA9AF219330F8002E132B /* AvnString.mm */; }; @@ -22,6 +23,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 1A002B9D232135EE00021753 /* app.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = app.mm; sourceTree = ""; }; 379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = ""; }; 37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = ""; }; 37A517B22159597E00FBA241 /* Screens.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = Screens.mm; sourceTree = ""; }; @@ -68,6 +70,7 @@ AB7A61E62147C814003C5833 = { isa = PBXGroup; children = ( + 1A002B9D232135EE00021753 /* app.mm */, 37DDA9B121933371002E132B /* AvnString.h */, 37DDA9AF219330F8002E132B /* AvnString.mm */, 37A4E71A2178846A00EACBCD /* headers */, @@ -164,6 +167,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 1A002B9E232135EE00021753 /* app.mm in Sources */, 5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */, 5B21A982216530F500CEE36E /* cursor.mm in Sources */, 37DDA9B0219330F8002E132B /* AvnString.mm in Sources */, diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 7d45720a8b..81855995b7 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -1,11 +1,11 @@ #include "common.h" @interface AvnAppDelegate : NSObject @end - +extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular; @implementation AvnAppDelegate - (void)applicationWillFinishLaunching:(NSNotification *)notification { - + [[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy]; } - (void)applicationDidFinishLaunching:(NSNotification *)notification diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index a716af1e2c..45ec40c361 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -20,11 +20,11 @@ extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlFeature* GetGlFeature(); extern IAvnGlSurfaceRenderTarget* CreateGlRenderTarget(NSWindow* window, NSView* view); extern void InitializeAvnApp(); +extern NSApplicationActivationPolicy AvnDesiredActivationPolicy; extern NSPoint ToNSPoint (AvnPoint p); extern AvnPoint ToAvnPoint (NSPoint p); extern AvnPoint ConvertPointY (AvnPoint p); extern NSSize ToNSSize (AvnSize s); - #ifdef DEBUG #define NSDebugLog(...) NSLog(__VA_ARGS__) #else diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index fec5b9b662..70bd1e67f6 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -5,21 +5,14 @@ #define COM_GUIDS_MATERIALIZE #include "common.h" -static BOOL ShowInDock = 1; - -static void SetActivationPolicy() -{ - [[NSApplication sharedApplication] setActivationPolicy: (ShowInDock ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory)]; -} - class MacOptions : public ComSingleObject { public: FORWARD_IUNKNOWN() virtual HRESULT SetShowInDock(int show) override { - ShowInDock = show; - SetActivationPolicy(); + AvnDesiredActivationPolicy = show + ? NSApplicationActivationPolicyRegular : NSApplicationActivationPolicyAccessory; return S_OK; } }; From 54399d2a51eac2c1664779c2f546b428ed1c2c5c Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 5 Sep 2019 22:53:48 +0200 Subject: [PATCH 45/68] Use ValueTuple instead of Tuple to reduce alocations. Invoke raise callback without reflection. --- src/Avalonia.Interactivity/RoutedEvent.cs | 50 ++++++----------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/src/Avalonia.Interactivity/RoutedEvent.cs b/src/Avalonia.Interactivity/RoutedEvent.cs index 2d752133c1..cfbaddb327 100644 --- a/src/Avalonia.Interactivity/RoutedEvent.cs +++ b/src/Avalonia.Interactivity/RoutedEvent.cs @@ -4,7 +4,6 @@ using System; using System.Reactive.Subjects; using System.Reflection; -using System.Runtime.ExceptionServices; namespace Avalonia.Interactivity { @@ -18,8 +17,8 @@ namespace Avalonia.Interactivity public class RoutedEvent { - private Subject> _raised = new Subject>(); - private Subject _routeFinished = new Subject(); + private readonly Subject<(object, RoutedEventArgs)> _raised = new Subject<(object, RoutedEventArgs)>(); + private readonly Subject _routeFinished = new Subject(); public RoutedEvent( string name, @@ -38,31 +37,15 @@ namespace Avalonia.Interactivity RoutingStrategies = routingStrategies; } - public Type EventArgsType - { - get; - private set; - } + public Type EventArgsType { get; } - public string Name - { - get; - private set; - } + public string Name { get; } - public Type OwnerType - { - get; - private set; - } + public Type OwnerType { get; } - public RoutingStrategies RoutingStrategies - { - get; - private set; - } + public RoutingStrategies RoutingStrategies { get; } - public IObservable> Raised => _raised; + public IObservable<(object, RoutedEventArgs)> Raised => _raised; public IObservable RouteFinished => _routeFinished; public static RoutedEvent Register( @@ -98,29 +81,20 @@ namespace Avalonia.Interactivity { return Raised.Subscribe(args => { - var sender = args.Item1; - var e = args.Item2; + (object sender, RoutedEventArgs e) = args; - if (targetType.GetTypeInfo().IsAssignableFrom(sender.GetType().GetTypeInfo()) && - ((e.Route == RoutingStrategies.Direct) || (e.Route & routes) != 0) && + if (targetType.IsInstanceOfType(sender) && + (e.Route == RoutingStrategies.Direct || (e.Route & routes) != 0) && (!e.Handled || handledEventsToo)) { - try - { - handler.DynamicInvoke(sender, e); - } - catch (TargetInvocationException ex) - { - // Unwrap the inner exception. - ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); - } + handler(sender, e); } }); } internal void InvokeRaised(object sender, RoutedEventArgs e) { - _raised.OnNext(Tuple.Create(sender, e)); + _raised.OnNext((sender, e)); } internal void InvokeRouteFinished(RoutedEventArgs e) From 50a8fc3571d6d911ccbca80ec4bc504c2385e943 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 5 Sep 2019 23:28:33 +0200 Subject: [PATCH 46/68] Get rid of pointless allocations in PriorityBindingEntry. --- src/Avalonia.Base/PriorityBindingEntry.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/PriorityBindingEntry.cs b/src/Avalonia.Base/PriorityBindingEntry.cs index d4a47306a7..95add0dfac 100644 --- a/src/Avalonia.Base/PriorityBindingEntry.cs +++ b/src/Avalonia.Base/PriorityBindingEntry.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Runtime.ExceptionServices; using Avalonia.Data; using Avalonia.Threading; @@ -10,9 +11,9 @@ namespace Avalonia /// /// A registered binding in a . /// - internal class PriorityBindingEntry : IDisposable + internal class PriorityBindingEntry : IDisposable, IObserver { - private PriorityLevel _owner; + private readonly PriorityLevel _owner; private IDisposable _subscription; /// @@ -85,7 +86,7 @@ namespace Avalonia Description = ((IDescription)binding).Description; } - _subscription = binding.Subscribe(ValueChanged, Completed); + _subscription = binding.Subscribe(this); } /// @@ -96,7 +97,7 @@ namespace Avalonia _subscription?.Dispose(); } - private void ValueChanged(object value) + void IObserver.OnNext(object value) { void Signal() { @@ -132,7 +133,7 @@ namespace Avalonia } } - private void Completed() + void IObserver.OnCompleted() { HasCompleted = true; @@ -145,5 +146,10 @@ namespace Avalonia Dispatcher.UIThread.Post(() => _owner.Completed(this)); } } + + void IObserver.OnError(Exception error) + { + ExceptionDispatchInfo.Capture(error).Throw(); + } } } From 11298df527718bc4466e08d51b3b70e4a2c10e58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Komosi=C5=84ski?= Date: Fri, 6 Sep 2019 14:26:19 +0200 Subject: [PATCH 47/68] Allow for setting up deferred setter notify handlers without extra allocations. --- src/Avalonia.Base/PriorityValue.cs | 14 +++++---- src/Avalonia.Base/Utilities/DeferredSetter.cs | 30 +++++++++++-------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index 4996420fe7..2871271062 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -24,13 +24,11 @@ namespace Avalonia /// method on the /// owner object is fired with the old and new values. /// - internal class PriorityValue + internal sealed class PriorityValue : ISetAndNotifyHandler<(object,int)> { private readonly Type _valueType; private readonly SingleOrDictionary _levels = new SingleOrDictionary(); - private readonly Func _validate; - private readonly SetAndNotifyCallback<(object, int)> _setAndNotifyCallback; private (object value, int priority) _value; private DeferredSetter _setter; @@ -52,7 +50,6 @@ namespace Avalonia _valueType = valueType; _value = (AvaloniaProperty.UnsetValue, int.MaxValue); _validate = validate; - _setAndNotifyCallback = SetAndNotify; } /// @@ -257,10 +254,15 @@ namespace Avalonia _setter = Owner.GetNonDirectDeferredSetter(Property); } - _setter.SetAndNotifyCallback(Property, _setAndNotifyCallback, ref _value, newValue); + _setter.SetAndNotifyCallback(Property, this, ref _value, newValue); + } + + void ISetAndNotifyHandler<(object, int)>.HandleSetAndNotify(AvaloniaProperty property, ref (object, int) backing, (object, int) value) + { + SetAndNotify(ref backing, value); } - private void SetAndNotify(AvaloniaProperty property, ref (object value, int priority) backing, (object value, int priority) update) + private void SetAndNotify(ref (object value, int priority) backing, (object value, int priority) update) { var val = update.value; var notification = val as BindingNotification; diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs index fd7a66fb52..fe9b0e58a0 100644 --- a/src/Avalonia.Base/Utilities/DeferredSetter.cs +++ b/src/Avalonia.Base/Utilities/DeferredSetter.cs @@ -5,15 +5,6 @@ using System; namespace Avalonia.Utilities { - /// - /// Callback invoked when deferred setter wants to set a value. - /// - /// Value type. - /// Property being set. - /// Backing field reference. - /// New value. - internal delegate void SetAndNotifyCallback(AvaloniaProperty property, ref TValue backing, TValue value); - /// /// A utility class to enable deferring assignment until after property-changed notifications are sent. /// Used to fix #855. @@ -70,14 +61,14 @@ namespace Avalonia.Utilities return false; } - public bool SetAndNotifyCallback(AvaloniaProperty property, SetAndNotifyCallback setAndNotifyCallback, ref TValue backing, TValue value) + public bool SetAndNotifyCallback(AvaloniaProperty property, ISetAndNotifyHandler setAndNotifyHandler, ref TValue backing, TValue value) where TValue : TSetRecord { if (!_isNotifying) { using (new NotifyDisposable(this)) { - setAndNotifyCallback(property, ref backing, value); + setAndNotifyHandler.HandleSetAndNotify(property, ref backing, value); } if (!_pendingValues.Empty) @@ -86,7 +77,7 @@ namespace Avalonia.Utilities { while (!_pendingValues.Empty) { - setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue()); + setAndNotifyHandler.HandleSetAndNotify(property, ref backing, (TValue)_pendingValues.Dequeue()); } } } @@ -119,4 +110,19 @@ namespace Avalonia.Utilities } } } + + /// + /// Handler for set and notify requests. + /// + /// Value type. + internal interface ISetAndNotifyHandler + { + /// + /// Handles deferred setter requests to set a value. + /// + /// Property being set. + /// Backing field reference. + /// New value. + void HandleSetAndNotify(AvaloniaProperty property, ref TValue backing, TValue value); + } } From 4e312e909a6ac575e816ccd66c6894535161fa1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Komosi=C5=84ski?= Date: Fri, 6 Sep 2019 18:48:22 +0200 Subject: [PATCH 48/68] Avoid constant boxing of StyledProperty default value. --- src/Avalonia.Base/BoxedValue.cs | 28 +++++++++++++++++++ src/Avalonia.Base/StyledPropertyBase.cs | 11 ++++++-- src/Avalonia.Base/StyledPropertyMetadata`1.cs | 14 ++++------ 3 files changed, 43 insertions(+), 10 deletions(-) create mode 100644 src/Avalonia.Base/BoxedValue.cs diff --git a/src/Avalonia.Base/BoxedValue.cs b/src/Avalonia.Base/BoxedValue.cs new file mode 100644 index 0000000000..5fc515f299 --- /dev/null +++ b/src/Avalonia.Base/BoxedValue.cs @@ -0,0 +1,28 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +namespace Avalonia +{ + /// + /// Represents boxed value of type . + /// + /// Type of stored value. + internal readonly struct BoxedValue + { + public BoxedValue(T value) + { + Boxed = value; + Typed = value; + } + + /// + /// Boxed value. + /// + public object Boxed { get; } + + /// + /// Typed value. + /// + public T Typed { get; } + } +} diff --git a/src/Avalonia.Base/StyledPropertyBase.cs b/src/Avalonia.Base/StyledPropertyBase.cs index eb112e753a..27a502246a 100644 --- a/src/Avalonia.Base/StyledPropertyBase.cs +++ b/src/Avalonia.Base/StyledPropertyBase.cs @@ -68,7 +68,7 @@ namespace Avalonia { Contract.Requires(type != null); - return GetMetadata(type).DefaultValue; + return GetMetadata(type).DefaultValue.Typed; } /// @@ -164,7 +164,14 @@ namespace Avalonia } /// - object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultValue(type); + object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type); + + private object GetDefaultBoxedValue(Type type) + { + Contract.Requires(type != null); + + return GetMetadata(type).DefaultValue.Boxed; + } [DebuggerHidden] private Func Cast(Func validate) diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index ed01f1bc70..d1a0e2dc53 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -19,26 +19,26 @@ namespace Avalonia /// A validation function. /// The default binding mode. public StyledPropertyMetadata( - TValue defaultValue = default(TValue), + TValue defaultValue = default, Func validate = null, BindingMode defaultBindingMode = BindingMode.Default) : base(defaultBindingMode) { - DefaultValue = defaultValue; + DefaultValue = new BoxedValue(defaultValue); Validate = validate; } /// /// Gets the default value for the property. /// - public TValue DefaultValue { get; private set; } + internal BoxedValue DefaultValue { get; private set; } /// /// Gets the validation callback. /// public Func Validate { get; private set; } - object IStyledPropertyMetadata.DefaultValue => DefaultValue; + object IStyledPropertyMetadata.DefaultValue => DefaultValue.Boxed; Func IStyledPropertyMetadata.Validate => Cast(Validate); @@ -47,11 +47,9 @@ namespace Avalonia { base.Merge(baseMetadata, property); - var src = baseMetadata as StyledPropertyMetadata; - - if (src != null) + if (baseMetadata is StyledPropertyMetadata src) { - if (DefaultValue == null) + if (DefaultValue.Boxed == null) { DefaultValue = src.DefaultValue; } From 33f154148b8929853480df3ef6c0946da7763762 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Sat, 7 Sep 2019 22:29:01 +0100 Subject: [PATCH 49/68] Add static method to open devtools. --- src/Avalonia.Diagnostics/DevTools.xaml.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Diagnostics/DevTools.xaml.cs b/src/Avalonia.Diagnostics/DevTools.xaml.cs index 1fcfb525cb..037e80e372 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml.cs +++ b/src/Avalonia.Diagnostics/DevTools.xaml.cs @@ -28,6 +28,11 @@ namespace Avalonia { Diagnostics.DevTools.Attach(control, gesture); } + + public static void OpenDevTools(this TopLevel control) + { + Diagnostics.DevTools.OpenDevTools(control); + } } } @@ -73,7 +78,7 @@ namespace Avalonia.Diagnostics RoutingStrategies.Tunnel); } - private static void OpenDevTools(TopLevel control) + internal static void OpenDevTools(TopLevel control) { if (s_open.TryGetValue(control, out var devToolsWindow)) { From 66d79c025da7bfe9046def4c72e1514804d4c601 Mon Sep 17 00:00:00 2001 From: danwalmsley Date: Sat, 7 Sep 2019 22:34:38 +0100 Subject: [PATCH 50/68] Update Default.xaml --- src/Avalonia.Controls.DataGrid/Themes/Default.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml index eaa267ba66..7b6870fec3 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml @@ -217,12 +217,12 @@ - + - + From 3dddb569821d2235b923662b8e6e4c590b940e59 Mon Sep 17 00:00:00 2001 From: ahopper Date: Sun, 8 Sep 2019 06:58:09 +0100 Subject: [PATCH 51/68] add space after while --- src/Avalonia.Base/AvaloniaObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index c619d80e23..94558c4367 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -610,7 +610,7 @@ namespace Avalonia } if (property.Inherits) { - while(aobj.InheritanceParent is AvaloniaObject parent) + while (aobj.InheritanceParent is AvaloniaObject parent) { aobj = parent; valuestore = aobj._values; From 04c2dfcc5484891761522764a8359e3b73638c9b Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 8 Sep 2019 14:26:35 +0200 Subject: [PATCH 52/68] Compiled expressions for RoutedEvent. --- .../EventSubscription.cs | 4 +++ src/Avalonia.Interactivity/Interactive.cs | 26 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Interactivity/EventSubscription.cs b/src/Avalonia.Interactivity/EventSubscription.cs index 9f763edcd3..ff2a28e570 100644 --- a/src/Avalonia.Interactivity/EventSubscription.cs +++ b/src/Avalonia.Interactivity/EventSubscription.cs @@ -5,8 +5,12 @@ using System; namespace Avalonia.Interactivity { + internal delegate void InvokeSignature(Delegate func, object sender, RoutedEventArgs args); + internal class EventSubscription { + public InvokeSignature RaiseHandler { get; set; } + public Delegate Handler { get; set; } public RoutingStrategies Routes { get; set; } diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs index 911fc2130e..033686a8c8 100644 --- a/src/Avalonia.Interactivity/Interactive.cs +++ b/src/Avalonia.Interactivity/Interactive.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.Layout; @@ -28,6 +29,10 @@ namespace Avalonia.Interactivity get { return _eventHandlers ?? (_eventHandlers = new Dictionary>()); } } + + + private static Dictionary s_invokeCache = new Dictionary(); + /// /// Adds a handler for the specified routed event. /// @@ -53,8 +58,27 @@ namespace Avalonia.Interactivity EventHandlers.Add(routedEvent, subscriptions); } + if (!s_invokeCache.TryGetValue(routedEvent.EventArgsType, out InvokeSignature raiseFunc)) + { + ParameterExpression funcParameter = Expression.Parameter(typeof(Delegate), "func"); + ParameterExpression senderParameter = Expression.Parameter(typeof(object), "sender"); + ParameterExpression argsParameter = Expression.Parameter(typeof(RoutedEventArgs), "args"); + + UnaryExpression convertedFunc = Expression.Convert(funcParameter, typeof(EventHandler<>).MakeGenericType(routedEvent.EventArgsType)); + UnaryExpression convertedArgs = Expression.Convert(argsParameter, routedEvent.EventArgsType); + + InvocationExpression invokeDelegate = Expression.Invoke(convertedFunc, senderParameter, convertedArgs); + + raiseFunc = Expression + .Lambda(invokeDelegate, funcParameter, senderParameter, argsParameter) + .Compile(); + + s_invokeCache.Add(routedEvent.EventArgsType, raiseFunc); + } + var sub = new EventSubscription { + RaiseHandler = raiseFunc, Handler = handler, Routes = routes, AlsoIfHandled = handledEventsToo, @@ -196,7 +220,7 @@ namespace Avalonia.Interactivity if (correctRoute && notFinished) { - sub.Handler.DynamicInvoke(this, e); + sub.RaiseHandler(sub.Handler, this, e); } } } From edd6fd626f7e21005bc828792cb7ee7848a01fe7 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 8 Sep 2019 17:31:18 +0200 Subject: [PATCH 53/68] Make sure that unlock API is still safe. --- .../AvaloniaNativeDeferredRendererLock.cs | 23 ++++++++++++++-- .../Rendering/ManagedDeferredRendererLock.cs | 26 ++++++++++--------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs b/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs index 6dd5337b27..0031cc5b03 100644 --- a/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs +++ b/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs @@ -1,5 +1,8 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; -using System.Reactive.Disposables; +using System.Threading; using Avalonia.Native.Interop; using Avalonia.Rendering; @@ -13,11 +16,27 @@ namespace Avalonia.Native { _window = window; } + public IDisposable TryLock() { if (_window.TryLock()) - return Disposable.Create(() => _window.Unlock()); + return new UnlockDisposable(_window); return null; } + + private class UnlockDisposable : IDisposable + { + private IAvnWindowBase _window; + + public UnlockDisposable(IAvnWindowBase window) + { + _window = window; + } + + public void Dispose() + { + Interlocked.Exchange(ref _window, null)?.Unlock(); + } + } } } diff --git a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs index 1295961a1b..a87fc9e39f 100644 --- a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs +++ b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs @@ -1,3 +1,6 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + using System; using System.Threading; @@ -6,12 +9,6 @@ namespace Avalonia.Rendering public class ManagedDeferredRendererLock : IDeferredRendererLock { private readonly object _lock = new object(); - private readonly LockDisposable _lockDisposable; - - public ManagedDeferredRendererLock() - { - _lockDisposable = new LockDisposable(_lock); - } /// /// Tries to lock the target surface or window @@ -20,7 +17,7 @@ namespace Avalonia.Rendering public IDisposable TryLock() { if (Monitor.TryEnter(_lock)) - return _lockDisposable; + return new UnlockDisposable(_lock); return null; } @@ -30,21 +27,26 @@ namespace Avalonia.Rendering public IDisposable Lock() { Monitor.Enter(_lock); - return _lockDisposable; + return new UnlockDisposable(_lock); } - private class LockDisposable : IDisposable + private class UnlockDisposable : IDisposable { - private readonly object _lock; + private object _lock; - public LockDisposable(object @lock) + public UnlockDisposable(object @lock) { _lock = @lock; } public void Dispose() { - Monitor.Exit(_lock); + object @lock = Interlocked.Exchange(ref _lock, null); + + if (@lock != null) + { + Monitor.Exit(@lock); + } } } } From c37b911fe5a8af3dcca826d556d88c70f3f64e68 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 8 Sep 2019 17:33:41 +0200 Subject: [PATCH 54/68] Seal unlock classes. --- src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs | 2 +- src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs b/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs index 0031cc5b03..ce2b03e355 100644 --- a/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs +++ b/src/Avalonia.Native/AvaloniaNativeDeferredRendererLock.cs @@ -24,7 +24,7 @@ namespace Avalonia.Native return null; } - private class UnlockDisposable : IDisposable + private sealed class UnlockDisposable : IDisposable { private IAvnWindowBase _window; diff --git a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs index a87fc9e39f..259874423e 100644 --- a/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs +++ b/src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs @@ -30,7 +30,7 @@ namespace Avalonia.Rendering return new UnlockDisposable(_lock); } - private class UnlockDisposable : IDisposable + private sealed class UnlockDisposable : IDisposable { private object _lock; From 64fdb5828ee6943a5add0f2877bcb29a1de32361 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 9 Sep 2019 02:02:54 +0800 Subject: [PATCH 55/68] un-LINQ the Transition property matching codepath. --- src/Avalonia.Animation/Animatable.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 5ff3b17fb5..3f548d207d 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -68,16 +68,19 @@ namespace Avalonia.Animation { if (_transitions is null || _previousTransitions is null || e.Priority == BindingPriority.Animation) return; - var match = Transitions.FirstOrDefault(x => x.Property == e.Property); - - if (match != null) + // PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations). + foreach (var transition in Transitions) { - if (_previousTransitions.TryGetValue(e.Property, out var dispose)) - dispose.Dispose(); + if (transition.Property == e.Property) + { + if (_previousTransitions.TryGetValue(e.Property, out var dispose)) + dispose.Dispose(); - var instance = match.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue); + var instance = transition.Apply(this, Clock ?? Avalonia.Animation.Clock.GlobalClock, e.OldValue, e.NewValue); - _previousTransitions[e.Property] = instance; + _previousTransitions[e.Property] = instance; + return; + } } } } From b8e4909b104423cb7163b3fbb8ea75b8c466c997 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 9 Sep 2019 02:18:47 +0800 Subject: [PATCH 56/68] Restore Lazy Init to avoid API break. --- src/Avalonia.Animation/Animatable.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 3f548d207d..68ca03f910 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -45,15 +45,16 @@ namespace Avalonia.Animation { get { + if (_transitions is null) + _transitions = new Transitions(); + + if (_previousTransitions is null) + _previousTransitions = new Dictionary(); + return _transitions; } set { - if (value is null) - return; - - if (_previousTransitions is null) - _previousTransitions = new Dictionary(); SetAndRaise(TransitionsProperty, ref _transitions, value); } From 3549f00fcba37fffe35c827fc20ded88743f8250 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 9 Sep 2019 02:19:25 +0800 Subject: [PATCH 57/68] Revert unit test changes. --- tests/Avalonia.Animation.UnitTests/TransitionsTests.cs | 4 ++-- tests/Avalonia.LeakTests/TransitionTests.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs index a8efc2c8ae..4ebe472125 100644 --- a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs +++ b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs @@ -23,7 +23,7 @@ namespace Avalonia.Animation.UnitTests { var border = new Border { - Transitions = new Transitions + Transitions = { new DoubleTransition { @@ -51,7 +51,7 @@ namespace Avalonia.Animation.UnitTests { var border = new Border { - Transitions = new Transitions + Transitions = { new DoubleTransition { diff --git a/tests/Avalonia.LeakTests/TransitionTests.cs b/tests/Avalonia.LeakTests/TransitionTests.cs index 699dec7229..5ab8c5c0dd 100644 --- a/tests/Avalonia.LeakTests/TransitionTests.cs +++ b/tests/Avalonia.LeakTests/TransitionTests.cs @@ -27,7 +27,7 @@ namespace Avalonia.LeakTests { var border = new Border { - Transitions = new Transitions + Transitions = { new DoubleTransition { From 4a7f370fc520413a4c59709c8c10ab25048722e9 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 8 Sep 2019 20:23:38 +0200 Subject: [PATCH 58/68] Cleanup. --- src/Avalonia.Interactivity/Interactive.cs | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs index 033686a8c8..59dfbcb8ab 100644 --- a/src/Avalonia.Interactivity/Interactive.cs +++ b/src/Avalonia.Interactivity/Interactive.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reactive.Disposables; -using System.Reactive.Linq; using Avalonia.Layout; using Avalonia.VisualTree; @@ -19,19 +18,14 @@ namespace Avalonia.Interactivity { private Dictionary> _eventHandlers; + private static readonly Dictionary s_invokeCache = new Dictionary(); + /// /// Gets the interactive parent of the object for bubbling and tunneling events. /// IInteractive IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive; - private Dictionary> EventHandlers - { - get { return _eventHandlers ?? (_eventHandlers = new Dictionary>()); } - } - - - - private static Dictionary s_invokeCache = new Dictionary(); + private Dictionary> EventHandlers => _eventHandlers ?? (_eventHandlers = new Dictionary>()); /// /// Adds a handler for the specified routed event. @@ -58,14 +52,16 @@ namespace Avalonia.Interactivity EventHandlers.Add(routedEvent, subscriptions); } - if (!s_invokeCache.TryGetValue(routedEvent.EventArgsType, out InvokeSignature raiseFunc)) + Type eventArgsType = routedEvent.EventArgsType; + + if (!s_invokeCache.TryGetValue(eventArgsType, out InvokeSignature raiseFunc)) { ParameterExpression funcParameter = Expression.Parameter(typeof(Delegate), "func"); ParameterExpression senderParameter = Expression.Parameter(typeof(object), "sender"); ParameterExpression argsParameter = Expression.Parameter(typeof(RoutedEventArgs), "args"); - UnaryExpression convertedFunc = Expression.Convert(funcParameter, typeof(EventHandler<>).MakeGenericType(routedEvent.EventArgsType)); - UnaryExpression convertedArgs = Expression.Convert(argsParameter, routedEvent.EventArgsType); + UnaryExpression convertedFunc = Expression.Convert(funcParameter, typeof(EventHandler<>).MakeGenericType(eventArgsType)); + UnaryExpression convertedArgs = Expression.Convert(argsParameter, eventArgsType); InvocationExpression invokeDelegate = Expression.Invoke(convertedFunc, senderParameter, convertedArgs); @@ -73,7 +69,7 @@ namespace Avalonia.Interactivity .Lambda(invokeDelegate, funcParameter, senderParameter, argsParameter) .Compile(); - s_invokeCache.Add(routedEvent.EventArgsType, raiseFunc); + s_invokeCache.Add(eventArgsType, raiseFunc); } var sub = new EventSubscription From 45f86a925f4bf0766b40b209cfd42558a8a7bcc9 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 8 Sep 2019 22:43:47 +0300 Subject: [PATCH 59/68] Fixed possible NRE in Gestures.cs --- src/Avalonia.Input/Gestures.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index bb8c8b8c40..ea4892ebfc 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -31,7 +31,7 @@ namespace Avalonia.Input RoutedEvent.Register( "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures)); - private static WeakReference s_lastPress; + private static WeakReference s_lastPress = new WeakReference(null); static Gestures() { From 5cae6bee7b94253df022fe20ecf48bd4f3660780 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 8 Sep 2019 21:55:17 +0200 Subject: [PATCH 60/68] Simplify creation of invoke adapters. --- .../EventSubscription.cs | 2 +- src/Avalonia.Interactivity/Interactive.cs | 115 ++++++++++++------ 2 files changed, 79 insertions(+), 38 deletions(-) diff --git a/src/Avalonia.Interactivity/EventSubscription.cs b/src/Avalonia.Interactivity/EventSubscription.cs index ff2a28e570..b495142998 100644 --- a/src/Avalonia.Interactivity/EventSubscription.cs +++ b/src/Avalonia.Interactivity/EventSubscription.cs @@ -9,7 +9,7 @@ namespace Avalonia.Interactivity internal class EventSubscription { - public InvokeSignature RaiseHandler { get; set; } + public InvokeSignature InvokeAdapter { get; set; } public Delegate Handler { get; set; } diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs index 59dfbcb8ab..0f607241ec 100644 --- a/src/Avalonia.Interactivity/Interactive.cs +++ b/src/Avalonia.Interactivity/Interactive.cs @@ -4,8 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Linq.Expressions; -using System.Reactive.Disposables; using Avalonia.Layout; using Avalonia.VisualTree; @@ -44,45 +42,14 @@ namespace Avalonia.Interactivity Contract.Requires(routedEvent != null); Contract.Requires(handler != null); - List subscriptions; - - if (!EventHandlers.TryGetValue(routedEvent, out subscriptions)) - { - subscriptions = new List(); - EventHandlers.Add(routedEvent, subscriptions); - } - - Type eventArgsType = routedEvent.EventArgsType; - - if (!s_invokeCache.TryGetValue(eventArgsType, out InvokeSignature raiseFunc)) - { - ParameterExpression funcParameter = Expression.Parameter(typeof(Delegate), "func"); - ParameterExpression senderParameter = Expression.Parameter(typeof(object), "sender"); - ParameterExpression argsParameter = Expression.Parameter(typeof(RoutedEventArgs), "args"); - - UnaryExpression convertedFunc = Expression.Convert(funcParameter, typeof(EventHandler<>).MakeGenericType(eventArgsType)); - UnaryExpression convertedArgs = Expression.Convert(argsParameter, eventArgsType); - - InvocationExpression invokeDelegate = Expression.Invoke(convertedFunc, senderParameter, convertedArgs); - - raiseFunc = Expression - .Lambda(invokeDelegate, funcParameter, senderParameter, argsParameter) - .Compile(); - - s_invokeCache.Add(eventArgsType, raiseFunc); - } - - var sub = new EventSubscription + var subscription = new EventSubscription { - RaiseHandler = raiseFunc, Handler = handler, Routes = routes, AlsoIfHandled = handledEventsToo, }; - subscriptions.Add(sub); - - return Disposable.Create(() => subscriptions.Remove(sub)); + return AddEventSubscription(routedEvent, subscription); } /// @@ -100,7 +67,37 @@ namespace Avalonia.Interactivity RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, bool handledEventsToo = false) where TEventArgs : RoutedEventArgs { - return AddHandler(routedEvent, (Delegate)handler, routes, handledEventsToo); + Contract.Requires(routedEvent != null); + Contract.Requires(handler != null); + + // EventHandler delegate is not covariant, this forces us to create small wrapper + // that will cast our type erased instance and invoke it. + Type eventArgsType = routedEvent.EventArgsType; + + if (!s_invokeCache.TryGetValue(eventArgsType, out var invokeAdapter)) + { + void InvokeAdapter(Delegate func, object sender, RoutedEventArgs args) + { + var typedHandler = (EventHandler)func; + var typedArgs = (TEventArgs)args; + + typedHandler(sender, typedArgs); + } + + invokeAdapter = InvokeAdapter; + + s_invokeCache.Add(eventArgsType, invokeAdapter); + } + + var subscription = new EventSubscription + { + InvokeAdapter = invokeAdapter, + Handler = handler, + Routes = routes, + AlsoIfHandled = handledEventsToo, + }; + + return AddEventSubscription(routedEvent, subscription); } /// @@ -216,10 +213,54 @@ namespace Avalonia.Interactivity if (correctRoute && notFinished) { - sub.RaiseHandler(sub.Handler, this, e); + if (sub.InvokeAdapter != null) + { + sub.InvokeAdapter(sub.Handler, this, e); + } + else + { + sub.Handler.DynamicInvoke(this, e); + } } } } } + + private List GetEventSubscriptions(RoutedEvent routedEvent) + { + if (!EventHandlers.TryGetValue(routedEvent, out var subscriptions)) + { + subscriptions = new List(); + EventHandlers.Add(routedEvent, subscriptions); + } + + return subscriptions; + } + + private IDisposable AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription) + { + List subscriptions = GetEventSubscriptions(routedEvent); + + subscriptions.Add(subscription); + + return new UnsubscribeDisposable(subscriptions, subscription); + } + + private sealed class UnsubscribeDisposable : IDisposable + { + private readonly List _subscriptions; + private readonly EventSubscription _subscription; + + public UnsubscribeDisposable(List subscriptions, EventSubscription subscription) + { + _subscriptions = subscriptions; + _subscription = subscription; + } + + public void Dispose() + { + _subscriptions.Remove(_subscription); + } + } } } From f5b317decbf7bededba9d4746a09261d6e6c1aa4 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 8 Sep 2019 22:33:39 +0200 Subject: [PATCH 61/68] Naming. --- src/Avalonia.Interactivity/EventSubscription.cs | 4 ++-- src/Avalonia.Interactivity/Interactive.cs | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Interactivity/EventSubscription.cs b/src/Avalonia.Interactivity/EventSubscription.cs index b495142998..e8fb1bfaf1 100644 --- a/src/Avalonia.Interactivity/EventSubscription.cs +++ b/src/Avalonia.Interactivity/EventSubscription.cs @@ -5,11 +5,11 @@ using System; namespace Avalonia.Interactivity { - internal delegate void InvokeSignature(Delegate func, object sender, RoutedEventArgs args); + internal delegate void HandlerInvokeSignature(Delegate baseHandler, object sender, RoutedEventArgs args); internal class EventSubscription { - public InvokeSignature InvokeAdapter { get; set; } + public HandlerInvokeSignature InvokeAdapter { get; set; } public Delegate Handler { get; set; } diff --git a/src/Avalonia.Interactivity/Interactive.cs b/src/Avalonia.Interactivity/Interactive.cs index 0f607241ec..f8d388ec89 100644 --- a/src/Avalonia.Interactivity/Interactive.cs +++ b/src/Avalonia.Interactivity/Interactive.cs @@ -16,7 +16,7 @@ namespace Avalonia.Interactivity { private Dictionary> _eventHandlers; - private static readonly Dictionary s_invokeCache = new Dictionary(); + private static readonly Dictionary s_invokeHandlerCache = new Dictionary(); /// /// Gets the interactive parent of the object for bubbling and tunneling events. @@ -74,11 +74,11 @@ namespace Avalonia.Interactivity // that will cast our type erased instance and invoke it. Type eventArgsType = routedEvent.EventArgsType; - if (!s_invokeCache.TryGetValue(eventArgsType, out var invokeAdapter)) + if (!s_invokeHandlerCache.TryGetValue(eventArgsType, out var invokeAdapter)) { - void InvokeAdapter(Delegate func, object sender, RoutedEventArgs args) + void InvokeAdapter(Delegate baseHandler, object sender, RoutedEventArgs args) { - var typedHandler = (EventHandler)func; + var typedHandler = (EventHandler)baseHandler; var typedArgs = (TEventArgs)args; typedHandler(sender, typedArgs); @@ -86,7 +86,7 @@ namespace Avalonia.Interactivity invokeAdapter = InvokeAdapter; - s_invokeCache.Add(eventArgsType, invokeAdapter); + s_invokeHandlerCache.Add(eventArgsType, invokeAdapter); } var subscription = new EventSubscription From d368d9675a17beb045e1e94dce880ee2f0b45042 Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 9 Sep 2019 14:25:59 +0800 Subject: [PATCH 62/68] Address review. --- src/Avalonia.Animation/Animatable.cs | 2 +- tests/Avalonia.Animation.UnitTests/TransitionsTests.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 68ca03f910..2c321b8b28 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -35,7 +35,7 @@ namespace Avalonia.Animation (o, v) => o.Transitions = v); private Transitions _transitions; - private bool _isTransitionsSet = false; + private Dictionary _previousTransitions; /// diff --git a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs index 4ebe472125..f1b4b0d071 100644 --- a/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs +++ b/tests/Avalonia.Animation.UnitTests/TransitionsTests.cs @@ -23,7 +23,7 @@ namespace Avalonia.Animation.UnitTests { var border = new Border { - Transitions = + Transitions = { new DoubleTransition { @@ -51,7 +51,7 @@ namespace Avalonia.Animation.UnitTests { var border = new Border { - Transitions = + Transitions = { new DoubleTransition { From 9970bf67223624c33d751f3aa7c9f8726aed7d0c Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Mon, 9 Sep 2019 14:38:53 +0800 Subject: [PATCH 63/68] Address review pt. 2 --- tests/Avalonia.LeakTests/TransitionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.LeakTests/TransitionTests.cs b/tests/Avalonia.LeakTests/TransitionTests.cs index 5ab8c5c0dd..c7add1fe11 100644 --- a/tests/Avalonia.LeakTests/TransitionTests.cs +++ b/tests/Avalonia.LeakTests/TransitionTests.cs @@ -27,7 +27,7 @@ namespace Avalonia.LeakTests { var border = new Border { - Transitions = + Transitions = { new DoubleTransition { From 9c04daace9a965af9205b89f3ce1d01b5bda3310 Mon Sep 17 00:00:00 2001 From: artyom Date: Mon, 9 Sep 2019 11:18:47 +0300 Subject: [PATCH 64/68] Add package metadata to SharedVersion.props --- build/SharedVersion.props | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/build/SharedVersion.props b/build/SharedVersion.props index 76abcf6912..44d5c239ef 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -4,11 +4,16 @@ Avalonia 0.8.999 Copyright 2019 © The AvaloniaUI Project - https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md - https://github.com/AvaloniaUI/Avalonia/ + https://avaloniaui.net https://github.com/AvaloniaUI/Avalonia/ true CS1591 latest + MIT + https://avatars2.githubusercontent.com/u/14075148?s=200 + Avalonia is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), MacOS and with experimental support for Android and iOS. + avalonia;avaloniaui;mvvm;rx;reactive extensions;android;ios;mac;forms;wpf;net;netstandard;net461;uwp;xamarin + https://github.com/AvaloniaUI/Avalonia/releases + git From 90eb54f8eb28df515c4c957fab32af8ad5ee73cf Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 9 Sep 2019 12:25:26 +0100 Subject: [PATCH 65/68] fix nuget replace script. --- scripts/ReplaceNugetCache.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/ReplaceNugetCache.sh b/scripts/ReplaceNugetCache.sh index 4cc11edd60..e1c0487d60 100755 --- a/scripts/ReplaceNugetCache.sh +++ b/scripts/ReplaceNugetCache.sh @@ -2,7 +2,6 @@ cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netcoreapp2.0/ cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netstandard2.0/ - cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.gtk3/$1/lib/netstandard2.0/ cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.skia/$1/lib/netstandard2.0/ cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.native/$1/lib/netstandard2.0/ From f56a47ed49343b804ec3041a22217eddfe7e5213 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 9 Sep 2019 12:22:31 +0100 Subject: [PATCH 66/68] fix osx managed file dialogs. --- .../MacOSMountedVolumeInfoProvider.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs b/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs index eea695d77e..92b2915e2e 100644 --- a/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs +++ b/src/Avalonia.Native/MacOSMountedVolumeInfoProvider.cs @@ -8,16 +8,16 @@ using Avalonia.Controls.Platform; namespace Avalonia.Native { - internal class WindowsMountedVolumeInfoListener : IDisposable + internal class MacOSMountedVolumeInfoListener : IDisposable { private readonly CompositeDisposable _disposables; - private readonly ObservableCollection _targetObs; private bool _beenDisposed = false; private ObservableCollection mountedDrives; - public WindowsMountedVolumeInfoListener(ObservableCollection mountedDrives) + public MacOSMountedVolumeInfoListener(ObservableCollection mountedDrives) { this.mountedDrives = mountedDrives; + _disposables = new CompositeDisposable(); var pollTimer = Observable.Interval(TimeSpan.FromSeconds(1)) @@ -30,7 +30,8 @@ namespace Avalonia.Native private void Poll(long _) { - var mountVolInfos = Directory.GetDirectories("/Volumes") + var mountVolInfos = Directory.GetDirectories("/Volumes/") + .Where(p=> p != null) .Select(p => new MountedVolumeInfo() { VolumeLabel = Path.GetFileName(p), @@ -38,15 +39,15 @@ namespace Avalonia.Native VolumeSizeBytes = 0 }) .ToArray(); - - if (_targetObs.SequenceEqual(mountVolInfos)) + + if (mountedDrives.SequenceEqual(mountVolInfos)) return; else { - _targetObs.Clear(); + mountedDrives.Clear(); foreach (var i in mountVolInfos) - _targetObs.Add(i); + mountedDrives.Add(i); } } @@ -72,7 +73,7 @@ namespace Avalonia.Native public IDisposable Listen(ObservableCollection mountedDrives) { Contract.Requires(mountedDrives != null); - return new WindowsMountedVolumeInfoListener(mountedDrives); + return new MacOSMountedVolumeInfoListener(mountedDrives); } } } From 734b9f39b39b38c091e5a36a4c8dcb18de88b2dd Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 9 Sep 2019 12:25:06 +0100 Subject: [PATCH 67/68] fix windows mounted listener. --- .../Avalonia.Win32/WindowsMountedVolumeInfoListener.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs index 102e027584..a17e6b8b51 100644 --- a/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs +++ b/src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs @@ -10,8 +10,7 @@ namespace Avalonia.Win32 { internal class WindowsMountedVolumeInfoListener : IDisposable { - private readonly CompositeDisposable _disposables; - private readonly ObservableCollection _targetObs = new ObservableCollection(); + private readonly CompositeDisposable _disposables; private bool _beenDisposed = false; private ObservableCollection mountedDrives; @@ -41,14 +40,14 @@ namespace Avalonia.Win32 }) .ToArray(); - if (_targetObs.SequenceEqual(mountVolInfos)) + if (mountedDrives.SequenceEqual(mountVolInfos)) return; else { - _targetObs.Clear(); + mountedDrives.Clear(); foreach (var i in mountVolInfos) - _targetObs.Add(i); + mountedDrives.Add(i); } } From 0eb7b2d25e458b2df2172b43fe3102119e03993a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 9 Sep 2019 18:01:43 +0200 Subject: [PATCH 68/68] Enable adding event handlers to Gestures events. Added the `Add*Handler` and `Remove*Handler` events required by XAML in order to use attached event syntax. Added unit test to ensure this works. --- src/Avalonia.Input/Gestures.cs | 30 ++++++++++++ .../Xaml/EventTests.cs | 46 +++++++++++-------- 2 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/Avalonia.Input/Gestures.cs b/src/Avalonia.Input/Gestures.cs index ea4892ebfc..6b06151773 100644 --- a/src/Avalonia.Input/Gestures.cs +++ b/src/Avalonia.Input/Gestures.cs @@ -39,6 +39,36 @@ namespace Avalonia.Input InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased); } + public static void AddTappedHandler(IInteractive element, EventHandler handler) + { + element.AddHandler(TappedEvent, handler); + } + + public static void AddDoubleTappedHandler(IInteractive element, EventHandler handler) + { + element.AddHandler(DoubleTappedEvent, handler); + } + + public static void AddRightTappedHandler(IInteractive element, EventHandler handler) + { + element.AddHandler(RightTappedEvent, handler); + } + + public static void RemoveTappedHandler(IInteractive element, EventHandler handler) + { + element.RemoveHandler(TappedEvent, handler); + } + + public static void RemoveDoubleTappedHandler(IInteractive element, EventHandler handler) + { + element.RemoveHandler(DoubleTappedEvent, handler); + } + + public static void RemoveRightTappedHandler(IInteractive element, EventHandler handler) + { + element.RemoveHandler(RightTappedEvent, handler); + } + private static void PointerPressed(RoutedEventArgs ev) { if (ev.Route == RoutingStrategies.Bubble) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs index dcb6533b5e..bde4e34643 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs @@ -1,7 +1,6 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; @@ -12,45 +11,56 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml public class EventTests : XamlTestBase { [Fact] - public void Event_Is_Attached() + public void Event_Is_Assigned() { var xaml = @"