Переглянути джерело

Merge branch 'master' into deferred-setter-notifyhandler

Dariusz Komosiński 6 роки тому
батько
коміт
b81e18ca72

+ 7 - 2
build/SharedVersion.props

@@ -4,11 +4,16 @@
     <Product>Avalonia</Product>
     <Version>0.8.999</Version>
     <Copyright>Copyright 2019 &#169; The AvaloniaUI Project</Copyright>
-    <PackageLicenseUrl>https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md</PackageLicenseUrl>
-    <PackageProjectUrl>https://github.com/AvaloniaUI/Avalonia/</PackageProjectUrl>
+    <PackageProjectUrl>https://avaloniaui.net</PackageProjectUrl>
     <RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <NoWarn>CS1591</NoWarn>
     <LangVersion>latest</LangVersion>
+    <PackageLicenseExpression>MIT</PackageLicenseExpression>
+    <PackageIconUrl>https://avatars2.githubusercontent.com/u/14075148?s=200</PackageIconUrl>
+    <PackageDescription>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.</PackageDescription>
+    <PackageTags>avalonia;avaloniaui;mvvm;rx;reactive extensions;android;ios;mac;forms;wpf;net;netstandard;net461;uwp;xamarin</PackageTags>
+    <PackageReleaseNotes>https://github.com/AvaloniaUI/Avalonia/releases</PackageReleaseNotes>
+    <RepositoryType>git</RepositoryType>
   </PropertyGroup>
 </Project>

+ 0 - 1
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/
  

+ 10 - 7
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<AvaloniaProperty, IDisposable>();
 
                 return _transitions;
             }
             set
             {
+
                 SetAndRaise(TransitionsProperty, ref _transitions, value);
             }
         }
@@ -66,18 +67,20 @@ namespace Avalonia.Animation
         /// <param name="e">The event args.</param>
         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;
                 }
             }
         }

+ 38 - 13
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());
         }
 
+        /// <summary>
+        /// Gets the value or default value for a property.
+        /// </summary>
+        /// <param name="property">The property.</param>
+        /// <returns>The default value.</returns>
+        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());
+        }
+
         /// <summary>
         /// Sets the value of a direct property.
         /// </summary>

+ 28 - 0
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
+{
+    /// <summary>
+    /// Represents boxed value of type <typeparamref name="T"/>.
+    /// </summary>
+    /// <typeparam name="T">Type of stored value.</typeparam>
+    internal readonly struct BoxedValue<T>
+    {
+        public BoxedValue(T value)
+        {
+            Boxed = value;
+            Typed = value;
+        }
+
+        /// <summary>
+        /// Boxed value.
+        /// </summary>
+        public object Boxed { get; }
+
+        /// <summary>
+        /// Typed value.
+        /// </summary>
+        public T Typed { get; }
+    }
+}

+ 11 - 5
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
     /// <summary>
     /// A registered binding in a <see cref="PriorityValue"/>.
     /// </summary>
-    internal class PriorityBindingEntry : IDisposable
+    internal class PriorityBindingEntry : IDisposable, IObserver<object>
     {
-        private PriorityLevel _owner;
+        private readonly PriorityLevel _owner;
         private IDisposable _subscription;
 
         /// <summary>
@@ -85,7 +86,7 @@ namespace Avalonia
                 Description = ((IDescription)binding).Description;
             }
 
-            _subscription = binding.Subscribe(ValueChanged, Completed);
+            _subscription = binding.Subscribe(this);
         }
 
         /// <summary>
@@ -96,7 +97,7 @@ namespace Avalonia
             _subscription?.Dispose();
         }
 
-        private void ValueChanged(object value)
+        void IObserver<object>.OnNext(object value)
         {
             void Signal()
             {
@@ -132,7 +133,7 @@ namespace Avalonia
             }
         }
 
-        private void Completed()
+        void IObserver<object>.OnCompleted()
         {
             HasCompleted = true;
 
@@ -145,5 +146,10 @@ namespace Avalonia
                 Dispatcher.UIThread.Post(() => _owner.Completed(this));
             }
         }
+
+        void IObserver<object>.OnError(Exception error)
+        {
+            ExceptionDispatchInfo.Capture(error).Throw();
+        }
     }
 }

+ 9 - 2
src/Avalonia.Base/StyledPropertyBase.cs

@@ -68,7 +68,7 @@ namespace Avalonia
         {
             Contract.Requires<ArgumentNullException>(type != null);
 
-            return GetMetadata(type).DefaultValue;
+            return GetMetadata(type).DefaultValue.Typed;
         }
 
         /// <summary>
@@ -164,7 +164,14 @@ namespace Avalonia
         }
 
         /// <inheritdoc/>
-        object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultValue(type);
+        object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type);
+
+        private object GetDefaultBoxedValue(Type type)
+        {
+            Contract.Requires<ArgumentNullException>(type != null);
+
+            return GetMetadata(type).DefaultValue.Boxed;
+        }
 
         [DebuggerHidden]
         private Func<IAvaloniaObject, TValue, TValue> Cast<THost>(Func<THost, TValue, TValue> validate)

+ 6 - 8
src/Avalonia.Base/StyledPropertyMetadata`1.cs

@@ -19,26 +19,26 @@ namespace Avalonia
         /// <param name="validate">A validation function.</param>
         /// <param name="defaultBindingMode">The default binding mode.</param>
         public StyledPropertyMetadata(
-            TValue defaultValue = default(TValue),
+            TValue defaultValue = default,
             Func<IAvaloniaObject, TValue, TValue> validate = null,
             BindingMode defaultBindingMode = BindingMode.Default)
                 : base(defaultBindingMode)
         {
-            DefaultValue = defaultValue;
+            DefaultValue = new BoxedValue<TValue>(defaultValue);
             Validate = validate;
         }
 
         /// <summary>
         /// Gets the default value for the property.
         /// </summary>
-        public TValue DefaultValue { get; private set; }
+        internal BoxedValue<TValue> DefaultValue { get; private set; }
 
         /// <summary>
         /// Gets the validation callback.
         /// </summary>
         public Func<IAvaloniaObject, TValue, TValue> Validate { get; private set; }
 
-        object IStyledPropertyMetadata.DefaultValue => DefaultValue;
+        object IStyledPropertyMetadata.DefaultValue => DefaultValue.Boxed;
 
         Func<IAvaloniaObject, object, object> IStyledPropertyMetadata.Validate => Cast(Validate);
 
@@ -47,11 +47,9 @@ namespace Avalonia
         {
             base.Merge(baseMetadata, property);
 
-            var src = baseMetadata as StyledPropertyMetadata<TValue>;
-
-            if (src != null)
+            if (baseMetadata is StyledPropertyMetadata<TValue> src)
             {
-                if (DefaultValue == null)
+                if (DefaultValue.Boxed == null)
                 {
                     DefaultValue = src.DefaultValue;
                 }

+ 2 - 2
src/Avalonia.Controls.DataGrid/Themes/Default.xaml

@@ -217,12 +217,12 @@
             <DataGridRowsPresenter Name="PART_RowsPresenter" Grid.ColumnSpan="2" Grid.Row="1" />
             <Rectangle Name="BottomRightCorner" Fill="#FFE9EEF4" Grid.Column="2" Grid.Row="2" />
             <Rectangle Name="BottomLeftCorner" Fill="#FFE9EEF4" Grid.Row="2" Grid.ColumnSpan="2" />
-            <ScrollBar Name="PART_VerticalScrollbar" Orientation="Vertical" Grid.Column="2" Grid.Row="1" Width="18" Margin="0,-1,-1,-1"/>
+            <ScrollBar Name="PART_VerticalScrollbar" Orientation="Vertical" Grid.Column="2" Grid.Row="1" Width="{DynamicResource ScrollBarThickness}" Margin="0,-1,-1,-1"/>
 
             <Grid Grid.Column="1" Grid.Row="2"
                   ColumnDefinitions="Auto,*">
               <Rectangle Name="PART_FrozenColumnScrollBarSpacer" />
-              <ScrollBar Name="PART_HorizontalScrollbar" Grid.Column="1" Orientation="Horizontal" Height="18" Margin="-1,0,-1,-1"/>
+              <ScrollBar Name="PART_HorizontalScrollbar" Grid.Column="1" Orientation="Horizontal" Height="{DynamicResource ScrollBarThickness}" Margin="-1,0,-1,-1"/>
             </Grid>
           </Grid>
         </Border>

+ 6 - 1
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))
             {

+ 31 - 1
src/Avalonia.Input/Gestures.cs

@@ -31,7 +31,7 @@ namespace Avalonia.Input
             RoutedEvent.Register<ScrollGestureEventArgs>(
                 "ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
 
-        private static WeakReference<IInteractive> s_lastPress;
+        private static WeakReference<IInteractive> s_lastPress = new WeakReference<IInteractive>(null);
 
         static Gestures()
         {
@@ -39,6 +39,36 @@ namespace Avalonia.Input
             InputElement.PointerReleasedEvent.RouteFinished.Subscribe(PointerReleased);
         }
 
+        public static void AddTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
+        {
+            element.AddHandler(TappedEvent, handler);
+        }
+
+        public static void AddDoubleTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
+        {
+            element.AddHandler(DoubleTappedEvent, handler);
+        }
+
+        public static void AddRightTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
+        {
+            element.AddHandler(RightTappedEvent, handler);
+        }
+
+        public static void RemoveTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
+        {
+            element.RemoveHandler(TappedEvent, handler);
+        }
+
+        public static void RemoveDoubleTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
+        {
+            element.RemoveHandler(DoubleTappedEvent, handler);
+        }
+
+        public static void RemoveRightTappedHandler(IInteractive element, EventHandler<RoutedEventArgs> handler)
+        {
+            element.RemoveHandler(RightTappedEvent, handler);
+        }
+
         private static void PointerPressed(RoutedEventArgs ev)
         {
             if (ev.Route == RoutingStrategies.Bubble)

+ 4 - 0
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; }

+ 81 - 20
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<RoutedEvent, List<EventSubscription>> _eventHandlers;
 
+        private static readonly Dictionary<Type, HandlerInvokeSignature> s_invokeHandlerCache = new Dictionary<Type, HandlerInvokeSignature>();
+
         /// <summary>
         /// Gets the interactive parent of the object for bubbling and tunneling events.
         /// </summary>
         IInteractive IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive;
 
-        private Dictionary<RoutedEvent, List<EventSubscription>> EventHandlers
-        {
-            get { return _eventHandlers ?? (_eventHandlers = new Dictionary<RoutedEvent, List<EventSubscription>>()); }
-        }
+        private Dictionary<RoutedEvent, List<EventSubscription>> EventHandlers => _eventHandlers ?? (_eventHandlers = new Dictionary<RoutedEvent, List<EventSubscription>>());
 
         /// <summary>
         /// Adds a handler for the specified routed event.
@@ -45,24 +42,14 @@ namespace Avalonia.Interactivity
             Contract.Requires<ArgumentNullException>(routedEvent != null);
             Contract.Requires<ArgumentNullException>(handler != null);
 
-            List<EventSubscription> subscriptions;
-
-            if (!EventHandlers.TryGetValue(routedEvent, out subscriptions))
-            {
-                subscriptions = new List<EventSubscription>();
-                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);
         }
 
         /// <summary>
@@ -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<ArgumentNullException>(routedEvent != null);
+            Contract.Requires<ArgumentNullException>(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<TEventArgs>)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);
         }
 
         /// <summary>
@@ -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<EventSubscription> GetEventSubscriptions(RoutedEvent routedEvent)
+        {
+            if (!EventHandlers.TryGetValue(routedEvent, out var subscriptions))
+            {
+                subscriptions = new List<EventSubscription>();
+                EventHandlers.Add(routedEvent, subscriptions);
+            }
+
+            return subscriptions;
+        }
+
+        private IDisposable AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription)
+        {
+            List<EventSubscription> subscriptions = GetEventSubscriptions(routedEvent);
+
+            subscriptions.Add(subscription);
+
+            return new UnsubscribeDisposable(subscriptions, subscription);
+        }
+
+        private sealed class UnsubscribeDisposable : IDisposable
+        {
+            private readonly List<EventSubscription> _subscriptions;
+            private readonly EventSubscription _subscription;
+
+            public UnsubscribeDisposable(List<EventSubscription> subscriptions, EventSubscription subscription)
+            {
+                _subscriptions = subscriptions;
+                _subscription = subscription;
+            }
+
+            public void Dispose()
+            {
+                _subscriptions.Remove(_subscription);
+            }
+        }
     }
 }

+ 12 - 38
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<Tuple<object, RoutedEventArgs>> _raised = new Subject<Tuple<object, RoutedEventArgs>>();
-        private Subject<RoutedEventArgs> _routeFinished = new Subject<RoutedEventArgs>();
+        private readonly Subject<(object, RoutedEventArgs)> _raised = new Subject<(object, RoutedEventArgs)>();
+        private readonly Subject<RoutedEventArgs> _routeFinished = new Subject<RoutedEventArgs>();
 
         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<Tuple<object, RoutedEventArgs>> Raised => _raised;
+        public IObservable<(object, RoutedEventArgs)> Raised => _raised;
         public IObservable<RoutedEventArgs> RouteFinished => _routeFinished;
 
         public static RoutedEvent<TEventArgs> Register<TOwner, TEventArgs>(
@@ -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)

+ 21 - 2
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 sealed class UnlockDisposable : IDisposable
+        {
+            private IAvnWindowBase _window;
+
+            public UnlockDisposable(IAvnWindowBase window)
+            {
+                _window = window;
+            }
+
+            public void Dispose()
+            {
+                Interlocked.Exchange(ref _window, null)?.Unlock();
+            }
+        }
     }
 }

+ 10 - 9
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<MountedVolumeInfo> _targetObs;
         private bool _beenDisposed = false;
         private ObservableCollection<MountedVolumeInfo> mountedDrives;
 
-        public WindowsMountedVolumeInfoListener(ObservableCollection<MountedVolumeInfo> mountedDrives)
+        public MacOSMountedVolumeInfoListener(ObservableCollection<MountedVolumeInfo> 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<MountedVolumeInfo> mountedDrives)
         {
             Contract.Requires<ArgumentNullException>(mountedDrives != null);
-            return new WindowsMountedVolumeInfoListener(mountedDrives);
+            return new MacOSMountedVolumeInfoListener(mountedDrives);
         }
     }
 }

+ 26 - 4
src/Avalonia.Visuals/Rendering/ManagedDeferredRendererLock.cs

@@ -1,5 +1,7 @@
+// 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;
 
 namespace Avalonia.Rendering
@@ -7,7 +9,7 @@ namespace Avalonia.Rendering
     public class ManagedDeferredRendererLock : IDeferredRendererLock
     {
         private readonly object _lock = new object();
-        
+
         /// <summary>
         /// Tries to lock the target surface or window
         /// </summary>
@@ -15,7 +17,7 @@ namespace Avalonia.Rendering
         public IDisposable TryLock()
         {
             if (Monitor.TryEnter(_lock))
-                return Disposable.Create(() => Monitor.Exit(_lock));
+                return new UnlockDisposable(_lock);
             return null;
         }
 
@@ -25,7 +27,27 @@ namespace Avalonia.Rendering
         public IDisposable Lock()
         {
             Monitor.Enter(_lock);
-            return Disposable.Create(() => Monitor.Exit(_lock));
+            return new UnlockDisposable(_lock);
+        }
+
+        private sealed class UnlockDisposable : IDisposable
+        {
+            private object _lock;
+
+            public UnlockDisposable(object @lock)
+            {
+                _lock = @lock;
+            }
+
+            public void Dispose()
+            {
+                object @lock = Interlocked.Exchange(ref _lock, null);
+
+                if (@lock != null)
+                {
+                    Monitor.Exit(@lock);
+                }
+            }
         }
     }
 }

+ 140 - 20
src/Avalonia.Visuals/Vector.cs

@@ -65,9 +65,7 @@ namespace Avalonia
         /// <param name="b">Second vector</param>
         /// <returns>The dot product</returns>
         public static double operator *(Vector a, Vector b)
-        {
-            return a.X * b.X + a.Y * b.Y;
-        }
+            => Dot(a, b);
 
         /// <summary>
         /// Scales a vector.
@@ -76,9 +74,7 @@ namespace Avalonia
         /// <param name="scale">The scaling factor.</param>
         /// <returns>The scaled vector.</returns>
         public static Vector operator *(Vector vector, double scale)
-        {
-            return new Vector(vector._x * scale, vector._y * scale);
-        }
+            => Multiply(vector, scale);
 
         /// <summary>
         /// Scales a vector.
@@ -87,14 +83,17 @@ namespace Avalonia
         /// <param name="scale">The divisor.</param>
         /// <returns>The scaled vector.</returns>
         public static Vector operator /(Vector vector, double scale)
-        {
-            return new Vector(vector._x / scale, vector._y / scale);
-        }
+            => Divide(vector, scale);
 
         /// <summary>
         /// Length of the vector
         /// </summary>
-        public double Length => Math.Sqrt(X * X + Y * Y);
+        public double Length => Math.Sqrt(SquaredLength);
+
+        /// <summary>
+        /// Squared Length of the vector
+        /// </summary>
+        public double SquaredLength => _x * _x + _y * _y;
 
         /// <summary>
         /// Negates a vector.
@@ -102,9 +101,7 @@ namespace Avalonia
         /// <param name="a">The vector.</param>
         /// <returns>The negated vector.</returns>
         public static Vector operator -(Vector a)
-        {
-            return new Vector(-a._x, -a._y);
-        }
+            => Negate(a);
 
         /// <summary>
         /// Adds two vectors.
@@ -113,9 +110,7 @@ namespace Avalonia
         /// <param name="b">The second vector.</param>
         /// <returns>A vector that is the result of the addition.</returns>
         public static Vector operator +(Vector a, Vector b)
-        {
-            return new Vector(a._x + b._x, a._y + b._y);
-        }
+            => Add(a, b);
 
         /// <summary>
         /// Subtracts two vectors.
@@ -124,9 +119,7 @@ namespace Avalonia
         /// <param name="b">The second vector.</param>
         /// <returns>A vector that is the result of the subtraction.</returns>
         public static Vector operator -(Vector a, Vector b)
-        {
-            return new Vector(a._x - b._x, a._y - b._y);
-        }
+            => Subtract(a, b);
 
         /// <summary>
         /// Check if two vectors are equal (bitwise).
@@ -155,7 +148,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 +200,131 @@ namespace Avalonia
         {
             return new Vector(_x, y);
         }
+
+        /// <summary>
+        /// Returns a normalized version of this vector.
+        /// </summary>
+        /// <returns>The normalized vector.</returns>
+        public Vector Normalize()
+            => Normalize(this);
+
+        /// <summary>
+        /// Returns a negated version of this vector.
+        /// </summary>
+        /// <returns>The negated vector.</returns>
+        public Vector Negate()
+            => Negate(this);
+
+        /// <summary>
+        /// Returns the dot product of two vectors.
+        /// </summary>
+        /// <param name="a">The first vector.</param>
+        /// <param name="b">The second vector.</param>
+        /// <returns>The dot product.</returns>
+        public static double Dot(Vector a, Vector b)
+            => a._x * b._x + a._y * b._y;
+
+        /// <summary>
+        /// Returns the cross product of two vectors.
+        /// </summary>
+        /// <param name="a">The first vector.</param>
+        /// <param name="b">The second vector.</param>
+        /// <returns>The cross product.</returns>
+        public static double Cross(Vector a, Vector b)
+            => a._x * b._y - a._y * b._x;
+
+        /// <summary>
+        /// Normalizes the given vector.
+        /// </summary>
+        /// <param name="vector">The vector</param>
+        /// <returns>The normalized vector.</returns>
+        public static Vector Normalize(Vector vector)
+            => Divide(vector, vector.Length);
+        
+        /// <summary>
+        /// Divides the first vector by the second.
+        /// </summary>
+        /// <param name="a">The first vector.</param>
+        /// <param name="b">The second vector.</param>
+        /// <returns>The scaled vector.</returns>
+        public static Vector Divide(Vector a, Vector b)
+            => new Vector(a._x / b._x, a._y / b._y);
+
+        /// <summary>
+        /// Divides the vector by the given scalar.
+        /// </summary>
+        /// <param name="vector">The vector</param>
+        /// <param name="scalar">The scalar value</param>
+        /// <returns>The scaled vector.</returns>
+        public static Vector Divide(Vector vector, double scalar)
+            => new Vector(vector._x / scalar, vector._y / scalar);
+
+        /// <summary>
+        /// Multiplies the first vector by the second.
+        /// </summary>
+        /// <param name="a">The first vector.</param>
+        /// <param name="b">The second vector.</param>
+        /// <returns>The scaled vector.</returns>
+        public static Vector Multiply(Vector a, Vector b)
+            => new Vector(a._x * b._x, a._y * b._y);
+
+        /// <summary>
+        /// Multiplies the vector by the given scalar.
+        /// </summary>
+        /// <param name="vector">The vector</param>
+        /// <param name="scalar">The scalar value</param>
+        /// <returns>The scaled vector.</returns>
+        public static Vector Multiply(Vector vector, double scalar)
+            => new Vector(vector._x * scalar, vector._y * scalar);
+
+        /// <summary>
+        /// Adds the second to the first vector
+        /// </summary>
+        /// <param name="a">The first vector.</param>
+        /// <param name="b">The second vector.</param>
+        /// <returns>The summed vector.</returns>
+        public static Vector Add(Vector a, Vector b)
+            => new Vector(a._x + b._x, a._y + b._y);
+
+        /// <summary>
+        /// Subtracts the second from the first vector
+        /// </summary>
+        /// <param name="a">The first vector.</param>
+        /// <param name="b">The second vector.</param>
+        /// <returns>The difference vector.</returns>
+        public static Vector Subtract(Vector a, Vector b)
+            => new Vector(a._x - b._x, a._y - b._y);
+
+        /// <summary>
+        /// Negates the vector
+        /// </summary>
+        /// <param name="vector">The vector to negate.</param>
+        /// <returns>The scaled vector.</returns>
+        public static Vector Negate(Vector vector)
+            => new Vector(-vector._x, -vector._y);
+
+        /// <summary>
+        /// Returnes the vector (0.0, 0.0)
+        /// </summary>
+        public static Vector Zero
+            => new Vector(0, 0);
+
+        /// <summary>
+        /// Returnes the vector (1.0, 1.0)
+        /// </summary>
+        public static Vector One
+            => new Vector(1, 1);
+
+        /// <summary>
+        /// Returnes the vector (1.0, 0.0)
+        /// </summary>
+        public static Vector UnitX
+            => new Vector(1, 0);
+
+        /// <summary>
+        /// Returnes the vector (0.0, 1.0)
+        /// </summary>
+        public static Vector UnitY
+            => new Vector(0, 1);
     }
 }

+ 6 - 6
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;

+ 4 - 5
src/Windows/Avalonia.Win32/WindowsMountedVolumeInfoListener.cs

@@ -10,8 +10,7 @@ namespace Avalonia.Win32
 {
     internal class WindowsMountedVolumeInfoListener : IDisposable
     {
-        private readonly CompositeDisposable _disposables;
-        private readonly ObservableCollection<MountedVolumeInfo> _targetObs = new ObservableCollection<MountedVolumeInfo>();
+        private readonly CompositeDisposable _disposables;        
         private bool _beenDisposed = false;
         private ObservableCollection<MountedVolumeInfo> 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);
             }
         }
 

+ 76 - 0
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 = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:c='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Converters;assembly=Avalonia.Markup.Xaml.UnitTests'>
+    <TextBlock Name='textBlock'>
+        <TextBlock.Text>
+            <MultiBinding Converter='{x:Static c:TestMultiValueConverter.Instance}' FallbackValue='bar'>
+                <Binding Path='Item1' />
+                <Binding Path='Item2' />
+            </MultiBinding>
+        </TextBlock.Text>
+    </TextBlock>
+</Window>";
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var textBlock = window.FindControl<TextBlock>("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<object> 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)";
+        }
+    }
+}

+ 28 - 18
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 = @"<Button xmlns='https://github.com/avaloniaui' Click='OnClick'/>";
             var loader = new AvaloniaXamlLoader();
             var target = new MyButton();
 
             loader.Load(xaml, rootInstance: target);
-            RaiseClick(target);
 
-            Assert.True(target.Clicked);
+            target.RaiseEvent(new RoutedEventArgs
+            {
+                RoutedEvent = Button.ClickEvent,
+            });
+
+            Assert.True(target.WasClicked);
         }
 
         [Fact]
-        public void Exception_Is_Thrown_If_Event_Not_Found()
+        public void Attached_Event_Is_Assigned()
         {
-            var xaml = @"<Button xmlns='https://github.com/avaloniaui' Click='NotFound'/>";
+            var xaml = @"<Button xmlns='https://github.com/avaloniaui' Gestures.Tapped='OnTapped'/>";
             var loader = new AvaloniaXamlLoader();
             var target = new MyButton();
 
-            XamlTestHelpers.AssertThrowsXamlException(() => loader.Load(xaml, rootInstance: target));
-        }
+            loader.Load(xaml, rootInstance: target);
 
-        private void RaiseClick(MyButton target)
-        {
-            target.RaiseEvent(new KeyEventArgs
+            target.RaiseEvent(new RoutedEventArgs
             {
-                RoutedEvent = Button.KeyDownEvent,
-                Key = Key.Enter,
+                RoutedEvent = Gestures.TappedEvent,
             });
+
+            Assert.True(target.WasTapped);
+        }
+
+        [Fact]
+        public void Exception_Is_Thrown_If_Event_Not_Found()
+        {
+            var xaml = @"<Button xmlns='https://github.com/avaloniaui' Click='NotFound'/>";
+            var loader = new AvaloniaXamlLoader();
+            var target = new MyButton();
+
+            XamlTestHelpers.AssertThrowsXamlException(() => loader.Load(xaml, rootInstance: target));
         }
 
         public class MyButton : Button
         {
-            public bool Clicked { get; private set; }
+            public bool WasClicked { get; private set; }
+            public bool WasTapped { get; private set; }
 
-            public void OnClick(object sender, RoutedEventArgs e)
-            {
-                Clicked = true;
-            }
+            public void OnClick(object sender, RoutedEventArgs e) => WasClicked = true;
+            public void OnTapped(object sender, RoutedEventArgs e) => WasTapped = true;
         }
     }
 }

+ 112 - 0
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));
+        }
+    }
+}