Browse Source

Merge branch 'master' into refactor/value-store

Steven Kirk 6 years ago
parent
commit
b140310b37
58 changed files with 1078 additions and 180 deletions
  1. 10 2
      azure-pipelines.yml
  2. 1 1
      build/SharedVersion.props
  3. 4 0
      native/Avalonia.Native/inc/avalonia-native.h
  4. 11 3
      native/Avalonia.Native/src/OSX/app.mm
  5. 49 0
      native/Avalonia.Native/src/OSX/window.mm
  6. 1 3
      samples/ControlCatalog/App.xaml.cs
  7. 10 2
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  8. 14 3
      src/Avalonia.Base/Data/BindingOperations.cs
  9. 12 1
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  10. 3 3
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  11. 17 3
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
  12. 11 3
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  13. 15 0
      src/Avalonia.Base/Platform/IMacOSTopLevelPlatformHandle.cs
  14. 19 6
      src/Avalonia.Base/Reactive/LightweightObservableBase.cs
  15. 1 1
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  16. 20 1
      src/Avalonia.Controls/Application.cs
  17. 5 0
      src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs
  18. 38 11
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  19. 14 11
      src/Avalonia.Controls/ItemsControl.cs
  20. 2 8
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  21. 15 10
      src/Avalonia.Controls/Primitives/RangeBase.cs
  22. 13 5
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  23. 7 7
      src/Avalonia.Controls/TopLevel.cs
  24. 1 2
      src/Avalonia.Controls/TreeView.cs
  25. 5 4
      src/Avalonia.Controls/TreeViewItem.cs
  26. 0 1
      src/Avalonia.Controls/Window.cs
  27. 9 9
      src/Avalonia.Input/FocusManager.cs
  28. 2 2
      src/Avalonia.Input/InputElement.cs
  29. 5 3
      src/Avalonia.Input/Pointer.cs
  30. 27 0
      src/Avalonia.Layout/LayoutHelper.cs
  31. 2 6
      src/Avalonia.Layout/Layoutable.cs
  32. 32 1
      src/Avalonia.Native/WindowImplBase.cs
  33. 13 0
      src/Avalonia.Styling/IDataContextProvider.cs
  34. 2 6
      src/Avalonia.Styling/IStyledElement.cs
  35. 2 2
      src/Avalonia.Styling/StyledElement.cs
  36. 3 2
      src/Avalonia.Themes.Default/RadioButton.xaml
  37. 8 19
      src/Avalonia.Visuals/Media/FontFamily.cs
  38. 32 2
      src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
  39. 18 4
      src/Avalonia.Visuals/Visual.cs
  40. 151 4
      src/Avalonia.Visuals/VisualTree/VisualExtensions.cs
  41. 1 1
      src/Avalonia.X11/X11Clipboard.cs
  42. 7 0
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  43. 9 2
      src/Markup/Avalonia.Markup/Data/Binding.cs
  44. 16 0
      src/Markup/Avalonia.Markup/Data/MultiBinding.cs
  45. 4 1
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  46. 17 6
      src/Windows/Avalonia.Win32/OleContext.cs
  47. 1 1
      src/Windows/Avalonia.Win32/OleDropTarget.cs
  48. 5 0
      src/Windows/Avalonia.Win32/WindowImpl.cs
  49. 28 0
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
  50. 59 0
      tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs
  51. 64 0
      tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs
  52. 0 16
      tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs
  53. 38 0
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  54. 1 2
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  55. 18 0
      tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
  56. 30 0
      tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs
  57. 4 0
      tests/Avalonia.UnitTests/MockWindowingPlatform.cs
  58. 172 0
      tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs

+ 10 - 2
azure-pipelines.yml

@@ -34,9 +34,17 @@ jobs:
   pool:
     vmImage: 'macOS-10.14'
   steps:
-  - task: DotNetCoreInstaller@0
+  - task: UseDotNet@2
+    displayName: 'Use .NET Core SDK 3.0.x'
     inputs:
-      version: '2.1.403'
+      packageType: sdk
+      version: 3.0.x
+
+  - task: UseDotNet@2
+    displayName: 'Use .NET Core Runtime 2.1.x'
+    inputs:
+      packageType: runtime
+      version: 2.1.x
 
   - task: CmdLine@2
     displayName: 'Install Mono 5.18'

+ 1 - 1
build/SharedVersion.props

@@ -2,7 +2,7 @@
   xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <PropertyGroup>
     <Product>Avalonia</Product>
-    <Version>0.8.999</Version>
+    <Version>0.9.999</Version>
     <Copyright>Copyright 2019 &#169; The AvaloniaUI Project</Copyright>
     <PackageProjectUrl>https://avaloniaui.net</PackageProjectUrl>
     <RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>

+ 4 - 0
native/Avalonia.Native/inc/avalonia-native.h

@@ -212,6 +212,10 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown
     virtual HRESULT GetSoftwareFramebuffer(AvnFramebuffer*ret) = 0;
     virtual HRESULT SetMainMenu(IAvnAppMenu* menu) = 0;
     virtual HRESULT ObtainMainMenu(IAvnAppMenu** retOut) = 0;
+    virtual HRESULT ObtainNSWindowHandle(void** retOut) = 0;
+    virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0;
+    virtual HRESULT ObtainNSViewHandle(void** retOut) = 0;
+    virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0;
     virtual bool TryLock() = 0;
     virtual void Unlock() = 0;
 };

+ 11 - 3
native/Avalonia.Native/src/OSX/app.mm

@@ -1,16 +1,25 @@
 #include "common.h"
 @interface AvnAppDelegate : NSObject<NSApplicationDelegate>
 @end
+
 extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular;
 @implementation AvnAppDelegate
 - (void)applicationWillFinishLaunching:(NSNotification *)notification
 {
-    [[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy];
+    if([[NSApplication sharedApplication] activationPolicy] != AvnDesiredActivationPolicy)
+    {
+        for (NSRunningApplication * app in [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.dock"]) {
+            [app activateWithOptions:NSApplicationActivateIgnoringOtherApps];
+            break;
+        }
+        
+        [[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy];
+    }
 }
 
 - (void)applicationDidFinishLaunching:(NSNotification *)notification
 {
-    [NSApp activateIgnoringOtherApps:true];
+    [[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
 }
 
 @end
@@ -20,5 +29,4 @@ extern void InitializeAvnApp()
     NSApplication* app = [NSApplication sharedApplication];
     id delegate = [AvnAppDelegate new];
     [app setDelegate:delegate];
-    
 }

+ 49 - 0
native/Avalonia.Native/src/OSX/window.mm

@@ -83,6 +83,54 @@ public:
         [Window setContentView: View];
     }
     
+    virtual HRESULT ObtainNSWindowHandle(void** ret) override
+    {
+        if (ret == nullptr)
+        {
+            return E_POINTER;
+        }
+        
+        *ret =  (__bridge void*)Window;
+        
+        return S_OK;
+    }
+    
+    virtual HRESULT ObtainNSWindowHandleRetained(void** ret) override
+    {
+        if (ret == nullptr)
+        {
+            return E_POINTER;
+        }
+        
+        *ret =  (__bridge_retained void*)Window;
+        
+        return S_OK;
+    }
+    
+    virtual HRESULT ObtainNSViewHandle(void** ret) override
+    {
+        if (ret == nullptr)
+        {
+            return E_POINTER;
+        }
+        
+        *ret =  (__bridge void*)View;
+        
+        return S_OK;
+    }
+    
+    virtual HRESULT ObtainNSViewHandleRetained(void** ret) override
+    {
+        if (ret == nullptr)
+        {
+            return E_POINTER;
+        }
+        
+        *ret =  (__bridge_retained void*)View;
+        
+        return S_OK;
+    }
+    
     virtual AvnWindow* GetNSWindow() override
     {
         return Window;
@@ -691,6 +739,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
 {
     self = [super init];
     [self setWantsBestResolutionOpenGLSurface:true];
+    [self setWantsLayer:YES];
     _parent = parent;
     _area = nullptr;
     return self;

+ 1 - 3
samples/ControlCatalog/App.xaml.cs

@@ -1,6 +1,4 @@
-using System;
 using Avalonia;
-using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Markup.Xaml;
 
@@ -19,7 +17,7 @@ namespace ControlCatalog
                 desktopLifetime.MainWindow = new MainWindow();
             else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
                 singleViewLifetime.MainView = new MainView();
-            
+
             base.OnFrameworkInitializationCompleted();
         }
     }

+ 10 - 2
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@@ -224,12 +224,20 @@ namespace Avalonia
             Contract.Requires<ArgumentNullException>(type != null);
             Contract.Requires<ArgumentNullException>(name != null);
 
-            if (name.Contains('.'))
+            if (name.Contains("."))
             {
                 throw new InvalidOperationException("Attached properties not supported.");
             }
 
-            return GetRegistered(type).FirstOrDefault(x => x.Name == name);
+            foreach (AvaloniaProperty x in GetRegistered(type))
+            {
+                if (x.Name == name)
+                {
+                    return x;
+                }
+            }
+
+            return null;
         }
 
         /// <summary>

+ 14 - 3
src/Avalonia.Base/Data/BindingOperations.cs

@@ -2,7 +2,6 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using System.Linq;
 using System.Reactive.Disposables;
 using System.Reactive.Linq;
 using Avalonia.Reactive;
@@ -57,22 +56,34 @@ namespace Avalonia.Data
 
                     if (source != null)
                     {
+                        // Perf: Avoid allocating closure in the outer scope.
+                        var targetCopy = target;
+                        var propertyCopy = property;
+                        var bindingCopy = binding;
+
                         return source
                             .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue)
                             .Take(1)
-                            .Subscribe(x => target.SetValue(property, x, binding.Priority));
+                            .Subscribe(x => targetCopy.SetValue(propertyCopy, x, bindingCopy.Priority));
                     }
                     else
                     {
                         target.SetValue(property, binding.Value, binding.Priority);
                         return Disposable.Empty;
                     }
+
                 case BindingMode.OneWayToSource:
+                {
+                    // Perf: Avoid allocating closure in the outer scope.
+                    var bindingCopy = binding;
+
                     return Observable.CombineLatest(
                         binding.Observable,
                         target.GetObservable(property),
                         (_, v) => v)
-                    .Subscribe(x => binding.Subject.OnNext(x));
+                    .Subscribe(x => bindingCopy.Subject.OnNext(x));
+                }
+
                 default:
                     throw new ArgumentException("Invalid binding mode.");
             }

+ 12 - 1
src/Avalonia.Base/Data/Core/BindingExpression.cs

@@ -21,6 +21,7 @@ namespace Avalonia.Data.Core
         private readonly ExpressionObserver _inner;
         private readonly Type _targetType;
         private readonly object _fallbackValue;
+        private readonly object _targetNullValue;
         private readonly BindingPriority _priority;
         InnerListener _innerListener;
         WeakReference<object> _value;
@@ -51,7 +52,7 @@ namespace Avalonia.Data.Core
             IValueConverter converter,
             object converterParameter = null,
             BindingPriority priority = BindingPriority.LocalValue)
-            : this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority)
+            : this(inner, targetType, AvaloniaProperty.UnsetValue, AvaloniaProperty.UnsetValue, converter, converterParameter, priority)
         {
         }
 
@@ -63,6 +64,9 @@ namespace Avalonia.Data.Core
         /// <param name="fallbackValue">
         /// The value to use when the binding is unable to produce a value.
         /// </param>
+        /// <param name="targetNullValue">
+        /// The value to use when the binding result is null.
+        /// </param>
         /// <param name="converter">The value converter to use.</param>
         /// <param name="converterParameter">
         /// A parameter to pass to <paramref name="converter"/>.
@@ -72,6 +76,7 @@ namespace Avalonia.Data.Core
             ExpressionObserver inner, 
             Type targetType,
             object fallbackValue,
+            object targetNullValue,
             IValueConverter converter,
             object converterParameter = null,
             BindingPriority priority = BindingPriority.LocalValue)
@@ -85,6 +90,7 @@ namespace Avalonia.Data.Core
             Converter = converter;
             ConverterParameter = converterParameter;
             _fallbackValue = fallbackValue;
+            _targetNullValue = targetNullValue;
             _priority = priority;
         }
 
@@ -196,6 +202,11 @@ namespace Avalonia.Data.Core
         /// <inheritdoc/>
         private object ConvertValue(object value)
         {
+            if (value == null && _targetNullValue != AvaloniaProperty.UnsetValue)
+            {
+                return _targetNullValue;
+            }
+
             if (value == BindingOperations.DoNothing)
             {
                 return value;

+ 3 - 3
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

@@ -21,7 +21,7 @@ namespace Avalonia.Data.Core
         /// An ordered collection of property accessor plugins that can be used to customize
         /// the reading and subscription of property values on a type.
         /// </summary>
-        public static readonly IList<IPropertyAccessorPlugin> PropertyAccessors =
+        public static readonly List<IPropertyAccessorPlugin> PropertyAccessors =
             new List<IPropertyAccessorPlugin>
             {
                 new AvaloniaPropertyAccessorPlugin(),
@@ -33,7 +33,7 @@ namespace Avalonia.Data.Core
         /// An ordered collection of validation checker plugins that can be used to customize
         /// the validation of view model and model data.
         /// </summary>
-        public static readonly IList<IDataValidationPlugin> DataValidators =
+        public static readonly List<IDataValidationPlugin> DataValidators =
             new List<IDataValidationPlugin>
             {
                 new DataAnnotationsValidationPlugin(),
@@ -45,7 +45,7 @@ namespace Avalonia.Data.Core
         /// An ordered collection of stream plugins that can be used to customize the behavior
         /// of the '^' stream binding operator.
         /// </summary>
-        public static readonly IList<IStreamPlugin> StreamHandlers =
+        public static readonly List<IStreamPlugin> StreamHandlers =
             new List<IStreamPlugin>
             {
                 new TaskStreamPlugin(),

+ 17 - 3
src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs

@@ -2,7 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using System.Reactive.Linq;
+using System.Runtime.ExceptionServices;
 
 namespace Avalonia.Data.Core.Plugins
 {
@@ -76,7 +76,7 @@ namespace Avalonia.Data.Core.Plugins
             return false;
         }
 
-        private class Accessor : PropertyAccessorBase
+        private class Accessor : PropertyAccessorBase, IObserver<object>
         {
             private readonly WeakReference<AvaloniaObject> _reference;
             private readonly AvaloniaProperty _property;
@@ -117,7 +117,7 @@ namespace Avalonia.Data.Core.Plugins
 
             protected override void SubscribeCore()
             {
-                _subscription = Instance?.GetObservable(_property).Subscribe(PublishValue);
+                _subscription = Instance?.GetObservable(_property).Subscribe(this);
             }
 
             protected override void UnsubscribeCore()
@@ -125,6 +125,20 @@ namespace Avalonia.Data.Core.Plugins
                 _subscription?.Dispose();
                 _subscription = null;
             }
+
+            void IObserver<object>.OnCompleted()
+            {
+            }
+
+            void IObserver<object>.OnError(Exception error)
+            {
+                ExceptionDispatchInfo.Capture(error).Throw();
+            }
+
+            void IObserver<object>.OnNext(object value)
+            {
+                PublishValue(value);
+            }
         }
     }
 }

+ 11 - 3
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

@@ -2,8 +2,6 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using System.Linq;
-using System.Reactive.Linq;
 using Avalonia.Data.Core.Plugins;
 
 namespace Avalonia.Data.Core
@@ -41,7 +39,17 @@ namespace Avalonia.Data.Core
         {
             reference.TryGetTarget(out object target);
 
-            var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(target, PropertyName));
+            IPropertyAccessorPlugin plugin = null;
+
+            foreach (IPropertyAccessorPlugin x in ExpressionObserver.PropertyAccessors)
+            {
+                if (x.Match(target, PropertyName))
+                {
+                    plugin = x;
+                    break;
+                }
+            }
+
             var accessor = plugin?.Start(reference, PropertyName);
 
             if (_enableValidation && Next == null)

+ 15 - 0
src/Avalonia.Base/Platform/IMacOSTopLevelPlatformHandle.cs

@@ -0,0 +1,15 @@
+// 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;
+
+namespace Avalonia.Platform
+{
+    public interface IMacOSTopLevelPlatformHandle
+    {
+        IntPtr NSView { get; }
+        IntPtr GetNSViewRetained();
+        IntPtr NSWindow { get; }
+        IntPtr GetNSWindowRetained();
+    }
+}

+ 19 - 6
src/Avalonia.Base/Reactive/LightweightObservableBase.cs

@@ -116,20 +116,33 @@ namespace Avalonia.Reactive
         {
             if (Volatile.Read(ref _observers) != null)
             {
-                IObserver<T>[] observers;
-
+                IObserver<T>[] observers = null;
+                IObserver<T> singleObserver = null;
                 lock (this)
                 {
                     if (_observers == null)
                     {
                         return;
                     }
-                    observers = _observers.ToArray();
+                    if (_observers.Count == 1)
+                    {
+                        singleObserver = _observers[0];
+                    }
+                    else
+                    {
+                        observers = _observers.ToArray();
+                    }
                 }
-
-                foreach (var observer in observers)
+                if (singleObserver != null)
                 {
-                    observer.OnNext(value);
+                    singleObserver.OnNext(value);
+                }
+                else
+                {
+                    foreach (var observer in observers)
+                    {
+                        observer.OnNext(value);
+                    }
                 }
             }
         }

+ 1 - 1
src/Avalonia.Base/Utilities/IdentifierParser.cs

@@ -15,7 +15,7 @@ namespace Avalonia.Utilities
         {
             if (IsValidIdentifierStart(r.Peek))
             {
-                return r.TakeWhile(IsValidIdentifierChar);
+                return r.TakeWhile(c => IsValidIdentifierChar(c));
             }
             else
             {

+ 20 - 1
src/Avalonia.Controls/Application.cs

@@ -32,7 +32,7 @@ namespace Avalonia
     /// method.
     /// - Tracks the lifetime of the application.
     /// </remarks>
-    public class Application : AvaloniaObject, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode
+    public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode
     {
         /// <summary>
         /// The application-global data templates.
@@ -45,6 +45,12 @@ namespace Avalonia
         private Styles _styles;
         private IResourceDictionary _resources;
 
+        /// <summary>
+        /// Defines the <see cref="DataContext"/> property.
+        /// </summary>
+        public static readonly StyledProperty<object> DataContextProperty =
+            StyledElement.DataContextProperty.AddOwner<Application>();
+
         /// <inheritdoc/>
         public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
 
@@ -56,6 +62,19 @@ namespace Avalonia
             Name = "Avalonia Application";
         }
 
+        /// <summary>
+        /// Gets or sets the Applications's data context.
+        /// </summary>
+        /// <remarks>
+        /// The data context property specifies the default object that will
+        /// be used for data binding.
+        /// </remarks>
+        public object DataContext
+        {
+            get { return GetValue(DataContextProperty); }
+            set { SetValue(DataContextProperty, value); }
+        }
+
         /// <summary>
         /// Gets the current instance of the <see cref="Application"/> class.
         /// </summary>

+ 5 - 0
src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs

@@ -12,5 +12,10 @@ namespace Avalonia.Controls.Generators
         /// Gets the container index for the tree.
         /// </summary>
         TreeContainerIndex Index { get; }
+
+        /// <summary>
+        /// Updates the index based on the parent <see cref="TreeView"/>.
+        /// </summary>
+        void UpdateIndex();
     }
 }

+ 38 - 11
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

@@ -3,8 +3,10 @@
 
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using Avalonia.Controls.Templates;
 using Avalonia.Data;
+using Avalonia.LogicalTree;
 
 namespace Avalonia.Controls.Generators
 {
@@ -15,6 +17,8 @@ namespace Avalonia.Controls.Generators
     public class TreeItemContainerGenerator<T> : ItemContainerGenerator<T>, ITreeItemContainerGenerator
         where T : class, IControl, new()
     {
+        private TreeView _treeView;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="TreeItemContainerGenerator{T}"/> class.
         /// </summary>
@@ -23,31 +27,28 @@ namespace Avalonia.Controls.Generators
         /// <param name="contentTemplateProperty">The container's ContentTemplate property.</param>
         /// <param name="itemsProperty">The container's Items property.</param>
         /// <param name="isExpandedProperty">The container's IsExpanded property.</param>
-        /// <param name="index">The container index for the tree</param>
         public TreeItemContainerGenerator(
             IControl owner,
             AvaloniaProperty contentProperty,
             AvaloniaProperty contentTemplateProperty,
             AvaloniaProperty itemsProperty,
-            AvaloniaProperty isExpandedProperty,
-            TreeContainerIndex index)
+            AvaloniaProperty isExpandedProperty)
             : base(owner, contentProperty, contentTemplateProperty)
         {
             Contract.Requires<ArgumentNullException>(owner != null);
             Contract.Requires<ArgumentNullException>(contentProperty != null);
             Contract.Requires<ArgumentNullException>(itemsProperty != null);
             Contract.Requires<ArgumentNullException>(isExpandedProperty != null);
-            Contract.Requires<ArgumentNullException>(index != null);
 
             ItemsProperty = itemsProperty;
             IsExpandedProperty = isExpandedProperty;
-            Index = index;
+            UpdateIndex();
         }
 
         /// <summary>
         /// Gets the container index for the tree.
         /// </summary>
-        public TreeContainerIndex Index { get; }
+        public TreeContainerIndex Index { get; private set; }
 
         /// <summary>
         /// Gets the item container's Items property.
@@ -70,7 +71,7 @@ namespace Avalonia.Controls.Generators
             }
             else if (container != null)
             {
-                Index.Add(item, container);
+                Index?.Add(item, container);
                 return container;
             }
             else
@@ -92,7 +93,7 @@ namespace Avalonia.Controls.Generators
                     result.DataContext = item;
                 }
 
-                Index.Add(item, result);
+                Index?.Add(item, result);
 
                 return result;
             }
@@ -101,24 +102,50 @@ namespace Avalonia.Controls.Generators
         public override IEnumerable<ItemContainerInfo> Clear()
         {
             var items = base.Clear();
-            Index.Remove(0, items);
+            Index?.Remove(0, items);
             return items;
         }
 
         public override IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count)
         {
-            Index.Remove(startingIndex, GetContainerRange(startingIndex, count));
+            Index?.Remove(startingIndex, GetContainerRange(startingIndex, count));
             return base.Dematerialize(startingIndex, count);
         }
 
         public override IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count)
         {
-            Index.Remove(startingIndex, GetContainerRange(startingIndex, count));
+            Index?.Remove(startingIndex, GetContainerRange(startingIndex, count));
             return base.RemoveRange(startingIndex, count);
         }
 
         public override bool TryRecycle(int oldIndex, int newIndex, object item) => false;
 
+        public void UpdateIndex()
+        {
+            if (Owner is TreeView treeViewOwner && Index == null)
+            {
+                Index = new TreeContainerIndex();
+                _treeView = treeViewOwner;
+            }
+            else if (Owner.IsAttachedToLogicalTree)
+            {
+                var treeView = Owner.GetSelfAndLogicalAncestors().OfType<TreeView>().FirstOrDefault();
+                
+                if (treeView != _treeView)
+                {
+                    Clear();
+                    Index = treeView?.ItemContainerGenerator?.Index;
+                    _treeView = treeView;
+                }
+            }
+            else
+            {
+                Clear();
+                Index = null;
+                _treeView = null;
+            }
+        }
+
         class WrapperTreeDataTemplate : ITreeDataTemplate
         {
             private readonly IDataTemplate _inner;

+ 14 - 11
src/Avalonia.Controls/ItemsControl.cs

@@ -5,7 +5,6 @@ using System;
 using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Specialized;
-using System.Linq;
 using Avalonia.Collections;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Presenters;
@@ -324,20 +323,24 @@ namespace Avalonia.Controls
                     return;
                 }
 
-                var current = focus.Current
-                    .GetSelfAndVisualAncestors()
-                    .OfType<IInputElement>()
-                    .FirstOrDefault(x => x.VisualParent == container);
+                IVisual current = focus.Current;
 
-                if (current != null)
+                while (current != null)
                 {
-                    var next = GetNextControl(container, direction.Value, current, false);
-
-                    if (next != null)
+                    if (current.VisualParent == container && current is IInputElement inputElement)
                     {
-                        focus.Focus(next, NavigationMethod.Directional);
-                        e.Handled = true;
+                        IInputElement next = GetNextControl(container, direction.Value, inputElement, false);
+
+                        if (next != null)
+                        {
+                            focus.Focus(next, NavigationMethod.Directional);
+                            e.Handled = true;
+                        }
+
+                        break;
                     }
+
+                    current = current.VisualParent;
                 }
             }
 

+ 2 - 8
src/Avalonia.Controls/Notifications/WindowNotificationManager.cs

@@ -149,15 +149,9 @@ namespace Avalonia.Controls.Notifications
         /// <param name="host">The <see cref="Window"/> that will be the host.</param>
         private void Install(Window host)
         {
-            var adornerLayer = host.GetVisualDescendants()
-                .OfType<VisualLayerManager>()
-                .FirstOrDefault()
-                ?.AdornerLayer;
+            var adornerLayer = host.FindDescendantOfType<VisualLayerManager>()?.AdornerLayer;
 
-            if (adornerLayer != null)
-            {
-                adornerLayer.Children.Add(this);
-            }
+            adornerLayer?.Children.Add(this);
         }
     }
 }

+ 15 - 10
src/Avalonia.Controls/Primitives/RangeBase.cs

@@ -75,7 +75,10 @@ namespace Avalonia.Controls.Primitives
 
             set
             {
-                ValidateDouble(value, "Minimum");
+                if (!ValidateDouble(value))
+                {
+                    return;
+                }
 
                 if (IsInitialized)
                 {
@@ -102,7 +105,10 @@ namespace Avalonia.Controls.Primitives
 
             set
             {
-                ValidateDouble(value, "Maximum");
+                if (!ValidateDouble(value))
+                {
+                    return;
+                }
 
                 if (IsInitialized)
                 {
@@ -129,7 +135,10 @@ namespace Avalonia.Controls.Primitives
 
             set
             {
-                ValidateDouble(value, "Value");
+                if (!ValidateDouble(value))
+                {
+                    return;
+                }
 
                 if (IsInitialized)
                 {
@@ -164,16 +173,12 @@ namespace Avalonia.Controls.Primitives
         }
 
         /// <summary>
-        /// Throws an exception if the double value is NaN or Inf.
+        /// Checks if the double value is not inifinity nor NaN.
         /// </summary>
         /// <param name="value">The value.</param>
-        /// <param name="property">The name of the property being set.</param>
-        private static void ValidateDouble(double value, string property)
+        private static bool ValidateDouble(double value)
         {
-            if (double.IsInfinity(value) || double.IsNaN(value))
-            {
-                throw new ArgumentException($"{value} is not a valid value for {property}.");
-            }
+            return !double.IsInfinity(value) || !double.IsNaN(value);
         }
 
         /// <summary>

+ 13 - 5
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -13,7 +13,6 @@ using Avalonia.Input;
 using Avalonia.Input.Platform;
 using Avalonia.Interactivity;
 using Avalonia.Logging;
-using Avalonia.Styling;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Controls.Primitives
@@ -269,11 +268,20 @@ namespace Avalonia.Controls.Primitives
         /// <returns>The container or null if the event did not originate in a container.</returns>
         protected IControl GetContainerFromEventSource(IInteractive eventSource)
         {
-            var item = ((IVisual)eventSource).GetSelfAndVisualAncestors()
-                .OfType<IControl>()
-                .FirstOrDefault(x => x.LogicalParent == this && ItemContainerGenerator?.IndexFromContainer(x) != -1);
+            var parent = (IVisual)eventSource;
 
-            return item;
+            while (parent != null)
+            {
+                if (parent is IControl control && control.LogicalParent == this
+                                               && ItemContainerGenerator?.IndexFromContainer(control) != -1)
+                {
+                    return control;
+                }
+
+                parent = parent.VisualParent;
+            }
+
+            return null;
         }
 
         /// <inheritdoc/>

+ 7 - 7
src/Avalonia.Controls/TopLevel.cs

@@ -2,9 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using System.Linq;
 using System.Reactive.Linq;
-using Avalonia.Controls.Notifications;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
@@ -15,7 +13,6 @@ using Avalonia.Platform;
 using Avalonia.Rendering;
 using Avalonia.Styling;
 using Avalonia.Utilities;
-using Avalonia.VisualTree;
 using JetBrains.Annotations;
 
 namespace Avalonia.Controls
@@ -269,6 +266,12 @@ namespace Avalonia.Controls
         /// </summary>
         protected virtual void HandleClosed()
         {
+            var logicalArgs = new LogicalTreeAttachmentEventArgs(this);
+            ((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs);
+
+            var visualArgs = new VisualTreeAttachmentEventArgs(this, this);
+            OnDetachedFromVisualTreeCore(visualArgs);
+
             (this as IInputRoot).MouseDevice?.TopLevelClosed(this);
             PlatformImpl = null;
             OnClosed(EventArgs.Empty);
@@ -296,10 +299,7 @@ namespace Avalonia.Controls
         /// <param name="scaling">The window scaling.</param>
         protected virtual void HandleScalingChanged(double scaling)
         {
-            foreach (ILayoutable control in this.GetSelfAndVisualDescendants())
-            {
-                control.InvalidateMeasure();
-            }
+            LayoutHelper.InvalidateSelfAndChildrenMeasure(this);
         }
 
         /// <inheritdoc/>

+ 1 - 2
src/Avalonia.Controls/TreeView.cs

@@ -393,8 +393,7 @@ namespace Avalonia.Controls
                 TreeViewItem.HeaderProperty,
                 TreeViewItem.ItemTemplateProperty,
                 TreeViewItem.ItemsProperty,
-                TreeViewItem.IsExpandedProperty,
-                new TreeContainerIndex());
+                TreeViewItem.IsExpandedProperty);
             result.Index.Materialized += ContainerMaterialized;
             return result;
         }

+ 5 - 4
src/Avalonia.Controls/TreeViewItem.cs

@@ -98,17 +98,18 @@ namespace Avalonia.Controls
                 TreeViewItem.HeaderProperty,
                 TreeViewItem.ItemTemplateProperty,
                 TreeViewItem.ItemsProperty,
-                TreeViewItem.IsExpandedProperty,
-                _treeView?.ItemContainerGenerator.Index ?? new TreeContainerIndex());
+                TreeViewItem.IsExpandedProperty);
         }
 
         /// <inheritdoc/>
         protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
             base.OnAttachedToLogicalTree(e);
+            
             _treeView = this.GetLogicalAncestors().OfType<TreeView>().FirstOrDefault();
-
+            
             Level = CalculateDistanceFromLogicalParent<TreeView>(this) - 1;
+            ItemContainerGenerator.UpdateIndex();
 
             if (ItemTemplate == null && _treeView?.ItemTemplate != null)
             {
@@ -119,7 +120,7 @@ namespace Avalonia.Controls
         protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
             base.OnDetachedFromLogicalTree(e);
-            ItemContainerGenerator.Clear();
+            ItemContainerGenerator.UpdateIndex();
         }
 
         protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e)

+ 0 - 1
src/Avalonia.Controls/Window.cs

@@ -336,7 +336,6 @@ namespace Avalonia.Controls
                 if (close)
                 {
                     PlatformImpl?.Dispose();
-                    HandleClosed();
                 }
             }
         }

+ 9 - 9
src/Avalonia.Input/FocusManager.cs

@@ -180,18 +180,18 @@ namespace Avalonia.Input
 
             if (sender == e.Source && ev.MouseButton == MouseButton.Left)
             {
-                var element = (ev.Pointer?.Captured as IInputElement) ?? (e.Source as IInputElement);
+                IVisual element = ev.Pointer?.Captured ?? e.Source as IInputElement;
 
-                if (element == null || !CanFocus(element))
+                while (element != null)
                 {
-                    element = element.GetSelfAndVisualAncestors()
-                        .OfType<IInputElement>()
-                        .FirstOrDefault(CanFocus);
-                }
+                    if (element is IInputElement inputElement && CanFocus(inputElement))
+                    {
+                        Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.InputModifiers);
 
-                if (element != null)
-                {
-                    Instance?.Focus(element, NavigationMethod.Pointer, ev.InputModifiers);
+                        break;
+                    }
+                    
+                    element = element.VisualParent;
                 }
             }
         }

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

@@ -342,7 +342,7 @@ namespace Avalonia.Input
         }
 
         /// <summary>
-        /// Gets or sets a value indicating whether the control is focused.
+        /// Gets a value indicating whether the control is focused.
         /// </summary>
         public bool IsFocused
         {
@@ -360,7 +360,7 @@ namespace Avalonia.Input
         }
 
         /// <summary>
-        /// Gets or sets a value indicating whether the pointer is currently over the control.
+        /// Gets a value indicating whether the pointer is currently over the control.
         /// </summary>
         public bool IsPointerOver
         {

+ 5 - 3
src/Avalonia.Input/Pointer.cs

@@ -55,9 +55,11 @@ namespace Avalonia.Input
                 Captured.DetachedFromVisualTree += OnCaptureDetached;
         }
 
-        IInputElement GetNextCapture(IVisual parent) =>
-            parent as IInputElement ?? parent.GetVisualAncestors().OfType<IInputElement>().FirstOrDefault();
-        
+        IInputElement GetNextCapture(IVisual parent)
+        {
+            return parent as IInputElement ?? parent.FindAncestorOfType<IInputElement>();
+        }
+
         private void OnCaptureDetached(object sender, VisualTreeAttachmentEventArgs e)
         {
             Capture(GetNextCapture(e.Parent));

+ 27 - 0
src/Avalonia.Layout/LayoutHelper.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using Avalonia.VisualTree;
 
 namespace Avalonia.Layout
 {
@@ -61,5 +62,31 @@ namespace Avalonia.Layout
 
             return availableSize;
         }
+
+        /// <summary>
+        /// Invalidates measure for given control and all visual children recursively.
+        /// </summary>
+        public static void InvalidateSelfAndChildrenMeasure(ILayoutable control)
+        {
+            void InnerInvalidateMeasure(IVisual target)
+            {
+                if (target is ILayoutable targetLayoutable)
+                {
+                    targetLayoutable.InvalidateMeasure();
+                }
+
+                var visualChildren = target.VisualChildren;
+                var visualChildrenCount = visualChildren.Count;
+
+                for (int i = 0; i < visualChildrenCount; i++)
+                {
+                    IVisual child = visualChildren[i];
+
+                    InnerInvalidateMeasure(child);
+                }
+            }
+
+            InnerInvalidateMeasure(control);
+        }
     }
 }

+ 2 - 6
src/Avalonia.Layout/Layoutable.cs

@@ -2,7 +2,6 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using System.Linq;
 using Avalonia.Logging;
 using Avalonia.VisualTree;
 
@@ -694,12 +693,9 @@ namespace Avalonia.Layout
         }
 
         /// <inheritdoc/>
-        protected override sealed void OnVisualParentChanged(IVisual oldParent, IVisual newParent)
+        protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent)
         {
-            foreach (ILayoutable i in this.GetSelfAndVisualDescendants())
-            {
-                i.InvalidateMeasure();
-            }
+            LayoutHelper.InvalidateSelfAndChildrenMeasure(this);
 
             base.OnVisualParentChanged(oldParent, newParent);
         }

+ 32 - 1
src/Avalonia.Native/WindowImplBase.cs

@@ -16,6 +16,34 @@ using Avalonia.Threading;
 
 namespace Avalonia.Native
 {
+    public class MacOSTopLevelWindowHandle : IPlatformHandle, IMacOSTopLevelPlatformHandle
+    {
+        IAvnWindowBase _native;
+
+        public MacOSTopLevelWindowHandle(IAvnWindowBase native)
+        {
+            _native = native;
+        }
+
+        public IntPtr Handle => NSWindow;
+
+        public string HandleDescriptor => "NSWindow";
+
+        public IntPtr NSView => _native.ObtainNSViewHandle();
+
+        public IntPtr NSWindow => _native.ObtainNSWindowHandle();
+
+        public IntPtr GetNSViewRetained()
+        {
+            return _native.ObtainNSViewHandleRetained();
+        }
+
+        public IntPtr GetNSWindowRetained()
+        {
+            return _native.ObtainNSWindowHandleRetained();
+        }
+    }
+
     public abstract class WindowBaseImpl : IWindowBaseImpl,
         IFramebufferPlatformSurface
     {
@@ -45,6 +73,9 @@ namespace Avalonia.Native
         protected void Init(IAvnWindowBase window, IAvnScreens screens)
         {
             _native = window;
+
+            Handle = new MacOSTopLevelWindowHandle(window);
+            
             _glSurface = new GlPlatformSurface(window);
             Screen = new ScreenImpl(screens);
             _savedLogicalSize = ClientSize;
@@ -349,6 +380,6 @@ namespace Avalonia.Native
 
         }
 
-        public IPlatformHandle Handle => new PlatformHandle(IntPtr.Zero, "NOT SUPPORTED");
+        public IPlatformHandle Handle { get; private set; }
     }
 }

+ 13 - 0
src/Avalonia.Styling/IDataContextProvider.cs

@@ -0,0 +1,13 @@
+namespace Avalonia
+{
+    /// <summary>
+    /// Defines an element with a data context that can be used for binding.
+    /// </summary>
+    public interface IDataContextProvider : IAvaloniaObject
+    {
+        /// <summary>
+        /// Gets or sets the element's data context.
+        /// </summary>
+        object DataContext { get; set; }
+    }
+}

+ 2 - 6
src/Avalonia.Styling/IStyledElement.cs

@@ -10,7 +10,8 @@ namespace Avalonia
         IStyleHost,
         ILogical,
         IResourceProvider,
-        IResourceNode
+        IResourceNode,
+        IDataContextProvider
     {
         /// <summary>
         /// Occurs when the control has finished initialization.
@@ -27,11 +28,6 @@ namespace Avalonia
         /// </summary>
         new Classes Classes { get; set; }
 
-        /// <summary>
-        /// Gets or sets the control's data context.
-        /// </summary>
-        object DataContext { get; set; }
-
         /// <summary>
         /// Gets the control's logical parent.
         /// </summary>

+ 2 - 2
src/Avalonia.Styling/StyledElement.cs

@@ -24,7 +24,7 @@ namespace Avalonia
     /// - Implements <see cref="ILogical"/> to form part of a logical tree.
     /// - A collection of class strings for custom styling.
     /// </summary>
-    public class StyledElement : Animatable, IStyledElement, ISetLogicalParent, ISetInheritanceParent
+    public class StyledElement : Animatable, IDataContextProvider, IStyledElement, ISetLogicalParent, ISetInheritanceParent
     {
         /// <summary>
         /// Defines the <see cref="DataContext"/> property.
@@ -288,7 +288,7 @@ namespace Avalonia
                     var list = new AvaloniaList<ILogical>
                     {
                         ResetBehavior = ResetBehavior.Remove,
-                        Validate = ValidateLogicalChild
+                        Validate = logical => ValidateLogicalChild(logical)
                     };
                     list.CollectionChanged += LogicalChildrenCollectionChanged;
                     _logicalChildren = list;

+ 3 - 2
src/Avalonia.Themes.Default/RadioButton.xaml

@@ -13,7 +13,6 @@
                    Height="18"
                    VerticalAlignment="Center"/>
           <Ellipse Name="checkMark"
-                   Fill="{DynamicResource HighlightBrush}"
                    Width="10"
                    Height="10"
                    Stretch="Uniform"
@@ -38,10 +37,12 @@
       </ControlTemplate>
     </Setter>
   </Style>
+  
   <Style Selector="RadioButton:pointerover /template/ Ellipse#border">
     <Setter Property="Stroke" Value="{DynamicResource ThemeBorderHighBrush}"/>
   </Style>
   <Style Selector="RadioButton /template/ Ellipse#checkMark">
+    <Setter Property="Fill" Value="{DynamicResource HighlightBrush}"/>
     <Setter Property="IsVisible" Value="False"/>
   </Style>
   <Style Selector="RadioButton /template/ Ellipse#indeterminateMark">
@@ -56,4 +57,4 @@
   <Style Selector="RadioButton:disabled /template/ Ellipse#border">
     <Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}"/>
   </Style>
-</Styles>
+</Styles>

+ 8 - 19
src/Avalonia.Visuals/Media/FontFamily.cs

@@ -184,36 +184,25 @@ namespace Avalonia.Media
         {
             unchecked
             {
-                var hash = (int)2186146271;
-
-                if (Key != null)
-                {
-                    hash = (hash * 15768619) ^ Key.GetHashCode();
-                }
-                else
-                {
-                    hash = (hash * 15768619) ^ FamilyNames.GetHashCode();
-                }
-
-                if (Key != null)
-                {
-                    hash = (hash * 15768619) ^ Key.GetHashCode();
-                }
-
-                return hash;
+                return ((FamilyNames != null ? FamilyNames.GetHashCode() : 0) * 397) ^ (Key != null ? Key.GetHashCode() : 0);
             }
         }
 
         public override bool Equals(object obj)
         {
+            if (ReferenceEquals(this, obj))
+            {
+                return true;
+            }
+
             if (!(obj is FontFamily other))
             {
                 return false;
             }
 
-            if (Key != null)
+            if (!Equals(Key, other.Key))
             {
-                return other.FamilyNames.Equals(FamilyNames) && other.Key.Equals(Key);
+                return false;
             }
 
             return other.FamilyNames.Equals(FamilyNames);

+ 32 - 2
src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs

@@ -111,7 +111,24 @@ namespace Avalonia.Media.Fonts
         /// </returns>
         public override int GetHashCode()
         {
-            return ToString().GetHashCode();
+            if (Count == 0)
+            {
+                return 0;
+            }
+
+            unchecked
+            {
+                int hash = 17;
+
+                for (var i = 0; i < Names.Count; i++)
+                {
+                    string name = Names[i];
+
+                    hash = hash * 23 + name.GetHashCode();
+                }
+
+                return hash;
+            }
         }
 
         /// <summary>
@@ -128,7 +145,20 @@ namespace Avalonia.Media.Fonts
                 return false;
             }
 
-            return other.ToString().Equals(ToString());
+            if (other.Count != Count)
+            {
+                return false;
+            }
+
+            for (int i = 0; i < Count; i++)
+            {
+                if (Names[i] != other.Names[i])
+                {
+                    return false;
+                }
+            }
+
+            return true;
         }
 
         public int Count => Names.Count;

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

@@ -4,7 +4,6 @@
 using System;
 using System.Collections.Specialized;
 using System.Linq;
-using System.Reactive.Linq;
 using Avalonia.Collections;
 using Avalonia.Data;
 using Avalonia.Logging;
@@ -121,7 +120,7 @@ namespace Avalonia
         {
             var visualChildren = new AvaloniaList<IVisual>();
             visualChildren.ResetBehavior = ResetBehavior.Remove;
-            visualChildren.Validate = ValidateVisualChild;
+            visualChildren.Validate = visual => ValidateVisualChild(visual);
             visualChildren.CollectionChanged += VisualChildrenChanged;
             VisualChildren = visualChildren;
         }
@@ -173,7 +172,22 @@ namespace Avalonia
         /// </summary>
         public bool IsEffectivelyVisible
         {
-            get { return this.GetSelfAndVisualAncestors().All(x => x.IsVisible); }
+            get
+            {
+                IVisual node = this;
+
+                while (node != null)
+                {
+                    if (!node.IsVisible)
+                    {
+                        return false;
+                    }
+
+                    node = node.VisualParent;
+                }
+
+                return true;
+            }
         }
 
         /// <summary>
@@ -556,7 +570,7 @@ namespace Avalonia
 
             if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true)
             {
-                var root = this.GetVisualAncestors().OfType<IRenderRoot>().FirstOrDefault();
+                var root = this.FindAncestorOfType<IRenderRoot>();
                 var e = new VisualTreeAttachmentEventArgs(_visualParent, root);
                 OnAttachedToVisualTreeCore(e);
             }

+ 151 - 4
src/Avalonia.Visuals/VisualTree/VisualExtensions.cs

@@ -14,7 +14,7 @@ namespace Avalonia.VisualTree
     public static class VisualExtensions
     {
         /// <summary>
-        /// Calculates the distance from a visual's <see cref="IRenderRoot"/>.
+        /// Calculates the distance from a visual's ancestor.
         /// </summary>
         /// <param name="visual">The visual.</param>
         /// <param name="ancestor">The ancestor visual.</param>
@@ -30,13 +30,39 @@ namespace Avalonia.VisualTree
 
             while (visual != null && visual != ancestor)
             {
-                ++result;
                 visual = visual.VisualParent;
+
+                result++;
             }
 
             return visual != null ? result : -1;
         }
 
+        /// <summary>
+        /// Calculates the distance from a visual's root.
+        /// </summary>
+        /// <param name="visual">The visual.</param>
+        /// <returns>
+        /// The number of steps from the visual to the root.
+        /// </returns>
+        public static int CalculateDistanceFromRoot(IVisual visual)
+        {
+            Contract.Requires<ArgumentNullException>(visual != null);
+
+            var result = 0;
+
+            visual = visual?.VisualParent;
+
+            while (visual != null)
+            {
+                visual = visual.VisualParent;
+
+                result++;
+            }
+
+            return result;
+        }
+
         /// <summary>
         /// Tries to get the first common ancestor of two visuals.
         /// </summary>
@@ -47,8 +73,53 @@ namespace Avalonia.VisualTree
         {
             Contract.Requires<ArgumentNullException>(visual != null);
 
-            return visual.GetSelfAndVisualAncestors().Intersect(target.GetSelfAndVisualAncestors())
-                .FirstOrDefault();
+            if (target is null)
+            {
+                return null;
+            }
+
+            void GoUpwards(ref IVisual node, int count)
+            {
+                for (int i = 0; i < count; ++i)
+                {
+                    node = node.VisualParent;
+                }
+            }
+
+            // We want to find lowest node first, then make sure that both nodes are at the same height.
+            // By doing that we can sometimes find out that other node is our lowest common ancestor.
+            var firstHeight = CalculateDistanceFromRoot(visual);
+            var secondHeight = CalculateDistanceFromRoot(target);
+
+            if (firstHeight > secondHeight)
+            {
+                GoUpwards(ref visual, firstHeight - secondHeight);
+            }
+            else
+            {
+                GoUpwards(ref target, secondHeight - firstHeight);
+            }
+
+            if (visual == target)
+            {
+                return visual;
+            }
+
+            while (visual != null && target != null)
+            {
+                IVisual firstParent = visual.VisualParent;
+                IVisual secondParent = target.VisualParent;
+
+                if (firstParent == secondParent)
+                {
+                    return firstParent;
+                }
+
+                visual = visual.VisualParent;
+                target = target.VisualParent;
+            }
+
+            return null;
         }
 
         /// <summary>
@@ -69,6 +140,57 @@ namespace Avalonia.VisualTree
             }
         }
 
+        /// <summary>
+        /// Finds first ancestor of given type.
+        /// </summary>
+        /// <typeparam name="T">Ancestor type.</typeparam>
+        /// <param name="visual">The visual.</param>
+        /// <param name="includeSelf">If given visual should be included in search.</param>
+        /// <returns>First ancestor of given type.</returns>
+        public static T FindAncestorOfType<T>(this IVisual visual, bool includeSelf = false) where T : class
+        {
+            if (visual is null)
+            {
+                return null;
+            }
+
+            IVisual parent = includeSelf ? visual : visual.VisualParent;
+
+            while (parent != null)
+            {
+                if (parent is T result)
+                {
+                    return result;
+                }
+
+                parent = parent.VisualParent;
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Finds first descendant of given type.
+        /// </summary>
+        /// <typeparam name="T">Descendant type.</typeparam>
+        /// <param name="visual">The visual.</param>
+        /// <param name="includeSelf">If given visual should be included in search.</param>
+        /// <returns>First descendant of given type.</returns>
+        public static T FindDescendantOfType<T>(this IVisual visual, bool includeSelf = false) where T : class
+        {
+            if (visual is null)
+            {
+                return null;
+            }
+
+            if (includeSelf && visual is T result)
+            {
+                return result;
+            }
+
+            return FindDescendantOfTypeCore<T>(visual);
+        }
+
         /// <summary>
         /// Enumerates an <see cref="IVisual"/> and its ancestors in the visual tree.
         /// </summary>
@@ -249,6 +371,31 @@ namespace Avalonia.VisualTree
                 .Select(x => x.Element);
         }
 
+        private static T FindDescendantOfTypeCore<T>(IVisual visual) where T : class
+        {
+            var visualChildren = visual.VisualChildren;
+            var visualChildrenCount = visualChildren.Count;
+
+            for (var i = 0; i < visualChildrenCount; i++)
+            {
+                IVisual child = visualChildren[i];
+
+                if (child is T result)
+                {
+                    return result;
+                }
+
+                var childResult = FindDescendantOfTypeCore<T>(child);
+
+                if (!(childResult is null))
+                {
+                    return childResult;
+                }
+            }
+
+            return null;
+        }
+
         private class ZOrderElement : IComparable<ZOrderElement>
         {
             public IVisual Element { get; set; }

+ 1 - 1
src/Avalonia.X11/X11Clipboard.cs

@@ -79,7 +79,7 @@ namespace Avalonia.X11
                     atoms = atoms.Concat(new[] {_x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE})
                         .ToArray();
                     XChangeProperty(_x11.Display, window, property,
-                        target, 32, PropertyMode.Replace, atoms, atoms.Length);
+                        _x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms, atoms.Length);
                     return property;
                 }
                 else if(target == _x11.Atoms.SAVE_TARGETS && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero)

+ 7 - 0
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@@ -52,6 +52,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
             // the context.
             object anchor = context.GetFirstParent<IControl>();
 
+            if(anchor is null)
+            {
+                // Try to find IDataContextProvider, this was added to allow us to find
+                // a datacontext for Application class when using NativeMenuItems.
+                anchor = context.GetFirstParent<IDataContextProvider>();
+            }
+
             // If a control was not found, then try to find the highest-level style as the XAML
             // file could be a XAML file containing only styles.
             return anchor ??

+ 9 - 2
src/Markup/Avalonia.Markup/Data/Binding.cs

@@ -26,6 +26,7 @@ namespace Avalonia.Data
         public Binding()
         {
             FallbackValue = AvaloniaProperty.UnsetValue;
+            TargetNullValue = AvaloniaProperty.UnsetValue;
         }
 
         /// <summary>
@@ -60,6 +61,11 @@ namespace Avalonia.Data
         /// </summary>
         public object FallbackValue { get; set; }
 
+        /// <summary>
+        /// Gets or sets the value to use when the binding result is null.
+        /// </summary>
+        public object TargetNullValue { get; set; }
+
         /// <summary>
         /// Gets or sets the binding mode.
         /// </summary>
@@ -209,6 +215,7 @@ namespace Avalonia.Data
                 observer,
                 targetType,
                 fallback,
+                TargetNullValue,
                 converter ?? DefaultValueConverter.Instance,
                 ConverterParameter,
                 Priority);
@@ -224,9 +231,9 @@ namespace Avalonia.Data
         {
             Contract.Requires<ArgumentNullException>(target != null);
 
-            if (!(target is IStyledElement))
+            if (!(target is IDataContextProvider))
             {
-                target = anchor as IStyledElement;
+                target = anchor as IDataContextProvider;
 
                 if (target == null)
                 {

+ 16 - 0
src/Markup/Avalonia.Markup/Data/MultiBinding.cs

@@ -37,6 +37,11 @@ namespace Avalonia.Data
         /// </summary>
         public object FallbackValue { get; set; }
 
+        /// <summary>
+        /// Gets or sets the value to use when the binding result is null.
+        /// </summary>
+        public object TargetNullValue { get; set; }
+
         /// <summary>
         /// Gets or sets the binding mode.
         /// </summary>
@@ -57,6 +62,12 @@ namespace Avalonia.Data
         /// </summary>
         public string StringFormat { get; set; }
 
+        public MultiBinding()
+        {
+            FallbackValue = AvaloniaProperty.UnsetValue;
+            TargetNullValue = AvaloniaProperty.UnsetValue;
+        }
+
         /// <inheritdoc/>
         public InstancedBinding Initiate(
             IAvaloniaObject target,
@@ -102,6 +113,11 @@ namespace Avalonia.Data
             var culture = CultureInfo.CurrentCulture;
             var converted = converter.Convert(values, targetType, ConverterParameter, culture);
 
+            if (converted == null)
+            {
+                converted = TargetNullValue;
+            }
+
             if (converted == AvaloniaProperty.UnsetValue)
             {
                 converted = FallbackValue;

+ 4 - 1
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@@ -1098,7 +1098,10 @@ namespace Avalonia.Win32.Interop
         
         [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
         public static extern HRESULT RegisterDragDrop(IntPtr hwnd, IDropTarget target);
-        
+
+        [DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
+        public static extern HRESULT RevokeDragDrop(IntPtr hwnd);
+
         [DllImport("ole32.dll", EntryPoint = "OleInitialize")]
         public static extern HRESULT OleInitialize(IntPtr val);
 

+ 17 - 6
src/Windows/Avalonia.Win32/OleContext.cs

@@ -7,9 +7,9 @@ using Avalonia.Win32.Interop;
 
 namespace Avalonia.Win32
 {
-    class OleContext
+    internal class OleContext
     {
-        private static OleContext fCurrent;
+        private static OleContext s_current;
 
         internal static OleContext Current
         {
@@ -18,13 +18,12 @@ namespace Avalonia.Win32
                 if (!IsValidOleThread())
                     return null;
 
-                if (fCurrent == null)
-                    fCurrent = new OleContext();
-                return fCurrent;
+                if (s_current == null)
+                    s_current = new OleContext();
+                return s_current;
             }
         }
 
-
         private OleContext()
         {
             UnmanagedMethods.HRESULT res = UnmanagedMethods.OleInitialize(IntPtr.Zero);
@@ -43,9 +42,21 @@ namespace Avalonia.Win32
         internal bool RegisterDragDrop(IPlatformHandle hwnd, IDropTarget target)
         {
             if (hwnd?.HandleDescriptor != "HWND" || target == null)
+            {
                 return false;
+            }
 
             return UnmanagedMethods.RegisterDragDrop(hwnd.Handle, target) == UnmanagedMethods.HRESULT.S_OK;
         }
+
+        internal bool UnregisterDragDrop(IPlatformHandle hwnd)
+        {
+            if (hwnd?.HandleDescriptor != "HWND")
+            {
+                return false;
+            }
+
+            return UnmanagedMethods.RevokeDragDrop(hwnd.Handle) == UnmanagedMethods.HRESULT.S_OK;
+        }
     }
 }

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

@@ -6,7 +6,7 @@ using IDataObject = Avalonia.Input.IDataObject;
 
 namespace Avalonia.Win32
 {
-    class OleDropTarget : IDropTarget
+    internal class OleDropTarget : IDropTarget
     {
         private readonly IInputRoot _target;
         private readonly ITopLevelImpl _tl;

+ 5 - 0
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -253,6 +253,11 @@ namespace Avalonia.Win32
 
         public void Dispose()
         {
+            if (_dropTarget != null)
+            {
+                OleContext.Current?.UnregisterDragDrop(Handle);
+                _dropTarget = null;
+            }
             if (_hwnd != IntPtr.Zero)
             {
                 UnmanagedMethods.DestroyWindow(_hwnd);

+ 28 - 0
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs

@@ -139,6 +139,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 ExpressionObserver.Create(data, o => o.StringValue),
                 typeof(int),
                 42,
+                AvaloniaProperty.UnsetValue,
                 DefaultValueConverter.Instance);
             var result = await target.Take(1);
 
@@ -160,6 +161,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 ExpressionObserver.Create(data, o => o.StringValue, true),
                 typeof(int),
                 42,
+                AvaloniaProperty.UnsetValue,
                 DefaultValueConverter.Instance);
             var result = await target.Take(1);
 
@@ -181,6 +183,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 ExpressionObserver.Create(data, o => o.StringValue),
                 typeof(int),
                 "bar",
+                AvaloniaProperty.UnsetValue,
                 DefaultValueConverter.Instance);
             var result = await target.Take(1);
 
@@ -203,6 +206,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 ExpressionObserver.Create(data, o => o.StringValue, true),
                 typeof(int),
                 "bar",
+                AvaloniaProperty.UnsetValue,
                 DefaultValueConverter.Instance);
             var result = await target.Take(1);
 
@@ -238,6 +242,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
                 ExpressionObserver.Create(data, o => o.DoubleValue),
                 typeof(string),
                 "9.8",
+                AvaloniaProperty.UnsetValue,
                 DefaultValueConverter.Instance);
 
             target.OnNext("foo");
@@ -353,6 +358,29 @@ namespace Avalonia.Base.UnitTests.Data.Core
             GC.KeepAlive(data);
         }
 
+        [Fact]
+        public async Task Null_Value_Should_Use_TargetNullValue()
+        {
+            var data = new Class1 { StringValue = "foo" };
+
+            var target = new BindingExpression(
+                ExpressionObserver.Create(data, o => o.StringValue),
+                typeof(string),
+                AvaloniaProperty.UnsetValue,
+                "bar",
+                DefaultValueConverter.Instance);
+
+            object result = null;
+            target.Subscribe(x => result = x);
+
+            Assert.Equal("foo", result);
+            
+            data.StringValue = null;
+            Assert.Equal("bar", result);
+
+            GC.KeepAlive(data);
+        }
+
         private class Class1 : NotifyingBase
         {
             private string _stringValue;

+ 59 - 0
tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs

@@ -0,0 +1,59 @@
+using Avalonia.Data;
+using BenchmarkDotNet.Attributes;
+
+namespace Avalonia.Benchmarks.Data
+{
+    [MemoryDiagnoser, InProcess]
+    public class BindingsBenchmark
+    {
+        [Benchmark]
+        public void TwoWayBinding_Via_Binding()
+        {
+            var instance = new TestClass();
+
+            var binding = new Binding(nameof(TestClass.BoundValue), BindingMode.TwoWay)
+            {
+                Source = instance
+            };
+
+            instance.Bind(TestClass.IntValueProperty, binding);
+        }
+
+        [Benchmark]
+        public void UpdateTwoWayBinding_Via_Binding()
+        {
+            var instance = new TestClass();
+
+            var binding = new Binding(nameof(TestClass.BoundValue), BindingMode.TwoWay)
+            {
+                Source = instance
+            };
+
+            instance.Bind(TestClass.IntValueProperty, binding);
+            for (int i = 0; i < 60; i++)
+            {
+                instance.IntValue = i;
+            }
+        }
+        private class TestClass : AvaloniaObject
+        {
+            public static readonly StyledProperty<int> IntValueProperty =
+                AvaloniaProperty.Register<TestClass, int>(nameof(IntValue));
+
+            public static readonly StyledProperty<int> BoundValueProperty =
+                AvaloniaProperty.Register<TestClass, int>(nameof(BoundValue));
+
+            public int IntValue
+            {
+                get => GetValue(IntValueProperty);
+                set => SetValue(IntValueProperty, value);
+            }
+
+            public int BoundValue
+            {
+                get => GetValue(BoundValueProperty);
+                set => SetValue(BoundValueProperty, value);
+            }
+        }
+    }
+}

+ 64 - 0
tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs

@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.UnitTests;
+using Avalonia.VisualTree;
+using BenchmarkDotNet.Attributes;
+
+namespace Avalonia.Benchmarks.Traversal
+{
+    [MemoryDiagnoser]
+    public class VisualTreeTraversal
+    {
+        private readonly TestRoot _root;
+        private readonly List<Control> _controls = new List<Control>();
+        private readonly List<Control> _shuffledControls;
+
+        public VisualTreeTraversal()
+        {
+            var panel = new StackPanel();
+            _root = new TestRoot { Child = panel, Renderer = new NullRenderer()};
+            _controls.Add(panel);
+            _controls = ControlHierarchyCreator.CreateChildren(_controls, panel, 3, 5, 4);
+
+            var random = new Random(1);
+
+            _shuffledControls = _controls.OrderBy(r => random.Next()).ToList();
+
+            _root.LayoutManager.ExecuteInitialLayoutPass(_root);
+        }
+
+        [Benchmark]
+        public void FindAncestorOfType_Linq()
+        {
+            foreach (Control control in _controls)
+            {
+                control.GetSelfAndVisualAncestors()
+                    .OfType<TestRoot>()
+                    .FirstOrDefault();
+            }
+        }
+
+        [Benchmark]
+        public void FindAncestorOfType_Optimized()
+        {
+            foreach (Control control in _controls)
+            {
+                control.FindAncestorOfType<TestRoot>();
+            }
+        }
+
+        [Benchmark]
+        public void FindCommonVisualAncestor()
+        {
+            foreach (IVisual first in _controls)
+            {
+                foreach (Control second in _shuffledControls)
+                {
+                    first.FindCommonVisualAncestor(second);
+                }
+            }
+        }
+    }
+}

+ 0 - 16
tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs

@@ -82,22 +82,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
             Assert.Equal(50, target.Value);
         }
 
-        [Fact]
-        public void Properties_Should_Not_Accept_Nan_And_Inifinity()
-        {
-            var target = new TestRange();
-
-            Assert.Throws<ArgumentException>(() => target.Minimum = double.NaN);
-            Assert.Throws<ArgumentException>(() => target.Minimum = double.PositiveInfinity);
-            Assert.Throws<ArgumentException>(() => target.Minimum = double.NegativeInfinity);
-            Assert.Throws<ArgumentException>(() => target.Maximum = double.NaN);
-            Assert.Throws<ArgumentException>(() => target.Maximum = double.PositiveInfinity);
-            Assert.Throws<ArgumentException>(() => target.Maximum = double.NegativeInfinity);
-            Assert.Throws<ArgumentException>(() => target.Value = double.NaN);
-            Assert.Throws<ArgumentException>(() => target.Value = double.PositiveInfinity);
-            Assert.Throws<ArgumentException>(() => target.Value = double.NegativeInfinity);
-        }
-
         [Theory]
         [InlineData(true)]
         [InlineData(false)]

+ 38 - 0
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@@ -10,6 +10,7 @@ using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Templates;
 using Avalonia.Data;
 using Avalonia.Data.Core;
+using Avalonia.Diagnostics;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
 using Avalonia.Interactivity;
@@ -33,6 +34,8 @@ namespace Avalonia.Controls.UnitTests
                 Items = CreateTestTreeData(),
             };
 
+            var root = new TestRoot(target);
+
             CreateNodeDataTemplate(target);
             ApplyTemplates(target);
 
@@ -77,6 +80,8 @@ namespace Avalonia.Controls.UnitTests
                 Items = CreateTestTreeData(),
             };
 
+            var root = new TestRoot(target);
+
             CreateNodeDataTemplate(target);
             ApplyTemplates(target);
 
@@ -527,6 +532,8 @@ namespace Avalonia.Controls.UnitTests
                 Items = data,
             };
 
+            var root = new TestRoot(target);
+
             CreateNodeDataTemplate(target);
             ApplyTemplates(target);
 
@@ -893,6 +900,37 @@ namespace Avalonia.Controls.UnitTests
             Assert.Equal(2, GetItem(target, 0, 1, 0).Level);
         }
 
+        [Fact]
+        public void Adding_Node_To_Removed_And_ReAdded_Parent_Should_Not_Crash()
+        {
+            // Issue #2985
+            var tree = CreateTestTreeData();
+            var target = new TreeView
+            {
+                Template = CreateTreeViewTemplate(),
+                Items = tree,
+            };
+
+            var visualRoot = new TestRoot();
+            visualRoot.Child = target;
+
+            CreateNodeDataTemplate(target);
+            ApplyTemplates(target);
+            ExpandAll(target);
+
+            var parent = tree[0];
+            var node = parent.Children[1];
+
+            parent.Children.Remove(node);
+            parent.Children.Add(node);
+
+            var item = target.ItemContainerGenerator.Index.ContainerFromItem(node);
+            ApplyTemplates(new[] { item });
+
+            // #2985 causes ArgumentException here.
+            node.Children.Add(new Node());
+        }
+
         [Fact]
         public void Auto_Expanding_In_Style_Should_Not_Break_Range_Selection()
         {

+ 1 - 2
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@@ -228,8 +228,7 @@ namespace Avalonia.Controls.UnitTests
         {
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
-                var windowImpl = Mock.Of<IWindowImpl>(x => x.Scaling == 1);
-                var target = new Window(windowImpl);
+                var target = new Window();
 
                 target.Show();
                 target.Close();

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

@@ -405,6 +405,24 @@ namespace Avalonia.Markup.UnitTests.Data
             Assert.Equal(42, target.Value);
         }
 
+        [Fact]
+        public void Should_Return_TargetNullValue_When_Value_Is_Null()
+        {
+            var target = new TextBlock();
+            var source = new Source { Foo = null };
+
+            var binding = new Binding
+            {
+                Source = source,
+                Path = "Foo",
+                TargetNullValue = "(null)",
+            };
+
+            target.Bind(TextBlock.TextProperty, binding);
+
+            Assert.Equal("(null)", target.Text);
+        }
+
         [Fact]
         public void Null_Path_Should_Bind_To_DataContext()
         {

+ 30 - 0
tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs

@@ -94,6 +94,28 @@ namespace Avalonia.Markup.UnitTests.Data
             Assert.Equal("fallback", target.Text);
         }
 
+        [Fact]
+        public void Should_Return_TargetNullValue_When_Value_Is_Null()
+        {
+            var target = new TextBlock();
+
+            var binding = new MultiBinding
+            {
+                Converter = new NullValueConverter(),
+                Bindings = new[]
+                {
+                    new Binding { Path = "A" },
+                    new Binding { Path = "B" },
+                    new Binding { Path = "C" },
+                },
+                TargetNullValue = "(null)",
+            };
+
+            target.Bind(TextBlock.TextProperty, binding);
+
+            Assert.Equal("(null)", target.Text);
+        }
+
         private class ConcatConverter : IMultiValueConverter
         {
             public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
@@ -109,5 +131,13 @@ namespace Avalonia.Markup.UnitTests.Data
                 return AvaloniaProperty.UnsetValue;
             }
         }
+
+        private class NullValueConverter : IMultiValueConverter
+        {
+            public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
+            {
+                return null;
+            }
+        }
     }
 }

+ 4 - 0
tests/Avalonia.UnitTests/MockWindowingPlatform.cs

@@ -28,6 +28,10 @@ namespace Avalonia.UnitTests
                 return CreatePopupMock().Object;
 
             });
+            mock.Setup(x => x.Dispose()).Callback(() =>
+            {
+                mock.Object.Closed?.Invoke();
+            });
             PixelPoint pos = default;
             mock.SetupGet(x => x.Position).Returns(() => pos);
             mock.Setup(x => x.Move(It.IsAny<PixelPoint>())).Callback(new Action<PixelPoint>(np => pos = np));

+ 172 - 0
tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs

@@ -2,12 +2,184 @@
 using Avalonia.Layout;
 using Avalonia.Media;
 using Avalonia.UnitTests;
+using Avalonia.VisualTree;
 using Xunit;
 
 namespace Avalonia.Visuals.UnitTests
 {
     public class VisualExtensionsTests
     {
+        [Fact]
+        public void FindAncestorOfType_Finds_Direct_Parent()
+        {
+            StackPanel target;
+
+            var root = new TestRoot
+            {
+                Child = target = new StackPanel()
+            };
+
+            Assert.Equal(root, target.FindAncestorOfType<TestRoot>());
+        }
+
+        [Fact]
+        public void FindAncestorOfType_Finds_Ancestor_Of_Nested_Child()
+        {
+            Button target;
+
+            var root = new TestRoot
+            {
+                Child = new StackPanel
+                {
+                    Children =
+                    {
+                        new StackPanel
+                        {
+                            Children =
+                            {
+                                (target = new Button())
+                            }
+                        }
+                    }
+                }
+            };
+
+            Assert.Equal(root, target.FindAncestorOfType<TestRoot>());
+        }
+
+        [Fact]
+        public void FindDescendantOfType_Finds_Direct_Child()
+        {
+            StackPanel target;
+
+            var root = new TestRoot
+            {
+                Child = target = new StackPanel()
+            };
+
+            Assert.Equal(target, root.FindDescendantOfType<StackPanel>());
+        }
+
+        [Fact]
+        public void FindDescendantOfType_Finds_Nested_Child()
+        {
+            Button target;
+
+            var root = new TestRoot
+            {
+                Child = new StackPanel
+                {
+                    Children =
+                    {
+                        new StackPanel
+                        {
+                            Children =
+                            {
+                                (target = new Button())
+                            }
+                        }
+                    }
+                }
+            };
+
+            Assert.Equal(target, root.FindDescendantOfType<Button>());
+        }
+
+        [Fact]
+        public void FindCommonVisualAncestor_First_Is_Parent_Of_Second()
+        {
+            Control left, right;
+
+            var root = new TestRoot
+            {
+                Child = left = new Decorator
+                {
+                    Child = right = new Decorator()
+                }
+            };
+
+            var ancestor = left.FindCommonVisualAncestor(right);
+            Assert.Equal(left, ancestor);
+
+            ancestor = right.FindCommonVisualAncestor(left);
+            Assert.Equal(left, ancestor);
+        }
+
+        [Fact]
+        public void FindCommonVisualAncestor_Two_Subtrees_Uniform_Height()
+        {
+            Control left, right;
+
+            var root = new TestRoot
+            {
+                Child = new StackPanel
+                {
+                    Children =
+                    {
+                        new Decorator
+                        {
+                            Child = new Decorator
+                            {
+                                Child = left = new Decorator()
+                            }
+                        },
+                        new Decorator
+                        {
+                            Child = new Decorator
+                            {
+                                Child = right = new Decorator()
+                            }
+                        }
+                    }
+                }
+            };
+
+            var ancestor = left.FindCommonVisualAncestor(right);
+            Assert.Equal(root.Child, ancestor);
+
+            ancestor = right.FindCommonVisualAncestor(left);
+            Assert.Equal(root.Child, ancestor);
+        }
+
+        [Fact]
+        public void FindCommonVisualAncestor_Two_Subtrees_NonUniform_Height()
+        {
+            Control left, right;
+
+            var root = new TestRoot
+            {
+                Child = new StackPanel
+                {
+                    Children =
+                    {
+                        new Decorator
+                        {
+                            Child = new Decorator
+                            {
+                                Child = left = new Decorator()
+                            }
+                        },
+                        new Decorator
+                        {
+                            Child = new Decorator
+                            {
+                                Child = new Decorator
+                                {
+                                    Child = right = new Decorator()
+                                }
+                            }
+                        }
+                    }
+                }
+            };
+
+            var ancestor = left.FindCommonVisualAncestor(right);
+            Assert.Equal(root.Child, ancestor);
+
+            ancestor = right.FindCommonVisualAncestor(left);
+            Assert.Equal(root.Child, ancestor);
+        }
+
         [Fact]
         public void TranslatePoint_Should_Respect_RenderTransforms()
         {