Browse Source

Merge branch 'master' into feature/tree-view-selection

Dariusz Komosiński 6 years ago
parent
commit
a19bad3cd5
74 changed files with 1736 additions and 508 deletions
  1. 1 1
      samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs
  2. 20 2
      samples/ControlCatalog/Pages/ContextMenuPage.xaml
  3. 2 0
      samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs
  4. 2 92
      samples/ControlCatalog/Pages/MenuPage.xaml.cs
  5. 8 0
      samples/ControlCatalog/SideBar.xaml
  6. 73 0
      samples/ControlCatalog/ViewModels/ContextMenuPageViewModel.cs
  7. 13 0
      samples/ControlCatalog/ViewModels/MenuItemViewModel.cs
  8. 89 0
      samples/ControlCatalog/ViewModels/MenuPageViewModel.cs
  9. 1 1
      src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
  10. 219 0
      src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs
  11. 2 2
      src/Avalonia.Controls/AppBuilderBase.cs
  12. 2 2
      src/Avalonia.Controls/Button.cs
  13. 30 66
      src/Avalonia.Controls/ContextMenu.cs
  14. 3 3
      src/Avalonia.Controls/Design.cs
  15. 3 1
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  16. 5 146
      src/Avalonia.Controls/Menu.cs
  17. 192 0
      src/Avalonia.Controls/MenuBase.cs
  18. 9 4
      src/Avalonia.Controls/MenuItem.cs
  19. 2 2
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  20. 47 33
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  21. 1 1
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  22. 18 7
      src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs
  23. 14 9
      src/Avalonia.Controls/Shapes/Shape.cs
  24. 1 1
      src/Avalonia.Controls/SystemDialog.cs
  25. 6 6
      src/Avalonia.DesignerSupport/DesignWindowLoader.cs
  26. 11 1
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  27. 1 1
      src/Avalonia.Input/InputElement.cs
  28. 5 9
      src/Avalonia.Input/MouseDevice.cs
  29. 2 0
      src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj
  30. 1 1
      src/Avalonia.Remote.Protocol/DefaultMessageTypeResolver.cs
  31. 8 0
      src/Avalonia.Remote.Protocol/DesignMessages.cs
  32. BIN
      src/Avalonia.Remote.Protocol/Key.snk
  33. 3 3
      src/Avalonia.Remote.Protocol/MetsysBson.cs
  34. 131 0
      src/Avalonia.Styling/Styling/OrSelector.cs
  35. 22 0
      src/Avalonia.Styling/Styling/Selectors.cs
  36. 76 3
      src/Avalonia.Styling/Styling/Styles.cs
  37. 16 6
      src/Avalonia.Themes.Default/ContextMenu.xaml
  38. 1 7
      src/Avalonia.Themes.Default/Separator.xaml
  39. 2 0
      src/Avalonia.Visuals/Media/DrawingContext.cs
  40. 5 2
      src/Avalonia.Visuals/Media/GeometryDrawing.cs
  41. 2 2
      src/Avalonia.Visuals/Rendering/RenderLoop.cs
  42. 8 3
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
  43. 2 2
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  44. 4 3
      src/Avalonia.Visuals/Visual.cs
  45. 1 1
      src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs
  46. 1 0
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  47. 3 1
      src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs
  48. 9 31
      src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs
  49. 99 0
      src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaEventConverter.cs
  50. 13 1
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaMemberAttributeProvider.cs
  51. 36 9
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs
  52. 1 1
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs
  53. 2 2
      src/Markup/Avalonia.Markup/Data/RelativeSource.cs
  54. 10 0
      src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs
  55. 21 9
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
  56. 20 0
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
  57. 7 1
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  58. 1 1
      src/Windows/Avalonia.Win32/SystemDialogImpl.cs
  59. 71 0
      tests/Avalonia.Base.UnitTests/WeakEventHandlerManagerTests.cs
  60. 44 27
      tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs
  61. 16 0
      tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs
  62. 11 0
      tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs
  63. 30 2
      tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs
  64. 33 0
      tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
  65. 9 0
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs
  66. 7 0
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests_Errors.cs
  67. 16 0
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
  68. 7 0
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs
  69. 66 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs
  70. 20 0
      tests/Avalonia.RenderTests/Shapes/RectangleTests.cs
  71. 106 0
      tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs
  72. 13 0
      tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs
  73. BIN
      tests/TestFiles/Direct2D1/Shapes/Rectangle/Rectangle_0px_Stroke.expected.png
  74. BIN
      tests/TestFiles/Skia/Shapes/Rectangle/Rectangle_0px_Stroke.expected.png

+ 1 - 1
samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs

@@ -21,7 +21,7 @@ namespace BindingDemo.ViewModels
                 }
                 else
                 {
-                    throw new ArgumentOutOfRangeException("Value must be less than 10.");
+                    throw new ArgumentOutOfRangeException(nameof(value), "Value must be less than 10.");
                 }
             }
         }

+ 20 - 2
samples/ControlCatalog/Pages/ContextMenuPage.xaml

@@ -10,7 +10,8 @@
               HorizontalAlignment="Center"
               Spacing="16">
             <Border Background="{DynamicResource ThemeAccentBrush}"
-              Padding="48,48,48,48">
+                    Margin="16"
+                    Padding="48,48,48,48">
                 <Border.ContextMenu>
                     <ContextMenu>
                         <MenuItem Header="Standard _Menu Item"/>
@@ -31,7 +32,24 @@
                         </MenuItem>
                     </ContextMenu>
                 </Border.ContextMenu>
-                <TextBlock Text="Right Click Here"/>
+                <TextBlock Text="Defined in XAML"/>
+            </Border>
+            <Border Background="{DynamicResource ThemeAccentBrush}"
+                    Margin="16"
+                    Padding="48,48,48,48">
+                <Border.ContextMenu>
+                    <ContextMenu Items="{Binding MenuItems}">
+                        <ContextMenu.Styles>
+                            <Style Selector="MenuItem">
+                                <Setter Property="Header" Value="{Binding Header}"/>
+                                <Setter Property="Items" Value="{Binding Items}"/>
+                                <Setter Property="Command" Value="{Binding Command}"/>
+                                <Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>
+                            </Style>
+                        </ContextMenu.Styles>
+                    </ContextMenu>
+                </Border.ContextMenu>
+                <TextBlock Text="Dynamically Generated"/>
             </Border>
         </StackPanel>
     </StackPanel>

+ 2 - 0
samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs

@@ -1,5 +1,6 @@
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
 
 namespace ControlCatalog.Pages
 {
@@ -8,6 +9,7 @@ namespace ControlCatalog.Pages
         public ContextMenuPage()
         {
             this.InitializeComponent();
+            DataContext = new ContextMenuPageViewModel();
         }
 
         private void InitializeComponent()

+ 2 - 92
samples/ControlCatalog/Pages/MenuPage.xaml.cs

@@ -4,6 +4,7 @@ using System.Threading.Tasks;
 using System.Windows.Input;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
 using ReactiveUI;
 
 namespace ControlCatalog.Pages
@@ -13,51 +14,7 @@ namespace ControlCatalog.Pages
         public MenuPage()
         {
             this.InitializeComponent();
-            var vm = new MenuPageViewModel();
-
-            vm.MenuItems = new[]
-            {
-                new MenuItemViewModel
-                {
-                    Header = "_File",
-                    Items = new[]
-                    {
-                        new MenuItemViewModel { Header = "_Open...", Command = vm.OpenCommand },
-                        new MenuItemViewModel { Header = "Save", Command = vm.SaveCommand },
-                        new MenuItemViewModel { Header = "-" },
-                        new MenuItemViewModel
-                        {
-                            Header = "Recent",
-                            Items = new[]
-                            {
-                                new MenuItemViewModel
-                                {
-                                    Header = "File1.txt",
-                                    Command = vm.OpenRecentCommand,
-                                    CommandParameter = @"c:\foo\File1.txt"
-                                },
-                                new MenuItemViewModel
-                                {
-                                    Header = "File2.txt",
-                                    Command = vm.OpenRecentCommand,
-                                    CommandParameter = @"c:\foo\File2.txt"
-                                },
-                            }
-                        },
-                    }
-                },
-                new MenuItemViewModel
-                {
-                    Header = "_Edit",
-                    Items = new[]
-                    {
-                        new MenuItemViewModel { Header = "_Copy" },
-                        new MenuItemViewModel { Header = "_Paste" },
-                    }
-                }
-            };
-
-            DataContext = vm;
+            DataContext = new MenuPageViewModel();
         }
 
         private void InitializeComponent()
@@ -65,51 +22,4 @@ namespace ControlCatalog.Pages
             AvaloniaXamlLoader.Load(this);
         }
     }
-
-    public class MenuPageViewModel
-    {
-        public MenuPageViewModel()
-        {
-            OpenCommand = ReactiveCommand.CreateFromTask(Open);
-            SaveCommand = ReactiveCommand.Create(Save);
-            OpenRecentCommand = ReactiveCommand.Create<string>(OpenRecent);
-        }
-
-        public IReadOnlyList<MenuItemViewModel> MenuItems { get; set; }
-        public ReactiveCommand<Unit, Unit> OpenCommand { get; }
-        public ReactiveCommand<Unit, Unit> SaveCommand { get; }
-        public ReactiveCommand<string, Unit> OpenRecentCommand { get; }
-
-        public async Task Open()
-        {
-            var dialog = new OpenFileDialog();
-            var result = await dialog.ShowAsync(App.Current.MainWindow);
-
-            if (result != null)
-            {
-                foreach (var path in result)
-                {
-                    System.Diagnostics.Debug.WriteLine($"Opened: {path}");
-                }
-            }
-        }
-
-        public void Save()
-        {
-            System.Diagnostics.Debug.WriteLine("Save");
-        }
-
-        public void OpenRecent(string path)
-        {
-            System.Diagnostics.Debug.WriteLine($"Open recent: {path}");
-        }
-    }
-
-    public class MenuItemViewModel
-    {
-        public string Header { get; set; }
-        public ICommand Command { get; set; }
-        public object CommandParameter { get; set; }
-        public IList<MenuItemViewModel> Items { get; set; }
-    }
 }

+ 8 - 0
samples/ControlCatalog/SideBar.xaml

@@ -1,6 +1,14 @@
 <Styles xmlns="https://github.com/avaloniaui"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         x:Class="ControlCatalog.SideBar">
+    <Design.PreviewWith>
+        <Border Padding="20">
+            <TabControl Classes="sidebar">
+                <TabItem Header="Item1"/>
+                <TabItem Header="Item2"/>
+            </TabControl> 
+        </Border>
+    </Design.PreviewWith>
     <Style Selector="TabControl.sidebar">
         <Setter Property="TabStripPlacement" Value="Left"/>
         <Setter Property="Padding" Value="8 0 0 0"/>

+ 73 - 0
samples/ControlCatalog/ViewModels/ContextMenuPageViewModel.cs

@@ -0,0 +1,73 @@
+using System.Collections.Generic;
+using System.Reactive;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using ReactiveUI;
+
+namespace ControlCatalog.ViewModels
+{
+    public class ContextMenuPageViewModel
+    {
+        public ContextMenuPageViewModel()
+        {
+            OpenCommand = ReactiveCommand.CreateFromTask(Open);
+            SaveCommand = ReactiveCommand.Create(Save);
+            OpenRecentCommand = ReactiveCommand.Create<string>(OpenRecent);
+
+            MenuItems = new[]
+            {
+                new MenuItemViewModel { Header = "_Open...", Command = OpenCommand },
+                new MenuItemViewModel { Header = "Save", Command = SaveCommand },
+                new MenuItemViewModel { Header = "-" },
+                new MenuItemViewModel
+                {
+                    Header = "Recent",
+                    Items = new[]
+                    {
+                        new MenuItemViewModel
+                        {
+                            Header = "File1.txt",
+                            Command = OpenRecentCommand,
+                            CommandParameter = @"c:\foo\File1.txt"
+                        },
+                        new MenuItemViewModel
+                        {
+                            Header = "File2.txt",
+                            Command = OpenRecentCommand,
+                            CommandParameter = @"c:\foo\File2.txt"
+                        },
+                    }
+                },
+            };
+        }
+
+        public IReadOnlyList<MenuItemViewModel> MenuItems { get; set; }
+        public ReactiveCommand<Unit, Unit> OpenCommand { get; }
+        public ReactiveCommand<Unit, Unit> SaveCommand { get; }
+        public ReactiveCommand<string, Unit> OpenRecentCommand { get; }
+
+        public async Task Open()
+        {
+            var dialog = new OpenFileDialog();
+            var result = await dialog.ShowAsync(App.Current.MainWindow);
+
+            if (result != null)
+            {
+                foreach (var path in result)
+                {
+                    System.Diagnostics.Debug.WriteLine($"Opened: {path}");
+                }
+            }
+        }
+
+        public void Save()
+        {
+            System.Diagnostics.Debug.WriteLine("Save");
+        }
+
+        public void OpenRecent(string path)
+        {
+            System.Diagnostics.Debug.WriteLine($"Open recent: {path}");
+        }
+    }
+}

+ 13 - 0
samples/ControlCatalog/ViewModels/MenuItemViewModel.cs

@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using System.Windows.Input;
+
+namespace ControlCatalog.ViewModels
+{
+    public class MenuItemViewModel
+    {
+        public string Header { get; set; }
+        public ICommand Command { get; set; }
+        public object CommandParameter { get; set; }
+        public IList<MenuItemViewModel> Items { get; set; }
+    }
+}

+ 89 - 0
samples/ControlCatalog/ViewModels/MenuPageViewModel.cs

@@ -0,0 +1,89 @@
+using System.Collections.Generic;
+using System.Reactive;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using ReactiveUI;
+
+namespace ControlCatalog.ViewModels
+{
+    public class MenuPageViewModel
+    {
+        public MenuPageViewModel()
+        {
+            OpenCommand = ReactiveCommand.CreateFromTask(Open);
+            SaveCommand = ReactiveCommand.Create(Save);
+            OpenRecentCommand = ReactiveCommand.Create<string>(OpenRecent);
+
+            MenuItems = new[]
+            {
+                new MenuItemViewModel
+                {
+                    Header = "_File",
+                    Items = new[]
+                    {
+                        new MenuItemViewModel { Header = "_Open...", Command = OpenCommand },
+                        new MenuItemViewModel { Header = "Save", Command = SaveCommand },
+                        new MenuItemViewModel { Header = "-" },
+                        new MenuItemViewModel
+                        {
+                            Header = "Recent",
+                            Items = new[]
+                            {
+                                new MenuItemViewModel
+                                {
+                                    Header = "File1.txt",
+                                    Command = OpenRecentCommand,
+                                    CommandParameter = @"c:\foo\File1.txt"
+                                },
+                                new MenuItemViewModel
+                                {
+                                    Header = "File2.txt",
+                                    Command = OpenRecentCommand,
+                                    CommandParameter = @"c:\foo\File2.txt"
+                                },
+                            }
+                        },
+                    }
+                },
+                new MenuItemViewModel
+                {
+                    Header = "_Edit",
+                    Items = new[]
+                    {
+                        new MenuItemViewModel { Header = "_Copy" },
+                        new MenuItemViewModel { Header = "_Paste" },
+                    }
+                }
+            };
+        }
+
+        public IReadOnlyList<MenuItemViewModel> MenuItems { get; set; }
+        public ReactiveCommand<Unit, Unit> OpenCommand { get; }
+        public ReactiveCommand<Unit, Unit> SaveCommand { get; }
+        public ReactiveCommand<string, Unit> OpenRecentCommand { get; }
+
+        public async Task Open()
+        {
+            var dialog = new OpenFileDialog();
+            var result = await dialog.ShowAsync(App.Current.MainWindow);
+
+            if (result != null)
+            {
+                foreach (var path in result)
+                {
+                    System.Diagnostics.Debug.WriteLine($"Opened: {path}");
+                }
+            }
+        }
+
+        public void Save()
+        {
+            System.Diagnostics.Debug.WriteLine("Save");
+        }
+
+        public void OpenRecent(string path)
+        {
+            System.Diagnostics.Debug.WriteLine($"Open recent: {path}");
+        }
+    }
+}

+ 1 - 1
src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs

@@ -21,7 +21,7 @@ namespace Avalonia.Data.Core.Plugins
             {
                 if (method.GetParameters().Length + (method.ReturnType == typeof(void) ? 0 : 1) > 8)
                 {
-                    var exception = new ArgumentException("Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", nameof(method));
+                    var exception = new ArgumentException("Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", nameof(methodName));
                     return new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
                 }
 

+ 219 - 0
src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs

@@ -0,0 +1,219 @@
+// 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.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+namespace Avalonia.Utilities
+{
+    /// <summary>
+    /// Manages subscriptions to events using weak listeners.
+    /// </summary>
+    public static class WeakEventHandlerManager
+    {
+        /// <summary>
+        /// Subscribes to an event on an object using a weak subscription.
+        /// </summary>
+        /// <typeparam name="TTarget">The type of the target.</typeparam>
+        /// <typeparam name="TEventArgs">The type of the event arguments.</typeparam>
+        /// <param name="target">The event source.</param>
+        /// <param name="eventName">The name of the event.</param>
+        /// <param name="subscriber">The subscriber.</param>
+        public static void Subscribe<TTarget, TEventArgs, TSubscriber>(TTarget target, string eventName, EventHandler<TEventArgs> subscriber)
+            where TEventArgs : EventArgs where TSubscriber : class
+        {
+            var dic = SubscriptionTypeStorage<TEventArgs, TSubscriber>.Subscribers.GetOrCreateValue(target);
+            Subscription<TEventArgs, TSubscriber> sub;
+
+            if (!dic.TryGetValue(eventName, out sub))
+            {
+                dic[eventName] = sub = new Subscription<TEventArgs, TSubscriber>(dic, typeof(TTarget), target, eventName);
+            }
+
+            sub.Add(subscriber);
+        }
+
+        /// <summary>
+        /// Unsubscribes from an event.
+        /// </summary>
+        /// <typeparam name="TEventArgs">The type of the event arguments.</typeparam>
+        /// <param name="target">The event source.</param>
+        /// <param name="eventName">The name of the event.</param>
+        /// <param name="subscriber">The subscriber.</param>
+        public static void Unsubscribe<TEventArgs, TSubscriber>(object target, string eventName, EventHandler<TEventArgs> subscriber)
+            where TEventArgs : EventArgs where TSubscriber : class
+        {
+            SubscriptionDic<TEventArgs, TSubscriber> dic;
+
+            if (SubscriptionTypeStorage<TEventArgs, TSubscriber>.Subscribers.TryGetValue(target, out dic))
+            {
+                Subscription<TEventArgs, TSubscriber> sub;
+
+                if (dic.TryGetValue(eventName, out sub))
+                {
+                    sub.Remove(subscriber);
+                }
+            }
+        }
+
+        private static class SubscriptionTypeStorage<TArgs, TSubscriber>
+            where TArgs : EventArgs where TSubscriber : class
+        {
+            public static readonly ConditionalWeakTable<object, SubscriptionDic<TArgs, TSubscriber>> Subscribers
+                = new ConditionalWeakTable<object, SubscriptionDic<TArgs, TSubscriber>>();
+        }
+
+        private class SubscriptionDic<T, TSubscriber> : Dictionary<string, Subscription<T, TSubscriber>>
+            where T : EventArgs where TSubscriber : class
+        {
+        }
+
+        private static readonly Dictionary<Type, Dictionary<string, EventInfo>> Accessors
+            = new Dictionary<Type, Dictionary<string, EventInfo>>();
+
+        private class Subscription<T, TSubscriber> where T : EventArgs where TSubscriber : class
+        {
+            private readonly EventInfo _info;
+            private readonly SubscriptionDic<T, TSubscriber> _sdic;
+            private readonly object _target;
+            private readonly string _eventName;
+            private readonly Delegate _delegate;
+
+            private Descriptor[] _data = new Descriptor[2];
+            private int _count = 0;
+
+            delegate void CallerDelegate(TSubscriber s, object sender, T args);
+            
+            struct Descriptor
+            {
+                public WeakReference<TSubscriber> Subscriber;
+                public CallerDelegate Caller;
+            }
+
+            private static Dictionary<MethodInfo, CallerDelegate> s_Callers =
+                new Dictionary<MethodInfo, CallerDelegate>();
+            
+            public Subscription(SubscriptionDic<T, TSubscriber> sdic, Type targetType, object target, string eventName)
+            {
+                _sdic = sdic;
+                _target = target;
+                _eventName = eventName;
+                Dictionary<string, EventInfo> evDic;
+                if (!Accessors.TryGetValue(targetType, out evDic))
+                    Accessors[targetType] = evDic = new Dictionary<string, EventInfo>();
+
+                if (!evDic.TryGetValue(eventName, out _info))
+                {
+                    var ev = targetType.GetRuntimeEvents().FirstOrDefault(x => x.Name == eventName);
+
+                    if (ev == null)
+                    {
+                        throw new ArgumentException(
+                            $"The event {eventName} was not found on {target.GetType()}.");
+                    }
+
+                    evDic[eventName] = _info = ev;
+                }
+
+                var del = new Action<object, T>(OnEvent);
+                _delegate = del.GetMethodInfo().CreateDelegate(_info.EventHandlerType, del.Target);
+                _info.AddMethod.Invoke(target, new[] { _delegate });
+            }
+
+            void Destroy()
+            {
+                _info.RemoveMethod.Invoke(_target, new[] { _delegate });
+                _sdic.Remove(_eventName);
+            }
+
+            public void Add(EventHandler<T> s)
+            {
+                Compact(true);
+                if (_count == _data.Length)
+                {
+                    //Extend capacity
+                    var ndata = new Descriptor[_data.Length*2];
+                    Array.Copy(_data, ndata, _data.Length);
+                    _data = ndata;
+                }
+
+                var subscriber = (TSubscriber)s.Target;
+                if (!s_Callers.TryGetValue(s.Method, out var caller))
+                    s_Callers[s.Method] = caller =
+                        (CallerDelegate)Delegate.CreateDelegate(typeof(CallerDelegate), null, s.Method);
+                _data[_count] = new Descriptor
+                {
+                    Caller = caller,
+                    Subscriber = new WeakReference<TSubscriber>(subscriber)
+                };
+                _count++;
+            }
+
+            public void Remove(EventHandler<T> s)
+            {
+                var removed = false;
+
+                for (int c = 0; c < _count; ++c)
+                {
+                    var reference = _data[c].Subscriber;
+                    TSubscriber instance;
+
+                    if (reference != null && reference.TryGetTarget(out instance) && instance == s)
+                    {
+                        _data[c] = default;
+                        removed = true;
+                    }
+                }
+
+                if (removed)
+                {
+                    Compact();
+                }
+            }
+
+            void Compact(bool preventDestroy = false)
+            {
+                int empty = -1;
+                for (int c = 0; c < _count; c++)
+                {
+                    var r = _data[c];
+                    //Mark current index as first empty
+                    if (r.Subscriber == null && empty == -1)
+                        empty = c;
+                    //If current element isn't null and we have an empty one
+                    if (r.Subscriber != null && empty != -1)
+                    {
+                        _data[c] = default;
+                        _data[empty] = r;
+                        empty++;
+                    }
+                }
+                if (empty != -1)
+                    _count = empty;
+                if (_count == 0 && !preventDestroy)
+                    Destroy();
+            }
+
+            void OnEvent(object sender, T eventArgs)
+            {
+                var needCompact = false;
+                for(var c=0; c<_count; c++)
+                {
+                    var r = _data[c].Subscriber;
+                    TSubscriber sub;
+                    if (r.TryGetTarget(out sub))
+                    {
+                        _data[c].Caller(sub, sender, eventArgs);
+                    }
+                    else
+                        needCompact = true;
+                }
+                if (needCompact)
+                    Compact();
+            }
+        }
+    }
+}

+ 2 - 2
src/Avalonia.Controls/AppBuilderBase.cs

@@ -210,7 +210,7 @@ namespace Avalonia.Controls
             var platformClassName = assemblyName.Replace("Avalonia.", string.Empty) + "Platform";
             var platformClassFullName = assemblyName + "." + platformClassName;
             var platformClass = assembly.GetType(platformClassFullName);
-            var init = platformClass.GetRuntimeMethod("Initialize", new Type[0]);
+            var init = platformClass.GetRuntimeMethod("Initialize", Type.EmptyTypes);
             init.Invoke(null, null);
         };
 
@@ -245,7 +245,7 @@ namespace Avalonia.Controls
                                      select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors
                                              where constructor.GetParameters().Length == 0 && !constructor.IsStatic
                                              select constructor).Single() into constructor
-                                     select (Action)(() => constructor.Invoke(new object[0]));
+                                     select (Action)(() => constructor.Invoke(Array.Empty<object>()));
             Delegate.Combine(moduleInitializers.ToArray()).DynamicInvoke();
         }
 

+ 2 - 2
src/Avalonia.Controls/Button.cs

@@ -32,6 +32,8 @@ namespace Avalonia.Controls
     /// </summary>
     public class Button : ContentControl
     {
+        private ICommand _command;
+
         /// <summary>
         /// Defines the <see cref="ClickMode"/> property.
         /// </summary>
@@ -69,8 +71,6 @@ namespace Avalonia.Controls
         public static readonly RoutedEvent<RoutedEventArgs> ClickEvent =
             RoutedEvent.Register<Button, RoutedEventArgs>(nameof(Click), RoutingStrategies.Bubble);
 
-        private ICommand _command;
-
         public static readonly StyledProperty<bool> IsPressedProperty =
             AvaloniaProperty.Register<Button, bool>(nameof(IsPressed));
 

+ 30 - 66
src/Avalonia.Controls/ContextMenu.cs

@@ -1,34 +1,31 @@
 using System;
-using System.Reactive.Linq;
-using System.Linq;
 using System.ComponentModel;
+using System.Linq;
+using System.Reactive.Linq;
+using Avalonia.Controls.Generators;
 using Avalonia.Controls.Platform;
-using System.Collections.Generic;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
 using Avalonia.Input;
 using Avalonia.LogicalTree;
-using Avalonia.Controls.Primitives;
 
 namespace Avalonia.Controls
 {
-    public class ContextMenu : SelectingItemsControl, IMenu
+    /// <summary>
+    /// A control context menu.
+    /// </summary>
+    public class ContextMenu : MenuBase
     {
-        private readonly IMenuInteractionHandler _interaction;
-        private bool _isOpen;
+        private static readonly ITemplate<IPanel> DefaultPanel =
+            new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Vertical });
         private Popup _popup;
 
-        /// <summary>
-        /// Defines the <see cref="IsOpen"/> property.
-        /// </summary>
-        public static readonly DirectProperty<ContextMenu, bool> IsOpenProperty =
-                            AvaloniaProperty.RegisterDirect<ContextMenu, bool>(nameof(IsOpen), o => o.IsOpen);
-
         /// <summary>
         /// Initializes a new instance of the <see cref="ContextMenu"/> class.
         /// </summary>
         public ContextMenu()
+            : this(new DefaultMenuInteractionHandler(true))
         {
-            _interaction = AvaloniaLocator.Current.GetService<IMenuInteractionHandler>() ??
-                new DefaultMenuInteractionHandler();
         }
 
         /// <summary>
@@ -36,10 +33,8 @@ namespace Avalonia.Controls
         /// </summary>
         /// <param name="interactionHandler">The menu interaction handler.</param>
         public ContextMenu(IMenuInteractionHandler interactionHandler)
+            : base(interactionHandler)
         {
-            Contract.Requires<ArgumentNullException>(interactionHandler != null);
-
-            _interaction = interactionHandler;
         }
 
         /// <summary>
@@ -47,44 +42,10 @@ namespace Avalonia.Controls
         /// </summary>
         static ContextMenu()
         {
+            ItemsPanelProperty.OverrideDefaultValue(typeof(ContextMenu), DefaultPanel);
             ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
         }
 
-        /// <summary>
-        /// Gets a value indicating whether the popup is open
-        /// </summary>
-        public bool IsOpen => _isOpen;
-
-        /// <inheritdoc/>
-        IMenuInteractionHandler IMenu.InteractionHandler => _interaction;
-
-        /// <inheritdoc/>
-        IMenuItem IMenuElement.SelectedItem
-        {
-            get
-            {
-                var index = SelectedIndex;
-                return (index != -1) ?
-                    (IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) :
-                    null;
-            }
-            set
-            {
-                SelectedIndex = ItemContainerGenerator.IndexFromContainer(value);
-            }
-        }
-
-        /// <inheritdoc/>
-        IEnumerable<IMenuItem> IMenuElement.SubItems
-        {
-            get
-            {
-                return ItemContainerGenerator.Containers
-                    .Select(x => x.ContainerControl)
-                    .OfType<IMenuItem>();
-            }
-        }
-
         /// <summary>
         /// Occurs when the value of the
         /// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" />
@@ -121,7 +82,7 @@ namespace Avalonia.Controls
         /// <summary>
         /// Opens the menu.
         /// </summary>
-        public void Open() => Open(null);
+        public override void Open() => Open(null);
 
         /// <summary>
         /// Opens a context menu on the specified control.
@@ -139,21 +100,20 @@ namespace Avalonia.Controls
                     ObeyScreenEdges = true
                 };
 
+                _popup.Opened += PopupOpened;
                 _popup.Closed += PopupClosed;
-                _interaction.Attach(this);
             }
 
             ((ISetLogicalParent)_popup).SetParent(control);
             _popup.Child = this;
             _popup.IsOpen = true;
-
-            SetAndRaise(IsOpenProperty, ref _isOpen, true);
+            IsOpen = true;
         }
 
         /// <summary>
         /// Closes the menu.
         /// </summary>
-        public void Close()
+        public override void Close()
         {
             if (_popup != null && _popup.IsVisible)
             {
@@ -161,8 +121,17 @@ namespace Avalonia.Controls
             }
 
             SelectedIndex = -1;
+            IsOpen = false;
+        }
+
+        protected override IItemContainerGenerator CreateItemContainerGenerator()
+        {
+            return new MenuItemContainerGenerator(this);
+        }
 
-            SetAndRaise(IsOpenProperty, ref _isOpen, false);
+        private void PopupOpened(object sender, EventArgs e)
+        {
+            Focus();
         }
 
         private void PopupClosed(object sender, EventArgs e)
@@ -176,7 +145,7 @@ namespace Avalonia.Controls
                     i.IsSubMenuOpen = false;
                 }
 
-                contextMenu._isOpen = false;
+                contextMenu.IsOpen = false;
                 contextMenu.SelectedIndex = -1;
             }
         }
@@ -186,7 +155,7 @@ namespace Avalonia.Controls
             var control = (Control)sender;
             var contextMenu = control.ContextMenu;
 
-            if (control.ContextMenu._isOpen)
+            if (control.ContextMenu.IsOpen)
             {
                 if (contextMenu.CancelClosing())
                     return;
@@ -218,10 +187,5 @@ namespace Avalonia.Controls
             ContextMenuOpening?.Invoke(this, eventArgs);
             return eventArgs.Cancel;
         }
-
-        bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap)
-        {
-            throw new NotImplementedException();
-        }
     }
 }

+ 3 - 3
src/Avalonia.Controls/Design.cs

@@ -48,14 +48,14 @@ namespace Avalonia.Controls
         }
         
         public static readonly AttachedProperty<Control> PreviewWithProperty = AvaloniaProperty
-            .RegisterAttached<Style, Control>("PreviewWith", typeof (Design));
+            .RegisterAttached<AvaloniaObject, Control>("PreviewWith", typeof (Design));
 
-        public static void SetPreviewWith(Style target, Control control)
+        public static void SetPreviewWith(AvaloniaObject target, Control control)
         {
             target.SetValue(PreviewWithProperty, control);
         }
 
-        public static Control GetPreviewWith(Style target)
+        public static Control GetPreviewWith(AvaloniaObject target)
         {
             return target.GetValue(PreviewWithProperty);
         }

+ 3 - 1
src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs

@@ -11,11 +11,13 @@ namespace Avalonia.Controls.Embedding.Offscreen
     {
         private double _scaling = 1;
         private Size _clientSize;
+
         public IInputRoot InputRoot { get; private set; }
+        public bool IsDisposed { get; private set; }
 
         public virtual void Dispose()
         {
-            //No-op
+            IsDisposed = true;
         }
 
         public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer(root);

+ 5 - 146
src/Avalonia.Controls/Menu.cs

@@ -1,56 +1,26 @@
 // 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.Collections.Generic;
-using System.Linq;
-using Avalonia.Controls.Generators;
 using Avalonia.Controls.Platform;
-using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Input;
 using Avalonia.Interactivity;
-using Avalonia.LogicalTree;
 
 namespace Avalonia.Controls
 {
     /// <summary>
     /// A top-level menu control.
     /// </summary>
-    public class Menu : SelectingItemsControl, IFocusScope, IMainMenu, IMenu
+    public class Menu : MenuBase, IMainMenu
     {
-        /// <summary>
-        /// Defines the <see cref="IsOpen"/> property.
-        /// </summary>
-        public static readonly DirectProperty<Menu, bool> IsOpenProperty =
-            AvaloniaProperty.RegisterDirect<Menu, bool>(
-                nameof(IsOpen),
-                o => o.IsOpen);
-
-        /// <summary>
-        /// Defines the <see cref="MenuOpened"/> event.
-        /// </summary>
-        public static readonly RoutedEvent<RoutedEventArgs> MenuOpenedEvent =
-            RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(MenuOpened), RoutingStrategies.Bubble);
-
-        /// <summary>
-        /// Defines the <see cref="MenuClosed"/> event.
-        /// </summary>
-        public static readonly RoutedEvent<RoutedEventArgs> MenuClosedEvent =
-            RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(MenuClosed), RoutingStrategies.Bubble);
-
         private static readonly ITemplate<IPanel> DefaultPanel =
             new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Horizontal });
-        private readonly IMenuInteractionHandler _interaction;
-        private bool _isOpen;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="Menu"/> class.
         /// </summary>
         public Menu()
         {
-            _interaction = AvaloniaLocator.Current.GetService<IMenuInteractionHandler>() ?? 
-                new DefaultMenuInteractionHandler();
         }
 
         /// <summary>
@@ -58,82 +28,17 @@ namespace Avalonia.Controls
         /// </summary>
         /// <param name="interactionHandler">The menu interaction handler.</param>
         public Menu(IMenuInteractionHandler interactionHandler)
+            : base(interactionHandler)
         {
-            Contract.Requires<ArgumentNullException>(interactionHandler != null);
-
-            _interaction = interactionHandler;
         }
 
-        /// <summary>
-        /// Initializes static members of the <see cref="Menu"/> class.
-        /// </summary>
         static Menu()
         {
             ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel);
-            MenuItem.SubmenuOpenedEvent.AddClassHandler<Menu>(x => x.OnSubmenuOpened);
-        }
-
-        /// <summary>
-        /// Gets a value indicating whether the menu is open.
-        /// </summary>
-        public bool IsOpen
-        {
-            get { return _isOpen; }
-            private set { SetAndRaise(IsOpenProperty, ref _isOpen, value); }
-        }
-
-        /// <inheritdoc/>
-        IMenuInteractionHandler IMenu.InteractionHandler => _interaction;
-
-        /// <inheritdoc/>
-        IMenuItem IMenuElement.SelectedItem
-        {
-            get
-            {
-                var index = SelectedIndex;
-                return (index != -1) ?
-                    (IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) :
-                    null;
-            }
-            set
-            {
-                SelectedIndex = ItemContainerGenerator.IndexFromContainer(value);
-            }
         }
 
         /// <inheritdoc/>
-        IEnumerable<IMenuItem> IMenuElement.SubItems
-        {
-            get
-            {
-                return ItemContainerGenerator.Containers
-                    .Select(x => x.ContainerControl)
-                    .OfType<IMenuItem>();
-            }
-        }
-
-        /// <summary>
-        /// Occurs when a <see cref="Menu"/> is opened.
-        /// </summary>
-        public event EventHandler<RoutedEventArgs> MenuOpened
-        {
-            add { AddHandler(MenuOpenedEvent, value); }
-            remove { RemoveHandler(MenuOpenedEvent, value); }
-        }
-
-        /// <summary>
-        /// Occurs when a <see cref="Menu"/> is closed.
-        /// </summary>
-        public event EventHandler<RoutedEventArgs> MenuClosed
-        {
-            add { AddHandler(MenuClosedEvent, value); }
-            remove { RemoveHandler(MenuClosedEvent, value); }
-        }
-
-        /// <summary>
-        /// Closes the menu.
-        /// </summary>
-        public void Close()
+        public override void Close()
         {
             if (IsOpen)
             {
@@ -153,10 +58,8 @@ namespace Avalonia.Controls
             }
         }
 
-        /// <summary>
-        /// Opens the menu in response to the Alt/F10 key.
-        /// </summary>
-        public void Open()
+        /// <inheritdoc/>
+        public override void Open()
         {
             if (!IsOpen)
             {
@@ -170,15 +73,6 @@ namespace Avalonia.Controls
             }
         }
 
-        /// <inheritdoc/>
-        bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
-
-        /// <inheritdoc/>
-        protected override IItemContainerGenerator CreateItemContainerGenerator()
-        {
-            return new ItemContainerGenerator<MenuItem>(this, MenuItem.HeaderProperty, null);
-        }
-
         /// <inheritdoc/>
         protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
         {
@@ -190,41 +84,6 @@ namespace Avalonia.Controls
             {
                 inputRoot.AccessKeyHandler.MainMenu = this;
             }
-
-            _interaction.Attach(this);
-        }
-
-        /// <inheritdoc/>
-        protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
-        {
-            base.OnDetachedFromVisualTree(e);
-            _interaction.Detach(this);
-        }
-
-        /// <inheritdoc/>
-        protected override void OnKeyDown(KeyEventArgs e)
-        {
-            // Don't handle here: let the interaction handler handle it.
-        }
-
-        /// <summary>
-        /// Called when a submenu opens somewhere in the menu.
-        /// </summary>
-        /// <param name="e">The event args.</param>
-        protected virtual void OnSubmenuOpened(RoutedEventArgs e)
-        {
-            if (e.Source is MenuItem menuItem && menuItem.Parent == this)
-            {
-                foreach (var child in this.GetLogicalChildren().OfType<MenuItem>())
-                {
-                    if (child != menuItem && child.IsSubMenuOpen)
-                    {
-                        child.IsSubMenuOpen = false;
-                    }
-                }
-            }
-
-            IsOpen = true;
         }
     }
 }

+ 192 - 0
src/Avalonia.Controls/MenuBase.cs

@@ -0,0 +1,192 @@
+// 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.Collections.Generic;
+using System.Linq;
+using Avalonia.Controls.Generators;
+using Avalonia.Controls.Platform;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.LogicalTree;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Base class for menu controls.
+    /// </summary>
+    public abstract class MenuBase : SelectingItemsControl, IFocusScope, IMenu
+    {
+        /// <summary>
+        /// Defines the <see cref="IsOpen"/> property.
+        /// </summary>
+        public static readonly DirectProperty<Menu, bool> IsOpenProperty =
+            AvaloniaProperty.RegisterDirect<Menu, bool>(
+                nameof(IsOpen),
+                o => o.IsOpen);
+
+        /// <summary>
+        /// Defines the <see cref="MenuOpened"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<RoutedEventArgs> MenuOpenedEvent =
+            RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(MenuOpened), RoutingStrategies.Bubble);
+
+        /// <summary>
+        /// Defines the <see cref="MenuClosed"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<RoutedEventArgs> MenuClosedEvent =
+            RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(MenuClosed), RoutingStrategies.Bubble);
+
+        private bool _isOpen;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MenuBase"/> class.
+        /// </summary>
+        public MenuBase()
+        {
+            InteractionHandler = new DefaultMenuInteractionHandler(false);
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MenuBase"/> class.
+        /// </summary>
+        /// <param name="interactionHandler">The menu interaction handler.</param>
+        public MenuBase(IMenuInteractionHandler interactionHandler)
+        {
+            Contract.Requires<ArgumentNullException>(interactionHandler != null);
+
+            InteractionHandler = interactionHandler;
+        }
+
+        /// <summary>
+        /// Initializes static members of the <see cref="MenuBase"/> class.
+        /// </summary>
+        static MenuBase()
+        {
+            MenuItem.SubmenuOpenedEvent.AddClassHandler<MenuBase>(x => x.OnSubmenuOpened);
+        }
+
+        /// <summary>
+        /// Gets a value indicating whether the menu is open.
+        /// </summary>
+        public bool IsOpen
+        {
+            get { return _isOpen; }
+            protected set { SetAndRaise(IsOpenProperty, ref _isOpen, value); }
+        }
+
+        /// <inheritdoc/>
+        IMenuInteractionHandler IMenu.InteractionHandler => InteractionHandler;
+
+        /// <inheritdoc/>
+        IMenuItem IMenuElement.SelectedItem
+        {
+            get
+            {
+                var index = SelectedIndex;
+                return (index != -1) ?
+                    (IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) :
+                    null;
+            }
+            set
+            {
+                SelectedIndex = ItemContainerGenerator.IndexFromContainer(value);
+            }
+        }
+
+        /// <inheritdoc/>
+        IEnumerable<IMenuItem> IMenuElement.SubItems
+        {
+            get
+            {
+                return ItemContainerGenerator.Containers
+                    .Select(x => x.ContainerControl)
+                    .OfType<IMenuItem>();
+            }
+        }
+
+        /// <summary>
+        /// Gets the interaction handler for the menu.
+        /// </summary>
+        protected IMenuInteractionHandler InteractionHandler { get; }
+
+        /// <summary>
+        /// Occurs when a <see cref="Menu"/> is opened.
+        /// </summary>
+        public event EventHandler<RoutedEventArgs> MenuOpened
+        {
+            add { AddHandler(MenuOpenedEvent, value); }
+            remove { RemoveHandler(MenuOpenedEvent, value); }
+        }
+
+        /// <summary>
+        /// Occurs when a <see cref="Menu"/> is closed.
+        /// </summary>
+        public event EventHandler<RoutedEventArgs> MenuClosed
+        {
+            add { AddHandler(MenuClosedEvent, value); }
+            remove { RemoveHandler(MenuClosedEvent, value); }
+        }
+
+        /// <summary>
+        /// Closes the menu.
+        /// </summary>
+        public abstract void Close();
+
+        /// <summary>
+        /// Opens the menu.
+        /// </summary>
+        public abstract void Open();
+
+        /// <inheritdoc/>
+        bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
+
+        /// <inheritdoc/>
+        protected override IItemContainerGenerator CreateItemContainerGenerator()
+        {
+            return new ItemContainerGenerator<MenuItem>(this, MenuItem.HeaderProperty, null);
+        }
+
+        /// <inheritdoc/>
+        protected override void OnKeyDown(KeyEventArgs e)
+        {
+            // Don't handle here: let the interaction handler handle it.
+        }
+
+        /// <inheritdoc/>
+        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            base.OnAttachedToVisualTree(e);
+            InteractionHandler.Attach(this);
+        }
+
+        /// <inheritdoc/>
+        protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+        {
+            base.OnDetachedFromVisualTree(e);
+            InteractionHandler.Detach(this);
+        }
+
+        /// <summary>
+        /// Called when a submenu opens somewhere in the menu.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        protected virtual void OnSubmenuOpened(RoutedEventArgs e)
+        {
+            if (e.Source is MenuItem menuItem && menuItem.Parent == this)
+            {
+                foreach (var child in this.GetLogicalChildren().OfType<MenuItem>())
+                {
+                    if (child != menuItem && child.IsSubMenuOpen)
+                    {
+                        child.IsSubMenuOpen = false;
+                    }
+                }
+            }
+
+            IsOpen = true;
+        }
+    }
+}

+ 9 - 4
src/Avalonia.Controls/MenuItem.cs

@@ -20,11 +20,16 @@ namespace Avalonia.Controls
     /// </summary>
     public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable
     {
+        private ICommand _command;
+
         /// <summary>
         /// Defines the <see cref="Command"/> property.
         /// </summary>
-        public static readonly StyledProperty<ICommand> CommandProperty =
-            AvaloniaProperty.Register<MenuItem, ICommand>(nameof(Command));
+        public static readonly DirectProperty<MenuItem, ICommand> CommandProperty =
+            Button.CommandProperty.AddOwner<MenuItem>(
+                menuItem => menuItem.Command, 
+                (menuItem, command) => menuItem.Command = command, 
+                enableDataValidation: true);
 
         /// <summary>
         /// Defines the <see cref="HotKey"/> property.
@@ -159,8 +164,8 @@ namespace Avalonia.Controls
         /// </summary>
         public ICommand Command
         {
-            get { return GetValue(CommandProperty); }
-            set { SetValue(CommandProperty, value); }
+            get { return _command; }
+            set { SetAndRaise(CommandProperty, ref _command, value); }
         }
 
         /// <summary>

+ 2 - 2
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@@ -965,11 +965,11 @@ namespace Avalonia.Controls
         {
             if (value < Minimum)
             {
-                throw new ArgumentOutOfRangeException(nameof(Minimum), string.Format("Value must be greater than Minimum value of {0}", Minimum));
+                throw new ArgumentOutOfRangeException(nameof(value), string.Format("Value must be greater than Minimum value of {0}", Minimum));
             }
             else if (value > Maximum)
             {
-                throw new ArgumentOutOfRangeException(nameof(Maximum), string.Format("Value must be less than Maximum value of {0}", Maximum));
+                throw new ArgumentOutOfRangeException(nameof(value), string.Format("Value must be less than Maximum value of {0}", Maximum));
             }
         }
 

+ 47 - 33
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@@ -13,18 +13,21 @@ namespace Avalonia.Controls.Platform
     /// </summary>
     public class DefaultMenuInteractionHandler : IMenuInteractionHandler
     {
+        private readonly bool _isContextMenu;
         private IDisposable _inputManagerSubscription;
         private IRenderRoot _root;
 
-        public DefaultMenuInteractionHandler()
-            : this(Input.InputManager.Instance, DefaultDelayRun)
+        public DefaultMenuInteractionHandler(bool isContextMenu)
+            : this(isContextMenu, Input.InputManager.Instance, DefaultDelayRun)
         {
         }
 
         public DefaultMenuInteractionHandler(
+            bool isContextMenu,
             IInputManager inputManager,
             Action<Action, TimeSpan> delayRun)
         {
+            _isContextMenu = isContextMenu;
             InputManager = inputManager;
             DelayRun = delayRun;
         }
@@ -59,7 +62,7 @@ namespace Avalonia.Controls.Platform
                 window.Deactivated += WindowDeactivated;
             }
 
-            _inputManagerSubscription = InputManager.Process.Subscribe(RawInput);
+            _inputManagerSubscription = InputManager?.Process.Subscribe(RawInput);
         }
 
         public virtual void Detach(IMenu menu)
@@ -125,23 +128,16 @@ namespace Avalonia.Controls.Platform
 
         protected internal virtual void KeyDown(object sender, KeyEventArgs e)
         {
-            var item = GetMenuItem(e.Source as IControl);
-
-            if (item != null)
-            {
-                KeyDown(item, e);
-            }
+            KeyDown(GetMenuItem(e.Source as IControl), e);
         }
 
         protected internal virtual void KeyDown(IMenuItem item, KeyEventArgs e)
         {
-            Contract.Requires<ArgumentNullException>(item != null);
-
             switch (e.Key)
             {
                 case Key.Up:
                 case Key.Down:
-                    if (item.IsTopLevel)
+                    if (item?.IsTopLevel == true)
                     {
                         if (item.HasSubMenu && !item.IsSubMenuOpen)
                         {
@@ -156,7 +152,7 @@ namespace Avalonia.Controls.Platform
                     break;
 
                 case Key.Left:
-                    if (item.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen)
+                    if (item?.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen)
                     {
                         parent.Close();
                         parent.Focus();
@@ -169,7 +165,7 @@ namespace Avalonia.Controls.Platform
                     break;
 
                 case Key.Right:
-                    if (!item.IsTopLevel && item.HasSubMenu)
+                    if (item != null && !item.IsTopLevel && item.HasSubMenu)
                     {
                         Open(item, true);
                         e.Handled = true;
@@ -181,47 +177,65 @@ namespace Avalonia.Controls.Platform
                     break;
 
                 case Key.Enter:
-                    if (!item.HasSubMenu)
+                    if (item != null)
                     {
-                        Click(item);
-                    }
-                    else
-                    {
-                        Open(item, true);
-                    }
+                        if (!item.HasSubMenu)
+                        {
+                            Click(item);
+                        }
+                        else
+                        {
+                            Open(item, true);
+                        }
 
-                    e.Handled = true;
+                        e.Handled = true;
+                    }
                     break;
 
                 case Key.Escape:
-                    if (item.Parent != null)
+                    if (item?.Parent != null)
                     {
                         item.Parent.Close();
                         item.Parent.Focus();
-                        e.Handled = true;
                     }
+                    else
+                    {
+                        Menu.Close();
+                    }
+
+                    e.Handled = true;
                     break;
 
                 default:
                     var direction = e.Key.ToNavigationDirection();
 
-                    if (direction.HasValue && item.Parent?.MoveSelection(direction.Value, true) == true)
+                    if (direction.HasValue)
                     {
-                        // If the the parent is an IMenu which successfully moved its selection,
-                        // and the current menu is open then close the current menu and open the
-                        // new menu.
-                        if (item.IsSubMenuOpen && item.Parent is IMenu)
+                        if (item == null && _isContextMenu)
                         {
-                            item.Close();
-                            Open(item.Parent.SelectedItem, true);
+                            if (Menu.MoveSelection(direction.Value, true) == true)
+                            {
+                                e.Handled = true;
+                            }
+                        }
+                        else if (item.Parent?.MoveSelection(direction.Value, true) == true)
+                        {
+                            // If the the parent is an IMenu which successfully moved its selection,
+                            // and the current menu is open then close the current menu and open the
+                            // new menu.
+                            if (item.IsSubMenuOpen && item.Parent is IMenu)
+                            {
+                                item.Close();
+                                Open(item.Parent.SelectedItem, true);
+                            }
+                            e.Handled = true;
                         }
-                        e.Handled = true;
                     }
 
                     break;
             }
 
-            if (!e.Handled && item.Parent is IMenuItem parentItem)
+            if (!e.Handled && item?.Parent is IMenuItem parentItem)
             {
                 KeyDown(parentItem, e);
             }

+ 1 - 1
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -99,7 +99,7 @@ namespace Avalonia.Controls.Primitives
                 "SelectionChanged",
                 RoutingStrategies.Bubble);
 
-        private static readonly IList Empty = new object[0];
+        private static readonly IList Empty = Array.Empty<object>();
 
         private int _selectedIndex = -1;
         private object _selectedItem;

+ 18 - 7
src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs

@@ -61,6 +61,11 @@ namespace Avalonia.Controls.Remote.Server
         {
             var result = InputModifiers.None;
 
+            if (modifiers == null)
+            {
+                return result;
+            }
+
             foreach(var modifier in modifiers)
             {
                 switch (modifier)
@@ -265,11 +270,15 @@ namespace Avalonia.Controls.Remote.Server
             var bpp = fmt == ProtocolPixelFormat.Rgb565 ? 2 : 4;
             var data = new byte[width * height * bpp];
             var handle = GCHandle.Alloc(data, GCHandleType.Pinned);
+
             try
             {
-                _framebuffer = new LockedFramebuffer(handle.AddrOfPinnedObject(), new PixelSize(width, height), width * bpp, _dpi, (PixelFormat)fmt,
-                    null);
-                Paint?.Invoke(new Rect(0, 0, width, height));
+                if (width > 0 && height > 0)
+                {
+                    _framebuffer = new LockedFramebuffer(handle.AddrOfPinnedObject(), new PixelSize(width, height), width * bpp, _dpi, (PixelFormat)fmt,
+                        null);
+                    Paint?.Invoke(new Rect(0, 0, width, height));
+                }
             }
             finally
             {
@@ -301,8 +310,7 @@ namespace Avalonia.Controls.Remote.Server
                     return;
 
             }
-            if (ClientSize.Width < 1 || ClientSize.Height < 1)
-                return;
+
             var format = ProtocolPixelFormat.Rgba8888;
             foreach(var fmt in _supportedFormats)
                 if (fmt <= ProtocolPixelFormat.MaxValue)
@@ -323,8 +331,11 @@ namespace Avalonia.Controls.Remote.Server
 
         public override void Invalidate(Rect rect)
         {
-            _invalidated = true;
-            Dispatcher.UIThread.Post(RenderIfNeeded);
+            if (!IsDisposed)
+            {
+                _invalidated = true;
+                Dispatcher.UIThread.Post(RenderIfNeeded);
+            }
         }
 
         public override IMouseDevice MouseDevice { get; } = new MouseDevice();

+ 14 - 9
src/Avalonia.Controls/Shapes/Shape.cs

@@ -195,7 +195,7 @@ namespace Avalonia.Controls.Shapes
             if (deferCalculateTransform)
             {
                 _calculateTransformOnArrange = true;
-                return DefiningGeometry.Bounds.Size;
+                return DefiningGeometry?.Bounds.Size ?? Size.Empty;
             }
             else
             {
@@ -217,17 +217,22 @@ namespace Avalonia.Controls.Shapes
 
         private Size CalculateShapeSizeAndSetTransform(Size availableSize)
         {
-            // This should probably use GetRenderBounds(strokeThickness) but then the calculations
-            // will multiply the stroke thickness as well, which isn't correct.
-            var (size, transform) = CalculateSizeAndTransform(availableSize, DefiningGeometry.Bounds, Stretch);
-
-            if (_transform != transform)
+            if (DefiningGeometry != null)
             {
-                _transform = transform;
-                _renderedGeometry = null;
+                // This should probably use GetRenderBounds(strokeThickness) but then the calculations
+                // will multiply the stroke thickness as well, which isn't correct.
+                var (size, transform) = CalculateSizeAndTransform(availableSize, DefiningGeometry.Bounds, Stretch);
+
+                if (_transform != transform)
+                {
+                    _transform = transform;
+                    _renderedGeometry = null;
+                }
+
+                return size;
             }
 
-            return size;
+            return Size.Empty;
         }
 
         internal static (Size, Matrix) CalculateSizeAndTransform(Size availableSize, Rect shapeBounds, Stretch Stretch)

+ 1 - 1
src/Avalonia.Controls/SystemDialog.cs

@@ -27,7 +27,7 @@ namespace Avalonia.Controls
                 throw new ArgumentNullException(nameof(parent));
             return ((await AvaloniaLocator.Current.GetService<ISystemDialogImpl>()
                  .ShowFileDialogAsync(this, parent?.PlatformImpl)) ??
-             new string[0]).FirstOrDefault();
+             Array.Empty<string>()).FirstOrDefault();
         }
     }
 

+ 6 - 6
src/Avalonia.DesignerSupport/DesignWindowLoader.cs

@@ -35,13 +35,13 @@ namespace Avalonia.DesignerSupport
 
                 var localAsm = assemblyPath != null ? Assembly.LoadFile(Path.GetFullPath(assemblyPath)) : null;
                 var loaded = loader.Load(stream, localAsm, null, baseUri);
-                var styles = loaded as Styles;
-                if (styles != null)
+                var style = loaded as IStyle;
+                if (style != null)
                 {
-                    var substitute = styles.OfType<Style>().Select(Design.GetPreviewWith).FirstOrDefault(s => s != null);
+                    var substitute = Design.GetPreviewWith((AvaloniaObject)style);
                     if (substitute != null)
                     {
-                        substitute.Styles.AddRange(styles);
+                        substitute.Styles.Add(style);
                         control = substitute;
                     }
                     else
@@ -51,8 +51,8 @@ namespace Avalonia.DesignerSupport
                             {
                                 new TextBlock {Text = "Styles can't be previewed without Design.PreviewWith. Add"},
                                 new TextBlock {Text = "<Design.PreviewWith>"},
-                                new TextBlock {Text = "    <Border Padding=20><!-- YOUR CONTROL FOR PREVIEW HERE--></Border>"},
-                                new TextBlock {Text = "<Design.PreviewWith>"},
+                                new TextBlock {Text = "    <Border Padding=20><!-- YOUR CONTROL FOR PREVIEW HERE --></Border>"},
+                                new TextBlock {Text = "</Design.PreviewWith>"},
                                 new TextBlock {Text = "before setters in your first Style"}
                             }
                         };

+ 11 - 1
src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs

@@ -8,6 +8,7 @@ using Avalonia.Remote.Protocol;
 using Avalonia.Remote.Protocol.Designer;
 using Avalonia.Remote.Protocol.Viewport;
 using Avalonia.Threading;
+using Portable.Xaml;
 
 namespace Avalonia.DesignerSupport.Remote
 {
@@ -204,9 +205,18 @@ namespace Avalonia.DesignerSupport.Remote
                 }
                 catch (Exception e)
                 {
+                    var xamlException = e as XamlException;
+
                     s_transport.Send(new UpdateXamlResultMessage
                     {
-                        Error = e.ToString()
+                        Error = e.ToString(),
+                        Exception = new ExceptionDetails
+                        {
+                            ExceptionType = e.GetType().FullName,
+                            Message = e.Message.ToString(),
+                            LineNumber = xamlException?.LineNumber,
+                            LinePosition = xamlException?.LinePosition,
+                        }
                     });
                 }
             }

+ 1 - 1
src/Avalonia.Input/InputElement.cs

@@ -375,7 +375,7 @@ namespace Avalonia.Input
         /// </summary>
         public void Focus()
         {
-            FocusManager.Instance.Focus(this);
+            FocusManager.Instance?.Focus(this);
         }
 
         /// <inheritdoc/>

+ 5 - 9
src/Avalonia.Input/MouseDevice.cs

@@ -84,18 +84,14 @@ namespace Avalonia.Input
         {
             Contract.Requires<ArgumentNullException>(relativeTo != null);
 
-            Point p = default(Point);
-            IVisual v = relativeTo;
-            IVisual root = null;
-
-            while (v != null)
+            if (relativeTo.VisualRoot == null)
             {
-                p += v.Bounds.Position;
-                root = v;
-                v = v.VisualParent;
+                throw new InvalidOperationException("Control is not attached to visual tree.");
             }
 
-            return root.PointToClient(Position) - p;
+            var rootPoint = relativeTo.VisualRoot.PointToClient(Position);
+            var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo);
+            return rootPoint * transform.Value;
         }
 
         public void ProcessRawEvent(RawInputEventArgs e)

+ 2 - 0
src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj

@@ -2,6 +2,8 @@
   <PropertyGroup>
     <TargetFramework>netstandard2.0</TargetFramework>
     <DefineConstants>AVALONIA_REMOTE_PROTOCOL;$(DefineConstants)</DefineConstants>
+    <SignAssembly>true</SignAssembly>
+    <AssemblyOriginatorKeyFile>Key.snk</AssemblyOriginatorKeyFile>
   </PropertyGroup>
   <ItemGroup>
     <Compile Include="..\Avalonia.Input\Key.cs" />

+ 1 - 1
src/Avalonia.Remote.Protocol/DefaultMessageTypeResolver.cs

@@ -12,7 +12,7 @@ namespace Avalonia.Remote.Protocol
         public DefaultMessageTypeResolver(params Assembly[] assemblies)
         {
             foreach (var asm in
-                (assemblies ?? new Assembly[0]).Concat(new[]
+                (assemblies ?? Array.Empty<Assembly>()).Concat(new[]
                     {typeof(AvaloniaRemoteMessageGuidAttribute).GetTypeInfo().Assembly}))
             {
                 foreach (var t in asm.ExportedTypes)

+ 8 - 0
src/Avalonia.Remote.Protocol/DesignMessages.cs

@@ -15,6 +15,7 @@ namespace Avalonia.Remote.Protocol.Designer
     {
         public string Error { get; set; }
         public string Handle { get; set; }
+        public ExceptionDetails Exception { get; set; }
     }
 
     [AvaloniaRemoteMessageGuid("854887CF-2694-4EB6-B499-7461B6FB96C7")]
@@ -23,4 +24,11 @@ namespace Avalonia.Remote.Protocol.Designer
         public string SessionId { get; set; }
     }
     
+    public class ExceptionDetails
+    {
+        public string ExceptionType { get; set; }
+        public string Message { get; set; }
+        public int? LineNumber { get; set; }
+        public int? LinePosition { get; set; }
+    }
 }

BIN
src/Avalonia.Remote.Protocol/Key.snk


+ 3 - 3
src/Avalonia.Remote.Protocol/MetsysBson.cs

@@ -749,7 +749,7 @@ namespace Metsys.Bson
 
                         if (memberExpression.Expression.NodeType != ExpressionType.Parameter && memberExpression.Expression.NodeType != ExpressionType.Convert)
                         {
-                            throw new ArgumentException(string.Format("Expression '{0}' must resolve to top-level member.", lambdaExpression), "lambdaExpression");
+                            throw new ArgumentException(string.Format("Expression '{0}' must resolve to top-level member.", lambdaExpression), nameof(lambdaExpression));
                         }
                         return memberExpression.Member.Name;
                     default:
@@ -806,7 +806,7 @@ namespace Metsys.Bson
             {
                 return Activator.CreateInstance(typeof(List<>).MakeGenericType(itemType));
             }
-            if (type.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, new Type[0], null) != null)
+            if (type.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, Type.EmptyTypes, null) != null)
             {
                 return Activator.CreateInstance(type);
             }
@@ -853,7 +853,7 @@ namespace Metsys.Bson
                 return (IDictionary)Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(keyType, valueType));
             }
 
-            if (dictionaryType.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, new Type[0], null) != null)
+            if (dictionaryType.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, Type.EmptyTypes, null) != null)
             {
                 return (IDictionary)Activator.CreateInstance(dictionaryType);
             }

+ 131 - 0
src/Avalonia.Styling/Styling/OrSelector.cs

@@ -0,0 +1,131 @@
+// 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.Collections.Generic;
+
+namespace Avalonia.Styling
+{
+    /// <summary>
+    /// The OR style selector.
+    /// </summary>
+    internal class OrSelector : Selector
+    {
+        private readonly IReadOnlyList<Selector> _selectors;
+        private string _selectorString;
+        private Type _targetType;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="OrSelector"/> class.
+        /// </summary>
+        /// <param name="selectors">The selectors to OR.</param>
+        public OrSelector(IReadOnlyList<Selector> selectors)
+        {
+            Contract.Requires<ArgumentNullException>(selectors != null);
+            Contract.Requires<ArgumentException>(selectors.Count > 1);
+
+            _selectors = selectors;
+        }
+
+        /// <inheritdoc/>
+        public override bool InTemplate => false;
+
+        /// <inheritdoc/>
+        public override bool IsCombinator => false;
+
+        /// <inheritdoc/>
+        public override Type TargetType
+        {
+            get
+            {
+                if (_targetType == null)
+                {
+                    _targetType = EvaluateTargetType();
+                }
+
+                return _targetType;
+            }
+        }
+
+        /// <inheritdoc/>
+        public override string ToString()
+        {
+            if (_selectorString == null)
+            {
+                _selectorString = string.Join(", ", _selectors);
+            }
+
+            return _selectorString;
+        }
+
+        protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
+        {
+            var activators = new List<IObservable<bool>>();
+            var neverThisInstance = false;
+
+            foreach (var selector in _selectors)
+            {
+                var match = selector.Match(control, subscribe);
+
+                switch (match.Result)
+                {
+                    case SelectorMatchResult.AlwaysThisType:
+                    case SelectorMatchResult.AlwaysThisInstance:
+                        return match;
+                    case SelectorMatchResult.NeverThisInstance:
+                        neverThisInstance = true;
+                        break;
+                    case SelectorMatchResult.Sometimes:
+                        activators.Add(match.Activator);
+                        break;
+                }
+            }
+
+            if (activators.Count > 1)
+            {
+                return new SelectorMatch(StyleActivator.Or(activators));
+            }
+            else if (activators.Count == 1)
+            {
+                return new SelectorMatch(activators[0]);
+            }
+            else if (neverThisInstance)
+            {
+                return SelectorMatch.NeverThisInstance;
+            }
+            else
+            {
+                return SelectorMatch.NeverThisType;
+            }
+        }
+
+        protected override Selector MovePrevious() => null;
+
+        private Type EvaluateTargetType()
+        {
+            var result = default(Type);
+
+            foreach (var selector in _selectors)
+            {
+                if (selector.TargetType == null)
+                {
+                    return null;
+                }
+                else if (result == null)
+                {
+                    result = selector.TargetType;
+                }
+                else
+                {
+                    while (!result.IsAssignableFrom(selector.TargetType))
+                    {
+                        result = result.BaseType;
+                    }
+                }
+            }
+
+            return result;
+        }
+    }
+}
+

+ 22 - 0
src/Avalonia.Styling/Styling/Selectors.cs

@@ -2,6 +2,8 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Collections.Generic;
+using System.Linq;
 
 namespace Avalonia.Styling
 {
@@ -137,6 +139,26 @@ namespace Avalonia.Styling
             return previous.OfType(typeof(T));
         }
 
+        /// <summary>
+        /// Returns a selector which ORs selectors.
+        /// </summary>
+        /// <param name="selectors">The selectors to be OR'd.</param>
+        /// <returns>The selector.</returns>
+        public static Selector Or(params Selector[] selectors)
+        {
+            return new OrSelector(selectors);
+        }
+
+        /// <summary>
+        /// Returns a selector which ORs selectors.
+        /// </summary>
+        /// <param name="selectors">The selectors to be OR'd.</param>
+        /// <returns>The selector.</returns>
+        public static Selector Or(IReadOnlyList<Selector> selectors)
+        {
+            return new OrSelector(selectors);
+        }
+
         /// <summary>
         /// Returns a selector which matches a control with the specified property value.
         /// </summary>

+ 76 - 3
src/Avalonia.Styling/Styling/Styles.cs

@@ -2,7 +2,9 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Collections;
 using System.Collections.Generic;
+using System.Collections.Specialized;
 using System.Linq;
 using Avalonia.Collections;
 using Avalonia.Controls;
@@ -12,16 +14,17 @@ namespace Avalonia.Styling
     /// <summary>
     /// A style that consists of a number of child styles.
     /// </summary>
-    public class Styles : AvaloniaList<IStyle>, IStyle, ISetStyleParent
+    public class Styles : AvaloniaObject, IAvaloniaList<IStyle>, IStyle, ISetStyleParent
     {
         private IResourceNode _parent;
         private IResourceDictionary _resources;
+        private AvaloniaList<IStyle> _styles = new AvaloniaList<IStyle>();
         private Dictionary<Type, List<IStyle>> _cache;
 
         public Styles()
         {
-            ResetBehavior = ResetBehavior.Remove;
-            this.ForEachItem(
+            _styles.ResetBehavior = ResetBehavior.Remove;
+            _styles.ForEachItem(
                 x =>
                 {
                     if (x.ResourceParent == null && x is ISetStyleParent setParent)
@@ -57,9 +60,18 @@ namespace Avalonia.Styling
                 () => { });
         }
 
+        public event NotifyCollectionChangedEventHandler CollectionChanged
+        {
+            add => _styles.CollectionChanged += value;
+            remove => _styles.CollectionChanged -= value;
+        }
+
         /// <inheritdoc/>
         public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
 
+        /// <inheritdoc/>
+        public int Count => _styles.Count;
+
         /// <inheritdoc/>
         public bool HasResources => _resources?.Count > 0 || this.Any(x => x.HasResources);
 
@@ -94,6 +106,19 @@ namespace Avalonia.Styling
         /// <inheritdoc/>
         IResourceNode IResourceNode.ResourceParent => _parent;
 
+        /// <inheritdoc/>
+        bool ICollection<IStyle>.IsReadOnly => false;
+
+        /// <inheritdoc/>
+        IStyle IReadOnlyList<IStyle>.this[int index] => _styles[index];
+
+        /// <inheritdoc/>
+        public IStyle this[int index]
+        {
+            get => _styles[index];
+            set => _styles[index] = value;
+        }
+
         /// <summary>
         /// Attaches the style to a control if the style's selector matches.
         /// </summary>
@@ -172,6 +197,54 @@ namespace Avalonia.Styling
             return false;
         }
 
+        /// <inheritdoc/>
+        public void AddRange(IEnumerable<IStyle> items) => _styles.AddRange(items);
+
+        /// <inheritdoc/>
+        public void InsertRange(int index, IEnumerable<IStyle> items) => _styles.InsertRange(index, items);
+
+        /// <inheritdoc/>
+        public void Move(int oldIndex, int newIndex) => _styles.Move(oldIndex, newIndex);
+
+        /// <inheritdoc/>
+        public void MoveRange(int oldIndex, int count, int newIndex) => _styles.MoveRange(oldIndex, count, newIndex);
+
+        /// <inheritdoc/>
+        public void RemoveAll(IEnumerable<IStyle> items) => _styles.RemoveAll(items);
+
+        /// <inheritdoc/>
+        public void RemoveRange(int index, int count) => _styles.RemoveRange(index, count);
+
+        /// <inheritdoc/>
+        public int IndexOf(IStyle item) => _styles.IndexOf(item);
+
+        /// <inheritdoc/>
+        public void Insert(int index, IStyle item) => _styles.Insert(index, item);
+
+        /// <inheritdoc/>
+        public void RemoveAt(int index) => _styles.RemoveAt(index);
+
+        /// <inheritdoc/>
+        public void Add(IStyle item) => _styles.Add(item);
+
+        /// <inheritdoc/>
+        public void Clear() => _styles.Clear();
+
+        /// <inheritdoc/>
+        public bool Contains(IStyle item) => _styles.Contains(item);
+
+        /// <inheritdoc/>
+        public void CopyTo(IStyle[] array, int arrayIndex) => _styles.CopyTo(array, arrayIndex);
+
+        /// <inheritdoc/>
+        public bool Remove(IStyle item) => _styles.Remove(item);
+
+        /// <inheritdoc/>
+        public IEnumerator<IStyle> GetEnumerator() => _styles.GetEnumerator();
+
+        /// <inheritdoc/>
+        IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator();
+
         /// <inheritdoc/>
         void ISetStyleParent.SetParent(IResourceNode parent)
         {

+ 16 - 6
src/Avalonia.Themes.Default/ContextMenu.xaml

@@ -10,12 +10,22 @@
               BorderBrush="{TemplateBinding BorderBrush}"
               BorderThickness="{TemplateBinding BorderThickness}"
               Padding="{TemplateBinding Padding}">
-        <ItemsPresenter Name="PART_ItemsPresenter"
-                        Items="{TemplateBinding Items}"
-                        ItemsPanel="{TemplateBinding ItemsPanel}"
-                        ItemTemplate="{TemplateBinding ItemTemplate}"
-                        KeyboardNavigation.TabNavigation="Continue"/>
+          <ScrollViewer>
+            <Panel>
+              <ItemsPresenter Name="PART_ItemsPresenter"
+                              Items="{TemplateBinding Items}"
+                              ItemsPanel="{TemplateBinding ItemsPanel}"
+                              ItemTemplate="{TemplateBinding ItemTemplate}"
+                              KeyboardNavigation.TabNavigation="Continue"/>
+              <Rectangle Name="iconSeparator"
+                         Fill="{DynamicResource ThemeControlMidBrush}"
+                         HorizontalAlignment="Left"
+                         IsHitTestVisible="False"
+                         Margin="29,2,0,2"
+                         Width="1"/>
+              </Panel>
+          </ScrollViewer>
       </Border>
     </ControlTemplate>
   </Setter>
-</Style>
+</Style>

+ 1 - 7
src/Avalonia.Themes.Default/Separator.xaml

@@ -11,13 +11,7 @@
     </Setter>
   </Style>
 
-  <Style Selector="MenuItem > Separator">
-    <Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}"/>
-    <Setter Property="Margin" Value="29,1,0,1"/>
-    <Setter Property="Height" Value="1"/>
-  </Style>
-
-  <Style Selector="ContextMenu > Separator">
+  <Style Selector="MenuItem > Separator, ContextMenu > Separator">
     <Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}"/>
     <Setter Property="Margin" Value="29,1,0,1"/>
     <Setter Property="Height" Value="1"/>

+ 2 - 0
src/Avalonia.Visuals/Media/DrawingContext.cs

@@ -109,6 +109,8 @@ namespace Avalonia.Media
         /// <param name="geometry">The geometry.</param>
         public void DrawGeometry(IBrush brush, Pen pen, Geometry geometry)
         {
+            Contract.Requires<ArgumentNullException>(geometry != null);
+
             if (brush != null || PenIsVisible(pen))
             {
                 PlatformImpl.DrawGeometry(brush, pen, geometry.PlatformImpl);

+ 5 - 2
src/Avalonia.Visuals/Media/GeometryDrawing.cs

@@ -31,7 +31,10 @@
 
         public override void Draw(DrawingContext context)
         {
-            context.DrawGeometry(Brush, Pen, Geometry);
+            if (Geometry != null)
+            {
+                context.DrawGeometry(Brush, Pen, Geometry);
+            }
         }
 
         public override Rect GetBounds()
@@ -41,4 +44,4 @@
             return Geometry?.GetRenderBounds(pen) ?? new Rect();
         }
     }
-}
+}

+ 2 - 2
src/Avalonia.Visuals/Rendering/RenderLoop.cs

@@ -117,9 +117,9 @@ namespace Avalonia.Rendering
                         }, DispatcherPriority.Render);
                     }
 
-                    foreach (var i in _items)
+                    for(int i = 0; i < _items.Count; i++)
                     {
-                        i.Render();
+                        _items[i].Render();
                     }
                 }
                 catch (Exception ex)

+ 8 - 3
src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs

@@ -89,9 +89,14 @@ namespace Avalonia.Rendering.SceneGraph
         /// <inheritdoc/>
         public override bool HitTest(Point p)
         {
-            p *= Transform.Invert();
-            return (Brush != null && Geometry.FillContains(p)) || 
-                (Pen != null && Geometry.StrokeContains(Pen, p));
+            if (Transform.HasInverse)
+            {
+                p *= Transform.Invert();
+                return (Brush != null && Geometry.FillContains(p)) ||
+                    (Pen != null && Geometry.StrokeContains(Pen, p));
+            }
+
+            return false;
         }
     }
 }

+ 2 - 2
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@@ -17,8 +17,8 @@ namespace Avalonia.Rendering.SceneGraph
     /// </summary>
     internal class VisualNode : IVisualNode
     {
-        private static readonly IReadOnlyList<IVisualNode> EmptyChildren = new IVisualNode[0];
-        private static readonly IReadOnlyList<IRef<IDrawOperation>> EmptyDrawOperations = new IRef<IDrawOperation>[0];
+        private static readonly IReadOnlyList<IVisualNode> EmptyChildren = Array.Empty<IVisualNode>();
+        private static readonly IReadOnlyList<IRef<IDrawOperation>> EmptyDrawOperations = Array.Empty<IRef<IDrawOperation>>();
 
         private Rect? _bounds;
         private double _opacity;

+ 4 - 3
src/Avalonia.Visuals/Visual.cs

@@ -11,6 +11,7 @@ using Avalonia.Logging;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.Rendering;
+using Avalonia.Utilities;
 using Avalonia.VisualTree;
 
 namespace Avalonia
@@ -347,12 +348,12 @@ namespace Avalonia
                 {
                     if (e.OldValue is IAffectsRender oldValue)
                     {
-                        oldValue.Invalidated -= sender.AffectsRenderInvalidated;
+                        WeakEventHandlerManager.Unsubscribe<EventArgs, T>(oldValue, nameof(oldValue.Invalidated), sender.AffectsRenderInvalidated);
                     }
 
                     if (e.NewValue is IAffectsRender newValue)
                     {
-                        newValue.Invalidated += sender.AffectsRenderInvalidated;
+                        WeakEventHandlerManager.Subscribe<IAffectsRender, EventArgs, T>(newValue, nameof(newValue.Invalidated), sender.AffectsRenderInvalidated);                        
                     }
 
                     sender.InvalidateVisual();
@@ -551,7 +552,7 @@ namespace Avalonia
         {
             if (c == null)
             {
-                throw new ArgumentNullException("Cannot add null to VisualChildren.");
+                throw new ArgumentNullException(nameof(c), "Cannot add null to VisualChildren.");
             }
 
             if (c.VisualParent != null)

+ 1 - 1
src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs

@@ -84,7 +84,7 @@ namespace Avalonia.Gtk3
             public RenderOp(GtkWidget widget, ManagedCairoSurface surface, double factor, int width, int height)
             {
                 _widget = widget;
-                _surface = surface ?? throw new ArgumentNullException();
+                _surface = surface ?? throw new ArgumentNullException(nameof(surface));
                 _factor = factor;
                 _width = width;
                 _height = height;

+ 1 - 0
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@@ -9,6 +9,7 @@
     <ItemGroup>
         <Compile Include="AvaloniaXamlLoader.cs" />
         <Compile Include="Converters\AvaloniaUriTypeConverter.cs" />
+        <Compile Include="Converters\AvaloniaEventConverter.cs" />
         <Compile Include="Converters\FontFamilyTypeConverter.cs" />
         <Compile Include="Converters\MemberSelectorTypeConverter.cs" />
         <Compile Include="Converters\NullableTypeConverter.cs" />

+ 3 - 1
src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs

@@ -11,6 +11,7 @@ using Avalonia.Controls.Templates;
 
 namespace Avalonia.Markup.Xaml
 {
+    using System.Reflection;
     using Avalonia.Media;
 
     /// <summary>
@@ -41,7 +42,8 @@ namespace Avalonia.Markup.Xaml
             { typeof(WindowIcon), typeof(IconTypeConverter) },
             { typeof(CultureInfo), typeof(CultureInfoConverter) },
             { typeof(Uri), typeof(AvaloniaUriTypeConverter) },
-            { typeof(FontFamily), typeof(FontFamilyTypeConverter) }
+            { typeof(FontFamily), typeof(FontFamilyTypeConverter) },
+            { typeof(EventInfo), typeof(AvaloniaEventConverter) },
         };
 
         internal static Type GetBuiltinTypeConverter(Type type)

+ 9 - 31
src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs

@@ -1,21 +1,20 @@
 // 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 Avalonia.Controls;
-using Avalonia.Markup.Data;
-using Avalonia.Markup.Xaml.PortableXaml;
-using Avalonia.Platform;
-using Portable.Xaml;
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.IO;
+using System.Linq;
 using System.Reflection;
 using System.Runtime.Serialization;
-using System.Runtime.Serialization.Json;
 using System.Text;
 using System.Xml.Linq;
-using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Markup.Data;
+using Avalonia.Markup.Xaml.PortableXaml;
+using Avalonia.Platform;
+using Portable.Xaml;
 
 namespace Avalonia.Markup.Xaml
 {
@@ -24,29 +23,7 @@ namespace Avalonia.Markup.Xaml
     /// </summary>
     public class AvaloniaXamlLoader
     {
-        private readonly AvaloniaXamlSchemaContext _context = GetContext();
-
-        public bool IsDesignMode
-        {
-            get => _context.IsDesignMode;
-            set => _context.IsDesignMode = value;
-        }
-
-        private static AvaloniaXamlSchemaContext GetContext()
-        {
-            var result = AvaloniaLocator.Current.GetService<AvaloniaXamlSchemaContext>();
-
-            if (result == null)
-            {
-                result = AvaloniaXamlSchemaContext.Create();
-
-                AvaloniaLocator.CurrentMutable
-                    .Bind<AvaloniaXamlSchemaContext>()
-                    .ToConstant(result);
-            }
-
-            return result;
-        }
+        public bool IsDesignMode { get; set; }
 
         /// <summary>
         /// Initializes a new instance of the <see cref="AvaloniaXamlLoader"/> class.
@@ -188,7 +165,8 @@ namespace Avalonia.Markup.Xaml
                 LocalAssembly = localAssembly
             };
 
-            var reader = new XamlXmlReader(stream, _context, readerSettings);
+            var context = IsDesignMode ? AvaloniaXamlSchemaContext.DesignInstance : AvaloniaXamlSchemaContext.Instance;
+            var reader = new XamlXmlReader(stream, context, readerSettings);
 
             object result = LoadFromReader(
                 reader,

+ 99 - 0
src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaEventConverter.cs

@@ -0,0 +1,99 @@
+using System;
+using System.ComponentModel;
+using System.Globalization;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml.PortableXaml;
+using Portable.Xaml;
+
+namespace Avalonia.Markup.Xaml.Converters
+{
+    internal class AvaloniaEventConverter : TypeConverter
+    {
+        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+        {
+            return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
+        }
+
+        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+        {
+            var text = value as string;
+            if (text != null)
+            {
+                var rootObjectProvider = context.GetService(typeof(IRootObjectProvider)) as IRootObjectProvider;
+                var destinationTypeProvider = context.GetService(typeof(IDestinationTypeProvider)) as IDestinationTypeProvider;
+                if (rootObjectProvider != null && destinationTypeProvider != null)
+                {
+                    var target = rootObjectProvider.RootObject;
+                    var eventType = destinationTypeProvider.GetDestinationType();
+                    var eventParameters = eventType.GetRuntimeMethods().First(r => r.Name == "Invoke").GetParameters();
+                    // go in reverse to match System.Xaml behaviour
+                    var methods = target.GetType().GetRuntimeMethods().Reverse();
+
+                    // find based on exact match parameter types first
+                    foreach (var method in methods)
+                    {
+                        if (method.Name != text)
+                            continue;
+                        var parameters = method.GetParameters();
+                        if (eventParameters.Length != parameters.Length)
+                            continue;
+                        if (parameters.Length == 0)
+                            return method.CreateDelegate(eventType, target);
+
+                        for (int i = 0; i < parameters.Length; i++)
+                        {
+                            var param = parameters[i];
+                            var eventParam = eventParameters[i];
+                            if (param.ParameterType != eventParam.ParameterType)
+                                break;
+                            if (i == parameters.Length - 1)
+                                return method.CreateDelegate(eventType, target);
+                        }
+                    }
+
+                    // EnhancedXaml: Find method with compatible base class parameters
+                    foreach (var method in methods)
+                    {
+                        if (method.Name != text)
+                            continue;
+                        var parameters = method.GetParameters();
+                        if (parameters.Length == 0 || eventParameters.Length != parameters.Length)
+                            continue;
+
+                        for (int i = 0; i < parameters.Length; i++)
+                        {
+                            var param = parameters[i];
+                            var eventParam = eventParameters[i];
+                            if (!param.ParameterType.GetTypeInfo().IsAssignableFrom(eventParam.ParameterType.GetTypeInfo()))
+                                break;
+                            if (i == parameters.Length - 1)
+                                return method.CreateDelegate(eventType, target);
+                        }
+                    }
+
+                    var contextProvider = (IXamlSchemaContextProvider)context.GetService(typeof(IXamlSchemaContextProvider));
+                    var avaloniaContext = (AvaloniaXamlSchemaContext)contextProvider.SchemaContext;
+
+                    if (avaloniaContext.IsDesignMode)
+                    {
+                        // We want to ignore missing events in the designer, so if event handler
+                        // wasn't found create an empty delegate.
+                        var lambdaExpression = Expression.Lambda(
+                            eventType,
+                            Expression.Empty(),
+                            eventParameters.Select(x => Expression.Parameter(x.ParameterType)));
+                        return lambdaExpression.Compile();
+                    }
+                    else
+                    {
+                        throw new XamlObjectWriterException($"Referenced value method {text} in type {target.GetType()} indicated by event {eventType.FullName} was not found");
+                    }
+                }
+            }
+            return base.ConvertFrom(context, culture, value);
+        }
+    }
+}

+ 13 - 1
src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaMemberAttributeProvider.cs

@@ -49,6 +49,18 @@ namespace Avalonia.Markup.Xaml.PortableXaml
                 //Portable.Xaml is not searching for Type Converter
                 result = new TypeConverterAttribute(typeof(SetterValueTypeConverter));
             }
+            else if (attributeType == typeof(TypeConverterAttribute) && _info is EventInfo)
+            {
+                // If a type converter for `EventInfo` is registered, then use that to convert
+                // event handler values. This is used by the designer to override the lookup
+                // for event handlers with a null handler.
+                var eventConverter = AvaloniaTypeConverters.GetTypeConverter(typeof(EventInfo));
+
+                if (eventConverter != null)
+                {
+                    result = new TypeConverterAttribute(eventConverter);
+                }
+            }
 
             if (result == null)
             {
@@ -68,4 +80,4 @@ namespace Avalonia.Markup.Xaml.PortableXaml
 
         private readonly MemberInfo _info;
     }
-}
+}

+ 36 - 9
src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs

@@ -1,21 +1,48 @@
-using Avalonia.Data;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Avalonia.Data;
 using Avalonia.Markup.Xaml.Context;
-using Avalonia.Markup.Data;
 using Avalonia.Markup.Xaml.MarkupExtensions;
 using Avalonia.Markup.Xaml.Styling;
 using Portable.Xaml;
-using Portable.Xaml.ComponentModel;
-using System.ComponentModel;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
 
 namespace Avalonia.Markup.Xaml.PortableXaml
 {
     internal class AvaloniaXamlSchemaContext : XamlSchemaContext
     {
-        public bool IsDesignMode { get; set; }
+        private static AvaloniaXamlSchemaContext s_instance;
+        private static AvaloniaXamlSchemaContext s_designInstance;
+
+        public static AvaloniaXamlSchemaContext Instance
+        {
+            get
+            {
+                if (s_instance == null)
+                {
+                    s_instance = Create();
+                }
+
+                return s_instance;
+            }
+        }
+
+        public static AvaloniaXamlSchemaContext DesignInstance
+        {
+            get
+            {
+                if (s_designInstance == null)
+                {
+                    s_designInstance = Create();
+                    s_designInstance.IsDesignMode = true;
+                }
+
+                return s_designInstance;
+            }
+        }
+
+        public bool IsDesignMode { get; private set; }
         public static AvaloniaXamlSchemaContext Create(IRuntimeTypeProvider typeProvider = null)
         {
             return new AvaloniaXamlSchemaContext(typeProvider ?? new AvaloniaRuntimeTypeProvider());

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs

@@ -385,4 +385,4 @@ namespace Avalonia.Markup.Xaml.PortableXaml
         {
         }
     }
-}
+}

+ 2 - 2
src/Markup/Avalonia.Markup/Data/RelativeSource.cs

@@ -85,9 +85,9 @@ namespace Avalonia.Data
             get { return _ancestorLevel; }
             set
             {
-                if (_ancestorLevel <= 0)
+                if (value <= 0)
                 {
-                    throw new ArgumentOutOfRangeException("AncestorLevel may not be set to less than 1.");
+                    throw new ArgumentOutOfRangeException(nameof(value), "AncestorLevel may not be set to less than 1.");
                 }
 
                 _ancestorLevel = value;

+ 10 - 0
src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs

@@ -106,6 +106,11 @@ namespace Avalonia.Markup.Parsers
             {
                 return State.Indexer;
             }
+            else if (ParseDot(ref r))
+            {
+                nodes.Add(new EmptyExpressionNode());
+                return State.End;
+            }
             else
             {
                 var identifier = r.ParseIdentifier();
@@ -317,6 +322,11 @@ namespace Avalonia.Markup.Parsers
             return !r.End && r.TakeIf('#');
         }
 
+        private static bool ParseDot(ref CharacterReader r)
+        {
+            return !r.End && r.TakeIf('.');
+        }
+
         private enum State
         {
             Start,

+ 21 - 9
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs

@@ -49,7 +49,7 @@ namespace Avalonia.Markup.Parsers
                         state = ParseStart(ref r);
                         break;
                     case State.Middle:
-                        state = ParseMiddle(ref r, end);
+                        (state, syntax) = ParseMiddle(ref r, end);
                         break;
                     case State.CanHaveType:
                         state = ParseCanHaveType(ref r);
@@ -113,33 +113,37 @@ namespace Avalonia.Markup.Parsers
             return State.TypeName;
         }
 
-        private static State ParseMiddle(ref CharacterReader r, char? end)
+        private static (State, ISyntax) ParseMiddle(ref CharacterReader r, char? end)
         {
             if (r.TakeIf(':'))
             {
-                return State.Colon;
+                return (State.Colon, null);
             }
             else if (r.TakeIf('.'))
             {
-                return State.Class;
+                return (State.Class, null);
             }
             else if (r.TakeIf(char.IsWhiteSpace) || r.Peek == '>')
             {
-                return State.Traversal;
+                return (State.Traversal, null);
             }
             else if (r.TakeIf('/'))
             {
-                return State.Template;
+                return (State.Template, null);
             }
             else if (r.TakeIf('#'))
             {
-                return State.Name;
+                return (State.Name, null);
+            }
+            else if (r.TakeIf(','))
+            {
+                return (State.Start, new CommaSyntax());
             }
             else if (end.HasValue && !r.End && r.Peek == end.Value)
             {
-                return State.End;
+                return (State.End, null);
             }
-            return State.TypeName;
+            return (State.TypeName, null);
         }
 
         private static State ParseCanHaveType(ref CharacterReader r)
@@ -415,5 +419,13 @@ namespace Avalonia.Markup.Parsers
                 return (obj is NotSyntax not) && Argument.SequenceEqual(not.Argument);
             }
         }
+
+        public class CommaSyntax : ISyntax
+        {
+            public override bool Equals(object obj)
+            {
+                return obj is CommaSyntax or;
+            }
+        }
     }
 }

+ 20 - 0
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs

@@ -43,6 +43,7 @@ namespace Avalonia.Markup.Parsers
         private Selector Create(IEnumerable<SelectorGrammar.ISyntax> syntax)
         {
             var result = default(Selector);
+            var results = default(List<Selector>);
 
             foreach (var i in syntax)
             {
@@ -106,11 +107,30 @@ namespace Avalonia.Markup.Parsers
                     case SelectorGrammar.NotSyntax not:
                         result = result.Not(x => Create(not.Argument));
                         break;
+                    case SelectorGrammar.CommaSyntax comma:
+                        if (results == null)
+                        {
+                            results = new List<Selector>();
+                        }
+
+                        results.Add(result);
+                        result = null;
+                        break;
                     default:
                         throw new NotSupportedException($"Unsupported selector grammar '{i.GetType()}'.");
                 }
             }
 
+            if (results != null)
+            {
+                if (result != null)
+                {
+                    results.Add(result);
+                }
+
+                result = results.Count > 1 ? Selectors.Or(results) : results[0];
+            }
+
             return result;
         }
 

+ 7 - 1
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -501,7 +501,6 @@ namespace Avalonia.Skia
         {
             var paint = new SKPaint
             {
-                IsStroke = false,
                 IsAntialias = true
             };
 
@@ -558,6 +557,13 @@ namespace Avalonia.Skia
         /// <returns></returns>
         private PaintWrapper CreatePaint(Pen pen, Size targetSize)
         {
+            // In Skia 0 thickness means - use hairline rendering
+            // and for us it means - there is nothing rendered.
+            if (pen.Thickness == 0d)
+            {
+                return default;
+            }
+
             var rv = CreatePaint(pen.Brush, targetSize);
             var paint = rv.Paint;
 

+ 1 - 1
src/Windows/Avalonia.Win32/SystemDialogImpl.cs

@@ -21,7 +21,7 @@ namespace Avalonia.Win32
             var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero;
             return Task.Factory.StartNew(() =>
             {
-                var result = new string[0];
+                var result = Array.Empty<string>();
 
                 Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog;
                 Guid iid = UnmanagedMethods.ShellIds.IFileDialog;

+ 71 - 0
tests/Avalonia.Base.UnitTests/WeakEventHandlerManagerTests.cs

@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Utilities;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests
+{
+    public class WeakEventHandlerManagerTests
+    {
+        class EventSource
+        {
+            public event EventHandler<EventArgs> Event;
+
+            public void Fire()
+            {
+                Event?.Invoke(this, new EventArgs());
+            }
+        }
+
+        class Subscriber
+        {
+            private readonly Action _onEvent;
+
+            public Subscriber(Action onEvent)
+            {
+                _onEvent = onEvent;
+            }
+
+            public void OnEvent(object sender, EventArgs ev)
+            {
+                _onEvent?.Invoke();
+            }
+        }
+
+        [Fact]
+        public void EventShoudBePassedToSubscriber()
+        {
+            bool handled = false;
+            var subscriber = new Subscriber(() => handled = true);
+            var source = new EventSource();
+            WeakEventHandlerManager.Subscribe<EventSource, EventArgs, Subscriber>(source, "Event",
+                subscriber.OnEvent);
+            source.Fire();
+            Assert.True(handled);
+        }
+
+ 
+        [Fact]
+        public void EventHandlerShouldNotBeKeptAlive()
+        {
+            bool handled = false;
+            var source = new EventSource();
+            AddCollectableSubscriber(source, "Event", () => handled = true);
+            for (int c = 0; c < 10; c++)
+            {
+                GC.Collect();
+                GC.Collect(3, GCCollectionMode.Forced, true);
+            }
+            source.Fire();
+            Assert.False(handled);
+        }
+
+        private void AddCollectableSubscriber(EventSource source, string name, Action func)
+        {
+            WeakEventHandlerManager.Subscribe<EventSource, EventArgs, Subscriber>(source, name, new Subscriber(func).OnEvent);
+        }
+    }
+}

+ 44 - 27
tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs

@@ -13,7 +13,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void Up_Opens_MenuItem_With_SubMenu()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true);
                 var e = new KeyEventArgs { Key = Key.Up, Source = item };
 
@@ -27,7 +27,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void Down_Opens_MenuItem_With_SubMenu()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true);
                 var e = new KeyEventArgs { Key = Key.Down, Source = item };
 
@@ -41,7 +41,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void Right_Selects_Next_MenuItem()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var menu = Mock.Of<IMenu>(x => x.MoveSelection(NavigationDirection.Right, true) == true);
                 var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.Parent == menu);
                 var e = new KeyEventArgs { Key = Key.Right, Source = item };
@@ -55,7 +55,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void Left_Selects_Previous_MenuItem()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var menu = Mock.Of<IMenu>(x => x.MoveSelection(NavigationDirection.Left, true) == true);
                 var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.Parent == menu);
                 var e = new KeyEventArgs { Key = Key.Left, Source = item };
@@ -69,7 +69,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void Enter_On_Item_With_No_SubMenu_Causes_Click()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var menu = Mock.Of<IMenu>();
                 var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.Parent == menu);
                 var e = new KeyEventArgs { Key = Key.Enter, Source = item };
@@ -84,7 +84,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void Enter_On_Item_With_SubMenu_Opens_SubMenu()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var menu = Mock.Of<IMenu>();
                 var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var e = new KeyEventArgs { Key = Key.Enter, Source = item };
@@ -99,7 +99,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void Escape_Closes_Parent_Menu()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var menu = Mock.Of<IMenu>();
                 var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.Parent == menu);
                 var e = new KeyEventArgs { Key = Key.Escape, Source = item };
@@ -113,7 +113,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void PointerEnter_Opens_Item_When_Old_Item_Is_Open()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var menu = new Mock<IMenu>();
                 var item = Mock.Of<IMenuItem>(x =>
                     x.IsSubMenuOpen == true &&
@@ -141,7 +141,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void PointerLeave_Deselects_Item_When_Menu_Not_Open()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var menu = new Mock<IMenu>();
                 var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.Parent == menu.Object);
                 var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item };
@@ -156,7 +156,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void PointerLeave_Doesnt_Deselect_Item_When_Menu_Open()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var menu = new Mock<IMenu>();
                 var item = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.Parent == menu.Object);
                 var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item };
@@ -175,7 +175,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void Up_Selects_Previous_MenuItem()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
                 var e = new KeyEventArgs { Key = Key.Up, Source = item };
@@ -189,7 +189,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void Down_Selects_Next_MenuItem()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
                 var e = new KeyEventArgs { Key = Key.Down, Source = item };
@@ -203,7 +203,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void Left_Closes_Parent_SubMenu()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var parentItem = Mock.Of<IMenuItem>(x => x.HasSubMenu == true && x.IsSubMenuOpen == true);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
                 var e = new KeyEventArgs { Key = Key.Left, Source = item };
@@ -218,7 +218,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void Right_With_SubMenu_Items_Opens_SubMenu()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true);
                 var e = new KeyEventArgs { Key = Key.Right, Source = item };
@@ -233,7 +233,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void Right_On_TopLevel_Child_Navigates_TopLevel_Selection()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var menu = new Mock<IMenu>();
                 var parentItem = Mock.Of<IMenuItem>(x => 
                     x.IsSubMenuOpen == true &&
@@ -263,7 +263,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void Enter_On_Item_With_No_SubMenu_Causes_Click()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var menu = Mock.Of<IMenu>();
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
@@ -279,7 +279,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void Enter_On_Item_With_SubMenu_Opens_SubMenu()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true);
                 var e = new KeyEventArgs { Key = Key.Enter, Source = item };
@@ -294,7 +294,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void Escape_Closes_Parent_MenuItem()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
                 var e = new KeyEventArgs { Key = Key.Escape, Source = item };
@@ -309,7 +309,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void PointerEnter_Selects_Item()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var menu = Mock.Of<IMenu>();
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
@@ -325,7 +325,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             public void PointerEnter_Opens_Submenu_After_Delay()
             {
                 var timer = new TestTimer();
-                var target = new DefaultMenuInteractionHandler(null, timer.RunOnce);
+                var target = new DefaultMenuInteractionHandler(false, null, timer.RunOnce);
                 var menu = Mock.Of<IMenu>();
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true);
@@ -344,7 +344,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             public void PointerEnter_Closes_Sibling_Submenu_After_Delay()
             {
                 var timer = new TestTimer();
-                var target = new DefaultMenuInteractionHandler(null, timer.RunOnce);
+                var target = new DefaultMenuInteractionHandler(false, null, timer.RunOnce);
                 var menu = Mock.Of<IMenu>();
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
@@ -365,7 +365,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void PointerLeave_Deselects_Item()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var menu = Mock.Of<IMenu>();
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
@@ -381,7 +381,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void PointerLeave_Doesnt_Deselect_Sibling()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var menu = Mock.Of<IMenu>();
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
@@ -398,7 +398,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void PointerLeave_Doesnt_Deselect_Item_If_Pointer_Over_Submenu()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var menu = Mock.Of<IMenu>();
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true && x.IsPointerOverSubMenu == true);
@@ -413,7 +413,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void PointerReleased_On_Item_With_No_SubMenu_Causes_Click()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var menu = Mock.Of<IMenu>();
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem);
@@ -430,7 +430,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             public void Selection_Is_Correct_When_Pointer_Temporarily_Exits_Item_To_Select_SubItem()
             {
                 var timer = new TestTimer();
-                var target = new DefaultMenuInteractionHandler(null, timer.RunOnce);
+                var target = new DefaultMenuInteractionHandler(false, null, timer.RunOnce);
                 var menu = Mock.Of<IMenu>();
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true);
@@ -467,7 +467,7 @@ namespace Avalonia.Controls.UnitTests.Platform
             [Fact]
             public void PointerPressed_On_Item_With_SubMenu_Causes_Opens_Submenu()
             {
-                var target = new DefaultMenuInteractionHandler();
+                var target = new DefaultMenuInteractionHandler(false);
                 var menu = Mock.Of<IMenu>();
                 var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu);
                 var item = Mock.Of<IMenuItem>(x => x.Parent == parentItem && x.HasSubMenu == true);
@@ -481,6 +481,23 @@ namespace Avalonia.Controls.UnitTests.Platform
             }
         }
 
+        public class ContextMenu
+        {
+            [Fact]
+            public void Down_Selects_Selects_First_MenuItem_When_No_Selection()
+            {
+                var target = new DefaultMenuInteractionHandler(true);
+                var contextMenu = Mock.Of<IMenu>(x => x.MoveSelection(NavigationDirection.Down, true) == true);
+                var e = new KeyEventArgs { Key = Key.Down, Source = contextMenu };
+
+                target.Attach(contextMenu);
+                target.KeyDown(contextMenu, e);
+
+                Mock.Get(contextMenu).Verify(x => x.MoveSelection(NavigationDirection.Down, true));
+                Assert.True(e.Handled);
+            }
+        }
+
         private class TestTimer
         {
             private Action _action;

+ 16 - 0
tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs

@@ -0,0 +1,16 @@
+using Avalonia.Controls.Shapes;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests.Shapes
+{
+    public class PathTests
+    {
+        [Fact]
+        public void Path_With_Null_Data_Does_Not_Throw_On_Measure()
+        {
+            var target = new Path();
+
+            target.Measure(Size.Infinity);
+        }
+    }
+}

+ 11 - 0
tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs

@@ -9,6 +9,7 @@ using System.Reflection;
 using System.Threading;
 using System.Threading.Tasks;
 using Avalonia.Remote.Protocol;
+using Avalonia.Remote.Protocol.Designer;
 using Avalonia.Remote.Protocol.Viewport;
 using Xunit;
 
@@ -109,6 +110,16 @@ namespace Avalonia.DesignerSupport.Tests
                     return Guid.NewGuid().ToString();
                 if (t == typeof(Guid))
                     return Guid.NewGuid();
+                if (t == typeof(Exception))
+                    return new Exception("Here");
+                if (t == typeof(ExceptionDetails))
+                    return new ExceptionDetails
+                    {
+                        ExceptionType = "Exception",
+                        LineNumber = 5,
+                        LinePosition = 6,
+                        Message = "Here",
+                    };
                 throw new Exception($"Doesn't know how to fabricate a random value for {t}, path {pathInfo}");
             }
             

+ 30 - 2
tests/Avalonia.Input.UnitTests/MouseDeviceTests.cs

@@ -1,6 +1,7 @@
 using Avalonia.Controls;
 using Avalonia.Input.Raw;
 using Avalonia.Interactivity;
+using Avalonia.Media;
 using Avalonia.Rendering;
 using Avalonia.UnitTests;
 using Avalonia.VisualTree;
@@ -184,6 +185,33 @@ namespace Avalonia.Input.UnitTests
             }
         }
 
+
+        [Fact]
+        public void GetPosition_Should_Respect_Control_RenderTransform()
+        {
+            var renderer = new Mock<IRenderer>();
+
+            using (TestApplication(renderer.Object))
+            {
+                var inputManager = InputManager.Instance;
+
+                var root = new TestRoot
+                {
+                    MouseDevice = new MouseDevice(),
+                    Child = new Border
+                    {
+                        Background = Brushes.Black,
+                        RenderTransform = new TranslateTransform(10, 0),
+                    }
+                };
+
+                SendMouseMove(inputManager, root, new Point(11, 11));
+
+                var result = root.MouseDevice.GetPosition(root.Child);
+                Assert.Equal(new Point(1, 11), result);
+            }
+        }
+
         private void AddEnterLeaveHandlers(
             EventHandler<PointerEventArgs> handler,
             params IControl[] controls)
@@ -195,14 +223,14 @@ namespace Avalonia.Input.UnitTests
             }
         }
 
-        private void SendMouseMove(IInputManager inputManager, TestRoot root)
+        private void SendMouseMove(IInputManager inputManager, TestRoot root, Point p = new Point())
         {
             inputManager.ProcessInput(new RawMouseEventArgs(
                 root.MouseDevice,
                 0,
                 root,
                 RawMouseEventType.Move,
-                new Point(),
+                p,
                 InputModifiers.None));
         }
 

+ 33 - 0
tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs

@@ -273,6 +273,39 @@ namespace Avalonia.Markup.UnitTests.Data
             Assert.Equal(42, target.Value);
         }
 
+        [Fact]
+        public void Null_Path_Should_Bind_To_DataContext()
+        {
+            var target = new TextBlock { DataContext = "foo" };
+            var binding = new Binding();
+
+            target.Bind(TextBlock.TextProperty, binding);
+
+            Assert.Equal("foo", target.Text);
+        }
+
+        [Fact]
+        public void Empty_Path_Should_Bind_To_DataContext()
+        {
+            var target = new TextBlock { DataContext = "foo" };
+            var binding = new Binding { Path = string.Empty };
+
+            target.Bind(TextBlock.TextProperty, binding);
+
+            Assert.Equal("foo", target.Text);
+        }
+
+        [Fact]
+        public void Dot_Path_Should_Bind_To_DataContext()
+        {
+            var target = new TextBlock { DataContext = "foo" };
+            var binding = new Binding { Path = "." };
+
+            target.Bind(TextBlock.TextProperty, binding);
+
+            Assert.Equal("foo", target.Text);
+        }
+
         /// <summary>
         /// Tests a problem discovered with ListBox with selection.
         /// </summary>

+ 9 - 0
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs

@@ -36,6 +36,15 @@ namespace Avalonia.Markup.UnitTests.Parsers
             AssertIsProperty(result[0], "F0o");
         }
 
+        [Fact]
+        public void Should_Build_Dot()
+        {
+            var result = ToList(ExpressionObserverBuilder.Parse("."));
+
+            Assert.Equal(1, result.Count);
+            Assert.IsType<EmptyExpressionNode>(result[0]);
+        }
+
         [Fact]
         public void Should_Build_Property_Chain()
         {

+ 7 - 0
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests_Errors.cs

@@ -30,6 +30,13 @@ namespace Avalonia.Markup.UnitTests.Parsers
                 () => ExpressionObserverBuilder.Parse("Foo.Bar."));
         }
 
+        [Fact]
+        public void Expression_Cannot_Start_With_Period_Then_Token()
+        {
+            Assert.Throws<ExpressionParseException>(
+                () => ExpressionObserverBuilder.Parse(".Bar"));
+        }
+
         [Fact]
         public void Expression_Cannot_Have_Empty_Indexer()
         {

+ 16 - 0
tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs

@@ -261,6 +261,22 @@ namespace Avalonia.Markup.UnitTests.Parsers
                 result);
         }
 
+        [Fact]
+        public void OfType_Comma_Is_Class()
+        {
+            var result = SelectorGrammar.Parse("TextBlock, :is(Button).foo");
+
+            Assert.Equal(
+                new SelectorGrammar.ISyntax[]
+                {
+                    new SelectorGrammar.OfTypeSyntax { TypeName = "TextBlock" },
+                    new SelectorGrammar.CommaSyntax(),
+                    new SelectorGrammar.IsSyntax { TypeName = "Button" },
+                    new SelectorGrammar.ClassSyntax { Class = "foo" },
+                },
+                result);
+        }
+
         [Fact]
         public void Namespace_Alone_Fails()
         {

+ 7 - 0
tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs

@@ -14,6 +14,13 @@ namespace Avalonia.Markup.UnitTests.Parsers
             var result = target.Parse("TextBlock[IsPointerOver=True]");
         }
 
+        [Fact]
+        public void Parses_Comma_Separated_Selectors()
+        {
+            var target = new SelectorParser((ns, type) => typeof(TextBlock));
+            var result = target.Parse("TextBlock, TextBlock:foo");
+        }
+
         [Fact]
         public void Throws_If_OfType_Type_Not_Found()
         {

+ 66 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs

@@ -0,0 +1,66 @@
+// 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 Portable.Xaml;
+using Xunit;
+
+namespace Avalonia.Markup.Xaml.UnitTests.Xaml
+{
+    public class EventTests
+    {
+        [Fact]
+        public void Event_Is_Attached()
+        {
+            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);
+        }
+
+        [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();
+
+            Assert.Throws<XamlObjectWriterException>(() => loader.Load(xaml, rootInstance: target));
+        }
+
+        [Fact]
+        public void Exception_Is_Not_Thrown_If_Event_Not_Found_In_Design_Mode()
+        {
+            var xaml = @"<Button xmlns='https://github.com/avaloniaui' Click='NotFound'/>";
+            var loader = new AvaloniaXamlLoader { IsDesignMode = true };
+            var target = new MyButton();
+
+            loader.Load(xaml, rootInstance: target);
+        }
+
+        private void RaiseClick(MyButton target)
+        {
+            target.RaiseEvent(new KeyEventArgs
+            {
+                RoutedEvent = Button.KeyDownEvent,
+                Key = Key.Enter,
+            });
+        }
+
+        class MyButton : Button
+        {
+            public bool Clicked { get; private set; }
+
+            public void OnClick(object sender, EventArgs e)
+            {
+                Clicked = true;
+            }
+        }
+    }
+}

+ 20 - 0
tests/Avalonia.RenderTests/Shapes/RectangleTests.cs

@@ -20,6 +20,26 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes
         {
         }
 
+        [Fact]
+        public async Task Rectangle_0px_Stroke()
+        {
+            Decorator target = new Decorator
+            {
+                Padding = new Thickness(8),
+                Width = 200,
+                Height = 200,
+                Child = new Rectangle
+                {
+                    Fill = Brushes.Transparent,
+                    Stroke = Brushes.Black,
+                    StrokeThickness = 0
+                }
+            };
+
+            await RenderToFile(target);
+            CompareImages();
+        }
+
         [Fact]
         public async Task Rectangle_1px_Stroke()
         {

+ 106 - 0
tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs

@@ -0,0 +1,106 @@
+// 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;
+
+namespace Avalonia.Styling.UnitTests
+{
+    public class SelectorTests_Or
+    {
+        [Fact]
+        public void Or_Selector_Should_Have_Correct_String_Representation()
+        {
+            var target = Selectors.Or(
+                default(Selector).OfType<Control1>().Class("foo"),
+                default(Selector).OfType<Control2>().Class("bar"));
+
+            Assert.Equal("Control1.foo, Control2.bar", target.ToString());
+        }
+
+        [Fact]
+        public void Or_Selector_Matches_Control_Of_Correct_Type()
+        {
+            var target = Selectors.Or(
+                default(Selector).OfType<Control1>(),
+                default(Selector).OfType<Control2>().Class("bar"));
+            var control = new Control1();
+
+            Assert.Equal(SelectorMatchResult.AlwaysThisType, target.Match(control).Result);
+        }
+
+        [Fact]
+        public void Or_Selector_Matches_Control_Of_Correct_Type_With_Class()
+        {
+            var target = Selectors.Or(
+                default(Selector).OfType<Control1>(),
+                default(Selector).OfType<Control2>().Class("bar"));
+            var control = new Control2();
+
+            Assert.Equal(SelectorMatchResult.Sometimes, target.Match(control).Result);
+        }
+
+        [Fact]
+        public void Or_Selector_Doesnt_Match_Control_Of_Incorrect_Type()
+        {
+            var target = Selectors.Or(
+                default(Selector).OfType<Control1>(),
+                default(Selector).OfType<Control2>().Class("bar"));
+            var control = new Control3();
+
+            Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(control).Result);
+        }
+
+        [Fact]
+        public void Or_Selector_Doesnt_Match_Control_With_Incorrect_Name()
+        {
+            var target = Selectors.Or(
+                default(Selector).OfType<Control1>().Name("foo"),
+                default(Selector).OfType<Control2>().Name("foo"));
+            var control = new Control1 { Name = "bar" };
+
+            Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(control).Result);
+        }
+
+        [Fact]
+        public void Returns_Correct_TargetType_When_Types_Same()
+        {
+            var target = Selectors.Or(
+                default(Selector).OfType<Control1>().Class("foo"),
+                default(Selector).OfType<Control1>().Class("bar"));
+
+            Assert.Equal(typeof(Control1), target.TargetType);
+        }
+
+        [Fact]
+        public void Returns_Common_TargetType()
+        {
+            var target = Selectors.Or(
+                default(Selector).OfType<Control1>().Class("foo"),
+                default(Selector).OfType<Control2>().Class("bar"));
+
+            Assert.Equal(typeof(TestControlBase), target.TargetType);
+        }
+
+        [Fact]
+        public void Returns_Null_TargetType_When_A_Selector_Has_No_TargetType()
+        {
+            var target = Selectors.Or(
+                default(Selector).OfType<Control1>().Class("foo"),
+                default(Selector).Class("bar"));
+
+            Assert.Equal(null, target.TargetType);
+        }
+
+        public class Control1 : TestControlBase
+        {
+        }
+
+        public class Control2 : TestControlBase
+        {
+        }
+
+        public class Control3 : TestControlBase
+        {
+        }
+    }
+}

+ 13 - 0
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs

@@ -61,6 +61,19 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
             Assert.Equal(1, bitmap.RefCount);
         }
 
+        [Fact]
+        public void HitTest_On_Geometry_Node_With_Zero_Transform_Does_Not_Throw()
+        {
+            var geometry = Mock.Of<IGeometryImpl>();
+            var geometryNode = new GeometryNode(
+                new Matrix(),
+                Brushes.Black,
+                null,
+                geometry);
+
+            geometryNode.HitTest(new Point());
+        }
+
         private class TestDrawOperation : DrawOperation
         {
             public TestDrawOperation(Rect bounds, Matrix transform, Pen pen)

BIN
tests/TestFiles/Direct2D1/Shapes/Rectangle/Rectangle_0px_Stroke.expected.png


BIN
tests/TestFiles/Skia/Shapes/Rectangle/Rectangle_0px_Stroke.expected.png