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 diff --git a/src/Avalonia.Animation/Animatable.cs b/src/Avalonia.Animation/Animatable.cs index 3a3d00b94a..2c321b8b28 100644 --- a/src/Avalonia.Animation/Animatable.cs +++ b/src/Avalonia.Animation/Animatable.cs @@ -45,16 +45,17 @@ namespace Avalonia.Animation { get { - if (_transitions == null) + if (_transitions is null) _transitions = new Transitions(); - if (_previousTransitions == null) + if (_previousTransitions is null) _previousTransitions = new Dictionary(); return _transitions; } set { + SetAndRaise(TransitionsProperty, ref _transitions, value); } } @@ -66,18 +67,20 @@ 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 || _previousTransitions is null || e.Priority == BindingPriority.Animation) return; - if (match != null) + // PERF-SENSITIVE: Called on every property change. Don't use LINQ here (too many allocations). + foreach (var transition in Transitions) + { + 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; + return; } } } diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 0e2f0feada..94558c4367 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 GetValueOrDefaultUnchecked(property); } } @@ -598,10 +587,46 @@ namespace Avalonia private object GetDefaultValue(AvaloniaProperty property) { if (property.Inherits && InheritanceParent is AvaloniaObject aobj) - return aobj.GetValue(property); + return aobj.GetValueOrDefaultUnchecked(property); return ((IStyledPropertyAccessor) property).GetDefaultValue(GetType()); } + /// + /// Gets the value or default value for a property. + /// + /// The property. + /// The default value. + private object GetValueOrDefaultUnchecked(AvaloniaProperty property) + { + var aobj = this; + var valuestore = aobj._values; + if (valuestore != null) + { + var result = valuestore.GetValue(property); + if (result != AvaloniaProperty.UnsetValue) + { + return result; + } + } + if (property.Inherits) + { + while (aobj.InheritanceParent is AvaloniaObject parent) + { + aobj = parent; + valuestore = aobj._values; + if (valuestore != null) + { + var result = valuestore.GetValue(property); + if (result != AvaloniaProperty.UnsetValue) + { + return result; + } + } + } + } + return ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); + } + /// /// Sets the value of a direct property. /// 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(); + } } } 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 @@ - + - + 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() { diff --git a/src/Avalonia.Interactivity/EventSubscription.cs b/src/Avalonia.Interactivity/EventSubscription.cs index 9f763edcd3..e8fb1bfaf1 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 HandlerInvokeSignature(Delegate baseHandler, object sender, RoutedEventArgs args); + internal class EventSubscription { + public HandlerInvokeSignature InvokeAdapter { 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..f8d388ec89 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.Reactive.Disposables; -using System.Reactive.Linq; using Avalonia.Layout; using Avalonia.VisualTree; @@ -18,15 +16,14 @@ namespace Avalonia.Interactivity { private Dictionary> _eventHandlers; + private static readonly Dictionary s_invokeHandlerCache = 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 Dictionary> EventHandlers => _eventHandlers ?? (_eventHandlers = new Dictionary>()); /// /// Adds a handler for the specified routed event. @@ -45,24 +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); - } - - var sub = new EventSubscription + var subscription = new EventSubscription { Handler = handler, Routes = routes, AlsoIfHandled = handledEventsToo, }; - subscriptions.Add(sub); - - return Disposable.Create(() => subscriptions.Remove(sub)); + return AddEventSubscription(routedEvent, subscription); } /// @@ -80,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_invokeHandlerCache.TryGetValue(eventArgsType, out var invokeAdapter)) + { + void InvokeAdapter(Delegate baseHandler, object sender, RoutedEventArgs args) + { + var typedHandler = (EventHandler)baseHandler; + var typedArgs = (TEventArgs)args; + + typedHandler(sender, typedArgs); + } + + invokeAdapter = InvokeAdapter; + + s_invokeHandlerCache.Add(eventArgsType, invokeAdapter); + } + + var subscription = new EventSubscription + { + InvokeAdapter = invokeAdapter, + Handler = handler, + Routes = routes, + AlsoIfHandled = handledEventsToo, + }; + + return AddEventSubscription(routedEvent, subscription); } /// @@ -196,10 +213,54 @@ namespace Avalonia.Interactivity if (correctRoute && notFinished) { - sub.Handler.DynamicInvoke(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); + } + } } } 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)