Procházet zdrojové kódy

Merge branch 'master' into grid-sharedsizegroup-layoutloop-bug

Jumar Macato před 4 roky
rodič
revize
a113c190d3
67 změnil soubory, kde provedl 2711 přidání a 212 odebrání
  1. 1 0
      native/Avalonia.Native/src/OSX/AvnString.h
  2. 15 0
      native/Avalonia.Native/src/OSX/AvnString.mm
  3. 25 3
      native/Avalonia.Native/src/OSX/app.mm
  4. 1 1
      native/Avalonia.Native/src/OSX/clipboard.mm
  5. 1 1
      native/Avalonia.Native/src/OSX/common.h
  6. 2 2
      native/Avalonia.Native/src/OSX/main.mm
  7. 6 1
      native/Avalonia.Native/src/OSX/window.mm
  8. 39 1
      src/Avalonia.Base/AvaloniaObject.cs
  9. 6 0
      src/Avalonia.Base/Data/Converters/BoolConverters.cs
  10. 50 7
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  11. 27 3
      src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs
  12. 8 0
      src/Avalonia.Base/PropertyStore/IBatchUpdate.cs
  13. 8 1
      src/Avalonia.Base/PropertyStore/IValue.cs
  14. 16 0
      src/Avalonia.Base/PropertyStore/LocalValueEntry.cs
  15. 95 26
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  16. 4 13
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  17. 230 34
      src/Avalonia.Base/ValueStore.cs
  18. 9 2
      src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs
  19. 2 1
      src/Avalonia.Controls/ApiCompatBaseline.txt
  20. 9 2
      src/Avalonia.Controls/Application.cs
  21. 14 0
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  22. 70 0
      src/Avalonia.Controls/ComboBox.cs
  23. 29 40
      src/Avalonia.Controls/Control.cs
  24. 12 5
      src/Avalonia.Controls/Notifications/NotificationCard.cs
  25. 7 0
      src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs
  26. 14 0
      src/Avalonia.Controls/UrlOpenedEventArgs.cs
  27. 25 20
      src/Avalonia.Controls/Window.cs
  28. 21 0
      src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs
  29. 225 1
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  30. 8 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  31. 51 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassViewModel.cs
  32. 27 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs
  33. 59 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs
  34. 43 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs
  35. 7 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs
  36. 121 3
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  37. 10 0
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  38. 1 1
      src/Avalonia.Layout/ElementManager.cs
  39. 5 5
      src/Avalonia.Layout/UniformGridLayoutState.cs
  40. 14 0
      src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs
  41. 4 3
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  42. 7 1
      src/Avalonia.Native/avn.idl
  43. 4 0
      src/Avalonia.Styling/ApiCompatBaseline.txt
  44. 21 0
      src/Avalonia.Styling/Diagnostics/StyleDiagnostics.cs
  45. 17 0
      src/Avalonia.Styling/Diagnostics/StyledElementExtensions.cs
  46. 3 1
      src/Avalonia.Styling/IStyledElement.cs
  47. 35 5
      src/Avalonia.Styling/StyledElement.cs
  48. 5 0
      src/Avalonia.Styling/Styling/IStyleInstance.cs
  49. 14 4
      src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs
  50. 1 1
      src/Avalonia.Styling/Styling/PropertySetterInstance.cs
  51. 128 0
      src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs
  52. 15 9
      src/Avalonia.Styling/Styling/Setter.cs
  53. 5 4
      src/Avalonia.Styling/Styling/StyleInstance.cs
  54. 7 7
      src/Avalonia.Visuals/Visual.cs
  55. 14 0
      src/Windows/Avalonia.Win32/WindowImpl.cs
  56. 1 0
      tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj
  57. 494 0
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs
  58. 1 0
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  59. 74 0
      tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs
  60. 36 0
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  61. 123 1
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  62. 4 2
      tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs
  63. 1 0
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  64. 96 0
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs
  65. 1 0
      tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj
  66. 282 0
      tests/Avalonia.Styling.UnitTests/StyleTests.cs
  67. 1 1
      tests/Avalonia.UnitTests/UnitTestApplication.cs

+ 1 - 0
native/Avalonia.Native/src/OSX/AvnString.h

@@ -11,6 +11,7 @@
 
 extern IAvnString* CreateAvnString(NSString* string);
 extern IAvnStringArray* CreateAvnStringArray(NSArray<NSString*>* array);
+extern IAvnStringArray* CreateAvnStringArray(NSArray<NSURL*>* array);
 extern IAvnStringArray* CreateAvnStringArray(NSString* string);
 extern IAvnString* CreateByteArray(void* data, int len);
 #endif /* AvnString_h */

+ 15 - 0
native/Avalonia.Native/src/OSX/AvnString.mm

@@ -85,6 +85,16 @@ public:
         }
     }
     
+    AvnStringArrayImpl(NSArray<NSURL*>* array)
+    {
+        for(int c = 0; c < [array count]; c++)
+        {
+            ComPtr<IAvnString> s;
+            *s.getPPV() = new AvnStringImpl([array objectAtIndex:c].absoluteString);
+            _list.push_back(s);
+        }
+    }
+    
     AvnStringArrayImpl(NSString* string)
     {
         ComPtr<IAvnString> s;
@@ -117,6 +127,11 @@ IAvnStringArray* CreateAvnStringArray(NSArray<NSString*> * array)
     return new AvnStringArrayImpl(array);
 }
 
+IAvnStringArray* CreateAvnStringArray(NSArray<NSURL*> * array)
+{
+    return new AvnStringArrayImpl(array);
+}
+
 IAvnStringArray* CreateAvnStringArray(NSString* string)
 {
     return new AvnStringArrayImpl(string);

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

@@ -1,10 +1,20 @@
 #include "common.h"
+#include "AvnString.h"
 @interface AvnAppDelegate : NSObject<NSApplicationDelegate>
+-(AvnAppDelegate* _Nonnull) initWithEvents: (IAvnApplicationEvents* _Nonnull) events;
 @end
 
 NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular;
 
 @implementation AvnAppDelegate
+ComPtr<IAvnApplicationEvents> _events;
+
+- (AvnAppDelegate *)initWithEvents:(IAvnApplicationEvents *)events
+{
+    _events = events;
+    return self;
+}
+
 - (void)applicationWillFinishLaunching:(NSNotification *)notification
 {
     if([[NSApplication sharedApplication] activationPolicy] != AvnDesiredActivationPolicy)
@@ -27,11 +37,23 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati
     [[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
 }
 
+- (void)application:(NSApplication *)sender openFiles:(NSArray<NSString *> *)filenames
+{
+    auto array = CreateAvnStringArray(filenames);
+    
+    _events->FilesOpened(array);
+}
+
+- (void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls
+{
+    auto array = CreateAvnStringArray(urls);
+    
+    _events->FilesOpened(array);
+}
 @end
 
 @interface AvnApplication : NSApplication
 
-
 @end
 
 @implementation AvnApplication
@@ -63,9 +85,9 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivati
 
 @end
 
-extern void InitializeAvnApp()
+extern void InitializeAvnApp(IAvnApplicationEvents* events)
 {
     NSApplication* app = [AvnApplication sharedApplication];
-    id delegate = [AvnAppDelegate new];
+    id delegate = [[AvnAppDelegate alloc] initWithEvents:events];
     [app setDelegate:delegate];
 }

+ 1 - 1
native/Avalonia.Native/src/OSX/clipboard.mm

@@ -56,7 +56,7 @@ public:
                 return S_OK;
             }
             
-            NSArray* arr = (NSArray*)data;
+            NSArray<NSString*>* arr = (NSArray*)data;
             
             for(int c = 0; c < [arr count]; c++)
                 if(![[arr objectAtIndex:c] isKindOfClass:[NSString class]])

+ 1 - 1
native/Avalonia.Native/src/OSX/common.h

@@ -31,7 +31,7 @@ extern NSMenuItem* GetAppMenuItem ();
 extern void SetAutoGenerateDefaultAppMenuItems (bool enabled);
 extern bool GetAutoGenerateDefaultAppMenuItems ();
 
-extern void InitializeAvnApp();
+extern void InitializeAvnApp(IAvnApplicationEvents* events);
 extern NSApplicationActivationPolicy AvnDesiredActivationPolicy;
 extern NSPoint ToNSPoint (AvnPoint p);
 extern AvnPoint ToAvnPoint (NSPoint p);

+ 2 - 2
native/Avalonia.Native/src/OSX/main.mm

@@ -163,13 +163,13 @@ class AvaloniaNative : public ComSingleObject<IAvaloniaNativeFactory, &IID_IAval
     
 public:
     FORWARD_IUNKNOWN()
-    virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) override
+    virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator, IAvnApplicationEvents* events) override
     {
         _deallocator = deallocator;
         @autoreleasepool{
             [[ThreadingInitializer new] do];
         }
-        InitializeAvnApp();
+        InitializeAvnApp(events);
         return S_OK;
     };
     

+ 6 - 1
native/Avalonia.Native/src/OSX/window.mm

@@ -1877,7 +1877,12 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     
     for(int i = 0; i < numWindows; i++)
     {
-        [[windows objectAtIndex:i] performClose:nil];
+        auto window = (AvnWindow*)[windows objectAtIndex:i];
+        
+        if([window parentWindow] == nullptr) // Avalonia will handle the child windows.
+        {
+            [window performClose:nil];
+        }
     }
 }
 

+ 39 - 1
src/Avalonia.Base/AvaloniaObject.cs

@@ -23,7 +23,7 @@ namespace Avalonia
         private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
         private List<IAvaloniaObject> _inheritanceChildren;
         private ValueStore _values;
-        private ValueStore Values => _values ?? (_values = new ValueStore(this));
+        private bool _batchUpdate;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
@@ -117,6 +117,22 @@ namespace Avalonia
             set { this.Bind(binding.Property, value); }
         }
 
+        private ValueStore Values
+        {
+            get
+            {
+                if (_values is null)
+                {
+                    _values = new ValueStore(this);
+
+                    if (_batchUpdate)
+                        _values.BeginBatchUpdate();
+                }
+
+                return _values;
+            }
+        }
+
         public bool CheckAccess() => Dispatcher.UIThread.CheckAccess();
 
         public void VerifyAccess() => Dispatcher.UIThread.VerifyAccess();
@@ -434,6 +450,28 @@ namespace Avalonia
             _values?.CoerceValue(property);
         }
 
+        public void BeginBatchUpdate()
+        {
+            if (_batchUpdate)
+            {
+                throw new InvalidOperationException("Batch update already in progress.");
+            }
+
+            _batchUpdate = true;
+            _values?.BeginBatchUpdate();
+        }
+
+        public void EndBatchUpdate()
+        {
+            if (!_batchUpdate)
+            {
+                throw new InvalidOperationException("No batch update in progress.");
+            }
+
+            _batchUpdate = false;
+            _values?.EndBatchUpdate();
+        }
+
         /// <inheritdoc/>
         void IAvaloniaObject.AddInheritanceChild(IAvaloniaObject child)
         {

+ 6 - 0
src/Avalonia.Base/Data/Converters/BoolConverters.cs

@@ -12,5 +12,11 @@ namespace Avalonia.Data.Converters
         /// </summary>
         public static readonly IMultiValueConverter And =
             new FuncMultiValueConverter<bool, bool>(x => x.All(y => y));
+
+        /// <summary>
+        /// A multi-value converter that returns true if any of the inputs is true.
+        /// </summary>
+        public static readonly IMultiValueConverter Or =
+            new FuncMultiValueConverter<bool, bool>(x => x.Any(y => y));
     }
 }

+ 50 - 7
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@@ -9,8 +9,9 @@ namespace Avalonia.PropertyStore
     /// <summary>
     /// Represents an untyped interface to <see cref="BindingEntry{T}"/>.
     /// </summary>
-    internal interface IBindingEntry : IPriorityValueEntry, IDisposable
+    internal interface IBindingEntry : IBatchUpdate, IPriorityValueEntry, IDisposable
     {
+        void Start(bool ignoreBatchUpdate);
     }
 
     /// <summary>
@@ -22,6 +23,8 @@ namespace Avalonia.PropertyStore
         private readonly IAvaloniaObject _owner;
         private IValueSink _sink;
         private IDisposable? _subscription;
+        private bool _isSubscribed;
+        private bool _batchUpdate;
         private Optional<T> _value;
 
         public BindingEntry(
@@ -39,10 +42,20 @@ namespace Avalonia.PropertyStore
         }
 
         public StyledPropertyBase<T> Property { get; }
-        public BindingPriority Priority { get; }
+        public BindingPriority Priority { get; private set; }
         public IObservable<BindingValue<T>> Source { get; }
         Optional<object> IValue.GetValue() => _value.ToObject();
 
+        public void BeginBatchUpdate() => _batchUpdate = true;
+
+        public void EndBatchUpdate()
+        {
+            _batchUpdate = false;
+
+            if (_sink is ValueStore)
+                Start();
+        }
+
         public Optional<T> GetValue(BindingPriority maxPriority)
         {
             return Priority >= maxPriority ? _value : Optional<T>.Empty;
@@ -52,10 +65,17 @@ namespace Avalonia.PropertyStore
         {
             _subscription?.Dispose();
             _subscription = null;
-            _sink.Completed(Property, this, _value);
+            _isSubscribed = false;
+            OnCompleted();
         }
 
-        public void OnCompleted() => _sink.Completed(Property, this, _value);
+        public void OnCompleted()
+        {
+            var oldValue = _value;
+            _value = default;
+            Priority = BindingPriority.Unset;
+            _sink.Completed(Property, this, oldValue);
+        }
 
         public void OnError(Exception error)
         {
@@ -79,13 +99,36 @@ namespace Avalonia.PropertyStore
             }
         }
 
-        public void Start()
+        public void Start() => Start(false);
+
+        public void Start(bool ignoreBatchUpdate)
         {
-            _subscription = Source.Subscribe(this);
+            // We can't use _subscription to check whether we're subscribed because it won't be set
+            // until Subscribe has finished, which will be too late to prevent reentrancy.
+            if (!_isSubscribed && (!_batchUpdate || ignoreBatchUpdate))
+            {
+                _isSubscribed = true;
+                _subscription = Source.Subscribe(this);
+            }
         }
 
         public void Reparent(IValueSink sink) => _sink = sink;
-        
+
+        public void RaiseValueChanged(
+            IValueSink sink,
+            IAvaloniaObject owner,
+            AvaloniaProperty property,
+            Optional<object> oldValue,
+            Optional<object> newValue)
+        {
+            sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
+                owner,
+                (AvaloniaProperty<T>)property,
+                oldValue.GetValueOrDefault<T>(),
+                newValue.GetValueOrDefault<T>(),
+                Priority));
+        }
+
         private void UpdateValue(BindingValue<T> value)
         {
             if (value.HasValue && Property.ValidateValue?.Invoke(value.Value) == false)

+ 27 - 3
src/Avalonia.Base/PropertyStore/ConstantValueEntry.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Diagnostics.CodeAnalysis;
 using Avalonia.Data;
 
 #nullable enable
@@ -17,7 +18,7 @@ namespace Avalonia.PropertyStore
 
         public ConstantValueEntry(
             StyledPropertyBase<T> property,
-            T value,
+            [AllowNull] T value,
             BindingPriority priority,
             IValueSink sink)
         {
@@ -28,7 +29,7 @@ namespace Avalonia.PropertyStore
         }
 
         public StyledPropertyBase<T> Property { get; }
-        public BindingPriority Priority { get; }
+        public BindingPriority Priority { get; private set; }
         Optional<object> IValue.GetValue() => _value.ToObject();
 
         public Optional<T> GetValue(BindingPriority maxPriority = BindingPriority.Animation)
@@ -36,7 +37,30 @@ namespace Avalonia.PropertyStore
             return Priority >= maxPriority ? _value : Optional<T>.Empty;
         }
 
-        public void Dispose() => _sink.Completed(Property, this, _value);
+        public void Dispose()
+        {
+            var oldValue = _value;
+            _value = default;
+            Priority = BindingPriority.Unset;
+            _sink.Completed(Property, this, oldValue);
+        }
+
         public void Reparent(IValueSink sink) => _sink = sink;
+        public void Start() { }
+
+        public void RaiseValueChanged(
+            IValueSink sink,
+            IAvaloniaObject owner,
+            AvaloniaProperty property,
+            Optional<object> oldValue,
+            Optional<object> newValue)
+        {
+            sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
+                owner,
+                (AvaloniaProperty<T>)property,
+                oldValue.GetValueOrDefault<T>(),
+                newValue.GetValueOrDefault<T>(),
+                Priority));
+        }
     }
 }

+ 8 - 0
src/Avalonia.Base/PropertyStore/IBatchUpdate.cs

@@ -0,0 +1,8 @@
+namespace Avalonia.PropertyStore
+{
+    internal interface IBatchUpdate
+    {
+        void BeginBatchUpdate();
+        void EndBatchUpdate();
+    }
+}

+ 8 - 1
src/Avalonia.Base/PropertyStore/IValue.cs

@@ -9,8 +9,15 @@ namespace Avalonia.PropertyStore
     /// </summary>
     internal interface IValue
     {
-        Optional<object> GetValue();
         BindingPriority Priority { get; }
+        Optional<object> GetValue();
+        void Start();
+        void RaiseValueChanged(
+            IValueSink sink,
+            IAvaloniaObject owner,
+            AvaloniaProperty property,
+            Optional<object> oldValue,
+            Optional<object> newValue);
     }
 
     /// <summary>

+ 16 - 0
src/Avalonia.Base/PropertyStore/LocalValueEntry.cs

@@ -24,5 +24,21 @@ namespace Avalonia.PropertyStore
         }
 
         public void SetValue(T value) => _value = value;
+        public void Start() { }
+
+        public void RaiseValueChanged(
+            IValueSink sink,
+            IAvaloniaObject owner,
+            AvaloniaProperty property,
+            Optional<object> oldValue,
+            Optional<object> newValue)
+        {
+            sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
+                owner,
+                (AvaloniaProperty<T>)property,
+                oldValue.GetValueOrDefault<T>(),
+                newValue.GetValueOrDefault<T>(),
+                BindingPriority.LocalValue));
+        }
     }
 }

+ 95 - 26
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@@ -18,7 +18,7 @@ namespace Avalonia.PropertyStore
     /// <see cref="IPriorityValueEntry{T}"/> entries (sorted first by priority and then in the order
     /// they were added) plus a local value.
     /// </remarks>
-    internal class PriorityValue<T> : IValue<T>, IValueSink
+    internal class PriorityValue<T> : IValue<T>, IValueSink, IBatchUpdate
     {
         private readonly IAvaloniaObject _owner;
         private readonly IValueSink _sink;
@@ -26,6 +26,8 @@ namespace Avalonia.PropertyStore
         private readonly Func<IAvaloniaObject, T, T>? _coerceValue;
         private Optional<T> _localValue;
         private Optional<T> _value;
+        private bool _isCalculatingValue;
+        private bool _batchUpdate;
 
         public PriorityValue(
             IAvaloniaObject owner,
@@ -53,6 +55,18 @@ namespace Avalonia.PropertyStore
             existing.Reparent(this);
             _entries.Add(existing);
 
+            if (existing is IBindingEntry binding &&
+                existing.Priority == BindingPriority.LocalValue)
+            {
+                // Bit of a special case here: if we have a local value binding that is being
+                // promoted to a priority value we need to make sure the binding is subscribed
+                // even if we've got a batch operation in progress because otherwise we don't know
+                // whether the binding or a subsequent SetValue with local priority will win. A
+                // notification won't be sent during batch update anyway because it will be
+                // caught and stored for later by the ValueStore.
+                binding.Start(ignoreBatchUpdate: true);
+            }
+
             var v = existing.GetValue();
             
             if (v.HasValue)
@@ -78,6 +92,28 @@ namespace Avalonia.PropertyStore
         public IReadOnlyList<IPriorityValueEntry<T>> Entries => _entries;
         Optional<object> IValue.GetValue() => _value.ToObject();
 
+        public void BeginBatchUpdate()
+        {
+            _batchUpdate = true;
+
+            foreach (var entry in _entries)
+            {
+                (entry as IBatchUpdate)?.BeginBatchUpdate();
+            }
+        }
+
+        public void EndBatchUpdate()
+        {
+            _batchUpdate = false;
+
+            foreach (var entry in _entries)
+            {
+                (entry as IBatchUpdate)?.EndBatchUpdate();
+            }
+
+            UpdateEffectiveValue(null);
+        }
+
         public void ClearLocalValue()
         {
             UpdateEffectiveValue(new AvaloniaPropertyChangedEventArgs<T>(
@@ -134,10 +170,37 @@ namespace Avalonia.PropertyStore
             var binding = new BindingEntry<T>(_owner, Property, source, priority, this);
             var insert = FindInsertPoint(binding.Priority);
             _entries.Insert(insert, binding);
+
+            if (_batchUpdate)
+            {
+                binding.BeginBatchUpdate();
+                
+                if (priority == BindingPriority.LocalValue)
+                {
+                    binding.Start(ignoreBatchUpdate: true);
+                }
+            }
+
             return binding;
         }
 
-        public void CoerceValue() => UpdateEffectiveValue(null);
+        public void UpdateEffectiveValue() => UpdateEffectiveValue(null);
+        public void Start() => UpdateEffectiveValue(null);
+
+        public void RaiseValueChanged(
+            IValueSink sink,
+            IAvaloniaObject owner,
+            AvaloniaProperty property,
+            Optional<object> oldValue,
+            Optional<object> newValue)
+        {
+            sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
+                owner,
+                (AvaloniaProperty<T>)property,
+                oldValue.GetValueOrDefault<T>(),
+                newValue.GetValueOrDefault<T>(),
+                Priority));
+        }
 
         void IValueSink.ValueChanged<TValue>(AvaloniaPropertyChangedEventArgs<TValue> change)
         {
@@ -146,7 +209,7 @@ namespace Avalonia.PropertyStore
                 _localValue = default;
             }
 
-            if (change is AvaloniaPropertyChangedEventArgs<T> c)
+            if (!_isCalculatingValue && change is AvaloniaPropertyChangedEventArgs<T> c)
             {
                 UpdateEffectiveValue(c);
             }
@@ -188,41 +251,47 @@ namespace Avalonia.PropertyStore
 
         public (Optional<T>, BindingPriority) CalculateValue(BindingPriority maxPriority)
         {
-            var reachedLocalValues = false;
+            _isCalculatingValue = true;
 
-            for (var i = _entries.Count - 1; i >= 0; --i)
+            try
             {
-                var entry = _entries[i];
-
-                if (entry.Priority < maxPriority)
+                for (var i = _entries.Count - 1; i >= 0; --i)
                 {
-                    continue;
+                    var entry = _entries[i];
+
+                    if (entry.Priority < maxPriority)
+                    {
+                        continue;
+                    }
+
+                    entry.Start();
+
+                    if (entry.Priority >= BindingPriority.LocalValue &&
+                        maxPriority <= BindingPriority.LocalValue &&
+                        _localValue.HasValue)
+                    {
+                        return (_localValue, BindingPriority.LocalValue);
+                    }
+
+                    var entryValue = entry.GetValue();
+
+                    if (entryValue.HasValue)
+                    {
+                        return (entryValue, entry.Priority);
+                    }
                 }
 
-                if (!reachedLocalValues &&
-                    entry.Priority >= BindingPriority.LocalValue &&
-                    maxPriority <= BindingPriority.LocalValue &&
-                    _localValue.HasValue)
+                if (maxPriority <= BindingPriority.LocalValue && _localValue.HasValue)
                 {
                     return (_localValue, BindingPriority.LocalValue);
                 }
 
-                var entryValue = entry.GetValue();
-
-                if (entryValue.HasValue)
-                {
-                    return (entryValue, entry.Priority);
-                }
+                return (default, BindingPriority.Unset);
             }
-
-            if (!reachedLocalValues &&
-                maxPriority <= BindingPriority.LocalValue &&
-                _localValue.HasValue)
+            finally
             {
-                return (_localValue, BindingPriority.LocalValue);
+                _isCalculatingValue = false;
             }
-
-            return (default, BindingPriority.Unset);
         }
 
         private void UpdateEffectiveValue(AvaloniaPropertyChangedEventArgs<T>? change)

+ 4 - 13
src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs

@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
 
@@ -22,6 +22,9 @@ namespace Avalonia.Utilities
             _entries = s_emptyEntries;
         }
 
+        public int Count => _entries.Length - 1;
+        public TValue this[int index] => _entries[index].Value;
+
         private (int, bool) TryFindEntry(int propertyId)
         {
             if (_entries.Length <= 12)
@@ -163,18 +166,6 @@ namespace Avalonia.Utilities
             }
         }
 
-        public Dictionary<AvaloniaProperty, TValue> ToDictionary()
-        {
-            var dict = new Dictionary<AvaloniaProperty, TValue>(_entries.Length - 1);
-
-            for (int i = 0; i < _entries.Length - 1; ++i)
-            {
-                dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
-            }
-
-            return dict;
-        }
-
         private struct Entry
         {
             internal int PropertyId;

+ 230 - 34
src/Avalonia.Base/ValueStore.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using Avalonia.Data;
 using Avalonia.PropertyStore;
 using Avalonia.Utilities;
@@ -26,6 +27,7 @@ namespace Avalonia
         private readonly AvaloniaObject _owner;
         private readonly IValueSink _sink;
         private readonly AvaloniaPropertyValueStore<IValue> _values;
+        private BatchUpdate? _batchUpdate;
 
         public ValueStore(AvaloniaObject owner)
         {
@@ -33,6 +35,25 @@ namespace Avalonia
             _values = new AvaloniaPropertyValueStore<IValue>();
         }
 
+        public void BeginBatchUpdate()
+        {
+            _batchUpdate ??= new BatchUpdate(this);
+            _batchUpdate.Begin();
+        }
+
+        public void EndBatchUpdate()
+        {
+            if (_batchUpdate is null)
+            {
+                throw new InvalidOperationException("No batch update in progress.");
+            }
+
+            if (_batchUpdate.End())
+            {
+                _batchUpdate = null;
+            }
+        }
+
         public bool IsAnimating(AvaloniaProperty property)
         {
             if (_values.TryGetValue(property, out var slot))
@@ -90,23 +111,21 @@ namespace Avalonia
             {
                 // If the property has any coercion callbacks then always create a PriorityValue.
                 var entry = new PriorityValue<T>(_owner, property, this);
-                _values.AddValue(property, entry);
+                AddValue(property, entry);
                 result = entry.SetValue(value, priority);
             }
             else
             {
-                var change = new AvaloniaPropertyChangedEventArgs<T>(_owner, property, default, value, priority);
-
                 if (priority == BindingPriority.LocalValue)
                 {
-                    _values.AddValue(property, new LocalValueEntry<T>(value));
-                    _sink.ValueChanged(change);
+                    AddValue(property, new LocalValueEntry<T>(value));
+                    NotifyValueChanged<T>(property, default, value, priority);
                 }
                 else
                 {
                     var entry = new ConstantValueEntry<T>(property, value, priority, this);
-                    _values.AddValue(property, entry);
-                    _sink.ValueChanged(change);
+                    AddValue(property, entry);
+                    NotifyValueChanged<T>(property, default, value, priority);
                     result = entry;
                 }
             }
@@ -128,15 +147,13 @@ namespace Avalonia
                 // If the property has any coercion callbacks then always create a PriorityValue.
                 var entry = new PriorityValue<T>(_owner, property, this);
                 var binding = entry.AddBinding(source, priority);
-                _values.AddValue(property, entry);
-                binding.Start();
+                AddValue(property, entry);
                 return binding;
             }
             else
             {
                 var entry = new BindingEntry<T>(_owner, property, source, priority, this);
-                _values.AddValue(property, entry);
-                entry.Start();
+                AddValue(property, entry);
                 return entry;
             }
         }
@@ -149,23 +166,32 @@ namespace Avalonia
                 {
                     p.ClearLocalValue();
                 }
-                else
+                else if (slot.Priority == BindingPriority.LocalValue)
                 {
-                    var remove = slot is ConstantValueEntry<T> c ?
-                        c.Priority == BindingPriority.LocalValue : 
-                        !(slot is IPriorityValueEntry<T>);
+                    var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default;
 
-                    if (remove)
+                    // During batch update values can't be removed immediately because they're needed to raise
+                    // a correctly-typed _sink.ValueChanged notification. They instead mark themselves for removal
+                    // by setting their priority to Unset.
+                    if (_batchUpdate is null)
                     {
-                        var old = TryGetValue(property, BindingPriority.LocalValue, out var value) ? value : default;
                         _values.Remove(property);
-                        _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
-                            _owner,
-                            property,
-                            new Optional<T>(old),
-                            default,
-                            BindingPriority.Unset));
                     }
+                    else if (slot is IDisposable d)
+                    {
+                        d.Dispose();
+                    }
+                    else
+                    {
+                        // Local value entries are optimized and contain only a single value field to save space,
+                        // so there's no way to mark them for removal at the end of a batch update. Instead convert
+                        // them to a constant value entry with Unset priority in the event of a local value being
+                        // cleared during a batch update.
+                        var sentinel = new ConstantValueEntry<T>(property, default, BindingPriority.Unset, _sink);
+                        _values.SetValue(property, sentinel);
+                    }
+
+                    NotifyValueChanged<T>(property, old, default, BindingPriority.Unset);
                 }
             }
         }
@@ -176,7 +202,7 @@ namespace Avalonia
             {
                 if (slot is PriorityValue<T> p)
                 {
-                    p.CoerceValue();
+                    p.UpdateEffectiveValue();
                 }
             }
         }
@@ -198,7 +224,17 @@ namespace Avalonia
 
         void IValueSink.ValueChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
         {
-            _sink.ValueChanged(change);
+            if (_batchUpdate is object)
+            {
+                if (change.IsEffectiveValueChange)
+                {
+                    NotifyValueChanged<T>(change.Property, change.OldValue, change.NewValue, change.Priority);
+                }
+            }
+            else
+            {
+                _sink.ValueChanged(change);
+            }
         }
 
         void IValueSink.Completed<T>(
@@ -206,13 +242,17 @@ namespace Avalonia
             IPriorityValueEntry entry,
             Optional<T> oldValue)
         {
-            if (_values.TryGetValue(property, out var slot))
+            if (_values.TryGetValue(property, out var slot) && slot == entry)
             {
-                if (slot == entry)
+                if (_batchUpdate is null)
                 {
                     _values.Remove(property);
                     _sink.Completed(property, entry, oldValue);
                 }
+                else
+                {
+                    _batchUpdate.ValueChanged(property, oldValue.ToObject());
+                }
             }
         }
 
@@ -240,16 +280,13 @@ namespace Avalonia
                 {
                     var old = l.GetValue(BindingPriority.LocalValue);
                     l.SetValue(value);
-                    _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
-                        _owner,
-                        property,
-                        old,
-                        value,
-                        priority));
+                    NotifyValueChanged<T>(property, old, value, priority);
                 }
                 else
                 {
                     var priorityValue = new PriorityValue<T>(_owner, property, this, l);
+                    if (_batchUpdate is object)
+                        priorityValue.BeginBatchUpdate();
                     result = priorityValue.SetValue(value, priority);
                     _values.SetValue(property, priorityValue);
                 }
@@ -273,6 +310,11 @@ namespace Avalonia
             if (slot is IPriorityValueEntry<T> e)
             {
                 priorityValue = new PriorityValue<T>(_owner, property, this, e);
+
+                if (_batchUpdate is object)
+                {
+                    priorityValue.BeginBatchUpdate();
+                }
             }
             else if (slot is PriorityValue<T> p)
             {
@@ -289,8 +331,162 @@ namespace Avalonia
 
             var binding = priorityValue.AddBinding(source, priority);
             _values.SetValue(property, priorityValue);
-            binding.Start();
+            priorityValue.UpdateEffectiveValue();
             return binding;
         }
+
+        private void AddValue(AvaloniaProperty property, IValue value)
+        {
+            _values.AddValue(property, value);
+            if (_batchUpdate is object && value is IBatchUpdate batch)
+                batch.BeginBatchUpdate();
+            value.Start();
+        }
+
+        private void NotifyValueChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
+        {
+            if (_batchUpdate is null)
+            {
+                _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs<T>(
+                    _owner,
+                    property,
+                    oldValue,
+                    newValue,
+                    priority));
+            }
+            else
+            {
+                _batchUpdate.ValueChanged(property, oldValue.ToObject());
+            }
+        }
+
+        private class BatchUpdate
+        {
+            private ValueStore _owner;
+            private List<Notification>? _notifications;
+            private int _batchUpdateCount;
+            private int _iterator = -1;
+
+            public BatchUpdate(ValueStore owner) => _owner = owner;
+
+            public void Begin()
+            {
+                if (_batchUpdateCount++ == 0)
+                {
+                    var values = _owner._values;
+
+                    for (var i = 0; i < values.Count; ++i)
+                    {
+                        (values[i] as IBatchUpdate)?.BeginBatchUpdate();
+                    }
+                }
+            }
+
+            public bool End()
+            {
+                if (--_batchUpdateCount > 0)
+                    return false;
+
+                var values = _owner._values;
+
+                // First call EndBatchUpdate on all bindings. This should cause the active binding to be subscribed
+                // but notifications will still not be raised because the owner ValueStore will still have a reference
+                // to this batch update object.
+                for (var i = 0; i < values.Count; ++i)
+                {
+                    (values[i] as IBatchUpdate)?.EndBatchUpdate();
+
+                    // Somehow subscribing to a binding caused a new batch update. This shouldn't happen but in case it
+                    // does, abort and continue batch updating.
+                    if (_batchUpdateCount > 0)
+                        return false;
+                }
+
+                if (_notifications is object)
+                {
+                    // Raise all batched notifications. Doing this can cause other notifications to be added and even
+                    // cause a new batch update to start, so we need to handle _notifications being modified by storing
+                    // the index in field.
+                    _iterator = 0;
+
+                    for (; _iterator < _notifications.Count; ++_iterator)
+                    {
+                        var entry = _notifications[_iterator];
+
+                        if (values.TryGetValue(entry.property, out var slot))
+                        {
+                            var oldValue = entry.oldValue;
+                            var newValue = slot.GetValue();
+
+                            // Raising this notification can cause a new batch update to be started, which in turn
+                            // results in another change to the property. In this case we need to update the old value
+                            // so that the *next* notification has an oldValue which follows on from the newValue
+                            // raised here.
+                            _notifications[_iterator] = new Notification
+                            {
+                                property = entry.property,
+                                oldValue = newValue,
+                            };
+
+                            // Call _sink.ValueChanged with an appropriately typed AvaloniaPropertyChangedEventArgs<T>.
+                            slot.RaiseValueChanged(_owner._sink, _owner._owner, entry.property, oldValue, newValue);
+
+                            // During batch update values can't be removed immediately because they're needed to raise
+                            // the _sink.ValueChanged notification. They instead mark themselves for removal by setting
+                            // their priority to Unset.
+                            if (slot.Priority == BindingPriority.Unset)
+                            {
+                                values.Remove(entry.property);
+                            }
+                        }
+                        else
+                        {
+                            throw new AvaloniaInternalException("Value could not be found at the end of batch update.");
+                        }
+
+                        // If a new batch update was started while ending this one, abort.
+                        if (_batchUpdateCount > 0)
+                            return false;
+                    }
+                }
+
+                _iterator = int.MaxValue - 1;
+                return true;
+            }
+
+            public void ValueChanged(AvaloniaProperty property, Optional<object> oldValue)
+            {
+                _notifications ??= new List<Notification>();
+
+                for (var i = 0; i < _notifications.Count; ++i)
+                {
+                    if (_notifications[i].property == property)
+                    {
+                        oldValue = _notifications[i].oldValue;
+                        _notifications.RemoveAt(i);
+
+                        if (i <= _iterator)
+                            --_iterator;
+                        break;
+                    }
+                }
+
+                _notifications.Add(new Notification
+                {
+                    property = property,
+                    oldValue = oldValue,
+                });
+            }
+
+            private struct Notification
+            {
+                public AvaloniaProperty property;
+                public Optional<object> oldValue;
+            }
+        }
     }
 }

+ 9 - 2
src/Avalonia.Controls.DataGrid/Collections/DataGridCollectionView.cs

@@ -3275,7 +3275,7 @@ namespace Avalonia.Collections
                 addIndex);
 
             // next check if we need to add an item into the current group
-            // bool needsGrouping = false;
+            bool needsGrouping = false;
             if (Count == 1 && GroupDescriptions.Count > 0)
             {
                 // if this is the first item being added
@@ -3302,7 +3302,7 @@ namespace Avalonia.Collections
                 // otherwise, we need to validate that it is within the current page.
                 if (PageSize == 0 || (PageIndex + 1) * PageSize > leafIndex)
                 {
-                    //needsGrouping = true;
+                    needsGrouping = true;
 
                     int pageStartIndex = PageIndex * PageSize;
 
@@ -3340,6 +3340,13 @@ namespace Avalonia.Collections
                 }
             }
 
+            // if we need to add the item into the current group
+            // that will be displayed
+            if (needsGrouping)
+            {
+                this._group.AddToSubgroups(addedItem, false /*loading*/);
+            }
+
             int addedIndex = IndexOf(addedItem);
 
             // if the item is within the current page

+ 2 - 1
src/Avalonia.Controls/ApiCompatBaseline.txt

@@ -1,6 +1,7 @@
 Compat issues with assembly Avalonia.Controls:
 MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
+MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.ICursorImpl)' is present in the implementation but not in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' is present in the contract but not in the implementation.
 MembersMustExist : Member 'public void Avalonia.Platform.ITopLevelImpl.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
-Total Issues: 4
+Total Issues: 5

+ 9 - 2
src/Avalonia.Controls/Application.cs

@@ -30,7 +30,7 @@ namespace Avalonia
     /// method.
     /// - Tracks the lifetime of the application.
     /// </remarks>
-    public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost
+    public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost, IApplicationPlatformEvents
     {
         /// <summary>
         /// The application-global data templates.
@@ -55,6 +55,8 @@ namespace Avalonia
         /// <inheritdoc/>
         public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
 
+        public event EventHandler<UrlOpenedEventArgs> UrlsOpened; 
+
         /// <summary>
         /// Creates an instance of the <see cref="Application"/> class.
         /// </summary>
@@ -247,7 +249,11 @@ namespace Avalonia
 
         public virtual void OnFrameworkInitializationCompleted()
         {
-            
+        }
+        
+        void  IApplicationPlatformEvents.RaiseUrlsOpened(string[] urls)
+        {
+            UrlsOpened?.Invoke(this, new UrlOpenedEventArgs (urls));
         }
 
         private void NotifyResourcesChanged(ResourcesChangedEventArgs e)
@@ -288,5 +294,6 @@ namespace Avalonia
             get => _name;
             set => SetAndRaise(NameProperty, ref _name, value);
         }
+        
     }
 }

+ 14 - 0
src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

@@ -5,6 +5,7 @@ using System.Threading;
 using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Interactivity;
+using Avalonia.Platform;
 using Avalonia.Threading;
 
 namespace Avalonia.Controls.ApplicationLifetimes
@@ -102,6 +103,14 @@ namespace Avalonia.Controls.ApplicationLifetimes
         public int Start(string[] args)
         {
             Startup?.Invoke(this, new ControlledApplicationLifetimeStartupEventArgs(args));
+
+            var options = AvaloniaLocator.Current.GetService<ClassicDesktopStyleApplicationLifetimeOptions>();
+            
+            if(options != null && options.ProcessUrlActivationCommandLine && args.Length > 0)
+            {
+                ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(args);
+            }
+
             _cts = new CancellationTokenSource();
             MainWindow?.Show();
             Dispatcher.UIThread.MainLoop(_cts.Token);
@@ -115,6 +124,11 @@ namespace Avalonia.Controls.ApplicationLifetimes
                 _activeLifetime = null;
         }
     }
+    
+    public class ClassicDesktopStyleApplicationLifetimeOptions
+    {
+        public bool ProcessUrlActivationCommandLine { get; set; }
+    }
 }
 
 namespace Avalonia

+ 70 - 0
src/Avalonia.Controls/ComboBox.cs

@@ -10,6 +10,7 @@ using Avalonia.Interactivity;
 using Avalonia.Layout;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
+using Avalonia.Threading;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Controls
@@ -76,6 +77,14 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
             ContentControl.VerticalContentAlignmentProperty.AddOwner<ComboBox>();
 
+        /// <summary>
+        /// Defines the <see cref="IsTextSearchEnabled"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsTextSearchEnabledProperty =
+            AvaloniaProperty.Register<ComboBox, bool>(nameof(IsTextSearchEnabled), true);
+
+        private string _textSearchTerm = string.Empty;
+        private DispatcherTimer _textSearchTimer;
         private bool _isDropDownOpen;
         private Popup _popup;
         private object _selectionBoxItem;
@@ -164,6 +173,15 @@ namespace Avalonia.Controls
             set { SetValue(VerticalContentAlignmentProperty, value); }
         }
 
+        /// <summary>
+        /// Gets or sets a value that specifies whether a user can jump to a value by typing.
+        /// </summary>
+        public bool IsTextSearchEnabled
+        {
+            get { return GetValue(IsTextSearchEnabledProperty); }
+            set { SetValue(IsTextSearchEnabledProperty, value); }
+        }
+
         /// <inheritdoc/>
         protected override IItemContainerGenerator CreateItemContainerGenerator()
         {
@@ -229,6 +247,32 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <inheritdoc />
+        protected override void OnTextInput(TextInputEventArgs e)
+        {
+            if (!IsTextSearchEnabled || e.Handled)
+                return;
+
+            StopTextSearchTimer();
+
+            _textSearchTerm += e.Text;
+
+            bool match(ItemContainerInfo info) => 
+                info.ContainerControl is IContentControl control &&
+                control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true;
+
+            var info = ItemContainerGenerator.Containers.FirstOrDefault(match);
+
+            if (info != null)
+            {
+                SelectedIndex = info.Index;
+            }
+
+            StartTextSearchTimer();
+
+            e.Handled = true;
+        }
+
         /// <inheritdoc/>
         protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
         {
@@ -426,5 +470,31 @@ namespace Avalonia.Controls
 
             SelectedIndex = prev;
         }
+
+        private void StartTextSearchTimer()
+        {
+            _textSearchTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
+            _textSearchTimer.Tick += TextSearchTimer_Tick;
+            _textSearchTimer.Start();
+        }
+
+        private void StopTextSearchTimer()
+        {
+            if (_textSearchTimer == null)
+            {
+                return;
+            }
+
+            _textSearchTimer.Stop();
+            _textSearchTimer.Tick -= TextSearchTimer_Tick;
+
+            _textSearchTimer = null;
+        }
+
+        private void TextSearchTimer_Tick(object sender, EventArgs e)
+        {
+            _textSearchTerm = string.Empty;
+            StopTextSearchTimer();
+        }
     }
 }

+ 29 - 40
src/Avalonia.Controls/Control.cs

@@ -8,6 +8,8 @@ using Avalonia.Rendering;
 using Avalonia.Styling;
 using Avalonia.VisualTree;
 
+#nullable enable
+
 namespace Avalonia.Controls
 {
     /// <summary>
@@ -18,25 +20,25 @@ namespace Avalonia.Controls
     ///
     /// - A <see cref="Tag"/> property to allow user-defined data to be attached to the control.
     /// </remarks>
-    public class Control : InputElement, IControl, INamed, ISupportInitialize, IVisualBrushInitialize, ISetterValue
+    public class Control : InputElement, IControl, INamed, IVisualBrushInitialize, ISetterValue
     {
         /// <summary>
         /// Defines the <see cref="FocusAdorner"/> property.
         /// </summary>
-        public static readonly StyledProperty<ITemplate<IControl>> FocusAdornerProperty =
-            AvaloniaProperty.Register<Control, ITemplate<IControl>>(nameof(FocusAdorner));
+        public static readonly StyledProperty<ITemplate<IControl>?> FocusAdornerProperty =
+            AvaloniaProperty.Register<Control, ITemplate<IControl>?>(nameof(FocusAdorner));
 
         /// <summary>
         /// Defines the <see cref="Tag"/> property.
         /// </summary>
-        public static readonly StyledProperty<object> TagProperty =
-            AvaloniaProperty.Register<Control, object>(nameof(Tag));
+        public static readonly StyledProperty<object?> TagProperty =
+            AvaloniaProperty.Register<Control, object?>(nameof(Tag));
         
         /// <summary>
         /// Defines the <see cref="ContextMenu"/> property.
         /// </summary>
-        public static readonly StyledProperty<ContextMenu> ContextMenuProperty =
-            AvaloniaProperty.Register<Control, ContextMenu>(nameof(ContextMenu));
+        public static readonly StyledProperty<ContextMenu?> ContextMenuProperty =
+            AvaloniaProperty.Register<Control, ContextMenu?>(nameof(ContextMenu));
 
         /// <summary>
         /// Event raised when an element wishes to be scrolled into view.
@@ -44,16 +46,16 @@ namespace Avalonia.Controls
         public static readonly RoutedEvent<RequestBringIntoViewEventArgs> RequestBringIntoViewEvent =
             RoutedEvent.Register<Control, RequestBringIntoViewEventArgs>("RequestBringIntoView", RoutingStrategies.Bubble);
 
-        private DataTemplates _dataTemplates;
-        private IControl _focusAdorner;
+        private DataTemplates? _dataTemplates;
+        private IControl? _focusAdorner;
 
         /// <summary>
         /// Gets or sets the control's focus adorner.
         /// </summary>
-        public ITemplate<IControl> FocusAdorner
+        public ITemplate<IControl>? FocusAdorner
         {
-            get { return GetValue(FocusAdornerProperty); }
-            set { SetValue(FocusAdornerProperty, value); }
+            get => GetValue(FocusAdornerProperty);
+            set => SetValue(FocusAdornerProperty, value);
         }
 
         /// <summary>
@@ -63,27 +65,27 @@ namespace Avalonia.Controls
         /// Each control may define data templates which are applied to the control itself and its
         /// children.
         /// </remarks>
-        public DataTemplates DataTemplates => _dataTemplates ?? (_dataTemplates = new DataTemplates());
+        public DataTemplates DataTemplates => _dataTemplates ??= new DataTemplates();
 
         /// <summary>
         /// Gets or sets a context menu to the control.
         /// </summary>
-        public ContextMenu ContextMenu
+        public ContextMenu? ContextMenu
         {
-            get { return GetValue(ContextMenuProperty); }
-            set { SetValue(ContextMenuProperty, value); }
+            get => GetValue(ContextMenuProperty);
+            set => SetValue(ContextMenuProperty, value);
         }
 
         /// <summary>
         /// Gets or sets a user-defined object attached to the control.
         /// </summary>
-        public object Tag
+        public object? Tag
         {
-            get { return GetValue(TagProperty); }
-            set { SetValue(TagProperty, value); }
+            get => GetValue(TagProperty);
+            set => SetValue(TagProperty, value);
         }
 
-        public new IControl Parent => (IControl)base.Parent;
+        public new IControl? Parent => (IControl?)base.Parent;
 
         /// <inheritdoc/>
         bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
@@ -106,15 +108,10 @@ namespace Avalonia.Controls
                     {
                         var c = i as IControl;
 
-                        if (c?.IsInitialized == false)
+                        if (c?.IsInitialized == false && c is ISupportInitialize init)
                         {
-                            var init = c as ISupportInitialize;
-
-                            if (init != null)
-                            {
-                                init.BeginInit();
-                                init.EndInit();
-                            }
+                            init.BeginInit();
+                            init.EndInit();
                         }
                     }
                 }
@@ -131,10 +128,7 @@ namespace Avalonia.Controls
         /// Gets the element that receives the focus adorner.
         /// </summary>
         /// <returns>The control that receives the focus adorner.</returns>
-        protected virtual IControl GetTemplateFocusTarget()
-        {
-            return this;
-        }
+        protected virtual IControl? GetTemplateFocusTarget() => this;
 
         /// <inheritdoc/>
         protected sealed override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e)
@@ -173,15 +167,10 @@ namespace Avalonia.Controls
                         }
                     }
 
-                    if (_focusAdorner != null)
+                    if (_focusAdorner != null && GetTemplateFocusTarget() is Visual target)
                     {
-                        var target = (Visual)GetTemplateFocusTarget();
-
-                        if (target != null)
-                        {
-                            AdornerLayer.SetAdornedElement((Visual)_focusAdorner, target);
-                            adornerLayer.Children.Add(_focusAdorner);
-                        }
+                        AdornerLayer.SetAdornedElement((Visual)_focusAdorner, target);
+                        adornerLayer.Children.Add(_focusAdorner);
                     }
                 }
             }

+ 12 - 5
src/Avalonia.Controls/Notifications/NotificationCard.cs

@@ -16,6 +16,11 @@ namespace Avalonia.Controls.Notifications
         private bool _isClosed;
         private bool _isClosing;
 
+        static NotificationCard()
+        {
+            CloseOnClickProperty.Changed.AddClassHandler<Button>(OnCloseOnClickPropertyChanged);
+        }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="NotificationCard"/> class.
         /// </summary>
@@ -105,22 +110,26 @@ namespace Avalonia.Controls.Notifications
 
         public static bool GetCloseOnClick(Button obj)
         {
+            Contract.Requires<ArgumentNullException>(obj != null);
             return (bool)obj.GetValue(CloseOnClickProperty);
         }
 
         public static void SetCloseOnClick(Button obj, bool value)
         {
+            Contract.Requires<ArgumentNullException>(obj != null);
             obj.SetValue(CloseOnClickProperty, value);
         }
 
         /// <summary>
         /// Defines the CloseOnClick property.
         /// </summary>
-        public static readonly AvaloniaProperty CloseOnClickProperty =
-          AvaloniaProperty.RegisterAttached<Button, bool>("CloseOnClick", typeof(NotificationCard)/*, validate: CloseOnClickChanged*/);
+        public static readonly AttachedProperty<bool> CloseOnClickProperty =
+          AvaloniaProperty.RegisterAttached<NotificationCard, Button, bool>("CloseOnClick", defaultValue: false);
 
-        private static bool CloseOnClickChanged(Button button, bool value)
+        private static void OnCloseOnClickPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
         {
+            var button = (Button)d;
+            var value = (bool)e.NewValue;
             if (value)
             {
                 button.Click += Button_Click;
@@ -129,8 +138,6 @@ namespace Avalonia.Controls.Notifications
             {
                 button.Click -= Button_Click;
             }
-
-            return true;
         }
 
         /// <summary>

+ 7 - 0
src/Avalonia.Controls/Platform/IApplicationPlatformEvents.cs

@@ -0,0 +1,7 @@
+namespace Avalonia.Platform
+{
+    public interface IApplicationPlatformEvents
+    {
+        void RaiseUrlsOpened(string[] urls);
+    }
+}

+ 14 - 0
src/Avalonia.Controls/UrlOpenedEventArgs.cs

@@ -0,0 +1,14 @@
+using System;
+
+namespace Avalonia
+{
+    public class UrlOpenedEventArgs : EventArgs
+    {
+        public UrlOpenedEventArgs(string[] urls)
+        {
+            Urls = urls;
+        }
+        
+        public string[] Urls { get; }
+    }
+}

+ 25 - 20
src/Avalonia.Controls/Window.cs

@@ -4,10 +4,7 @@ using System.ComponentModel;
 using System.Linq;
 using System.Reactive.Linq;
 using System.Threading.Tasks;
-using Avalonia.Controls.Chrome;
 using Avalonia.Controls.Platform;
-using Avalonia.Controls.Primitives;
-using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.Layout;
@@ -482,10 +479,9 @@ namespace Avalonia.Controls
 
             try
             {
-                if (!ignoreCancel && HandleClosing())
+                if (!ignoreCancel && ShouldCancelClose())
                 {
                     close = false;
-                    return;
                 }
             }
             finally
@@ -497,11 +493,25 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Handles a closing notification from <see cref="IWindowImpl.Closing"/>.
+        /// <returns>true if closing is cancelled. Otherwise false.</returns>
+        /// </summary>
+        protected virtual bool HandleClosing()
+        {
+            if (!ShouldCancelClose())
+            {
+                CloseInternal();
+                return false;
+            }
+            
+            return true;
+        }
+
         private void CloseInternal()
         {
             foreach (var (child, _) in _children.ToList())
             {
-                // if we HandleClosing() before then there will be no children.
                 child.CloseInternal();
             }
 
@@ -515,20 +525,18 @@ namespace Avalonia.Controls
             PlatformImpl?.Dispose();
         }
 
-        /// <summary>
-        /// Handles a closing notification from <see cref="IWindowImpl.Closing"/>.
-        /// </summary>
-        protected virtual bool HandleClosing()
+        private bool ShouldCancelClose(CancelEventArgs args = null)
         {
+            if (args is null)
+            {
+                args = new CancelEventArgs();
+            }
+            
             bool canClose = true;
 
             foreach (var (child, _) in _children.ToList())
             {
-                if (!child.HandleClosing())
-                {
-                    child.CloseInternal();
-                }
-                else
+                if (child.ShouldCancelClose(args))
                 {
                     canClose = false;
                 }
@@ -536,15 +544,12 @@ namespace Avalonia.Controls
 
             if (canClose)
             {
-                var args = new CancelEventArgs();
                 OnClosing(args);
 
                 return args.Cancel;
             }
-            else
-            {
-                return !canClose;
-            }
+
+            return true;
         }
 
         protected virtual void HandleWindowStateChanged(WindowState state)

+ 21 - 0
src/Avalonia.Diagnostics/Diagnostics/Converters/BoolToOpacityConverter.cs

@@ -0,0 +1,21 @@
+using System;
+using System.Globalization;
+using Avalonia.Data.Converters;
+
+namespace Avalonia.Diagnostics.Converters
+{
+    internal class BoolToOpacityConverter : IValueConverter
+    {
+        public double Opacity { get; set; }
+
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            return (bool)value ? 1d : Opacity;
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 225 - 1
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@@ -1,8 +1,15 @@
 using System;
 using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
 using System.ComponentModel;
 using System.Linq;
+using System.Reflection;
 using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
+using Avalonia.Markup.Xaml.MarkupExtensions;
+using Avalonia.Styling;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Diagnostics.ViewModels
@@ -12,6 +19,10 @@ namespace Avalonia.Diagnostics.ViewModels
         private readonly IVisual _control;
         private readonly IDictionary<object, List<PropertyViewModel>> _propertyIndex;
         private AvaloniaPropertyViewModel _selectedProperty;
+        private string _styleFilter;
+        private bool _snapshotStyles;
+        private bool _showInactiveStyles;
+        private string _styleStatus;
 
         public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control)
         {
@@ -43,20 +54,160 @@ namespace Avalonia.Diagnostics.ViewModels
             {
                 ao.PropertyChanged += ControlPropertyChanged;
             }
+
+            AppliedStyles = new ObservableCollection<StyleViewModel>();
+            PseudoClasses = new ObservableCollection<PseudoClassViewModel>();
+
+            if (control is StyledElement styledElement)
+            {
+                styledElement.Classes.CollectionChanged += OnClassesChanged;
+
+                var pseudoClassAttributes = styledElement.GetType().GetCustomAttributes<PseudoClassesAttribute>(true);
+
+                foreach (var classAttribute in pseudoClassAttributes)
+                {
+                    foreach (var className in classAttribute.PseudoClasses)
+                    {
+                        PseudoClasses.Add(new PseudoClassViewModel(className, styledElement));
+                    }
+                }
+
+                var styleDiagnostics = styledElement.GetStyleDiagnostics();
+
+                foreach (var appliedStyle in styleDiagnostics.AppliedStyles)
+                {
+                    var styleSource = appliedStyle.Source;
+
+                    var setters = new List<SetterViewModel>();
+
+                    if (styleSource is Style style)
+                    {
+                        foreach (var setter in style.Setters)
+                        {
+                            if (setter is Setter regularSetter)
+                            {
+                                var setterValue = regularSetter.Value;
+
+                                var resourceInfo = GetResourceInfo(setterValue);
+
+                                SetterViewModel setterVm;
+
+                                if (resourceInfo.HasValue)
+                                {
+                                    var resourceKey = resourceInfo.Value.resourceKey;
+                                    var resourceValue = styledElement.FindResource(resourceKey);
+
+                                    setterVm = new ResourceSetterViewModel(regularSetter.Property, resourceKey, resourceValue, resourceInfo.Value.isDynamic);
+                                }
+                                else
+                                {
+                                    setterVm = new SetterViewModel(regularSetter.Property, setterValue);
+                                }
+
+                                setters.Add(setterVm);
+                            }
+                        }
+
+                        AppliedStyles.Add(new StyleViewModel(appliedStyle, style.Selector?.ToString() ?? "No selector", setters));
+                    }
+                }
+
+                UpdateStyles();
+            }
+        }
+
+        private (object resourceKey, bool isDynamic)? GetResourceInfo(object value)
+        {
+            if (value is StaticResourceExtension staticResource)
+            {
+                return (staticResource.ResourceKey, false);
+            }
+            else if (value is DynamicResourceExtension dynamicResource)
+            {
+                return (dynamicResource.ResourceKey, true);
+            }
+
+            return null;
         }
 
         public TreePageViewModel TreePage { get; }
 
         public DataGridCollectionView PropertiesView { get; }
 
+        public ObservableCollection<StyleViewModel> AppliedStyles { get; }
+
+        public ObservableCollection<PseudoClassViewModel> PseudoClasses { get; }
+
         public AvaloniaPropertyViewModel SelectedProperty
         {
             get => _selectedProperty;
             set => RaiseAndSetIfChanged(ref _selectedProperty, value);
         }
-        
+
+        public string StyleFilter
+        {
+            get => _styleFilter;
+            set => RaiseAndSetIfChanged(ref _styleFilter, value);
+        }
+
+        public bool SnapshotStyles
+        {
+            get => _snapshotStyles;
+            set => RaiseAndSetIfChanged(ref _snapshotStyles, value);
+        }
+
+        public bool ShowInactiveStyles
+        {
+            get => _showInactiveStyles;
+            set => RaiseAndSetIfChanged(ref _showInactiveStyles, value);
+        }
+
+        public string StyleStatus
+        {
+            get => _styleStatus;
+            set => RaiseAndSetIfChanged(ref _styleStatus, value);
+        }
+
         public ControlLayoutViewModel Layout { get; }
 
+        protected override void OnPropertyChanged(PropertyChangedEventArgs e)
+        {
+            base.OnPropertyChanged(e);
+
+            if (e.PropertyName == nameof(StyleFilter))
+            {
+                UpdateStyleFilters();
+            }
+            else if (e.PropertyName == nameof(SnapshotStyles))
+            {
+                if (!SnapshotStyles)
+                {
+                    UpdateStyles();
+                }
+            }
+        }
+
+        private void UpdateStyleFilters()
+        {
+            var filter = StyleFilter;
+            bool hasFilter = !string.IsNullOrEmpty(filter);
+
+            foreach (var style in AppliedStyles)
+            {
+                var hasVisibleSetter = false;
+
+                foreach (var setter in style.Setters)
+                {
+                    setter.IsVisible =
+                        !hasFilter || setter.Name.IndexOf(filter, StringComparison.OrdinalIgnoreCase) >= 0;
+
+                    hasVisibleSetter |= setter.IsVisible;
+                }
+
+                style.IsVisible = hasVisibleSetter;
+            }
+        }
+
         public void Dispose()
         {
             if (_control is INotifyPropertyChanged inpc)
@@ -68,6 +219,11 @@ namespace Avalonia.Diagnostics.ViewModels
             {
                 ao.PropertyChanged -= ControlPropertyChanged;
             }
+
+            if (_control is StyledElement se)
+            {
+                se.Classes.CollectionChanged -= OnClassesChanged;
+            }
         }
 
         private IEnumerable<PropertyViewModel> GetAvaloniaProperties(object o)
@@ -129,6 +285,74 @@ namespace Avalonia.Diagnostics.ViewModels
                     property.Update();
                 }
             }
+
+            if (!SnapshotStyles)
+            {
+                UpdateStyles();
+            }
+        }
+
+        private void OnClassesChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            if (!SnapshotStyles)
+            {
+                UpdateStyles();
+            }
+        }
+
+        private void UpdateStyles()
+        {
+            int activeCount = 0;
+
+            foreach (var style in AppliedStyles)
+            {
+                style.Update();
+
+                if (style.IsActive)
+                {
+                    activeCount++;
+                }
+            }
+
+            var propertyBuckets = new Dictionary<AvaloniaProperty, List<SetterViewModel>>();
+
+            foreach (var style in AppliedStyles)
+            {
+                if (!style.IsActive)
+                {
+                    continue;
+                }
+
+                foreach (var setter in style.Setters)
+                {
+                    if (propertyBuckets.TryGetValue(setter.Property, out var setters))
+                    {
+                        foreach (var otherSetter in setters)
+                        {
+                            otherSetter.IsActive = false;
+                        }
+
+                        setter.IsActive = true;
+
+                        setters.Add(setter);
+                    }
+                    else
+                    {
+                        setter.IsActive = true;
+
+                        setters = new List<SetterViewModel> { setter };
+
+                        propertyBuckets.Add(setter.Property, setters);
+                    }
+                }
+            }
+
+            foreach (var pseudoClass in PseudoClasses)
+            {
+                pseudoClass.Update();
+            }
+
+            StyleStatus = $"Styles ({activeCount}/{AppliedStyles.Count} active)";
         }
 
         private bool FilterProperty(object arg)

+ 8 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@@ -163,6 +163,14 @@ namespace Avalonia.Diagnostics.ViewModels
             tree?.SelectControl(control);
         }
 
+        public void EnableSnapshotStyles(bool enable)
+        {
+            if (Content is TreePageViewModel treeVm && treeVm.Details != null)
+            {
+                treeVm.Details.SnapshotStyles = enable;
+            }
+        }
+
         public void Dispose()
         {
             KeyboardDevice.Instance.PropertyChanged -= KeyboardPropertyChanged;

+ 51 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/PseudoClassViewModel.cs

@@ -0,0 +1,51 @@
+using Avalonia.Controls;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+    internal class PseudoClassViewModel : ViewModelBase
+    {
+        private readonly IPseudoClasses _pseudoClasses;
+        private readonly StyledElement _source;
+        private bool _isActive;
+        private bool _isUpdating;
+
+        public PseudoClassViewModel(string name, StyledElement source)
+        {
+            Name = name;
+            _source = source;
+            _pseudoClasses = _source.Classes;
+
+            Update();
+        }
+
+        public string Name { get; }
+
+        public bool IsActive
+        {
+            get => _isActive;
+            set
+            {
+                RaiseAndSetIfChanged(ref _isActive, value);
+
+                if (!_isUpdating)
+                {
+                    _pseudoClasses.Set(Name, value);
+                }
+            }
+        }
+
+        public void Update()
+        {
+            try
+            {
+                _isUpdating = true;
+
+                IsActive = _source.Classes.Contains(Name);
+            }
+            finally
+            {
+                _isUpdating = false;
+            }
+        }
+    }
+}

+ 27 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ResourceSetterViewModel.cs

@@ -0,0 +1,27 @@
+using Avalonia.Media;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+    internal class ResourceSetterViewModel : SetterViewModel
+    {
+        public object Key { get; }
+
+        public IBrush Tint { get; }
+
+        public ResourceSetterViewModel(AvaloniaProperty property, object resourceKey, object resourceValue, bool isDynamic) : base(property, resourceValue)
+        {
+            Key = resourceKey;
+            Tint = isDynamic ? Brushes.Orange : Brushes.Brown;
+        }
+
+        public void CopyResourceKey()
+        {
+            if (Key is null)
+            {
+                return;
+            }
+
+            CopyToClipboard(Key.ToString());
+        }
+    }
+}

+ 59 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/SetterViewModel.cs

@@ -0,0 +1,59 @@
+using Avalonia.Input.Platform;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+    internal class SetterViewModel : ViewModelBase
+    {
+        private bool _isActive;
+        private bool _isVisible;
+
+        public AvaloniaProperty Property { get; }
+
+        public string Name { get; }
+
+        public object Value { get; }
+
+        public bool IsActive
+        {
+            get => _isActive;
+            set => RaiseAndSetIfChanged(ref _isActive, value);
+        }
+
+        public bool IsVisible
+        {
+            get => _isVisible;
+            set => RaiseAndSetIfChanged(ref _isVisible, value);
+        }
+
+        public SetterViewModel(AvaloniaProperty property, object value)
+        {
+            Property = property;
+            Name = property.Name;
+            Value = value;
+            IsActive = true;
+            IsVisible = true;
+        }
+
+        public void CopyValue()
+        {
+            if (Value is null)
+            {
+                return;
+            }
+
+            CopyToClipboard(Value.ToString());
+        }
+
+        public void CopyPropertyName()
+        {
+            CopyToClipboard(Property.Name);
+        }
+
+        protected static void CopyToClipboard(string value)
+        {
+            var clipboard = AvaloniaLocator.Current.GetService<IClipboard>();
+
+            clipboard?.SetTextAsync(value);
+        }
+    }
+}

+ 43 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs

@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+using Avalonia.Styling;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+    internal class StyleViewModel : ViewModelBase
+    {
+        private readonly IStyleInstance _styleInstance;
+        private bool _isActive;
+        private bool _isVisible;
+
+        public StyleViewModel(IStyleInstance styleInstance, string name, List<SetterViewModel> setters)
+        {
+            _styleInstance = styleInstance;
+            IsVisible = true;
+            Name = name;
+            Setters = setters;
+
+            Update();
+        }
+
+        public bool IsActive
+        {
+            get => _isActive;
+            set => RaiseAndSetIfChanged(ref _isActive, value);
+        }
+
+        public bool IsVisible
+        {
+            get => _isVisible;
+            set => RaiseAndSetIfChanged(ref _isVisible, value);
+        }
+
+        public string Name { get; }
+
+        public List<SetterViewModel> Setters { get; }
+
+        public void Update()
+        {
+            IsActive = _styleInstance.IsActive;
+        }
+    }
+}

+ 7 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreePageViewModel.cs

@@ -31,11 +31,18 @@ namespace Avalonia.Diagnostics.ViewModels
             get => _selectedNode;
             private set
             {
+                var oldDetails = Details;
+
                 if (RaiseAndSetIfChanged(ref _selectedNode, value))
                 {
                     Details = value != null ?
                         new ControlDetailsViewModel(this, value.Visual) :
                         null;
+
+                    if (Details != null && oldDetails != null)
+                    {
+                        Details.StyleFilter = oldDetails.StyleFilter;
+                    }
                 }
             }
         }

+ 121 - 3
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@@ -2,7 +2,9 @@
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:conv="clr-namespace:Avalonia.Diagnostics.Converters"
              xmlns:local="clr-namespace:Avalonia.Diagnostics.Views"
-             x:Class="Avalonia.Diagnostics.Views.ControlDetailsView">
+             xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels"
+             x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"
+             x:Name="Main">
 
   <UserControl.Resources>
     <SolidColorBrush x:Key="ThicknessBorderBrush" Color="#666666" />
@@ -11,6 +13,7 @@
     <SolidColorBrush x:Key="BorderBackgroundBrush" Color="#E3C381" />
     <SolidColorBrush x:Key="PaddingBackgroundBrush" Color="#B8C47F" />
     <SolidColorBrush x:Key="SizeBackgroundBrush" Color="#88B2BD" />
+    <conv:BoolToOpacityConverter x:Key="BoolToOpacity" Opacity="0.6"/>
   </UserControl.Resources>
 
   <UserControl.Styles>
@@ -105,7 +108,7 @@
 
     <GridSplitter Grid.Column="1" />
 
-    <Grid Grid.Column="2" RowDefinitions="Auto,*" >
+    <Grid Grid.Column="2" RowDefinitions="Auto,*, Auto,*,Auto" >
       <TextBlock Grid.Row="0" Text="Layout Visualizer" Margin="4" />
       
       <Grid Grid.Row="1" x:Name="LayoutRoot" Margin="8,0,8,8" RowDefinitions="Auto,Auto" ColumnDefinitions="Auto,Auto">
@@ -148,7 +151,122 @@
           <Rectangle x:Name="VerticalSizeEnd" />
         </Canvas>
       </Grid>
-      
+
+      <Grid Grid.Row="2" Margin="4" RowDefinitions="Auto,Auto">
+
+        <Grid Grid.Row="0" Margin="2" ColumnDefinitions="Auto,*,Auto,Auto">
+          <TextBlock FontWeight="Bold" Grid.Column="0" Text="{Binding StyleStatus}" VerticalAlignment="Center" />
+          <CheckBox Margin="2,0,0,0" Grid.Column="2" Content="Show inactive" IsChecked="{Binding ShowInactiveStyles}" ToolTip.Tip="Show styles that are currently inactive" />
+          <ToggleButton Margin="2,0,0,0" Grid.Column="3" ToolTip.Tip="Snapshot current styles (Alt+S/Alt+D to enable/disable within debugged window)" Content="Snapshot" IsChecked="{Binding SnapshotStyles}" />
+        </Grid>
+
+        <TextBox Grid.Row="1" Margin="2" Grid.Column="0" Watermark="Filter" Text="{Binding StyleFilter}" />
+      </Grid>
+
+        <ScrollViewer Grid.Row="3" HorizontalScrollBarVisibility="Disabled">
+        <ItemsControl Items="{Binding AppliedStyles}" >
+          <ItemsControl.ItemTemplate>
+            <DataTemplate>
+              <Border BorderThickness="0,0,0,1" BorderBrush="#6C6C6C" Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacity}}">
+                <Border.IsVisible>
+                  <MultiBinding Converter="{x:Static BoolConverters.And}">
+                    <MultiBinding Converter="{x:Static BoolConverters.Or}" >
+                      <Binding Path="IsActive" />
+                      <Binding Path="#Main.DataContext.ShowInactiveStyles" />
+                    </MultiBinding>
+                    <Binding Path="IsVisible" />
+                  </MultiBinding>
+                </Border.IsVisible>
+                <Expander IsExpanded="True" Margin="0" Padding="8,0" ContentTransition="{x:Null}" >
+                  <Expander.Header>
+                    <TextBlock Grid.Row="0" Text="{Binding Name}" />
+                  </Expander.Header>
+
+                  <ItemsControl Margin="20,0,0,0" Grid.Row="1" Items="{Binding Setters}">
+                    <ItemsControl.DataTemplates>
+
+                      <DataTemplate DataType="IBrush">
+                        <StackPanel Orientation="Horizontal" Spacing="2">
+                          <Border BorderThickness="1" BorderBrush="Black" Background="{Binding}" Width="8" Height="8"/>
+                          <TextBlock Text="{Binding}" />
+                        </StackPanel>
+                      </DataTemplate>
+
+                      <DataTemplate DataType="Color">
+                        <StackPanel Orientation="Horizontal" Spacing="2">
+                          <Border BorderThickness="1" BorderBrush="Black" Width="8" Height="8">
+                            <Border.Background>
+                              <SolidColorBrush Color="{Binding}" />
+                            </Border.Background>
+                          </Border>
+                          <TextBlock Text="{Binding}" />
+                        </StackPanel>
+                      </DataTemplate>
+
+                      <DataTemplate DataType="vm:ResourceSetterViewModel">
+                        <Panel Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacity}}" IsVisible="{Binding IsVisible}" HorizontalAlignment="Left">
+                          <Panel.ContextMenu>
+                            <ContextMenu>
+                              <MenuItem Header="Copy property name" Command="{Binding CopyPropertyName} "/>
+                              <MenuItem Header="Copy value" Command="{Binding CopyValue} "/>
+                              <MenuItem Header="Copy resource key" Command="{Binding CopyResourceKey}" />
+                            </ContextMenu>
+                          </Panel.ContextMenu>
+                          <StackPanel Orientation="Horizontal" Spacing="2" HorizontalAlignment="Left">
+                            <TextBlock Text="{Binding Name}" FontWeight="SemiBold" />
+                            <TextBlock Text=":" />
+                            <ContentControl Content="{Binding Value}"/>
+                            <TextBlock>(</TextBlock>
+                            <Ellipse Height="8" Width="8" VerticalAlignment="Center" Fill="{Binding Tint}"/>
+                            <TextBlock FontStyle="Italic" Text="{Binding Key}" />
+                            <TextBlock>)</TextBlock>
+                          </StackPanel>
+                          <Rectangle Height="1" Fill="#6C6C6C" IsVisible="{Binding !IsActive}" />
+                        </Panel>
+                      </DataTemplate>
+
+                      <DataTemplate DataType="vm:SetterViewModel">
+                        <Panel Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacity}}" IsVisible="{Binding IsVisible}" HorizontalAlignment="Left">
+                          <Panel.ContextMenu>
+                            <ContextMenu>
+                              <MenuItem Header="Copy property name" Command="{Binding CopyPropertyName} "/>
+                              <MenuItem Header="Copy value" Command="{Binding CopyValue} "/>
+                            </ContextMenu>
+                          </Panel.ContextMenu>
+                          <StackPanel Orientation="Horizontal" Spacing="2">
+                            <TextBlock Text="{Binding Name}" FontWeight="SemiBold" />
+                            <TextBlock Text=":" />
+                            <ContentControl Content="{Binding Value}"/>
+                          </StackPanel>
+                          <Rectangle Height="1" Fill="#6C6C6C" VerticalAlignment="Center" IsVisible="{Binding !IsActive}" />
+                        </Panel>
+                      </DataTemplate>
+
+                    </ItemsControl.DataTemplates>
+                  </ItemsControl>
+
+                </Expander>
+              </Border>
+            </DataTemplate>
+          </ItemsControl.ItemTemplate>
+        </ItemsControl>
+      </ScrollViewer>
+
+      <Expander Header="Pseudo Classes" Grid.Row="4">
+        <ItemsControl Items="{Binding PseudoClasses}">
+          <ItemsControl.ItemsPanel>
+            <ItemsPanelTemplate>
+              <WrapPanel />
+            </ItemsPanelTemplate>
+          </ItemsControl.ItemsPanel>
+          <ItemsControl.ItemTemplate>
+            <DataTemplate>
+              <CheckBox Margin="2" Content="{Binding Name}" IsChecked="{Binding IsActive, Mode=TwoWay}" />
+            </DataTemplate>
+          </ItemsControl.ItemTemplate>
+        </ItemsControl>
+      </Expander>
+
     </Grid>
   </Grid>
 

+ 10 - 0
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs

@@ -90,6 +90,16 @@ namespace Avalonia.Diagnostics.Views
                     var vm = (MainViewModel)DataContext;
                     vm.SelectControl((IControl)control);
                 }
+            } 
+            else if (e.Modifiers == RawInputModifiers.Alt)
+            {
+                if (e.Key == Key.S || e.Key == Key.D)
+                {
+                    var enable = e.Key == Key.S;
+
+                    var vm = (MainViewModel)DataContext;
+                    vm.EnableSnapshotStyles(enable);
+                }
             }
         }
 

+ 1 - 1
src/Avalonia.Layout/ElementManager.cs

@@ -207,7 +207,7 @@ namespace Avalonia.Layout
             }
         }
 
-        public bool IsIndexValidInData(int currentIndex) => currentIndex >= 0 && currentIndex < _context.ItemCount;
+        public bool IsIndexValidInData(int currentIndex) => (uint)currentIndex < _context.ItemCount;
 
         public ILayoutable GetRealizedElement(int dataIndex)
         {

+ 5 - 5
src/Avalonia.Layout/UniformGridLayoutState.cs

@@ -44,7 +44,7 @@ namespace Avalonia.Layout
             Size availableSize,
             VirtualizingLayoutContext context,
             double layoutItemWidth,
-            double LayoutItemHeight,
+            double layoutItemHeight,
             UniformGridLayoutItemsStretch stretch,
             Orientation orientation,
             double minRowSpacing,
@@ -63,7 +63,7 @@ namespace Avalonia.Layout
                 if (realizedElement != null)
                 {
                     realizedElement.Measure(availableSize);
-                    SetSize(realizedElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine);
+                    SetSize(realizedElement, layoutItemWidth, layoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine);
                     _cachedFirstElement = null;
                 }
                 else
@@ -78,7 +78,7 @@ namespace Avalonia.Layout
 
                     _cachedFirstElement.Measure(availableSize);
 
-                    SetSize(_cachedFirstElement, layoutItemWidth, LayoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine);
+                    SetSize(_cachedFirstElement, layoutItemWidth, layoutItemHeight, availableSize, stretch, orientation, minRowSpacing, minColumnSpacing, maxItemsPerLine);
 
                     // See if we can move ownership to the flow algorithm. If we can, we do not need a local cache.
                     bool added = FlowAlgorithm.TryAddElement0(_cachedFirstElement);
@@ -93,7 +93,7 @@ namespace Avalonia.Layout
         private void SetSize(
             ILayoutable element,
             double layoutItemWidth,
-            double LayoutItemHeight,
+            double layoutItemHeight,
             Size availableSize,
             UniformGridLayoutItemsStretch stretch,
             Orientation orientation,
@@ -107,7 +107,7 @@ namespace Avalonia.Layout
             }
 
             EffectiveItemWidth = (double.IsNaN(layoutItemWidth) ? element.DesiredSize.Width : layoutItemWidth);
-            EffectiveItemHeight = (double.IsNaN(LayoutItemHeight) ? element.DesiredSize.Height : LayoutItemHeight);
+            EffectiveItemHeight = (double.IsNaN(layoutItemHeight) ? element.DesiredSize.Height : layoutItemHeight);
 
             var availableSizeMinor = orientation == Orientation.Horizontal ? availableSize.Width : availableSize.Height;
             var minorItemSpacing = orientation == Orientation.Vertical ? minRowSpacing : minColumnSpacing;

+ 14 - 0
src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs

@@ -0,0 +1,14 @@
+using System;
+using Avalonia.Native.Interop;
+using Avalonia.Platform;
+
+namespace Avalonia.Native
+{
+    internal class AvaloniaNativeApplicationPlatform : CallbackBase, IAvnApplicationEvents
+    {
+        void IAvnApplicationEvents.FilesOpened(IAvnStringArray urls)
+        {
+            ((IApplicationPlatformEvents)Application.Current).RaiseUrlsOpened(urls.ToStringArray());
+        }
+    }
+}

+ 4 - 3
src/Avalonia.Native/AvaloniaNativePlatform.cs

@@ -1,6 +1,5 @@
 using System;
 using System.Runtime.InteropServices;
-using System.Security.Cryptography;
 using Avalonia.Controls.Platform;
 using Avalonia.Input;
 using Avalonia.Input.Platform;
@@ -9,7 +8,6 @@ using Avalonia.Native.Interop;
 using Avalonia.OpenGL;
 using Avalonia.Platform;
 using Avalonia.Rendering;
-using Avalonia.Platform.Interop;
 
 namespace Avalonia.Native
 {
@@ -86,7 +84,10 @@ namespace Avalonia.Native
         void DoInitialize(AvaloniaNativePlatformOptions options)
         {
             _options = options;
-            _factory.Initialize(new GCHandleDeallocator());
+            
+            var applicationPlatform = new AvaloniaNativeApplicationPlatform();
+            
+            _factory.Initialize(new GCHandleDeallocator(), applicationPlatform);
             if (_factory.MacOptions != null)
             {
                 var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>();

+ 7 - 1
src/Avalonia.Native/avn.idl

@@ -403,7 +403,7 @@ enum AvnExtendClientAreaChromeHints
 [uuid(809c652e-7396-11d2-9771-00a0c9b4d50c)]
 interface IAvaloniaNativeFactory : IUnknown
 {
-     HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator);
+     HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator, IAvnApplicationEvents* appCb);
      IAvnMacOptions* GetMacOptions();
      HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv);
      HRESULT CreatePopup(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv);
@@ -728,3 +728,9 @@ interface IAvnNativeControlHostTopLevelAttachment : IUnknown
      void HideWithSize(float width, float height);
      void ReleaseChild();
 }
+
+[uuid(6575b5af-f27a-4609-866c-f1f014c20f79)]
+interface IAvnApplicationEvents : IUnknown
+{
+     void FilesOpened (IAvnStringArray* urls);
+}

+ 4 - 0
src/Avalonia.Styling/ApiCompatBaseline.txt

@@ -0,0 +1,4 @@
+Compat issues with assembly Avalonia.Styling:
+InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Styling.IStyleInstance.IsActive' is present in the implementation but not in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Styling.IStyleInstance.IsActive.get()' is present in the implementation but not in the contract.
+Total Issues: 2

+ 21 - 0
src/Avalonia.Styling/Diagnostics/StyleDiagnostics.cs

@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using Avalonia.Styling;
+
+namespace Avalonia.Diagnostics
+{
+    /// <summary>
+    /// Contains information about style related diagnostics of a control.
+    /// </summary>
+    public class StyleDiagnostics
+    {
+        /// <summary>
+        /// Currently applied styles.
+        /// </summary>
+        public IReadOnlyList<IStyleInstance> AppliedStyles { get; }
+
+        public StyleDiagnostics(IReadOnlyList<IStyleInstance> appliedStyles)
+        {
+            AppliedStyles = appliedStyles;
+        }
+    }
+}

+ 17 - 0
src/Avalonia.Styling/Diagnostics/StyledElementExtensions.cs

@@ -0,0 +1,17 @@
+namespace Avalonia.Diagnostics
+{
+    /// <summary>
+    /// Defines diagnostic extensions on <see cref="StyledElement"/>s.
+    /// </summary>
+    public static class StyledElementExtensions
+    {
+        /// <summary>
+        /// Gets a style diagnostics for a <see cref="StyledElement"/>.
+        /// </summary>
+        /// <param name="styledElement">The element.</param>
+        public static StyleDiagnostics GetStyleDiagnostics(this StyledElement styledElement)
+        {
+            return styledElement.GetStyleDiagnosticsInternal();
+        }
+    }
+}

+ 3 - 1
src/Avalonia.Styling/IStyledElement.cs

@@ -1,4 +1,5 @@
 using System;
+using System.ComponentModel;
 using Avalonia.Controls;
 using Avalonia.LogicalTree;
 using Avalonia.Styling;
@@ -10,7 +11,8 @@ namespace Avalonia
         IStyleHost,
         ILogical,
         IResourceHost,
-        IDataContextProvider
+        IDataContextProvider,
+        ISupportInitialize
     {
         /// <summary>
         /// Occurs when the control has finished initialization.

+ 35 - 5
src/Avalonia.Styling/StyledElement.cs

@@ -334,7 +334,16 @@ namespace Avalonia
         {
             if (_initCount == 0 && !_styled)
             {
-                AvaloniaLocator.Current.GetService<IStyler>()?.ApplyStyles(this);
+                try
+                {
+                    BeginBatchUpdate();
+                    AvaloniaLocator.Current.GetService<IStyler>()?.ApplyStyles(this);
+                }
+                finally
+                {
+                    EndBatchUpdate();
+                }
+
                 _styled = true;
             }
 
@@ -356,6 +365,18 @@ namespace Avalonia
             }
         }
 
+        internal StyleDiagnostics GetStyleDiagnosticsInternal()
+        {
+            IReadOnlyList<IStyleInstance>? appliedStyles = _appliedStyles;
+
+            if (appliedStyles is null)
+            {
+                appliedStyles = Array.Empty<IStyleInstance>();
+            }
+
+            return new StyleDiagnostics(appliedStyles);
+        }
+
         /// <inheritdoc/>
         void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
@@ -736,12 +757,21 @@ namespace Avalonia
         {
             if (_appliedStyles is object)
             {
-                foreach (var i in _appliedStyles)
+                BeginBatchUpdate();
+
+                try
                 {
-                    i.Dispose();
-                }
+                    foreach (var i in _appliedStyles)
+                    {
+                        i.Dispose();
+                    }
 
-                _appliedStyles.Clear();
+                    _appliedStyles.Clear();
+                }
+                finally
+                {
+                    EndBatchUpdate();
+                }
             }
 
             _styled = false;

+ 5 - 0
src/Avalonia.Styling/Styling/IStyleInstance.cs

@@ -14,6 +14,11 @@ namespace Avalonia.Styling
         /// </summary>
         IStyle Source { get; }
 
+        /// <summary>
+        /// Gets a value indicating whether this style is active.
+        /// </summary>
+        bool IsActive { get; }
+
         /// <summary>
         /// Instructs the style to start acting upon the control.
         /// </summary>

+ 14 - 4
src/Avalonia.Styling/Styling/PropertySetterBindingInstance.cs

@@ -92,6 +92,7 @@ namespace Avalonia.Styling
         {
             if (!_isActive)
             {
+                _innerSubscription ??= _binding.Observable.Subscribe(_inner);
                 _isActive = true;
                 PublishNext();
             }
@@ -102,6 +103,8 @@ namespace Avalonia.Styling
             if (_isActive)
             {
                 _isActive = false;
+                _innerSubscription?.Dispose();
+                _innerSubscription = null;
                 PublishNext();
             }
         }
@@ -122,9 +125,6 @@ namespace Avalonia.Styling
                 sub.Dispose();
             }
 
-            _innerSubscription?.Dispose();
-            _innerSubscription = null;
-
             base.Dispose();
         }
 
@@ -148,7 +148,17 @@ namespace Avalonia.Styling
 
         protected override void Subscribed()
         {
-            _innerSubscription = _binding.Observable.Subscribe(_inner);
+            if (_isActive)
+            {
+                if (_innerSubscription is null)
+                {
+                    _innerSubscription ??= _binding.Observable.Subscribe(_inner);
+                }
+                else
+                {
+                    PublishNext();
+                }
+            }
         }
 
         protected override void Unsubscribed()

+ 1 - 1
src/Avalonia.Styling/Styling/PropertySetterInstance.cs

@@ -7,7 +7,7 @@ using Avalonia.Reactive;
 namespace Avalonia.Styling
 {
     /// <summary>
-    /// A <see cref="Setter"/> which has been instance on a control.
+    /// A <see cref="Setter"/> which has been instanced on a control.
     /// </summary>
     /// <typeparam name="T">The target property type.</typeparam>
     internal class PropertySetterInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>,

+ 128 - 0
src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs

@@ -0,0 +1,128 @@
+using System;
+using Avalonia.Data;
+using Avalonia.Reactive;
+
+#nullable enable
+
+namespace Avalonia.Styling
+{
+    /// <summary>
+    /// A <see cref="Setter"/> which has been instanced on a control and whose value is lazily
+    /// evaluated.
+    /// </summary>
+    /// <typeparam name="T">The target property type.</typeparam>
+    internal class PropertySetterLazyInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>,
+        ISetterInstance
+    {
+        private readonly IStyleable _target;
+        private readonly StyledPropertyBase<T>? _styledProperty;
+        private readonly DirectPropertyBase<T>? _directProperty;
+        private readonly Func<T> _valueFactory;
+        private BindingValue<T> _value;
+        private IDisposable? _subscription;
+        private bool _isActive;
+
+        public PropertySetterLazyInstance(
+            IStyleable target,
+            StyledPropertyBase<T> property,
+            Func<T> valueFactory)
+        {
+            _target = target;
+            _styledProperty = property;
+            _valueFactory = valueFactory;
+        }
+
+        public PropertySetterLazyInstance(
+            IStyleable target,
+            DirectPropertyBase<T> property,
+            Func<T> valueFactory)
+        {
+            _target = target;
+            _directProperty = property;
+            _valueFactory = valueFactory;
+        }
+
+        public void Start(bool hasActivator)
+        {
+            _isActive = !hasActivator;
+
+            if (_styledProperty is object)
+            {
+                var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style;
+                _subscription = _target.Bind(_styledProperty, this, priority);
+            }
+            else
+            {
+                _subscription = _target.Bind(_directProperty, this);
+            }
+        }
+
+        public void Activate()
+        {
+            if (!_isActive)
+            {
+                _isActive = true;
+                PublishNext();
+            }
+        }
+
+        public void Deactivate()
+        {
+            if (_isActive)
+            {
+                _isActive = false;
+                PublishNext();
+            }
+        }
+
+        public override void Dispose()
+        {
+            if (_subscription is object)
+            {
+                var sub = _subscription;
+                _subscription = null;
+                sub.Dispose();
+            }
+            else if (_isActive)
+            {
+                if (_styledProperty is object)
+                {
+                    _target.ClearValue(_styledProperty);
+                }
+                else
+                {
+                    _target.ClearValue(_directProperty);
+                }
+            }
+
+            base.Dispose();
+        }
+
+        protected override void Subscribed() => PublishNext();
+        protected override void Unsubscribed() { }
+
+        private T GetValue()
+        {
+            if (_value.HasValue)
+            {
+                return _value.Value;
+            }
+
+            _value = _valueFactory();
+            return _value.Value;
+        }
+
+        private void PublishNext()
+        {
+            if (_isActive)
+            {
+                GetValue();
+                PublishNext(_value);
+            }
+            else
+            {
+                PublishNext(default);
+            }
+        }
+    }
+}

+ 15 - 9
src/Avalonia.Styling/Styling/Setter.cs

@@ -68,18 +68,10 @@ namespace Avalonia.Styling
                 throw new InvalidOperationException("Setter.Property must be set.");
             }
 
-            var value = Value;
-
-            if (value is ITemplate template &&
-                !typeof(ITemplate).IsAssignableFrom(Property.PropertyType))
-            {
-                value = template.Build();
-            }
-
             var data = new SetterVisitorData
             {
                 target = target,
-                value = value,
+                value = Value,
             };
 
             Property.Accept(this, ref data);
@@ -97,6 +89,13 @@ namespace Avalonia.Styling
                     property,
                     binding);
             }
+            else if (data.value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(property.PropertyType))
+            {
+                data.result = new PropertySetterLazyInstance<T>(
+                    data.target,
+                    property,
+                    () => (T)template.Build());
+            }
             else
             {
                 data.result = new PropertySetterInstance<T>(
@@ -117,6 +116,13 @@ namespace Avalonia.Styling
                     property,
                     binding);
             }
+            else if (data.value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(property.PropertyType))
+            {
+                data.result = new PropertySetterLazyInstance<T>(
+                    data.target,
+                    property,
+                    () => (T)template.Build());
+            }
             else
             {
                 data.result = new PropertySetterInstance<T>(

+ 5 - 4
src/Avalonia.Styling/Styling/StyleInstance.cs

@@ -17,7 +17,6 @@ namespace Avalonia.Styling
         private readonly List<IDisposable>? _animations;
         private readonly IStyleActivator? _activator;
         private readonly Subject<bool>? _animationTrigger;
-        private bool _active;
 
         public StyleInstance(
             IStyle source,
@@ -29,6 +28,7 @@ namespace Avalonia.Styling
             Source = source ?? throw new ArgumentNullException(nameof(source));
             Target = target ?? throw new ArgumentNullException(nameof(target));
             _activator = activator;
+            IsActive = _activator is null;
 
             if (setters is object)
             {
@@ -56,6 +56,7 @@ namespace Avalonia.Styling
             }
         }
 
+        public bool IsActive { get; private set; }
         public IStyle Source { get; }
         public IStyleable Target { get; }
 
@@ -104,15 +105,15 @@ namespace Avalonia.Styling
 
         private void ActivatorChanged(bool value)
         {
-            if (_active != value)
+            if (IsActive != value)
             {
-                _active = value;
+                IsActive = value;
 
                 _animationTrigger?.OnNext(value);
 
                 if (_setters is object)
                 {
-                    if (_active)
+                    if (IsActive)
                     {
                         foreach (var setter in _setters)
                         {

+ 7 - 7
src/Avalonia.Visuals/Visual.cs

@@ -150,7 +150,7 @@ namespace Avalonia
         public TransformedBounds? TransformedBounds => _transformedBounds;
 
         /// <summary>
-        /// Gets a value indicating whether the control should be clipped to its bounds.
+        /// Gets or sets a value indicating whether the control should be clipped to its bounds.
         /// </summary>
         public bool ClipToBounds
         {
@@ -191,7 +191,7 @@ namespace Avalonia
         }
 
         /// <summary>
-        /// Gets a value indicating whether this control is visible.
+        /// Gets or sets a value indicating whether this control is visible.
         /// </summary>
         public bool IsVisible
         {
@@ -200,7 +200,7 @@ namespace Avalonia
         }
 
         /// <summary>
-        /// Gets the opacity of the control.
+        /// Gets or sets the opacity of the control.
         /// </summary>
         public double Opacity
         {
@@ -209,7 +209,7 @@ namespace Avalonia
         }
 
         /// <summary>
-        /// Gets the opacity mask of the control.
+        /// Gets or sets the opacity mask of the control.
         /// </summary>
         public IBrush OpacityMask
         {
@@ -218,7 +218,7 @@ namespace Avalonia
         }
 
         /// <summary>
-        /// Gets the render transform of the control.
+        /// Gets or sets the render transform of the control.
         /// </summary>
         public ITransform RenderTransform
         {
@@ -227,7 +227,7 @@ namespace Avalonia
         }
 
         /// <summary>
-        /// Gets the transform origin of the control.
+        /// Gets or sets the transform origin of the control.
         /// </summary>
         public RelativePoint RenderTransformOrigin
         {
@@ -236,7 +236,7 @@ namespace Avalonia
         }
 
         /// <summary>
-        /// Gets the Z index of the control.
+        /// Gets or sets the Z index of the control.
         /// </summary>
         /// <remarks>
         /// Controls with a higher <see cref="ZIndex"/> will appear in front of controls with

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

@@ -84,6 +84,7 @@ namespace Avalonia.Win32
         private WindowImpl _parent;        
         private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default;
         private bool _isCloseRequested;
+        private bool _shown;
 
         public WindowImpl()
         {
@@ -565,6 +566,7 @@ namespace Avalonia.Win32
         public void Hide()
         {
             UnmanagedMethods.ShowWindow(_hwnd, ShowWindowCommand.Hide);
+            _shown = false;
         }
 
         public virtual void Show(bool activate)
@@ -871,6 +873,11 @@ namespace Avalonia.Win32
 
         private void ExtendClientArea()
         {
+            if (!_shown)
+            {
+                return;
+            }
+            
             if (DwmIsCompositionEnabled(out bool compositionEnabled) < 0 || !compositionEnabled)
             {
                 _isClientAreaExtended = false;
@@ -916,6 +923,13 @@ namespace Avalonia.Win32
 
         private void ShowWindow(WindowState state, bool activate)
         {
+            _shown = true;
+            
+            if (_isClientAreaExtended)
+            {
+                ExtendClientArea();
+            }
+            
             ShowWindowCommand? command;
 
             var newWindowProperties = _windowProperties;

+ 1 - 0
tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj

@@ -3,6 +3,7 @@
     <TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
     <OutputType>Library</OutputType>
     <IsTestProject>true</IsTestProject>
+    <LangVersion>latest</LangVersion>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\UnitTests.NetFX.props" />

+ 494 - 0
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_BatchUpdate.cs

@@ -0,0 +1,494 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using System.Text;
+using Avalonia.Data;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests
+{
+    public class AvaloniaObjectTests_BatchUpdate
+    {
+        [Fact]
+        public void SetValue_Should_Not_Raise_Property_Changes_During_Batch_Update()
+        {
+            var target = new TestClass();
+            var raised = new List<string>();
+
+            target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
+            target.BeginBatchUpdate();
+            target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
+
+            Assert.Empty(raised);
+        }
+
+        [Fact]
+        public void Binding_Should_Not_Raise_Property_Changes_During_Batch_Update()
+        {
+            var target = new TestClass();
+            var observable = new TestObservable<string>("foo");
+            var raised = new List<string>();
+
+            target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
+            target.BeginBatchUpdate();
+            target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
+
+            Assert.Empty(raised);
+        }
+
+        [Fact]
+        public void Binding_Completion_Should_Not_Raise_Property_Changes_During_Batch_Update()
+        {
+            var target = new TestClass();
+            var observable = new TestObservable<string>("foo");
+            var raised = new List<string>();
+
+            target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
+            target.GetObservable(TestClass.FooProperty).Skip(1).Subscribe(x => raised.Add(x));
+            target.BeginBatchUpdate();
+            observable.OnCompleted();
+
+            Assert.Empty(raised);
+        }
+
+        [Fact]
+        public void SetValue_Change_Should_Be_Raised_After_Batch_Update_1()
+        {
+            var target = new TestClass();
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
+            target.EndBatchUpdate();
+
+            Assert.Equal(1, raised.Count);
+            Assert.Equal("foo", target.Foo);
+            Assert.Null(raised[0].OldValue);
+            Assert.Equal("foo", raised[0].NewValue);
+        }
+
+        [Fact]
+        public void SetValue_Change_Should_Be_Raised_After_Batch_Update_2()
+        {
+            var target = new TestClass();
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.SetValue(TestClass.FooProperty, "bar", BindingPriority.LocalValue);
+            target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue);
+            target.EndBatchUpdate();
+
+            Assert.Equal(1, raised.Count);
+            Assert.Equal("baz", target.Foo);
+        }
+
+        [Fact]
+        public void SetValue_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update()
+        {
+            var target = new TestClass();
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
+            target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue);
+            target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue);
+            target.EndBatchUpdate();
+
+            Assert.Equal(2, raised.Count);
+            Assert.Equal(TestClass.BarProperty, raised[0].Property);
+            Assert.Equal(TestClass.FooProperty, raised[1].Property);
+            Assert.Equal("baz", target.Foo);
+            Assert.Equal("bar", target.Bar);
+        }
+
+        [Fact]
+        public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_1()
+        {
+            var target = new TestClass();
+            var observable = new TestObservable<string>("baz");
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
+            target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue);
+            target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
+            target.EndBatchUpdate();
+
+            Assert.Equal(2, raised.Count);
+            Assert.Equal(TestClass.BarProperty, raised[0].Property);
+            Assert.Equal(TestClass.FooProperty, raised[1].Property);
+            Assert.Equal("baz", target.Foo);
+            Assert.Equal("bar", target.Bar);
+        }
+
+        [Fact]
+        public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_2()
+        {
+            var target = new TestClass();
+            var observable = new TestObservable<string>("foo");
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
+            target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue);
+            target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue);
+            target.EndBatchUpdate();
+
+            Assert.Equal(2, raised.Count);
+            Assert.Equal(TestClass.BarProperty, raised[0].Property);
+            Assert.Equal(TestClass.FooProperty, raised[1].Property);
+            Assert.Equal("baz", target.Foo);
+            Assert.Equal("bar", target.Bar);
+        }
+
+        [Fact]
+        public void SetValue_And_Binding_Changes_Should_Be_Raised_In_Correct_Order_After_Batch_Update_3()
+        {
+            var target = new TestClass();
+            var observable1 = new TestObservable<string>("foo");
+            var observable2 = new TestObservable<string>("qux");
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue);
+            target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
+            target.SetValue(TestClass.BarProperty, "bar", BindingPriority.LocalValue);
+            target.SetValue(TestClass.FooProperty, "baz", BindingPriority.LocalValue);
+            target.EndBatchUpdate();
+
+            Assert.Equal(2, raised.Count);
+            Assert.Equal(TestClass.BarProperty, raised[0].Property);
+            Assert.Equal(TestClass.FooProperty, raised[1].Property);
+            Assert.Equal("baz", target.Foo);
+            Assert.Equal("bar", target.Bar);
+        }
+
+        [Fact]
+        public void Binding_Change_Should_Be_Raised_After_Batch_Update_1()
+        {
+            var target = new TestClass();
+            var observable = new TestObservable<string>("foo");
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
+            target.EndBatchUpdate();
+
+            Assert.Equal(1, raised.Count);
+            Assert.Equal("foo", target.Foo);
+            Assert.Null(raised[0].OldValue);
+            Assert.Equal("foo", raised[0].NewValue);
+        }
+
+        [Fact]
+        public void Binding_Change_Should_Be_Raised_After_Batch_Update_2()
+        {
+            var target = new TestClass();
+            var observable1 = new TestObservable<string>("bar");
+            var observable2 = new TestObservable<string>("baz");
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.SetValue(TestClass.FooProperty, "foo", BindingPriority.LocalValue);
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
+            target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue);
+            target.EndBatchUpdate();
+
+            Assert.Equal(1, raised.Count);
+            Assert.Equal("baz", target.Foo);
+            Assert.Equal("foo", raised[0].OldValue);
+            Assert.Equal("baz", raised[0].NewValue);
+        }
+
+        [Fact]
+        public void Binding_Completion_Should_Be_Raised_After_Batch_Update()
+        {
+            var target = new TestClass();
+            var observable = new TestObservable<string>("foo");
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.Bind(TestClass.FooProperty, observable, BindingPriority.LocalValue);
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            observable.OnCompleted();
+            target.EndBatchUpdate();
+
+            Assert.Equal(1, raised.Count);
+            Assert.Null(target.Foo);
+            Assert.Equal("foo", raised[0].OldValue);
+            Assert.Null(raised[0].NewValue);
+            Assert.Equal(BindingPriority.Unset, raised[0].Priority);
+        }
+
+        [Fact]
+        public void ClearValue_Change_Should_Be_Raised_After_Batch_Update_1()
+        {
+            var target = new TestClass();
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.Foo = "foo";
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.ClearValue(TestClass.FooProperty);
+            target.EndBatchUpdate();
+
+            Assert.Equal(1, raised.Count);
+            Assert.Null(target.Foo);
+            Assert.Equal("foo", raised[0].OldValue);
+            Assert.Null(raised[0].NewValue);
+            Assert.Equal(BindingPriority.Unset, raised[0].Priority);
+        }
+
+        [Fact]
+        public void Bindings_Should_Be_Subscribed_Before_Batch_Update()
+        {
+            var target = new TestClass();
+            var observable1 = new TestObservable<string>("foo");
+            var observable2 = new TestObservable<string>("bar");
+
+            target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
+            target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue);
+
+            Assert.Equal(1, observable1.SubscribeCount);
+            Assert.Equal(1, observable2.SubscribeCount);
+        }
+
+        [Fact]
+        public void Non_Active_Binding_Should_Not_Be_Subscribed_Before_Batch_Update()
+        {
+            var target = new TestClass();
+            var observable1 = new TestObservable<string>("foo");
+            var observable2 = new TestObservable<string>("bar");
+
+            target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
+            target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style);
+
+            Assert.Equal(1, observable1.SubscribeCount);
+            Assert.Equal(0, observable2.SubscribeCount);
+        }
+
+        [Fact]
+        public void LocalValue_Bindings_Should_Be_Subscribed_During_Batch_Update()
+        {
+            var target = new TestClass();
+            var observable1 = new TestObservable<string>("foo");
+            var observable2 = new TestObservable<string>("bar");
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            // We need to subscribe to LocalValue bindings even if we've got a batch operation
+            // in progress because otherwise we don't know whether the binding or a subsequent
+            // SetValue with local priority will win. Notifications however shouldn't be sent.
+            target.BeginBatchUpdate();
+            target.Bind(TestClass.FooProperty, observable1, BindingPriority.LocalValue);
+            target.Bind(TestClass.FooProperty, observable2, BindingPriority.LocalValue);
+
+            Assert.Equal(1, observable1.SubscribeCount);
+            Assert.Equal(1, observable2.SubscribeCount);
+            Assert.Empty(raised);
+        }
+
+        [Fact]
+        public void Style_Bindings_Should_Not_Be_Subscribed_During_Batch_Update()
+        {
+            var target = new TestClass();
+            var observable1 = new TestObservable<string>("foo");
+            var observable2 = new TestObservable<string>("bar");
+
+            target.BeginBatchUpdate();
+            target.Bind(TestClass.FooProperty, observable1, BindingPriority.Style);
+            target.Bind(TestClass.FooProperty, observable2, BindingPriority.StyleTrigger);
+
+            Assert.Equal(0, observable1.SubscribeCount);
+            Assert.Equal(0, observable2.SubscribeCount);
+        }
+
+        [Fact]
+        public void Active_Style_Binding_Should_Be_Subscribed_After_Batch_Uppdate_1()
+        {
+            var target = new TestClass();
+            var observable1 = new TestObservable<string>("foo");
+            var observable2 = new TestObservable<string>("bar");
+
+            target.BeginBatchUpdate();
+            target.Bind(TestClass.FooProperty, observable1, BindingPriority.Style);
+            target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style);
+            target.EndBatchUpdate();
+
+            Assert.Equal(0, observable1.SubscribeCount);
+            Assert.Equal(1, observable2.SubscribeCount);
+        }
+
+        [Fact]
+        public void Active_Style_Binding_Should_Be_Subscribed_After_Batch_Uppdate_2()
+        {
+            var target = new TestClass();
+            var observable1 = new TestObservable<string>("foo");
+            var observable2 = new TestObservable<string>("bar");
+
+            target.BeginBatchUpdate();
+            target.Bind(TestClass.FooProperty, observable1, BindingPriority.StyleTrigger);
+            target.Bind(TestClass.FooProperty, observable2, BindingPriority.Style);
+            target.EndBatchUpdate();
+
+            Assert.Equal(1, observable1.SubscribeCount);
+            Assert.Equal(0, observable2.SubscribeCount);
+        }
+
+        [Fact]
+        public void Change_Can_Be_Triggered_By_Ending_Batch_Update_1()
+        {
+            var target = new TestClass();
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.Foo = "foo";
+
+            target.PropertyChanged += (s, e) =>
+            {
+                if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo")
+                    target.Bar = "bar";
+            };
+
+            target.EndBatchUpdate();
+
+            Assert.Equal("foo", target.Foo);
+            Assert.Equal("bar", target.Bar);
+            Assert.Equal(2, raised.Count);
+            Assert.Equal(TestClass.FooProperty, raised[0].Property);
+            Assert.Equal(TestClass.BarProperty, raised[1].Property);
+        }
+
+        [Fact]
+        public void Change_Can_Be_Triggered_By_Ending_Batch_Update_2()
+        {
+            var target = new TestClass();
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.Foo = "foo";
+            target.Bar = "baz";
+
+            target.PropertyChanged += (s, e) =>
+            {
+                if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo")
+                    target.Bar = "bar";
+            };
+
+            target.EndBatchUpdate();
+
+            Assert.Equal("foo", target.Foo);
+            Assert.Equal("bar", target.Bar);
+            Assert.Equal(2, raised.Count);
+        }
+
+        [Fact]
+        public void Batch_Update_Can_Be_Triggered_By_Ending_Batch_Update()
+        {
+            var target = new TestClass();
+            var raised = new List<AvaloniaPropertyChangedEventArgs>();
+
+            target.PropertyChanged += (s, e) => raised.Add(e);
+
+            target.BeginBatchUpdate();
+            target.Foo = "foo";
+            target.Bar = "baz";
+
+            // Simulates the following scenario:
+            // - A control is added to the logical tree
+            // - A batch update is started to apply styles
+            // - Ending the batch update triggers something which removes the control from the logical tree
+            // - A new batch update is started to detach styles
+            target.PropertyChanged += (s, e) =>
+            {
+                if (e.Property == TestClass.FooProperty && (string)e.NewValue == "foo")
+                {
+                    target.BeginBatchUpdate();
+                    target.ClearValue(TestClass.FooProperty);
+                    target.ClearValue(TestClass.BarProperty);
+                    target.EndBatchUpdate();
+                }
+            };
+
+            target.EndBatchUpdate();
+
+            Assert.Null(target.Foo);
+            Assert.Null(target.Bar);
+            Assert.Equal(2, raised.Count);
+            Assert.Equal(TestClass.FooProperty, raised[0].Property);
+            Assert.Null(raised[0].OldValue);
+            Assert.Equal("foo", raised[0].NewValue);
+            Assert.Equal(TestClass.FooProperty, raised[1].Property);
+            Assert.Equal("foo", raised[1].OldValue);
+            Assert.Null(raised[1].NewValue);
+        }
+
+        public class TestClass : AvaloniaObject
+        {
+            public static readonly StyledProperty<string> FooProperty =
+                AvaloniaProperty.Register<TestClass, string>(nameof(Foo));
+
+            public static readonly StyledProperty<string> BarProperty =
+                AvaloniaProperty.Register<TestClass, string>(nameof(Bar));
+
+            public string Foo
+            {
+                get => GetValue(FooProperty);
+                set => SetValue(FooProperty, value);
+            }
+
+            public string Bar
+            {
+                get => GetValue(BarProperty);
+                set => SetValue(BarProperty, value);
+            }
+        }
+
+        public class TestObservable<T> : ObservableBase<BindingValue<T>>
+        {
+            private readonly T _value;
+            private IObserver<BindingValue<T>> _observer;
+
+            public TestObservable(T value) => _value = value;
+
+            public int SubscribeCount { get; private set; }
+
+            public void OnCompleted() => _observer.OnCompleted();
+            public void OnError(Exception e) => _observer.OnError(e);
+
+            protected override IDisposable SubscribeCore(IObserver<BindingValue<T>> observer)
+            {
+                ++SubscribeCount;
+                _observer = observer;
+                observer.OnNext(_value);
+                return Disposable.Empty;
+            }
+        }
+    }
+}

+ 1 - 0
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@@ -12,6 +12,7 @@
     <ProjectReference Include="..\..\src\Avalonia.Input\Avalonia.Input.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Interactivity\Avalonia.Interactivity.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Layout\Avalonia.Layout.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Visuals\Avalonia.Visuals.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Themes.Default\Avalonia.Themes.Default.csproj" />

+ 74 - 0
tests/Avalonia.Benchmarks/Themes/FluentBenchmark.cs

@@ -0,0 +1,74 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.Markup.Xaml.Styling;
+using Avalonia.Platform;
+using Avalonia.Shared.PlatformSupport;
+using Avalonia.Styling;
+using Avalonia.UnitTests;
+using BenchmarkDotNet.Attributes;
+using Moq;
+
+namespace Avalonia.Benchmarks.Themes
+{
+    [MemoryDiagnoser]
+    public class FluentBenchmark
+    {
+        private readonly IDisposable _app;
+        private readonly TestRoot _root;
+
+        public FluentBenchmark()
+        {
+            _app = CreateApp();
+            _root = new TestRoot(true, null)
+            {
+                Renderer = new NullRenderer()
+            };
+
+            _root.LayoutManager.ExecuteInitialLayoutPass();
+        }
+
+        public void Dispose()
+        {
+            _app.Dispose();
+        }
+
+        [Benchmark]
+        public void RepeatButton()
+        {
+            var button = new RepeatButton();
+            _root.Child = button;
+            _root.LayoutManager.ExecuteLayoutPass();
+        }
+
+        private static IDisposable CreateApp()
+        {
+            var services = new TestServices(
+                assetLoader: new AssetLoader(),
+                globalClock: new MockGlobalClock(),
+                platform: new AppBuilder().RuntimePlatform,
+                renderInterface: new MockPlatformRenderInterface(),
+                standardCursorFactory: Mock.Of<ICursorFactory>(),
+                styler: new Styler(),
+                theme: () => LoadFluentTheme(),
+                threadingInterface: new NullThreadingPlatform(),
+                fontManagerImpl: new MockFontManagerImpl(),
+                textShaperImpl: new MockTextShaperImpl(),
+                windowingPlatform: new MockWindowingPlatform());
+
+            return UnitTestApplication.Start(services);
+        }
+
+        private static Styles LoadFluentTheme()
+        {
+            AssetLoader.RegisterResUriParsers();
+            return new Styles
+            {
+                new Avalonia.Themes.Fluent.FluentTheme(new Uri("avares://Avalonia.Benchmarks"))
+                {
+
+                }
+            };
+        }
+    }
+}

+ 36 - 0
tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

@@ -1,7 +1,9 @@
+using System.Linq;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Shapes;
 using Avalonia.Controls.Templates;
+using Avalonia.Input;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.UnitTests;
@@ -137,5 +139,39 @@ namespace Avalonia.Controls.UnitTests
                 Assert.True(other.IsFocused);
             }
         }
+
+        [Theory]
+        [InlineData(-1, 2, "c", "A item", "B item", "C item")]
+        [InlineData(0, 1, "b", "A item", "B item", "C item")]
+        [InlineData(2, 2, "x", "A item", "B item", "C item")]
+        public void TextSearch_Should_Have_Expected_SelectedIndex(
+            int initialSelectedIndex,
+            int expectedSelectedIndex,
+            string searchTerm,
+            params string[] items)
+        {
+            using (UnitTestApplication.Start(TestServices.MockThreadingInterface))
+            {
+                var target = new ComboBox
+                {
+                    Template = GetTemplate(),                    
+                    Items = items.Select(x => new ComboBoxItem { Content = x })
+                };
+
+                target.ApplyTemplate();
+                target.Presenter.ApplyTemplate();
+                target.SelectedIndex = initialSelectedIndex;
+
+                var args = new TextInputEventArgs
+                {
+                    Text = searchTerm,
+                    RoutedEvent = InputElement.TextInputEvent
+                };
+
+                target.RaiseEvent(args);
+
+                Assert.Equal(expectedSelectedIndex, target.SelectedIndex);
+            }
+        }
     }
 }

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

@@ -1,7 +1,6 @@
 using System;
 using System.Collections.Generic;
 using System.Threading.Tasks;
-using Avalonia.Layout;
 using Avalonia.Platform;
 using Avalonia.Rendering;
 using Avalonia.UnitTests;
@@ -137,6 +136,129 @@ namespace Avalonia.Controls.UnitTests
                 Assert.Equal(1, count);
             }
         }
+        
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public void Child_windows_should_be_closed_before_parent(bool programaticClose)
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var window = new Window();
+                var child = new Window();
+
+                int count = 0;
+                int windowClosing = 0;
+                int childClosing = 0;
+                int windowClosed = 0;
+                int childClosed = 0;
+
+                window.Closing += (sender, e) =>
+                {
+                    count++;
+                    windowClosing = count;
+                };
+                
+                child.Closing += (sender, e) =>
+                {
+                    count++;
+                    childClosing = count;
+                };
+                
+                window.Closed += (sender, e) =>
+                {
+                    count++;
+                    windowClosed = count;
+                };
+                
+                child.Closed += (sender, e) =>
+                {
+                    count++;
+                    childClosed = count;
+                };
+
+                window.Show();
+                child.Show(window);
+
+                if (programaticClose)
+                {
+                    window.Close();
+                }
+                else
+                {
+                    var cancel = window.PlatformImpl.Closing();
+
+                    Assert.Equal(false, cancel);
+                }
+
+                Assert.Equal(2, windowClosing);
+                Assert.Equal(1, childClosing);
+                Assert.Equal(4, windowClosed);
+                Assert.Equal(3, childClosed);
+            }
+        }
+        
+        [Theory]
+        [InlineData(true)]
+        [InlineData(false)]
+        public void Child_windows_must_not_close_before_parent_has_chance_to_Cancel_OSCloseButton(bool programaticClose)
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var window = new Window();
+                var child = new Window();
+
+                int count = 0;
+                int windowClosing = 0;
+                int childClosing = 0;
+                int windowClosed = 0;
+                int childClosed = 0;
+
+                window.Closing += (sender, e) =>
+                {
+                    count++;
+                    windowClosing = count;
+                    e.Cancel = true;
+                };
+                
+                child.Closing += (sender, e) =>
+                {
+                    count++;
+                    childClosing = count;
+                };
+                
+                window.Closed += (sender, e) =>
+                {
+                    count++;
+                    windowClosed = count;
+                };
+                
+                child.Closed += (sender, e) =>
+                {
+                    count++;
+                    childClosed = count;
+                };
+
+                window.Show();
+                child.Show(window);
+                
+                if (programaticClose)
+                {
+                    window.Close();
+                }
+                else
+                {
+                    var cancel = window.PlatformImpl.Closing();
+
+                    Assert.Equal(true, cancel);
+                }
+
+                Assert.Equal(2, windowClosing);
+                Assert.Equal(1, childClosing);
+                Assert.Equal(0, windowClosed);
+                Assert.Equal(0, childClosed);
+            }
+        }
 
         [Fact]
         public void Showing_Should_Start_Renderer()

+ 4 - 2
tests/Avalonia.Markup.UnitTests/Parsers/ExpressionObserverBuilderTests_AttachedProperty.cs

@@ -117,9 +117,11 @@ namespace Avalonia.Markup.UnitTests.Parsers
             var result = run();
             result.Item1.Subscribe(x => { });
 
-            GC.Collect();
+            // Mono trickery
+            GC.Collect(2);
             GC.WaitForPendingFinalizers();
-            GC.Collect();
+            GC.WaitForPendingFinalizers();
+            GC.Collect(2);
 
             Assert.Null(result.Item2.Target);
         }

+ 1 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj

@@ -3,6 +3,7 @@
     <TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
     <OutputType>Library</OutputType>
     <IsTestProject>true</IsTestProject>
+    <LangVersion>latest</LangVersion>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\UnitTests.NetFX.props" />

+ 96 - 0
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Linq;
 using Avalonia.Controls;
 using Avalonia.Controls.Presenters;
@@ -809,6 +810,82 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
             Assert.Equal(0xff506070, brush.Color.ToUint32());
         }
 
+        [Fact]
+        public void Resource_In_Non_Matching_Style_Is_Not_Resolved()
+        {
+            using var app = StyledWindow();
+
+            var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+             xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions'>
+    <Window.Resources>
+        <ResourceDictionary>
+            <ResourceDictionary.MergedDictionaries>
+                <local:TrackingResourceProvider/>
+            </ResourceDictionary.MergedDictionaries>
+        </ResourceDictionary>
+    </Window.Resources>
+
+    <Window.Styles>
+        <Style Selector='Border.nomatch'>
+            <Setter Property='Tag' Value='{DynamicResource foo}'/>
+        </Style>
+        <Style Selector='Border'>
+            <Setter Property='Tag' Value='{DynamicResource bar}'/>
+        </Style>
+    </Window.Styles>
+
+    <Border Name='border'/>
+</Window>";
+
+            var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+            var border = window.FindControl<Border>("border");
+
+            Assert.Equal("bar", border.Tag);
+
+            var resourceProvider = (TrackingResourceProvider)window.Resources.MergedDictionaries[0];
+            Assert.Equal(new[] { "bar" }, resourceProvider.RequestedResources);
+        }
+
+        [Fact]
+        public void Resource_In_Non_Active_Style_Is_Not_Resolved()
+        {
+            using var app = StyledWindow();
+
+            var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+             xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions'>
+    <Window.Resources>
+        <ResourceDictionary>
+            <ResourceDictionary.MergedDictionaries>
+                <local:TrackingResourceProvider/>
+            </ResourceDictionary.MergedDictionaries>
+        </ResourceDictionary>
+    </Window.Resources>
+
+    <Window.Styles>
+        <Style Selector='Border'>
+            <Setter Property='Tag' Value='{DynamicResource foo}'/>
+        </Style>
+        <Style Selector='Border'>
+            <Setter Property='Tag' Value='{DynamicResource bar}'/>
+        </Style>
+    </Window.Styles>
+
+    <Border Name='border'/>
+</Window>";
+
+            var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+            var border = window.FindControl<Border>("border");
+
+            Assert.Equal("bar", border.Tag);
+
+            var resourceProvider = (TrackingResourceProvider)window.Resources.MergedDictionaries[0];
+            Assert.Equal(new[] { "bar" }, resourceProvider.RequestedResources);
+        }
+
         private IDisposable StyledWindow(params (string, string)[] assets)
         {
             var services = TestServices.StyledWindow.With(
@@ -839,4 +916,23 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
             };
         }
     }
+
+    public class TrackingResourceProvider : IResourceProvider
+    {
+        public IResourceHost Owner { get; private set; }
+        public bool HasResources => true;
+        public List<object> RequestedResources { get; } = new List<object>();
+
+        public event EventHandler OwnerChanged;
+
+        public void AddOwner(IResourceHost owner) => Owner = owner;
+        public void RemoveOwner(IResourceHost owner) => Owner = null;
+
+        public bool TryGetResource(object key, out object value)
+        {
+            RequestedResources.Add(key);
+            value = key;
+            return true;
+        }
+    }
 }

+ 1 - 0
tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj

@@ -4,6 +4,7 @@
     <OutputType>Library</OutputType>
     <NoWarn>CS0067</NoWarn>
     <IsTestProject>true</IsTestProject>
+    <LangVersion>latest</LangVersion>
   </PropertyGroup>
   <Import Project="..\..\build\UnitTests.NetCore.targets" />
   <Import Project="..\..\build\UnitTests.NetFX.props" />

+ 282 - 0
tests/Avalonia.Styling.UnitTests/StyleTests.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using Avalonia.Controls;
+using Avalonia.Controls.Templates;
 using Avalonia.Data;
 using Avalonia.UnitTests;
 using Moq;
@@ -217,6 +218,278 @@ namespace Avalonia.Styling.UnitTests
             Assert.Equal(new[] { "foodefault", "Bar" }, values);
         }
 
+        [Fact]
+        public void Inactive_Values_Should_Not_Be_Made_Active_During_Style_Attach()
+        {
+            using var app = UnitTestApplication.Start(TestServices.RealStyler);
+
+            var root = new TestRoot
+            {
+                Styles =
+                {
+                    new Style(x => x.OfType<Class1>())
+                    {
+                        Setters =
+                        {
+                            new Setter(Class1.FooProperty, "Foo"),
+                        },
+                    },
+
+                    new Style(x => x.OfType<Class1>())
+                    {
+                        Setters =
+                        {
+                            new Setter(Class1.FooProperty, "Bar"),
+                        },
+                    }
+                }
+            };
+
+            var values = new List<string>();
+            var target = new Class1();
+
+            target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x));
+            root.Child = target;
+
+            Assert.Equal(new[] { "foodefault", "Bar" }, values);
+        }
+
+        [Fact]
+        public void Inactive_Bindings_Should_Not_Be_Made_Active_During_Style_Attach()
+        {
+            using var app = UnitTestApplication.Start(TestServices.RealStyler);
+
+            var root = new TestRoot
+            {
+                Styles =
+                {
+                    new Style(x => x.OfType<Class1>())
+                    {
+                        Setters =
+                        {
+                            new Setter(Class1.FooProperty, new Binding("Foo")),
+                        },
+                    },
+
+                    new Style(x => x.OfType<Class1>())
+                    {
+                        Setters =
+                        {
+                            new Setter(Class1.FooProperty, new Binding("Bar")),
+                        },
+                    }
+                }
+            };
+
+            var values = new List<string>();
+            var target = new Class1
+            {
+                DataContext = new
+                {
+                    Foo = "Foo",
+                    Bar = "Bar",
+                }
+            };
+
+            target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x));
+            root.Child = target;
+
+            Assert.Equal(new[] { "foodefault", "Bar" }, values);
+        }
+
+        [Fact]
+        public void Inactive_Values_Should_Not_Be_Made_Active_During_Style_Detach()
+        {
+            using var app = UnitTestApplication.Start(TestServices.RealStyler);
+
+            var root = new TestRoot
+            {
+                Styles =
+                {
+                    new Style(x => x.OfType<Class1>())
+                    {
+                        Setters =
+                        {
+                            new Setter(Class1.FooProperty, "Foo"),
+                        },
+                    },
+
+                    new Style(x => x.OfType<Class1>())
+                    {
+                        Setters =
+                        {
+                            new Setter(Class1.FooProperty, "Bar"),
+                        },
+                    }
+                }
+            };
+
+            var target = new Class1();
+            root.Child = target;
+
+            var values = new List<string>();
+            target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x));
+            root.Child = null;
+
+            Assert.Equal(new[] { "Bar", "foodefault" }, values);
+        }
+
+        [Fact]
+        public void Inactive_Values_Should_Not_Be_Made_Active_During_Style_Detach_2()
+        {
+            using var app = UnitTestApplication.Start(TestServices.RealStyler);
+
+            var root = new TestRoot
+            {
+                Styles =
+                {
+                    new Style(x => x.OfType<Class1>().Class("foo"))
+                    {
+                        Setters =
+                        {
+                            new Setter(Class1.FooProperty, "Foo"),
+                        },
+                    },
+
+                    new Style(x => x.OfType<Class1>())
+                    {
+                        Setters =
+                        {
+                            new Setter(Class1.FooProperty, "Bar"),
+                        },
+                    }
+                }
+            };
+
+            var target = new Class1 { Classes = { "foo" } };
+            root.Child = target;
+
+            var values = new List<string>();
+            target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x));
+            root.Child = null;
+
+            Assert.Equal(new[] { "Foo", "foodefault" }, values);
+        }
+
+        [Fact]
+        public void Inactive_Bindings_Should_Not_Be_Made_Active_During_Style_Detach()
+        {
+            using var app = UnitTestApplication.Start(TestServices.RealStyler);
+
+            var root = new TestRoot
+            {
+                Styles =
+                {
+                    new Style(x => x.OfType<Class1>())
+                    {
+                        Setters =
+                        {
+                            new Setter(Class1.FooProperty, new Binding("Foo")),
+                        },
+                    },
+
+                    new Style(x => x.OfType<Class1>())
+                    {
+                        Setters =
+                        {
+                            new Setter(Class1.FooProperty, new Binding("Bar")),
+                        },
+                    }
+                }
+            };
+
+            var target = new Class1
+            {
+                DataContext = new
+                {
+                    Foo = "Foo",
+                    Bar = "Bar",
+                }
+            };
+
+            root.Child = target;
+
+            var values = new List<string>();
+            target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x));
+            root.Child = null;
+
+            Assert.Equal(new[] { "Bar", "foodefault" }, values);
+        }
+
+        [Fact]
+        public void Template_In_Non_Matching_Style_Is_Not_Built()
+        {
+            var instantiationCount = 0;
+            var template = new FuncTemplate<Class1>(() =>
+            {
+                ++instantiationCount;
+                return new Class1();
+            });
+
+            Styles styles = new Styles
+            {
+                new Style(x => x.OfType<Class1>().Class("foo"))
+                {
+                    Setters =
+                    {
+                        new Setter(Class1.ChildProperty, template),
+                    },
+                },
+
+                new Style(x => x.OfType<Class1>())
+                {
+                    Setters =
+                    {
+                        new Setter(Class1.ChildProperty, template),
+                    },
+                }
+            };
+
+            var target = new Class1();
+            styles.TryAttach(target, null);
+
+            Assert.NotNull(target.Child);
+            Assert.Equal(1, instantiationCount);
+        }
+
+        [Fact]
+        public void Template_In_Inactive_Style_Is_Not_Built()
+        {
+            var instantiationCount = 0;
+            var template = new FuncTemplate<Class1>(() =>
+            {
+                ++instantiationCount;
+                return new Class1();
+            });
+
+            Styles styles = new Styles
+            {
+                new Style(x => x.OfType<Class1>())
+                {
+                    Setters =
+                    {
+                        new Setter(Class1.ChildProperty, template),
+                    },
+                },
+
+                new Style(x => x.OfType<Class1>())
+                {
+                    Setters =
+                    {
+                        new Setter(Class1.ChildProperty, template),
+                    },
+                }
+            };
+
+            var target = new Class1();
+            target.BeginBatchUpdate();
+            styles.TryAttach(target, null);
+            target.EndBatchUpdate();
+
+            Assert.NotNull(target.Child);
+            Assert.Equal(1, instantiationCount);
+        }
+
         [Fact]
         public void Style_Should_Detach_When_Control_Removed_From_Logical_Tree()
         {
@@ -453,12 +726,21 @@ namespace Avalonia.Styling.UnitTests
             public static readonly StyledProperty<string> FooProperty =
                 AvaloniaProperty.Register<Class1, string>(nameof(Foo), "foodefault");
 
+            public static readonly StyledProperty<Class1> ChildProperty =
+                AvaloniaProperty.Register<Class1, Class1>(nameof(Child));
+
             public string Foo
             {
                 get { return GetValue(FooProperty); }
                 set { SetValue(FooProperty, value); }
             }
 
+            public Class1 Child
+            {
+                get => GetValue(ChildProperty);
+                set => SetValue(ChildProperty, value);
+            }
+
             protected override Size MeasureOverride(Size availableSize)
             {
                 throw new NotImplementedException();

+ 1 - 1
tests/Avalonia.UnitTests/UnitTestApplication.cs

@@ -25,6 +25,7 @@ namespace Avalonia.UnitTests
         public UnitTestApplication(TestServices services)
         {
             _services = services ?? new TestServices();
+            AvaloniaLocator.CurrentMutable.BindToSelf<Application>(this);
             RegisterServices();
         }
 
@@ -36,7 +37,6 @@ namespace Avalonia.UnitTests
         {
             var scope = AvaloniaLocator.EnterScope();
             var app = new UnitTestApplication(services);
-            AvaloniaLocator.CurrentMutable.BindToSelf<Application>(app);
             Dispatcher.UIThread.UpdateServices();
             return Disposable.Create(() =>
             {