diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index d718f5917c..01daeafc3a 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -173,12 +173,20 @@ namespace Avalonia Contract.Requires(type != null); Contract.Requires(name != null); - if (name.Contains('.')) + if (name.Contains(".")) { throw new InvalidOperationException("Attached properties not supported."); } - return GetRegistered(type).FirstOrDefault(x => x.Name == name); + foreach (AvaloniaProperty x in GetRegistered(type)) + { + if (x.Name == name) + { + return x; + } + } + + return null; } /// diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 44b47329ac..bcef896fb7 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -56,22 +56,34 @@ namespace Avalonia.Data if (source != null) { + // Perf: Avoid allocating closure in the outer scope. + var targetCopy = target; + var propertyCopy = property; + var bindingCopy = binding; + return source .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue) .Take(1) - .Subscribe(x => target.SetValue(property, x, binding.Priority)); + .Subscribe(x => targetCopy.SetValue(propertyCopy, x, bindingCopy.Priority)); } else { target.SetValue(property, binding.Value, binding.Priority); return Disposable.Empty; } + case BindingMode.OneWayToSource: + { + // Perf: Avoid allocating closure in the outer scope. + var bindingCopy = binding; + return Observable.CombineLatest( binding.Observable, target.GetObservable(property), (_, v) => v) - .Subscribe(x => binding.Subject.OnNext(x)); + .Subscribe(x => bindingCopy.Subject.OnNext(x)); + } + default: throw new ArgumentException("Invalid binding mode."); } diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 7060fd3451..91a27be634 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -21,7 +21,7 @@ namespace Avalonia.Data.Core /// An ordered collection of property accessor plugins that can be used to customize /// the reading and subscription of property values on a type. /// - public static readonly IList PropertyAccessors = + public static readonly List PropertyAccessors = new List { new AvaloniaPropertyAccessorPlugin(), @@ -33,7 +33,7 @@ namespace Avalonia.Data.Core /// An ordered collection of validation checker plugins that can be used to customize /// the validation of view model and model data. /// - public static readonly IList DataValidators = + public static readonly List DataValidators = new List { new DataAnnotationsValidationPlugin(), @@ -45,7 +45,7 @@ namespace Avalonia.Data.Core /// An ordered collection of stream plugins that can be used to customize the behavior /// of the '^' stream binding operator. /// - public static readonly IList StreamHandlers = + public static readonly List StreamHandlers = new List { new TaskStreamPlugin(), diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index ab4a109cc2..4ee8e30631 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -3,6 +3,7 @@ using System; using System.Reactive.Linq; +using System.Runtime.ExceptionServices; namespace Avalonia.Data.Core.Plugins { @@ -76,7 +77,7 @@ namespace Avalonia.Data.Core.Plugins return false; } - private class Accessor : PropertyAccessorBase + private class Accessor : PropertyAccessorBase, IObserver { private readonly WeakReference _reference; private readonly AvaloniaProperty _property; @@ -117,7 +118,7 @@ namespace Avalonia.Data.Core.Plugins protected override void SubscribeCore() { - _subscription = Instance?.GetObservable(_property).Subscribe(PublishValue); + _subscription = Instance?.GetObservable(_property).Subscribe(this); } protected override void UnsubscribeCore() @@ -125,6 +126,20 @@ namespace Avalonia.Data.Core.Plugins _subscription?.Dispose(); _subscription = null; } + + void IObserver.OnCompleted() + { + } + + void IObserver.OnError(Exception error) + { + ExceptionDispatchInfo.Capture(error).Throw(); + } + + void IObserver.OnNext(object value) + { + PublishValue(value); + } } } } diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 70f53b8b88..7255fb93b3 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -41,7 +41,17 @@ namespace Avalonia.Data.Core { reference.TryGetTarget(out object target); - var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(target, PropertyName)); + IPropertyAccessorPlugin plugin = null; + + foreach (IPropertyAccessorPlugin x in ExpressionObserver.PropertyAccessors) + { + if (x.Match(target, PropertyName)) + { + plugin = x; + break; + } + } + var accessor = plugin?.Start(reference, PropertyName); if (_enableValidation && Next == null) diff --git a/src/Avalonia.Base/Utilities/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs index a57a2b7ba5..973b0aa641 100644 --- a/src/Avalonia.Base/Utilities/IdentifierParser.cs +++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs @@ -15,7 +15,7 @@ namespace Avalonia.Utilities { if (IsValidIdentifierStart(r.Peek)) { - return r.TakeWhile(IsValidIdentifierChar); + return r.TakeWhile(c => IsValidIdentifierChar(c)); } else { diff --git a/tests/Avalonia.Benchmarks/Data/BindingOperations.cs b/tests/Avalonia.Benchmarks/Data/BindingOperations.cs new file mode 100644 index 0000000000..d1ca77c980 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Data/BindingOperations.cs @@ -0,0 +1,43 @@ +using Avalonia.Data; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Data +{ + [MemoryDiagnoser, InProcess] + public class BindingsBenchmark + { + [Benchmark] + public void TwoWayBinding_Via_Binding() + { + var instance = new TestClass(); + + var binding = new Binding(nameof(TestClass.BoundValue), BindingMode.TwoWay) + { + Source = instance + }; + + instance.Bind(TestClass.IntValueProperty, binding); + } + + private class TestClass : AvaloniaObject + { + public static readonly StyledProperty IntValueProperty = + AvaloniaProperty.Register(nameof(IntValue)); + + public static readonly StyledProperty BoundValueProperty = + AvaloniaProperty.Register(nameof(BoundValue)); + + public int IntValue + { + get => GetValue(IntValueProperty); + set => SetValue(IntValueProperty, value); + } + + public int BoundValue + { + get => GetValue(BoundValueProperty); + set => SetValue(BoundValueProperty, value); + } + } + } +}