Browse Source

Merge branch 'master' into feature/new-devtools

Steven Kirk 5 years ago
parent
commit
03e0396ff3
65 changed files with 1264 additions and 605 deletions
  1. 3 1
      readme.md
  2. 6 2
      samples/ControlCatalog/Pages/ProgressBarPage.xaml
  3. 7 2
      src/Avalonia.Base/AvaloniaObject.cs
  4. 0 39
      src/Avalonia.Base/AvaloniaProperty.cs
  5. 0 45
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  6. 0 15
      src/Avalonia.Base/DirectPropertyBase.cs
  7. 2 2
      src/Avalonia.Base/PropertyStore/BindingEntry.cs
  8. 4 1
      src/Avalonia.Base/PropertyStore/IValueSink.cs
  9. 4 1
      src/Avalonia.Base/PropertyStore/PriorityValue.cs
  10. 0 15
      src/Avalonia.Base/StyledPropertyBase.cs
  11. 7 2
      src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs
  12. 6 2
      src/Avalonia.Base/ValueStore.cs
  13. 10 3
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  14. 2 2
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  15. 12 9
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  16. 1 1
      src/Avalonia.Controls/AutoCompleteBox.cs
  17. 24 1
      src/Avalonia.Controls/Button.cs
  18. 26 2
      src/Avalonia.Controls/ButtonSpinner.cs
  19. 6 6
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  20. 1 1
      src/Avalonia.Controls/Calendar/DatePicker.cs
  21. 2 2
      src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs
  22. 0 20
      src/Avalonia.Controls/ControlExtensions.cs
  23. 33 8
      src/Avalonia.Controls/Expander.cs
  24. 6 38
      src/Avalonia.Controls/ItemsControl.cs
  25. 25 5
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  26. 173 110
      src/Avalonia.Controls/Primitives/Popup.cs
  27. 21 3
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  28. 4 4
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  29. 18 6
      src/Avalonia.Controls/Primitives/ToggleButton.cs
  30. 26 2
      src/Avalonia.Controls/Primitives/Track.cs
  31. 49 4
      src/Avalonia.Controls/ProgressBar.cs
  32. 2 2
      src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs
  33. 16 0
      src/Avalonia.Controls/ScrollViewer.cs
  34. 3 3
      src/Avalonia.Controls/SelectionChangedEventArgs.cs
  35. 22 2
      src/Avalonia.Controls/Slider.cs
  36. 2 2
      src/Avalonia.Controls/TreeView.cs
  37. 2 2
      src/Avalonia.Controls/Window.cs
  38. 38 4
      src/Avalonia.Input/InputElement.cs
  39. 10 2
      src/Avalonia.Layout/LayoutManager.cs
  40. 28 0
      src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs
  41. 0 77
      src/Avalonia.Styling/StyledElement.cs
  42. 11 10
      src/Avalonia.Themes.Default/ComboBox.xaml
  43. 52 34
      src/Avalonia.Themes.Default/ProgressBar.xaml
  44. 12 5
      src/Avalonia.X11/X11Window.cs
  45. 1 1
      src/Windows/Avalonia.Win32/WindowImpl.cs
  46. 0 17
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs
  47. 105 0
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs
  48. 1 16
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs
  49. 1 0
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs
  50. 0 23
      tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs
  51. 0 22
      tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs
  52. 1 1
      tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs
  53. 46 0
      tests/Avalonia.Benchmarks/Layout/CalendarBenchmark.cs
  54. 36 0
      tests/Avalonia.Benchmarks/NullFormattedTextImpl.cs
  55. 78 0
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  56. 28 0
      tests/Avalonia.Benchmarks/NullThreadingPlatform.cs
  57. 23 23
      tests/Avalonia.Controls.UnitTests/CarouselTests.cs
  58. 31 0
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
  59. 28 4
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  60. 27 1
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  61. 22 0
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  62. 24 0
      tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
  63. 51 0
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  64. 33 0
      tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs
  65. 52 0
      tests/Avalonia.LeakTests/ControlTests.cs

+ 3 - 1
readme.md

@@ -10,7 +10,7 @@
 
 **Avalonia** is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS and with experimental support for Android and iOS.
 
-**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development. To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239).
+**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and [breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) as we continue along into this project's development. To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239).
 
 | Control catalog | Desktop platforms | Mobile platforms |
 |---|---|---|
@@ -24,6 +24,8 @@ Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?it
 
 For those without Visual Studio, a starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core).
 
+If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature,  so only **YOU** can make it happen.
+
 Avalonia is delivered via <b>NuGet</b> package manager. You can find the packages here: [stable(ish)](https://www.nuget.org/packages/Avalonia/)
 
 Use these commands in the Package Manager console to install Avalonia manually:

+ 6 - 2
samples/ControlCatalog/Pages/ProgressBarPage.xaml

@@ -6,15 +6,19 @@
     <TextBlock Classes="h2">A progress bar control</TextBlock>
 
     <StackPanel>
+      <CheckBox
+          x:Name="showProgress"
+          Margin="10,16,0,0"
+          Content="Show Progress Text" />
       <StackPanel Orientation="Horizontal"
                   Margin="0,16,0,0"
                   HorizontalAlignment="Center"
                   Spacing="16">
         <StackPanel Spacing="16">
-          <ProgressBar Value="{Binding #hprogress.Value}" />
+          <ProgressBar ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #hprogress.Value}" />
           <ProgressBar IsIndeterminate="True"/>
         </StackPanel>
-        <ProgressBar Value="{Binding #vprogress.Value}" Orientation="Vertical" />
+        <ProgressBar ShowProgressText="{Binding #showProgress.IsChecked}" Value="{Binding #vprogress.Value}" Orientation="Vertical" />
         <ProgressBar Orientation="Vertical" IsIndeterminate="True" />
       </StackPanel>
       <StackPanel Margin="16">

+ 7 - 2
src/Avalonia.Base/AvaloniaObject.cs

@@ -34,7 +34,6 @@ namespace Avalonia
         public AvaloniaObject()
         {
             VerifyAccess();
-            AvaloniaPropertyRegistry.Instance.NotifyInitialized(this);
         }
 
         /// <summary>
@@ -479,7 +478,13 @@ namespace Avalonia
             }
         }
 
-        void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) { }
+        void IValueSink.Completed<T>(
+            StyledPropertyBase<T> property,
+            IPriorityValueEntry entry,
+            Optional<T> oldValue) 
+        {
+            ((IValueSink)this).ValueChanged(property, BindingPriority.Unset, oldValue, default);
+        }
 
         /// <summary>
         /// Called for each inherited property when the <see cref="InheritanceParent"/> changes.

+ 0 - 39
src/Avalonia.Base/AvaloniaProperty.cs

@@ -20,7 +20,6 @@ namespace Avalonia
         public static readonly object UnsetValue = new UnsetValueType();
 
         private static int s_nextId;
-        private readonly Subject<AvaloniaPropertyChangedEventArgs> _initialized;
         private readonly Subject<AvaloniaPropertyChangedEventArgs> _changed;
         private readonly PropertyMetadata _defaultMetadata;
         private readonly Dictionary<Type, PropertyMetadata> _metadata;
@@ -53,7 +52,6 @@ namespace Avalonia
                 throw new ArgumentException("'name' may not contain periods.");
             }
 
-            _initialized = new Subject<AvaloniaPropertyChangedEventArgs>();
             _changed = new Subject<AvaloniaPropertyChangedEventArgs>();
             _metadata = new Dictionary<Type, PropertyMetadata>();
 
@@ -81,7 +79,6 @@ namespace Avalonia
             Contract.Requires<ArgumentNullException>(source != null);
             Contract.Requires<ArgumentNullException>(ownerType != null);
 
-            _initialized = source._initialized;
             _changed = source._changed;
             _metadata = new Dictionary<Type, PropertyMetadata>();
 
@@ -136,22 +133,6 @@ namespace Avalonia
         /// </summary>
         public virtual bool IsReadOnly => false;
 
-        /// <summary>
-        /// Gets an observable that is fired when this property is initialized on a
-        /// new <see cref="AvaloniaObject"/> instance.
-        /// </summary>
-        /// <remarks>
-        /// This observable is fired each time a new <see cref="AvaloniaObject"/> is constructed
-        /// for all properties registered on the object's type. The default value of the property
-        /// for the object is passed in the args' NewValue (OldValue will always be
-        /// <see cref="UnsetValue"/>.
-        /// </remarks>
-        /// <value>
-        /// An observable that is fired when this property is initialized on a new
-        /// <see cref="AvaloniaObject"/> instance.
-        /// </value>
-        public IObservable<AvaloniaPropertyChangedEventArgs> Initialized => _initialized;
-
         /// <summary>
         /// Gets an observable that is fired when this property changes on any
         /// <see cref="AvaloniaObject"/> instance.
@@ -488,26 +469,6 @@ namespace Avalonia
             return Name;
         }
 
-        /// <summary>
-        /// True if <see cref="Initialized"/> has any observers.
-        /// </summary>
-        internal bool HasNotifyInitializedObservers => _initialized.HasObservers;
-
-        /// <summary>
-        /// Notifies the <see cref="Initialized"/> observable.
-        /// </summary>
-        /// <param name="o">The object being initialized.</param>
-        internal abstract void NotifyInitialized(IAvaloniaObject o);
-
-        /// <summary>
-        /// Notifies the <see cref="Initialized"/> observable.
-        /// </summary>
-        /// <param name="e">The observable arguments.</param>
-        internal void NotifyInitialized(AvaloniaPropertyChangedEventArgs e)
-        {
-            _initialized.OnNext(e);
-        }
-
         /// <summary>
         /// Notifies the <see cref="Changed"/> observable.
         /// </summary>

+ 0 - 45
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@@ -415,51 +415,6 @@ namespace Avalonia
             _inheritedCache.Clear();
         }
 
-        internal void NotifyInitialized(AvaloniaObject o)
-        {
-            Contract.Requires<ArgumentNullException>(o != null);
-
-            var type = o.GetType();
-
-            if (!_initializedCache.TryGetValue(type, out var initializationData))
-            {
-                var visited = new HashSet<AvaloniaProperty>();
-
-                initializationData = new List<PropertyInitializationData>();
-
-                foreach (AvaloniaProperty property in GetRegistered(type))
-                {
-                    if (property.IsDirect)
-                    {
-                        initializationData.Add(new PropertyInitializationData(property, (IDirectPropertyAccessor)property));
-                    }
-                    else
-                    {
-                        initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type));
-                    }
-
-                    visited.Add(property);
-                }
-
-                foreach (AvaloniaProperty property in GetRegisteredAttached(type))
-                {
-                    if (!visited.Contains(property))
-                    {
-                        initializationData.Add(new PropertyInitializationData(property, (IStyledPropertyAccessor)property, type));
-
-                        visited.Add(property);
-                    }
-                }
-
-                _initializedCache.Add(type, initializationData);
-            }
-
-            foreach (PropertyInitializationData data in initializationData)
-            {
-                data.Property.NotifyInitialized(o);
-            }
-        }
-
         private readonly struct PropertyInitializationData
         {
             public AvaloniaProperty Property { get; }

+ 0 - 15
src/Avalonia.Base/DirectPropertyBase.cs

@@ -101,21 +101,6 @@ namespace Avalonia
             return (DirectPropertyMetadata<TValue>)base.GetMetadata(type);
         }
 
-        /// <inheritdoc/>
-        internal override void NotifyInitialized(IAvaloniaObject o)
-        {
-            if (HasNotifyInitializedObservers)
-            {
-                var e = new AvaloniaPropertyChangedEventArgs<TValue>(
-                    o,
-                    this,
-                    default,
-                    InvokeGetter(o),
-                    BindingPriority.Unset);
-                NotifyInitialized(e);
-            }
-        }
-
         /// <inheritdoc/>
         internal override void RouteClearValue(IAvaloniaObject o)
         {

+ 2 - 2
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@@ -48,10 +48,10 @@ namespace Avalonia.PropertyStore
         {
             _subscription?.Dispose();
             _subscription = null;
-            _sink.Completed(Property, this);
+            _sink.Completed(Property, this, Value);
         }
 
-        public void OnCompleted() => _sink.Completed(Property, this);
+        public void OnCompleted() => _sink.Completed(Property, this, Value);
 
         public void OnError(Exception error)
         {

+ 4 - 1
src/Avalonia.Base/PropertyStore/IValueSink.cs

@@ -15,6 +15,9 @@ namespace Avalonia.PropertyStore
             Optional<T> oldValue,
             BindingValue<T> newValue);
 
-        void Completed(AvaloniaProperty property, IPriorityValueEntry entry);
+        void Completed<T>(
+            StyledPropertyBase<T> property,
+            IPriorityValueEntry entry,
+            Optional<T> oldValue);
     }
 }

+ 4 - 1
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@@ -117,7 +117,10 @@ namespace Avalonia.PropertyStore
             UpdateEffectiveValue();
         }
 
-        void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry)
+        void IValueSink.Completed<TValue>(
+            StyledPropertyBase<TValue> property,
+            IPriorityValueEntry entry,
+            Optional<TValue> oldValue)
         {
             _entries.Remove((IPriorityValueEntry<T>)entry);
             UpdateEffectiveValue();

+ 0 - 15
src/Avalonia.Base/StyledPropertyBase.cs

@@ -181,21 +181,6 @@ namespace Avalonia
         /// <inheritdoc/>
         object IStyledPropertyAccessor.GetDefaultValue(Type type) => GetDefaultBoxedValue(type);
 
-        /// <inheritdoc/>
-        internal override void NotifyInitialized(IAvaloniaObject o)
-        {
-            if (HasNotifyInitializedObservers)
-            {
-                var e = new AvaloniaPropertyChangedEventArgs<TValue>(
-                    o,
-                    this,
-                    default,
-                    o.GetValue(this),
-                    BindingPriority.Unset);
-                NotifyInitialized(e);
-            }
-        }
-
         /// <inheritdoc/>
         internal override void RouteClearValue(IAvaloniaObject o)
         {

+ 7 - 2
src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs

@@ -183,11 +183,16 @@ namespace Avalonia.Utilities
                 for (int c = 0; c < _count; c++)
                 {
                     var r = _data[c];
+
+                    TSubscriber target = null;
+
+                    r.Subscriber?.TryGetTarget(out target);
+
                     //Mark current index as first empty
-                    if (r.Subscriber == null && empty == -1)
+                    if (target == null && empty == -1)
                         empty = c;
                     //If current element isn't null and we have an empty one
-                    if (r.Subscriber != null && empty != -1)
+                    if (target != null && empty != -1)
                     {
                         _data[c] = default;
                         _data[empty] = r;

+ 6 - 2
src/Avalonia.Base/ValueStore.cs

@@ -148,7 +148,7 @@ namespace Avalonia
                         _values.Remove(property);
                         _sink.ValueChanged(
                             property,
-                            BindingPriority.LocalValue,
+                            BindingPriority.Unset,
                             old,
                             BindingValue<T>.Unset);
                     }
@@ -190,13 +190,17 @@ namespace Avalonia
             _sink.ValueChanged(property, priority, oldValue, newValue);
         }
 
-        void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry)
+        void IValueSink.Completed<T>(
+            StyledPropertyBase<T> property,
+            IPriorityValueEntry entry,
+            Optional<T> oldValue)
         {
             if (_values.TryGetValue(property, out var slot))
             {
                 if (slot == entry)
                 {
                     _values.Remove(property);
+                    _sink.Completed(property, entry, oldValue);
                 }
             }
         }

+ 10 - 3
src/Avalonia.Controls.DataGrid/DataGrid.cs

@@ -149,6 +149,9 @@ namespace Avalonia.Controls
 
         private IEnumerable _items;
 
+        public event EventHandler<ScrollEventArgs> HorizontalScroll;
+        public event EventHandler<ScrollEventArgs> VerticalScroll;
+
         /// <summary>
         /// Identifies the CanUserReorderColumns dependency property.
         /// </summary>
@@ -373,7 +376,11 @@ namespace Avalonia.Controls
         public bool IsValid
         {
             get { return _isValid; }
-            internal set { SetAndRaise(IsValidProperty, ref _isValid, value); }
+            internal set 
+            { 
+                SetAndRaise(IsValidProperty, ref _isValid, value);
+                PseudoClasses.Set(":invalid", !value);
+            }
         }
 
         public static readonly StyledProperty<double> MaxColumnWidthProperty =
@@ -656,8 +663,6 @@ namespace Avalonia.Controls
                 HorizontalScrollBarVisibilityProperty,
                 VerticalScrollBarVisibilityProperty);
 
-            PseudoClass<DataGrid, bool>(IsValidProperty, x => !x, ":invalid");
-
             ItemsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnItemsPropertyChanged(e));
             CanUserResizeColumnsProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnCanUserResizeColumnsChanged(e));
             ColumnWidthProperty.Changed.AddClassHandler<DataGrid>((x, e) => x.OnColumnWidthChanged(e));
@@ -4223,6 +4228,7 @@ namespace Avalonia.Controls
         private void HorizontalScrollBar_Scroll(object sender, ScrollEventArgs e)
         {
             ProcessHorizontalScroll(e.ScrollEventType);
+            HorizontalScroll?.Invoke(sender, e);
         }
 
         private bool IsColumnOutOfBounds(int columnIndex)
@@ -5555,6 +5561,7 @@ namespace Avalonia.Controls
         private void VerticalScrollBar_Scroll(object sender, ScrollEventArgs e)
         {
             ProcessVerticalScroll(e.ScrollEventType);
+            VerticalScroll?.Invoke(sender, e);
         }
 
         //TODO: Ensure left button is checked for

+ 2 - 2
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@@ -326,7 +326,7 @@ namespace Avalonia.Controls
 
             if (OwningGrid != null && OwningGrid.ColumnHeaders != null)
             {
-                args.Device.Capture(this);
+                args.Pointer.Capture(this);
 
                 _dragMode = DragMode.MouseDown;
                 _frozenColumnsWidth = OwningGrid.ColumnsInternal.GetVisibleFrozenEdgedColumnsWidth();
@@ -391,7 +391,7 @@ namespace Avalonia.Controls
                 SetDragCursor(mousePosition);
 
                 // Variables that track drag mode states get reset in DataGridColumnHeader_LostMouseCapture
-                args.Device.Capture(null);
+                args.Pointer.Capture(null);
                 OnLostMouseCapture();
                 _dragMode = DragMode.None;
                 handled = true;

+ 12 - 9
src/Avalonia.Controls.DataGrid/Themes/Default.xaml

@@ -45,7 +45,7 @@
 
             <Path Name="SortIcon"
                   Grid.Column="1"
-                  Fill="#FF444444"
+                  Fill="{TemplateBinding Foreground}"
                   HorizontalAlignment="Left"
                   VerticalAlignment="Center"
                   Stretch="Uniform"
@@ -113,7 +113,7 @@
 
   <Style Selector="DataGridRow /template/ Rectangle#BackgroundRectangle">
     <Setter Property="IsVisible" Value="False"/>
-    <Setter Property="Fill" Value="#FFBADDE9" />
+    <Setter Property="Fill" Value="{DynamicResource HighlightBrush}" />
   </Style>
 
   <Style Selector="DataGridRow:pointerover /template/ Rectangle#BackgroundRectangle">
@@ -126,6 +126,10 @@
     <Setter Property="Opacity" Value="1"/>
   </Style>
 
+  <Style Selector="DataGridRow:selected">
+    <Setter Property="Foreground" Value="{DynamicResource HighlightForegroundBrush}" />
+  </Style>
+
   <Style Selector="DataGridRowHeader">
     <Setter Property="Template">
       <ControlTemplate>
@@ -139,7 +143,7 @@
   </Style>
 
   <Style Selector="DataGridRowGroupHeader">
-    <Setter Property="Background" Value="#FFE4E8EA" />
+    <Setter Property="Background" Value="{DynamicResource ThemeControlMidHighBrush}" />
     <Setter Property="Height" Value="20"/>
     <Setter Property="Template">
       <ControlTemplate>
@@ -148,7 +152,6 @@
                                  ColumnDefinitions="Auto,Auto,Auto,Auto"
                                  RowDefinitions="Auto,*,Auto">
 
-          <Rectangle Grid.Column="1" Grid.ColumnSpan="5" Fill="#FFFFFFFF" Height="1"/>
           <Rectangle Grid.Column="1" Grid.Row="1" Name="IndentSpacer" />
           <ToggleButton Grid.Column="2" Grid.Row="1" Name="ExpanderButton" Margin="2,0,0,0"/>
 
@@ -169,7 +172,7 @@
     <Setter Property="Template">
       <ControlTemplate>
         <Border Grid.Column="0" Width="20" Height="20" Background="Transparent" HorizontalAlignment="Center" VerticalAlignment="Center">
-          <Path Fill="Black"
+          <Path Fill="{TemplateBinding Foreground}"
                 HorizontalAlignment="Center"
                 VerticalAlignment="Center"
                 Data="M 0 2 L 4 6 L 0 10 Z" />
@@ -204,10 +207,10 @@
     </Setter>
     <Setter Property="Template">
       <ControlTemplate>
-        <Border BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}">
-          <Grid
-          RowDefinitions="Auto,*,Auto,Auto"
-          ColumnDefinitions="Auto,*,Auto">
+        <Border Background="{TemplateBinding Background}"
+                BorderThickness="{TemplateBinding BorderThickness}"
+                BorderBrush="{TemplateBinding BorderBrush}">
+          <Grid RowDefinitions="Auto,*,Auto,Auto" ColumnDefinitions="Auto,*,Auto">
 
             <DataGridColumnHeader Name="PART_TopLeftCornerHeader" Width="22" />
             <DataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter" Grid.Column="1"/>

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

@@ -683,7 +683,7 @@ namespace Avalonia.Controls
                 added.Add(e.NewValue);
             }
 
-            OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, added, removed));
+            OnSelectionChanged(new SelectionChangedEventArgs(SelectionChangedEvent, removed, added));
         }
 
         /// <summary>

+ 24 - 1
src/Avalonia.Controls/Button.cs

@@ -91,7 +91,11 @@ namespace Avalonia.Controls
             CommandProperty.Changed.Subscribe(CommandChanged);
             IsDefaultProperty.Changed.Subscribe(IsDefaultChanged);
             IsCancelProperty.Changed.Subscribe(IsCancelChanged);
-            PseudoClass<Button>(IsPressedProperty, ":pressed");
+        }
+
+        public Button()
+        {
+            UpdatePseudoClasses(IsPressed);
         }
 
         /// <summary>
@@ -312,6 +316,20 @@ namespace Avalonia.Controls
             IsPressed = false;
         }
 
+        protected override void OnPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
+        {
+            base.OnPropertyChanged(property, oldValue, newValue, priority);
+
+            if (property == IsPressedProperty)
+            {
+                UpdatePseudoClasses(newValue.GetValueOrDefault<bool>());
+            }
+        }
+
         protected override void UpdateDataValidation<T>(AvaloniaProperty<T> property, BindingValue<T> value)
         {
             base.UpdateDataValidation(property, value);
@@ -474,5 +492,10 @@ namespace Avalonia.Controls
                 OnClick();
             }
         }
+
+        private void UpdatePseudoClasses(bool isPressed)
+        {
+            PseudoClasses.Set(":pressed", isPressed);
+        }
     }
 }

+ 26 - 2
src/Avalonia.Controls/ButtonSpinner.cs

@@ -1,5 +1,6 @@
 using System;
 using Avalonia.Controls.Primitives;
+using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 
@@ -34,6 +35,11 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<Location> ButtonSpinnerLocationProperty =
             AvaloniaProperty.Register<ButtonSpinner, Location>(nameof(ButtonSpinnerLocation), Location.Right);
 
+        public ButtonSpinner()
+        {
+            UpdatePseudoClasses(ButtonSpinnerLocation);
+        }
+
         private Button _decreaseButton;
         /// <summary>
         /// Gets or sets the DecreaseButton template part.
@@ -85,8 +91,6 @@ namespace Avalonia.Controls
         static ButtonSpinner()
         {
             AllowSpinProperty.Changed.Subscribe(AllowSpinChanged);
-            PseudoClass<ButtonSpinner, Location>(ButtonSpinnerLocationProperty, location => location == Location.Left, ":left");
-            PseudoClass<ButtonSpinner, Location>(ButtonSpinnerLocationProperty, location => location == Location.Right, ":right");
         }
 
         /// <summary>
@@ -201,6 +205,20 @@ namespace Avalonia.Controls
             }
         }
 
+        protected override void OnPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
+        {
+            base.OnPropertyChanged(property, oldValue, newValue, priority);
+
+            if (property == ButtonSpinnerLocationProperty)
+            {
+                UpdatePseudoClasses(newValue.GetValueOrDefault<Location>());
+            }
+        }
+
         protected override void OnValidSpinDirectionChanged(ValidSpinDirections oldValue, ValidSpinDirections newValue)
         {
             SetButtonUsage();
@@ -259,5 +277,11 @@ namespace Avalonia.Controls
                 OnSpin(new SpinEventArgs(SpinEvent, direction));
             }
         }
+
+        private void UpdatePseudoClasses(Location location)
+        {
+            PseudoClasses.Set(":left", location == Location.Left);
+            PseudoClasses.Set(":right", location == Location.Right);
+        }
     }
 }

+ 6 - 6
src/Avalonia.Controls/Calendar/CalendarItem.cs

@@ -943,8 +943,8 @@ namespace Avalonia.Controls.Primitives
             {
                 CalendarDayButton b = (CalendarDayButton)sender;
                 // The button is in Pressed state. Change the state to normal.
-                if (e.Device.Captured == b)
-                    e.Device.Capture(null);
+                if (e.Pointer.Captured == b)
+                    e.Pointer.Capture(null);
                 _lastCalendarDayButton = b;
             }
         }
@@ -1213,8 +1213,8 @@ namespace Avalonia.Controls.Primitives
             {
                 CalendarButton b = (CalendarButton)sender;
                 // The button is in Pressed state. Change the state to normal.
-                if (e.Device.Captured == b)
-                    e.Device.Capture(null);
+                if (e.Pointer.Captured == b)
+                    e.Pointer.Capture(null);
                 //b.ReleaseMouseCapture();
 
                 _lastCalendarButton = b;
@@ -1224,7 +1224,7 @@ namespace Avalonia.Controls.Primitives
         {
             if (_lastCalendarDayButton != null)
             {
-                e.Device.Capture(_lastCalendarDayButton);
+                e.Pointer.Capture(_lastCalendarDayButton);
             }
         }
 
@@ -1232,7 +1232,7 @@ namespace Avalonia.Controls.Primitives
         {
             if (_lastCalendarButton != null)
             {
-                e.Device.Capture(_lastCalendarButton);
+                e.Pointer.Capture(_lastCalendarButton);
             }
         }
         

+ 1 - 1
src/Avalonia.Controls/Calendar/DatePicker.cs

@@ -788,7 +788,7 @@ namespace Avalonia.Controls
                     removedItems.Add(removedDate.Value);
                 }
 
-                handler(this, new SelectionChangedEventArgs(SelectingItemsControl.SelectionChangedEvent, addedItems, removedItems));
+                handler(this, new SelectionChangedEventArgs(SelectingItemsControl.SelectionChangedEvent, removedItems, addedItems));
             }
         }
         private void OnCalendarClosed(EventArgs e)

+ 2 - 2
src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs

@@ -49,7 +49,7 @@ namespace Avalonia.Controls.Primitives
 
         private void InvokeCollectionChanged(System.Collections.IList removedItems, System.Collections.IList addedItems)
         {
-            _owner.OnSelectedDatesCollectionChanged(new SelectionChangedEventArgs(null, addedItems, removedItems));
+            _owner.OnSelectedDatesCollectionChanged(new SelectionChangedEventArgs(null, removedItems, addedItems));
         }
 
         /// <summary>
@@ -119,7 +119,7 @@ namespace Avalonia.Controls.Primitives
                 }
             }
 
-            _owner.OnSelectedDatesCollectionChanged(new SelectionChangedEventArgs(null, _addedItems, _owner.RemovedItems));
+            _owner.OnSelectedDatesCollectionChanged(new SelectionChangedEventArgs(null, _owner.RemovedItems, _addedItems));
             _owner.RemovedItems.Clear();
             _owner.UpdateMonths();
             _isRangeAdded = false;

+ 0 - 20
src/Avalonia.Controls/ControlExtensions.cs

@@ -69,26 +69,6 @@ namespace Avalonia.Controls
             return nameScope.Find<T>(name);
         }
 
-        /// <summary>
-        /// Adds or removes a pseudoclass depending on a boolean value.
-        /// </summary>
-        /// <param name="classes">The pseudoclasses collection.</param>
-        /// <param name="name">The name of the pseudoclass to set.</param>
-        /// <param name="value">True to add the pseudoclass or false to remove.</param>
-        public static void Set(this IPseudoClasses classes, string name, bool value)
-        {
-            Contract.Requires<ArgumentNullException>(classes != null);
-
-            if (value)
-            {
-                classes.Add(name);
-            }
-            else
-            {
-                classes.Remove(name);
-            }
-        }
-
         /// <summary>
         /// Sets a pseudoclass depending on an observable trigger.
         /// </summary>

+ 33 - 8
src/Avalonia.Controls/Expander.cs

@@ -1,5 +1,6 @@
 using Avalonia.Animation;
 using Avalonia.Controls.Primitives;
+using Avalonia.Data;
 
 namespace Avalonia.Controls
 {
@@ -30,16 +31,14 @@ namespace Avalonia.Controls
 
         static Expander()
         {
-            PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Down, ":down");
-            PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Up, ":up");
-            PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Left, ":left");
-            PseudoClass<Expander, ExpandDirection>(ExpandDirectionProperty, d => d == ExpandDirection.Right, ":right");
-
-            PseudoClass<Expander>(IsExpandedProperty, ":expanded");
-
             IsExpandedProperty.Changed.AddClassHandler<Expander>((x, e) => x.OnIsExpandedChanged(e));
         }
 
+        public Expander()
+        {
+            UpdatePseudoClasses(ExpandDirection);
+        }
+
         public IPageTransition ContentTransition
         {
             get => GetValue(ContentTransitionProperty);
@@ -55,7 +54,11 @@ namespace Avalonia.Controls
         public bool IsExpanded
         {
             get { return _isExpanded; }
-            set { SetAndRaise(IsExpandedProperty, ref _isExpanded, value); }
+            set 
+            { 
+                SetAndRaise(IsExpandedProperty, ref _isExpanded, value);
+                PseudoClasses.Set(":expanded", value);
+            }
         }
 
         protected virtual void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e)
@@ -74,5 +77,27 @@ namespace Avalonia.Controls
                 }
             }
         }
+
+        protected override void OnPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
+        {
+            base.OnPropertyChanged(property, oldValue, newValue, priority);
+
+            if (property == ExpandDirectionProperty)
+            {
+                UpdatePseudoClasses(newValue.GetValueOrDefault<ExpandDirection>());
+            }
+        }
+
+        private void UpdatePseudoClasses(ExpandDirection d)
+        {
+            PseudoClasses.Set(":up", d == ExpandDirection.Up);
+            PseudoClasses.Set(":down", d == ExpandDirection.Down);
+            PseudoClasses.Set(":left", d == ExpandDirection.Left);
+            PseudoClasses.Set(":right", d == ExpandDirection.Right);
+        }
     }
 }

+ 6 - 38
src/Avalonia.Controls/ItemsControl.cs

@@ -236,25 +236,7 @@ namespace Avalonia.Controls
                 // it was added to the Items collection.
                 if (container.ContainerControl != null && container.ContainerControl != container.Item)
                 {
-                    if (ItemContainerGenerator.ContainerType == null)
-                    {
-                        var containerControl = container.ContainerControl as ContentPresenter;
-
-                        if (containerControl != null)
-                        {
-                            ((ISetLogicalParent)containerControl).SetParent(this);
-                            containerControl.UpdateChild();
-
-                            if (containerControl.Child != null)
-                            {
-                                LogicalChildren.Add(containerControl.Child);
-                            }
-                        }
-                    }
-                    else
-                    {
-                        LogicalChildren.Add(container.ContainerControl);
-                    }
+                    LogicalChildren.Add(container.ContainerControl);
                 }
             }
         }
@@ -272,24 +254,7 @@ namespace Avalonia.Controls
                 // when it is removed from the Items collection.
                 if (container?.ContainerControl != container?.Item)
                 {
-                    if (ItemContainerGenerator.ContainerType == null)
-                    {
-                        var containerControl = container.ContainerControl as ContentPresenter;
-
-                        if (containerControl != null)
-                        {
-                            ((ISetLogicalParent)containerControl).SetParent(null);
-
-                            if (containerControl.Child != null)
-                            {
-                                LogicalChildren.Remove(containerControl.Child);
-                            }
-                        }
-                    }
-                    else
-                    {
-                        LogicalChildren.Remove(container.ContainerControl);
-                    }
+                    LogicalChildren.Remove(container.ContainerControl);
                 }
             }
         }
@@ -507,7 +472,10 @@ namespace Avalonia.Controls
                 result = container.GetControl(direction, c, wrap);
                 from = from ?? result;
 
-                if (result?.Focusable == true)
+                if (result != null &&
+                    result.Focusable &&
+                    result.IsEffectivelyEnabled &&
+                    result.IsEffectivelyVisible)
                 {
                     return result;
                 }

+ 25 - 5
src/Avalonia.Controls/Notifications/WindowNotificationManager.cs

@@ -8,6 +8,7 @@ using System.Reactive.Linq;
 using System.Threading.Tasks;
 using Avalonia.Controls.Primitives;
 using Avalonia.Rendering;
+using Avalonia.Data;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Controls.Notifications
@@ -68,15 +69,12 @@ namespace Avalonia.Controls.Notifications
                         Install(host);
                     });
             }
+
+            UpdatePseudoClasses(Position);
         }
 
         static WindowNotificationManager()
         {
-            PseudoClass<WindowNotificationManager, NotificationPosition>(PositionProperty, x => x == NotificationPosition.TopLeft, ":topleft");
-            PseudoClass<WindowNotificationManager, NotificationPosition>(PositionProperty, x => x == NotificationPosition.TopRight, ":topright");
-            PseudoClass<WindowNotificationManager, NotificationPosition>(PositionProperty, x => x == NotificationPosition.BottomLeft, ":bottomleft");
-            PseudoClass<WindowNotificationManager, NotificationPosition>(PositionProperty, x => x == NotificationPosition.BottomRight, ":bottomright");
-
             HorizontalAlignmentProperty.OverrideDefaultValue<WindowNotificationManager>(Layout.HorizontalAlignment.Stretch);
             VerticalAlignmentProperty.OverrideDefaultValue<WindowNotificationManager>(Layout.VerticalAlignment.Stretch);
         }
@@ -143,6 +141,20 @@ namespace Avalonia.Controls.Notifications
             notificationControl.Close();
         }
 
+        protected override void OnPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
+        {
+            base.OnPropertyChanged(property, oldValue, newValue, priority);
+
+            if (property == PositionProperty)
+            {
+                UpdatePseudoClasses(newValue.GetValueOrDefault<NotificationPosition>());
+            }
+        }
+
         /// <summary>
         /// Installs the <see cref="WindowNotificationManager"/> within the <see cref="AdornerLayer"/>
         /// of the host <see cref="Window"/>.
@@ -155,6 +167,14 @@ namespace Avalonia.Controls.Notifications
             adornerLayer?.Children.Add(this);
         }
 
+        private void UpdatePseudoClasses(NotificationPosition position)
+        {
+            PseudoClasses.Set(":topleft", position == NotificationPosition.TopLeft);
+            PseudoClasses.Set(":topright", position == NotificationPosition.TopRight);
+            PseudoClasses.Set(":bottomleft", position == NotificationPosition.BottomLeft);
+            PseudoClasses.Set(":bottomright", position == NotificationPosition.BottomRight);
+        }
+
         public bool HitTest(Point point) => VisualChildren.HitTestCustom(point);
     }
 }

+ 173 - 110
src/Avalonia.Controls/Primitives/Popup.cs

@@ -2,12 +2,10 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using System.Collections.Generic;
 using System.Diagnostics;
 using System.Linq;
 using System.Reactive.Disposables;
 using Avalonia.Controls.Presenters;
-using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
 using Avalonia.Interactivity;
@@ -15,6 +13,8 @@ using Avalonia.LogicalTree;
 using Avalonia.Metadata;
 using Avalonia.VisualTree;
 
+#nullable enable
+
 namespace Avalonia.Controls.Primitives
 {
     /// <summary>
@@ -25,8 +25,8 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Defines the <see cref="Child"/> property.
         /// </summary>
-        public static readonly StyledProperty<Control> ChildProperty =
-            AvaloniaProperty.Register<Popup, Control>(nameof(Child));
+        public static readonly StyledProperty<Control?> ChildProperty =
+            AvaloniaProperty.Register<Popup, Control?>(nameof(Child));
 
         /// <summary>
         /// Defines the <see cref="IsOpen"/> property.
@@ -43,11 +43,13 @@ namespace Avalonia.Controls.Primitives
         public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
             AvaloniaProperty.Register<Popup, PlacementMode>(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
 
+#pragma warning disable 618
         /// <summary>
         /// Defines the <see cref="ObeyScreenEdges"/> property.
         /// </summary>
         public static readonly StyledProperty<bool> ObeyScreenEdgesProperty =
             AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges), true);
+#pragma warning restore 618
 
         /// <summary>
         /// Defines the <see cref="HorizontalOffset"/> property.
@@ -64,8 +66,8 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Defines the <see cref="PlacementTarget"/> property.
         /// </summary>
-        public static readonly StyledProperty<Control> PlacementTargetProperty =
-            AvaloniaProperty.Register<Popup, Control>(nameof(PlacementTarget));
+        public static readonly StyledProperty<Control?> PlacementTargetProperty =
+            AvaloniaProperty.Register<Popup, Control?>(nameof(PlacementTarget));
 
         /// <summary>
         /// Defines the <see cref="StaysOpen"/> property.
@@ -80,12 +82,8 @@ namespace Avalonia.Controls.Primitives
             AvaloniaProperty.Register<Popup, bool>(nameof(Topmost));
 
         private bool _isOpen;
-        private IPopupHost _popupHost;
-        private TopLevel _topLevel;
-        private IDisposable _nonClientListener;
-        private IDisposable _presenterSubscription;
-        bool _ignoreIsOpenChanged = false;
-        private List<IDisposable> _bindings = new List<IDisposable>();
+        private bool _ignoreIsOpenChanged;
+        private PopupOpenState? _openState;
 
         /// <summary>
         /// Initializes static members of the <see cref="Popup"/> class.
@@ -94,31 +92,26 @@ namespace Avalonia.Controls.Primitives
         {
             IsHitTestVisibleProperty.OverrideDefaultValue<Popup>(false);
             ChildProperty.Changed.AddClassHandler<Popup>((x, e) => x.ChildChanged(e));
-            IsOpenProperty.Changed.AddClassHandler<Popup>((x, e) => x.IsOpenChanged(e));
-        }
-
-        public Popup()
-        {
-            
+            IsOpenProperty.Changed.AddClassHandler<Popup>((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs<bool>)e));
         }
 
         /// <summary>
         /// Raised when the popup closes.
         /// </summary>
-        public event EventHandler Closed;
+        public event EventHandler? Closed;
 
         /// <summary>
         /// Raised when the popup opens.
         /// </summary>
-        public event EventHandler Opened;
+        public event EventHandler? Opened;
 
-        public IPopupHost Host => _popupHost;
+        public IPopupHost? Host => _openState?.PopupHost;
 
         /// <summary>
         /// Gets or sets the control to display in the popup.
         /// </summary>
         [Content]
-        public Control Child
+        public Control? Child
         {
             get { return GetValue(ChildProperty); }
             set { SetValue(ChildProperty, value); }
@@ -131,7 +124,7 @@ namespace Avalonia.Controls.Primitives
         /// This property allows a client to customize the behaviour of the popup by injecting
         /// a specialized dependency resolver into the <see cref="PopupRoot"/>'s constructor.
         /// </remarks>
-        public IAvaloniaDependencyResolver DependencyResolver
+        public IAvaloniaDependencyResolver? DependencyResolver
         {
             get;
             set;
@@ -183,7 +176,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Gets or sets the control that is used to determine the popup's position.
         /// </summary>
-        public Control PlacementTarget
+        public Control? PlacementTarget
         {
             get { return GetValue(PlacementTargetProperty); }
             set { SetValue(PlacementTargetProperty, value); }
@@ -211,7 +204,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Gets the root of the popup window.
         /// </summary>
-        IVisual IVisualTreeHost.Root => _popupHost?.HostedVisualTreeRoot;
+        IVisual? IVisualTreeHost.Root => _openState?.PopupHost.HostedVisualTreeRoot;
 
         /// <summary>
         /// Opens the popup.
@@ -219,50 +212,91 @@ namespace Avalonia.Controls.Primitives
         public void Open()
         {
             // Popup is currently open
-            if (_topLevel != null)
+            if (_openState != null)
+            {
                 return;
-            CloseCurrent();
+            }
+
             var placementTarget = PlacementTarget ?? this.GetLogicalAncestors().OfType<IVisual>().FirstOrDefault();
+
             if (placementTarget == null)
+            {
                 throw new InvalidOperationException("Popup has no logical parent and PlacementTarget is null");
+            }
             
-            _topLevel = placementTarget.GetVisualRoot() as TopLevel;
+            var topLevel = placementTarget.VisualRoot as TopLevel;
 
-            if (_topLevel == null)
+            if (topLevel == null)
             {
                 throw new InvalidOperationException(
                     "Attempted to open a popup not attached to a TopLevel");
             }
 
-            _popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver);
+            var popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver);
+
+            var handlerCleanup = new CompositeDisposable(5);
 
-            _bindings.Add(_popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty,
+            void DeferCleanup(IDisposable? disposable)
+            {
+                if (disposable is null)
+                {
+                    return;
+                }
+
+                handlerCleanup.Add(disposable);
+            }
+
+            DeferCleanup(popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty,
                 HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty));
 
-            _popupHost.SetChild(Child);
-            ((ISetLogicalParent)_popupHost).SetParent(this);
-            _popupHost.ConfigurePosition(placementTarget,
-                PlacementMode, new Point(HorizontalOffset, VerticalOffset));
-            _popupHost.TemplateApplied += RootTemplateApplied;
-            
-            var window = _topLevel as Window;
-            if (window != null)
+            popupHost.SetChild(Child);
+            ((ISetLogicalParent)popupHost).SetParent(this);
+
+            popupHost.ConfigurePosition(
+                placementTarget,
+                PlacementMode, 
+                new Point(HorizontalOffset, VerticalOffset));
+
+            DeferCleanup(SubscribeToEventHandler<IPopupHost, EventHandler<TemplateAppliedEventArgs>>(popupHost, RootTemplateApplied,
+                (x, handler) => x.TemplateApplied += handler,
+                (x, handler) => x.TemplateApplied -= handler));
+
+            if (topLevel is Window window)
             {
-                window.Deactivated += WindowDeactivated;
+                DeferCleanup(SubscribeToEventHandler<Window, EventHandler>(window, WindowDeactivated,
+                    (x, handler) => x.Deactivated += handler,
+                    (x, handler) => x.Deactivated -= handler));
             }
             else
             {
-                var parentPopuproot = _topLevel as PopupRoot;
-                if (parentPopuproot?.Parent is Popup popup)
+                var parentPopupRoot = topLevel as PopupRoot;
+
+                if (parentPopupRoot?.Parent is Popup popup)
                 {
-                    popup.Closed += ParentClosed;
+                    DeferCleanup(SubscribeToEventHandler<Popup, EventHandler>(popup, ParentClosed,
+                        (x, handler) => x.Closed += handler,
+                        (x, handler) => x.Closed -= handler));
                 }
             }
-            _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel);
-            _nonClientListener = InputManager.Instance?.Process.Subscribe(ListenForNonClientClick);
-        
 
-            _popupHost.Show();
+            DeferCleanup(topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel));
+
+            DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick));
+
+            var cleanupPopup = Disposable.Create((popupHost, handlerCleanup), state =>
+            {
+                state.handlerCleanup.Dispose();
+
+                state.popupHost.SetChild(null);
+                state.popupHost.Hide();
+
+                ((ISetLogicalParent)state.popupHost).SetParent(null);
+                state.popupHost.Dispose();
+            });
+
+            _openState = new PopupOpenState(topLevel, popupHost, cleanupPopup);
+
+            popupHost.Show();
 
             using (BeginIgnoringIsOpen())
             {
@@ -277,14 +311,19 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public void Close()
         {
-            if (_popupHost != null)
+            if (_openState is null)
             {
-                _popupHost.TemplateApplied -= RootTemplateApplied;
+                using (BeginIgnoringIsOpen())
+                {
+                    IsOpen = false;
+                }
+
+                return;
             }
 
-            _presenterSubscription?.Dispose();
+            _openState.Dispose();
+            _openState = null;
 
-            CloseCurrent();
             using (BeginIgnoringIsOpen())
             {
                 IsOpen = false;
@@ -293,41 +332,6 @@ namespace Avalonia.Controls.Primitives
             Closed?.Invoke(this, EventArgs.Empty);
         }
 
-        void CloseCurrent()
-        {
-            if (_topLevel != null)
-            {
-                _topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside);
-                var window = _topLevel as Window;
-                if (window != null)
-                    window.Deactivated -= WindowDeactivated;
-                else
-                {
-                    var parentPopuproot = _topLevel as PopupRoot;
-                    if (parentPopuproot?.Parent is Popup popup)
-                    {
-                        popup.Closed -= ParentClosed;
-                    }
-                }
-                _nonClientListener?.Dispose();
-                _nonClientListener = null;
-                
-                _topLevel = null;
-            }
-            if (_popupHost != null)
-            {
-                foreach(var b in _bindings)
-                    b.Dispose();
-                _bindings.Clear();
-                _popupHost.SetChild(null);
-                _popupHost.Hide();
-                ((ISetLogicalParent)_popupHost).SetParent(null);
-                _popupHost.Dispose();
-                _popupHost = null;
-            }
-
-        }
-
         /// <summary>
         /// Measures the control.
         /// </summary>
@@ -345,16 +349,22 @@ namespace Avalonia.Controls.Primitives
             Close();
         }
 
+        private static IDisposable SubscribeToEventHandler<T, TEventHandler>(T target, TEventHandler handler, Action<T, TEventHandler> subscribe, Action<T, TEventHandler> unsubscribe)
+        {
+            subscribe(target, handler);
+
+            return Disposable.Create((unsubscribe, target, handler), state => state.unsubscribe(state.target, state.handler));
+        }
 
         /// <summary>
         /// Called when the <see cref="IsOpen"/> property changes.
         /// </summary>
         /// <param name="e">The event args.</param>
-        private void IsOpenChanged(AvaloniaPropertyChangedEventArgs e)
+        private void IsOpenChanged(AvaloniaPropertyChangedEventArgs<bool> e)
         {
             if (!_ignoreIsOpenChanged)
             {
-                if ((bool)e.NewValue)
+                if (e.NewValue.Value)
                 {
                     Open();
                 }
@@ -373,7 +383,7 @@ namespace Avalonia.Controls.Primitives
         {
             LogicalChildren.Clear();
 
-            ((ISetLogicalParent)e.OldValue)?.SetParent(null);
+            ((ISetLogicalParent?)e.OldValue)?.SetParent(null);
 
             if (e.NewValue != null)
             {
@@ -394,34 +404,37 @@ namespace Avalonia.Controls.Primitives
 
         private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
         {
-            if (!StaysOpen)
+            if (!StaysOpen && !IsChildOrThis((IVisual)e.Source))
             {
-                if (!IsChildOrThis((IVisual)e.Source))
-                {
-                    Close();
-                    e.Handled = true;
-                }
+                Close();
+                e.Handled = true;
             }
         }
 
         private void RootTemplateApplied(object sender, TemplateAppliedEventArgs e)
         {
-            _popupHost.TemplateApplied -= RootTemplateApplied;
-
-            if (_presenterSubscription != null)
+            if (_openState is null)
             {
-                _presenterSubscription.Dispose();
-                _presenterSubscription = null;
+                return;
             }
 
+            var popupHost = _openState.PopupHost;
+
+            popupHost.TemplateApplied -= RootTemplateApplied;
+
+            _openState.SetPresenterSubscription(null);
+
             // If the Popup appears in a control template, then the child controls
             // that appear in the popup host need to have their TemplatedParent
             // properties set.
-            if (TemplatedParent != null)
+            if (TemplatedParent != null && popupHost.Presenter != null)
             {
-                _popupHost.Presenter?.ApplyTemplate();
-                _popupHost.Presenter?.GetObservable(ContentPresenter.ChildProperty)
+                popupHost.Presenter.ApplyTemplate();
+
+                var presenterSubscription = popupHost.Presenter.GetObservable(ContentPresenter.ChildProperty)
                     .Subscribe(SetTemplatedParentAndApplyChildTemplates);
+
+                _openState.SetPresenterSubscription(presenterSubscription);
             }
         }
 
@@ -440,7 +453,7 @@ namespace Avalonia.Controls.Primitives
 
                 if (!(control is IPresenter) && control.TemplatedParent == templatedParent)
                 {
-                    foreach (IControl child in control.GetVisualChildren())
+                    foreach (IControl child in control.VisualChildren)
                     {
                         SetTemplatedParentAndApplyChildTemplates(child);
                     }
@@ -450,22 +463,41 @@ namespace Avalonia.Controls.Primitives
 
         private bool IsChildOrThis(IVisual child)
         {
-            IVisual root = child.GetVisualRoot();
-            while (root is IHostedVisualTreeRoot hostedRoot )
+            if (_openState is null)
             {
-                if (root == this._popupHost)
+                return false;
+            }
+
+            var popupHost = _openState.PopupHost;
+
+            IVisual? root = child.VisualRoot;
+            
+            while (root is IHostedVisualTreeRoot hostedRoot)
+            {
+                if (root == popupHost)
+                {
                     return true;
-                root = hostedRoot.Host?.GetVisualRoot();
+                }
+
+                root = hostedRoot.Host?.VisualRoot;
             }
+
             return false;
         }
         
         public bool IsInsidePopup(IVisual visual)
         {
-            return _popupHost != null && ((IVisual)_popupHost)?.IsVisualAncestorOf(visual) == true;
+            if (_openState is null)
+            {
+                return false;
+            }
+
+            var popupHost = _openState.PopupHost;
+
+            return popupHost != null && ((IVisual)popupHost).IsVisualAncestorOf(visual);
         }
 
-        public bool IsPointerOverPopup => ((IInputElement)_popupHost).IsPointerOver;
+        public bool IsPointerOverPopup => ((IInputElement?)_openState?.PopupHost)?.IsPointerOver ?? false;
 
         private void WindowDeactivated(object sender, EventArgs e)
         {
@@ -503,5 +535,36 @@ namespace Avalonia.Controls.Primitives
                 _owner._ignoreIsOpenChanged = false;
             }
         }
+
+        private class PopupOpenState : IDisposable
+        {
+            private readonly IDisposable _cleanup;
+            private IDisposable? _presenterCleanup;
+
+            public PopupOpenState(TopLevel topLevel, IPopupHost popupHost, IDisposable cleanup)
+            {
+                TopLevel = topLevel;
+                PopupHost = popupHost;
+                _cleanup = cleanup;
+            }
+
+            public TopLevel TopLevel { get; }
+
+            public IPopupHost PopupHost { get; }
+
+            public void SetPresenterSubscription(IDisposable? presenterCleanup)
+            {
+                _presenterCleanup?.Dispose();
+
+                _presenterCleanup = presenterCleanup;
+            }
+
+            public void Dispose()
+            {
+                _presenterCleanup?.Dispose();
+
+                _cleanup.Dispose();
+            }
+        }
     }
 }

+ 21 - 3
src/Avalonia.Controls/Primitives/ScrollBar.cs

@@ -55,9 +55,6 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         static ScrollBar()
         {
-            PseudoClass<ScrollBar, Orientation>(OrientationProperty, o => o == Orientation.Vertical, ":vertical");
-            PseudoClass<ScrollBar, Orientation>(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal");
-
             Thumb.DragDeltaEvent.AddClassHandler<ScrollBar>((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble);
             Thumb.DragCompletedEvent.AddClassHandler<ScrollBar>((x, e) => x.OnThumbDragComplete(e), RoutingStrategies.Bubble);
         }
@@ -74,6 +71,7 @@ namespace Avalonia.Controls.Primitives
                 this.GetObservable(VisibilityProperty).Select(_ => Unit.Default))
                 .Select(_ => CalculateIsVisible());
             this.Bind(IsVisibleProperty, isVisible, BindingPriority.Style);
+            UpdatePseudoClasses(Orientation);
         }
 
         /// <summary>
@@ -143,6 +141,20 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
+        protected override void OnPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
+        {
+            base.OnPropertyChanged(property, oldValue, newValue, priority);
+
+            if (property == OrientationProperty)
+            {
+                UpdatePseudoClasses(newValue.GetValueOrDefault<Orientation>());
+            }
+        }
+
         protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
         {
             base.OnTemplateApplied(e);
@@ -252,5 +264,11 @@ namespace Avalonia.Controls.Primitives
         {
             Scroll?.Invoke(this, new ScrollEventArgs(scrollEventType, Value));
         }
+
+        private void UpdatePseudoClasses(Orientation o)
+        {
+            PseudoClasses.Set(":vertical", o == Orientation.Vertical);
+            PseudoClasses.Set(":horizontal", o == Orientation.Horizontal);
+        }
     }
 }

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

@@ -939,8 +939,8 @@ namespace Avalonia.Controls.Primitives
             {
                 var changed = new SelectionChangedEventArgs(
                     SelectionChangedEvent,
-                    added ?? Empty,
-                    removed ?? Empty);
+                    removed ?? Empty,
+                    added ?? Empty);
                 RaiseEvent(changed);
             }
         }
@@ -1055,8 +1055,8 @@ namespace Avalonia.Controls.Primitives
 
                 var e = new SelectionChangedEventArgs(
                     SelectionChangedEvent,
-                    added != -1 ? new[] { ElementAt(Items, added) } : Array.Empty<object>(),
-                    removed?.Select(x => ElementAt(Items, x)).ToArray() ?? Array.Empty<object>());
+                    removed?.Select(x => ElementAt(Items, x)).ToArray() ?? Array.Empty<object>(),
+                    added != -1 ? new[] { ElementAt(Items, added) } : Array.Empty<object>());
                 RaiseEvent(e);
             }
 

+ 18 - 6
src/Avalonia.Controls/Primitives/ToggleButton.cs

@@ -2,8 +2,8 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using Avalonia.Interactivity;
 using Avalonia.Data;
+using Avalonia.Interactivity;
 
 namespace Avalonia.Controls.Primitives
 {
@@ -51,13 +51,14 @@ namespace Avalonia.Controls.Primitives
 
         static ToggleButton()
         {
-            PseudoClass<ToggleButton, bool?>(IsCheckedProperty, c => c == true, ":checked");
-            PseudoClass<ToggleButton, bool?>(IsCheckedProperty, c => c == false, ":unchecked");
-            PseudoClass<ToggleButton, bool?>(IsCheckedProperty, c => c == null, ":indeterminate");
-
             IsCheckedProperty.Changed.AddClassHandler<ToggleButton>((x, e) => x.OnIsCheckedChanged(e));
         }
 
+        public ToggleButton()
+        {
+            UpdatePseudoClasses(IsChecked);
+        }
+
         /// <summary>
         /// Raised when a <see cref="ToggleButton"/> is checked.
         /// </summary>
@@ -91,7 +92,11 @@ namespace Avalonia.Controls.Primitives
         public bool? IsChecked
         {
             get => _isChecked;
-            set => SetAndRaise(IsCheckedProperty, ref _isChecked, value);
+            set 
+            { 
+                SetAndRaise(IsCheckedProperty, ref _isChecked, value);
+                UpdatePseudoClasses(value);
+            }
         }
 
         /// <summary>
@@ -182,5 +187,12 @@ namespace Avalonia.Controls.Primitives
                     break;
             }
         }
+
+        private void UpdatePseudoClasses(bool? isChecked)
+        {
+            PseudoClasses.Set(":checked", isChecked == true);
+            PseudoClasses.Set(":unchecked", isChecked == false);
+            PseudoClasses.Set(":indeterminate", isChecked == null);
+        }
     }
 }

+ 26 - 2
src/Avalonia.Controls/Primitives/Track.cs

@@ -4,6 +4,7 @@
 // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
 
 using System;
+using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Layout;
 using Avalonia.Metadata;
@@ -46,14 +47,17 @@ namespace Avalonia.Controls.Primitives
 
         static Track()
         {
-            PseudoClass<Track, Orientation>(OrientationProperty, o => o == Orientation.Vertical, ":vertical");
-            PseudoClass<Track, Orientation>(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal");
             ThumbProperty.Changed.AddClassHandler<Track>((x,e) => x.ThumbChanged(e));
             IncreaseButtonProperty.Changed.AddClassHandler<Track>((x, e) => x.ButtonChanged(e));
             DecreaseButtonProperty.Changed.AddClassHandler<Track>((x, e) => x.ButtonChanged(e));
             AffectsArrange<Track>(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty);
         }
 
+        public Track()
+        {
+            UpdatePseudoClasses(Orientation);
+        }
+
         public double Minimum
         {
             get { return _minimum; }
@@ -276,6 +280,20 @@ namespace Avalonia.Controls.Primitives
             return arrangeSize;
         }
 
+        protected override void OnPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
+        {
+            base.OnPropertyChanged(property, oldValue, newValue, priority);
+
+            if (property == OrientationProperty)
+            {
+                UpdatePseudoClasses(newValue.GetValueOrDefault<Orientation>());
+            }
+        }
+
         private static void CoerceLength(ref double componentLength, double trackLength)
         {
             if (componentLength < 0)
@@ -433,5 +451,11 @@ namespace Avalonia.Controls.Primitives
                 DecreaseButton.IsVisible = visible;
             }
         }
+
+        private void UpdatePseudoClasses(Orientation o)
+        {
+            PseudoClasses.Set(":vertical", o == Orientation.Vertical);
+            PseudoClasses.Set(":horizontal", o == Orientation.Horizontal);
+        }
     }
 }

+ 49 - 4
src/Avalonia.Controls/ProgressBar.cs

@@ -4,6 +4,7 @@
 
 using System;
 using Avalonia.Controls.Primitives;
+using Avalonia.Data;
 using Avalonia.Layout;
 
 namespace Avalonia.Controls
@@ -16,6 +17,9 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<bool> IsIndeterminateProperty =
             AvaloniaProperty.Register<ProgressBar, bool>(nameof(IsIndeterminate));
 
+        public static readonly StyledProperty<bool> ShowProgressTextProperty =
+            AvaloniaProperty.Register<ProgressBar, bool>(nameof(ShowProgressText));
+
         public static readonly StyledProperty<Orientation> OrientationProperty =
             AvaloniaProperty.Register<ProgressBar, Orientation>(nameof(Orientation), Orientation.Horizontal);
 
@@ -35,20 +39,27 @@ namespace Avalonia.Controls
 
         static ProgressBar()
         {
-            PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Orientation.Vertical, ":vertical");
-            PseudoClass<ProgressBar, Orientation>(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal");
-            PseudoClass<ProgressBar>(IsIndeterminateProperty, ":indeterminate");
-
             ValueProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));
             IsIndeterminateProperty.Changed.AddClassHandler<ProgressBar>((x, e) => x.UpdateIndicatorWhenPropChanged(e));
         }
 
+        public ProgressBar()
+        {
+            UpdatePseudoClasses(IsIndeterminate, Orientation);
+        }
+
         public bool IsIndeterminate
         {
             get => GetValue(IsIndeterminateProperty);
             set => SetValue(IsIndeterminateProperty, value);
         }
 
+        public bool ShowProgressText
+        {
+            get => GetValue(ShowProgressTextProperty);
+            set => SetValue(ShowProgressTextProperty, value);
+        }
+
         public Orientation Orientation
         {
             get => GetValue(OrientationProperty);
@@ -75,6 +86,24 @@ namespace Avalonia.Controls
             return base.ArrangeOverride(finalSize);
         }
 
+        protected override void OnPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
+        {
+            base.OnPropertyChanged(property, oldValue, newValue, priority);
+
+            if (property == IsIndeterminateProperty)
+            {
+                UpdatePseudoClasses(newValue.GetValueOrDefault<bool>(), null);
+            }
+            else if (property == OrientationProperty)
+            {
+                UpdatePseudoClasses(null, newValue.GetValueOrDefault<Orientation>());
+            }
+        }
+
         /// <inheritdoc/>
         protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
         {
@@ -121,5 +150,21 @@ namespace Avalonia.Controls
         {
             UpdateIndicator(Bounds.Size);
         }
+
+        private void UpdatePseudoClasses(
+            bool? isIndeterminate,
+            Orientation? o)
+        {
+            if (isIndeterminate.HasValue)
+            {
+                PseudoClasses.Set(":indeterminate", isIndeterminate.Value);
+            }
+
+            if (o.HasValue)
+            {
+                PseudoClasses.Set(":vertical", o == Orientation.Vertical);
+                PseudoClasses.Set(":horizontal", o == Orientation.Horizontal);
+            }
+        }
     }
 }

+ 2 - 2
src/Avalonia.Controls/Repeater/ItemsRepeaterElementIndexChangedEventArgs.cs

@@ -12,11 +12,11 @@ namespace Avalonia.Controls
     /// </summary>
     public class ItemsRepeaterElementIndexChangedEventArgs : EventArgs
     {
-        internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int newIndex, int oldIndex)
+        internal ItemsRepeaterElementIndexChangedEventArgs(IControl element, int oldIndex, int newIndex)
         {
             Element = element;
-            NewIndex = newIndex;
             OldIndex = oldIndex;
+            NewIndex = newIndex;
         }
 
         /// <summary>

+ 16 - 0
src/Avalonia.Controls/ScrollViewer.cs

@@ -249,6 +249,22 @@ namespace Avalonia.Controls
             set { SetValue(VerticalScrollBarVisibilityProperty, value); }
         }
 
+        /// <summary>
+        /// Scrolls to the top-left corner of the content.
+        /// </summary>
+        public void ScrollToHome()
+        {
+            Offset = new Vector(double.NegativeInfinity, double.NegativeInfinity);
+        }
+
+        /// <summary>
+        /// Scrolls to the bottom-left corner of the content.
+        /// </summary>
+        public void ScrollToEnd()
+        {
+            Offset = new Vector(double.NegativeInfinity, double.PositiveInfinity);
+        }
+
         /// <summary>
         /// Gets a value indicating whether the viewer can scroll horizontally.
         /// </summary>

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

@@ -16,13 +16,13 @@ namespace Avalonia.Controls
         /// Initializes a new instance of the <see cref="SelectionChangedEventArgs"/> class.
         /// </summary>
         /// <param name="routedEvent">The event being raised.</param>
-        /// <param name="addedItems">The items added to the selection.</param>
         /// <param name="removedItems">The items removed from the selection.</param>
-        public SelectionChangedEventArgs(RoutedEvent routedEvent, IList addedItems, IList removedItems)
+        /// <param name="addedItems">The items added to the selection.</param>
+        public SelectionChangedEventArgs(RoutedEvent routedEvent, IList removedItems, IList addedItems)
             : base(routedEvent)
         {
-            AddedItems = addedItems;
             RemovedItems = removedItems;
+            AddedItems = addedItems;
         }
 
         /// <summary>

+ 22 - 2
src/Avalonia.Controls/Slider.cs

@@ -3,6 +3,7 @@
 
 using System;
 using Avalonia.Controls.Primitives;
+using Avalonia.Data;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.Layout;
@@ -43,8 +44,6 @@ namespace Avalonia.Controls
         static Slider()
         {
             OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal);
-            PseudoClass<Slider, Orientation>(OrientationProperty, o => o == Orientation.Vertical, ":vertical");
-            PseudoClass<Slider, Orientation>(OrientationProperty, o => o == Orientation.Horizontal, ":horizontal");
             Thumb.DragStartedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragStarted(e), RoutingStrategies.Bubble);
             Thumb.DragDeltaEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragDelta(e), RoutingStrategies.Bubble);
             Thumb.DragCompletedEvent.AddClassHandler<Slider>((x, e) => x.OnThumbDragCompleted(e), RoutingStrategies.Bubble);
@@ -55,6 +54,7 @@ namespace Avalonia.Controls
         /// </summary>
         public Slider()
         {
+            UpdatePseudoClasses(Orientation);
         }
 
         /// <summary>
@@ -137,6 +137,20 @@ namespace Avalonia.Controls
             }
         }
 
+        protected override void OnPropertyChanged<T>(
+            AvaloniaProperty<T> property,
+            Optional<T> oldValue,
+            BindingValue<T> newValue,
+            BindingPriority priority)
+        {
+            base.OnPropertyChanged(property, oldValue, newValue, priority);
+
+            if (property == OrientationProperty)
+            {
+                UpdatePseudoClasses(newValue.GetValueOrDefault<Orientation>());
+            }
+        }
+
         /// <summary>
         /// Called when user start dragging the <see cref="Thumb"/>.
         /// </summary>
@@ -190,5 +204,11 @@ namespace Avalonia.Controls
 
             return value;
         }
+
+        private void UpdatePseudoClasses(Orientation o)
+        {
+            PseudoClasses.Set(":vertical", o == Orientation.Vertical);
+            PseudoClasses.Set(":horizontal", o == Orientation.Horizontal);
+        }
     }
 }

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

@@ -324,8 +324,8 @@ namespace Avalonia.Controls
             {
                 var changed = new SelectionChangedEventArgs(
                     SelectingItemsControl.SelectionChangedEvent,
-                    added ?? Empty,
-                    removed ?? Empty);
+                    removed ?? Empty,
+                    added ?? Empty);
                 RaiseEvent(changed);
             }
         }

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

@@ -129,7 +129,7 @@ namespace Avalonia.Controls
 
             ShowInTaskbarProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue));
 
-            IconProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetIcon(((WindowIcon)e.NewValue).PlatformImpl));
+            IconProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetIcon(((WindowIcon)e.NewValue)?.PlatformImpl));
 
             CanResizeProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.CanResize((bool)e.NewValue));
 
@@ -529,7 +529,7 @@ namespace Avalonia.Controls
         {
             var sizeToContent = SizeToContent;
             var clientSize = ClientSize;
-            Size constraint = clientSize;
+            var constraint = availableSize;
 
             if ((sizeToContent & SizeToContent.Width) != 0)
             {

+ 38 - 4
src/Avalonia.Input/InputElement.cs

@@ -4,6 +4,8 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Data;
 using Avalonia.Input.GestureRecognizers;
 using Avalonia.Interactivity;
 using Avalonia.VisualTree;
@@ -181,10 +183,11 @@ namespace Avalonia.Input
             PointerReleasedEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerReleased(e));
             PointerCaptureLostEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerCaptureLost(e));
             PointerWheelChangedEvent.AddClassHandler<InputElement>((x, e) => x.OnPointerWheelChanged(e));
+        }
 
-            PseudoClass<InputElement, bool>(IsEffectivelyEnabledProperty, x => !x, ":disabled");
-            PseudoClass<InputElement>(IsFocusedProperty, ":focus");
-            PseudoClass<InputElement>(IsPointerOverProperty, ":pointerover");
+        public InputElement()
+        {
+            UpdatePseudoClasses(IsFocused, IsPointerOver);
         }
 
         /// <summary>
@@ -372,7 +375,11 @@ namespace Avalonia.Input
         public bool IsEffectivelyEnabled
         {
             get => _isEffectivelyEnabled;
-            private set => SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value);
+            private set
+            {
+                SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value);
+                PseudoClasses.Set(":disabled", !value);
+            }
         }
 
         public List<KeyBinding> KeyBindings { get; } = new List<KeyBinding>();
@@ -522,6 +529,20 @@ namespace Avalonia.Input
         {
         }
 
+        protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
+        {
+            base.OnPropertyChanged(property, oldValue, newValue, priority);
+
+            if (property == IsFocusedProperty)
+            {
+                UpdatePseudoClasses(newValue.GetValueOrDefault<bool>(), null);
+            }
+            else if (property == IsPointerOverProperty)
+            {
+                UpdatePseudoClasses(null, newValue.GetValueOrDefault<bool>());
+            }
+        }
+
         /// <summary>
         /// Updates the <see cref="IsEffectivelyEnabled"/> property value according to the parent
         /// control's enabled state and <see cref="IsEnabledCore"/>.
@@ -578,5 +599,18 @@ namespace Avalonia.Input
                 child?.UpdateIsEffectivelyEnabled(this);
             }
         }
+
+        private void UpdatePseudoClasses(bool? isFocused, bool? isPointerOver)
+        {
+            if (isFocused.HasValue)
+            {
+                PseudoClasses.Set(":focus", isFocused.Value);
+            }
+            
+            if (isPointerOver.HasValue)
+            {
+                PseudoClasses.Set(":pointerover", isPointerOver.Value);
+            }
+        }
     }
 }

+ 10 - 2
src/Avalonia.Layout/LayoutManager.cs

@@ -132,8 +132,16 @@ namespace Avalonia.Layout
         /// <inheritdoc/>
         public void ExecuteInitialLayoutPass(ILayoutRoot root)
         {
-            Measure(root);
-            Arrange(root);
+            try
+            {
+                _running = true;
+                Measure(root);
+                Arrange(root);
+            }
+            finally
+            {
+                _running = false;
+            }
 
             // Running the initial layout pass may have caused some control to be invalidated
             // so run a full layout pass now (this usually due to scrollbars; its not known

+ 28 - 0
src/Avalonia.Styling/Controls/PseudoClassesExtensions.cs

@@ -0,0 +1,28 @@
+using System;
+
+namespace Avalonia.Controls
+{
+    public static class PseudolassesExtensions
+    {
+        /// <summary>
+        /// Adds or removes a pseudoclass depending on a boolean value.
+        /// </summary>
+        /// <param name="classes">The pseudoclasses collection.</param>
+        /// <param name="name">The name of the pseudoclass to set.</param>
+        /// <param name="value">True to add the pseudoclass or false to remove.</param>
+        public static void Set(this IPseudoClasses classes, string name, bool value)
+        {
+            Contract.Requires<ArgumentNullException>(classes != null);
+
+            if (value)
+            {
+                classes.Add(name);
+            }
+            else
+            {
+                classes.Remove(name);
+            }
+        }
+
+    }
+}

+ 0 - 77
src/Avalonia.Styling/StyledElement.cs

@@ -488,83 +488,6 @@ namespace Avalonia
             InheritanceParent = parent;
         }
 
-        /// <summary>
-        /// Adds a pseudo-class to be set when a property is true.
-        /// </summary>
-        /// <param name="property">The property.</param>
-        /// <param name="className">The pseudo-class.</param>
-        [Obsolete("Use PseudoClass<TOwner> and specify the control type.")]
-        protected static void PseudoClass(AvaloniaProperty<bool> property, string className)
-        {
-            PseudoClass<StyledElement>(property, className);
-        }
-
-        /// <summary>
-        /// Adds a pseudo-class to be set when a property is true.
-        /// </summary>
-        /// <typeparam name="TOwner">The type to apply the pseudo-class to.</typeparam>
-        /// <param name="property">The property.</param>
-        /// <param name="className">The pseudo-class.</param>
-        protected static void PseudoClass<TOwner>(AvaloniaProperty<bool> property, string className)
-            where TOwner : class, IStyledElement
-        {
-            PseudoClass<TOwner, bool>(property, x => x, className);
-        }
-
-        /// <summary>
-        /// Adds a pseudo-class to be set when a property equals a certain value.
-        /// </summary>
-        /// <typeparam name="TProperty">The type of the property.</typeparam>
-        /// <param name="property">The property.</param>
-        /// <param name="selector">Returns a boolean value based on the property value.</param>
-        /// <param name="className">The pseudo-class.</param>
-        [Obsolete("Use PseudoClass<TOwner, TProperty> and specify the control type.")]
-        protected static void PseudoClass<TProperty>(
-            AvaloniaProperty<TProperty> property,
-            Func<TProperty, bool> selector,
-            string className)
-        {
-            PseudoClass<StyledElement, TProperty>(property, selector, className);
-        }
-
-        /// <summary>
-        /// Adds a pseudo-class to be set when a property equals a certain value.
-        /// </summary>
-        /// <typeparam name="TProperty">The type of the property.</typeparam>
-        /// <typeparam name="TOwner">The type to apply the pseudo-class to.</typeparam>
-        /// <param name="property">The property.</param>
-        /// <param name="selector">Returns a boolean value based on the property value.</param>
-        /// <param name="className">The pseudo-class.</param>
-        protected static void PseudoClass<TOwner, TProperty>(
-            AvaloniaProperty<TProperty> property,
-            Func<TProperty, bool> selector,
-            string className)
-                where TOwner : class, IStyledElement
-        {
-            Contract.Requires<ArgumentNullException>(property != null);
-            Contract.Requires<ArgumentNullException>(selector != null);
-            Contract.Requires<ArgumentNullException>(className != null);
-
-            if (string.IsNullOrWhiteSpace(className))
-            {
-                throw new ArgumentException("Cannot supply an empty className.");
-            }
-
-            property.Changed.Merge(property.Initialized)
-                .Where(e => e.Sender is TOwner)
-                .Subscribe(e =>
-                {
-                    if (selector((TProperty)e.NewValue))
-                    {
-                        ((StyledElement)e.Sender).PseudoClasses.Add(className);
-                    }
-                    else
-                    {
-                        ((StyledElement)e.Sender).PseudoClasses.Remove(className);
-                    }
-                });
-        }
-
         protected virtual void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
         {
             switch (e.Action)

+ 11 - 10
src/Avalonia.Themes.Default/ComboBox.xaml

@@ -4,6 +4,7 @@
     <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
     <Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
     <Setter Property="Padding" Value="4"/>
+    <Setter Property="MinHeight" Value="20"/>
     <Setter Property="Template">
       <ControlTemplate>
         <Border Name="border"
@@ -40,14 +41,14 @@
                    StaysOpen="False">
               <Border BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                       BorderThickness="1">
-                  <ScrollViewer>
-                      <ItemsPresenter Name="PART_ItemsPresenter"
-                                      Items="{TemplateBinding Items}"
-                                      ItemsPanel="{TemplateBinding ItemsPanel}"
-                                      ItemTemplate="{TemplateBinding ItemTemplate}"
-                                      VirtualizationMode="{TemplateBinding VirtualizationMode}"
+                <ScrollViewer>
+                  <ItemsPresenter Name="PART_ItemsPresenter"
+                                  Items="{TemplateBinding Items}"
+                                  ItemsPanel="{TemplateBinding ItemsPanel}"
+                                  ItemTemplate="{TemplateBinding ItemTemplate}"
+                                  VirtualizationMode="{TemplateBinding VirtualizationMode}"
                               />
-                  </ScrollViewer>
+                </ScrollViewer>
               </Border>
             </Popup>
           </Grid>
@@ -58,7 +59,7 @@
   <Style Selector="ComboBox:pointerover /template/ Border#border">
     <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderHighBrush}"/>
   </Style>
-    <Style Selector="ComboBox:disabled /template/ Border#border">
-        <Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}" />
-    </Style>
+  <Style Selector="ComboBox:disabled /template/ Border#border">
+    <Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}" />
+  </Style>
 </Styles>

+ 52 - 34
src/Avalonia.Themes.Default/ProgressBar.xaml

@@ -1,14 +1,25 @@
 <Styles xmlns="https://github.com/avaloniaui">
   <Style Selector="ProgressBar">
     <Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
-    <Setter Property="Foreground" Value="{DynamicResource ThemeAccentBrush}"/> 
+    <Setter Property="Foreground" Value="{DynamicResource ThemeAccentBrush}"/>
     <Setter Property="Template">
       <ControlTemplate>
-        <Border Background="{TemplateBinding Background}"
-                BorderBrush="{TemplateBinding BorderBrush}"
-                BorderThickness="{TemplateBinding BorderThickness}">
-          <Border Name="PART_Indicator" Background="{TemplateBinding Foreground}"/>
-        </Border>
+        <Grid>
+          <Border Background="{TemplateBinding Background}"
+                  BorderBrush="{TemplateBinding BorderBrush}"
+                  BorderThickness="{TemplateBinding BorderThickness}">
+            <Border Name="PART_Indicator" Background="{TemplateBinding Foreground}"/>
+          </Border>
+          <LayoutTransformControl
+                HorizontalAlignment="Center"
+                VerticalAlignment="Center"
+                IsVisible="{Binding ShowProgressText, RelativeSource={RelativeSource TemplatedParent}}"
+                Name="PART_LayoutTransformControl">
+            <TextBlock
+                Foreground="{DynamicResource ThemeForegroundBrush}"
+                Text="{Binding Value, RelativeSource={RelativeSource TemplatedParent}, StringFormat={}{0:0}%}" />
+          </LayoutTransformControl>
+        </Grid>
       </ControlTemplate>
     </Setter>
   </Style>
@@ -22,42 +33,49 @@
   </Style>
   <Style Selector="ProgressBar:horizontal">
     <Setter Property="MinWidth" Value="200"/>
-    <Setter Property="MinHeight" Value="14"/>
+    <Setter Property="MinHeight" Value="16"/>
   </Style>
   <Style Selector="ProgressBar:vertical">
-    <Setter Property="MinWidth" Value="14"/>
+    <Setter Property="MinWidth" Value="16"/>
     <Setter Property="MinHeight" Value="200"/>
   </Style>
+  <Style Selector="ProgressBar:vertical /template/ LayoutTransformControl#PART_LayoutTransformControl">
+    <Setter Property="LayoutTransform">
+      <Setter.Value>
+        <RotateTransform Angle="90"/>
+      </Setter.Value>
+    </Setter>
+  </Style>
   <Style Selector="ProgressBar:horizontal:indeterminate /template/ Border#PART_Indicator">
-      <Style.Animations>
-          <Animation Duration="0:0:3"
-                     IterationCount="Infinite"
-                     Easing="LinearEasing">
-              <KeyFrame Cue="0%">
-                  <Setter Property="TranslateTransform.X"
-                          Value="{Binding IndeterminateStartingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
-              </KeyFrame>
-              <KeyFrame Cue="100%">
-                  <Setter Property="TranslateTransform.X"
-                          Value="{Binding IndeterminateEndingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
-              </KeyFrame>
+    <Style.Animations>
+      <Animation Duration="0:0:3"
+                 IterationCount="Infinite"
+                 Easing="LinearEasing">
+        <KeyFrame Cue="0%">
+          <Setter Property="TranslateTransform.X"
+                  Value="{Binding IndeterminateStartingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
+        </KeyFrame>
+        <KeyFrame Cue="100%">
+          <Setter Property="TranslateTransform.X"
+                  Value="{Binding IndeterminateEndingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
+        </KeyFrame>
       </Animation>
-      </Style.Animations>
+    </Style.Animations>
   </Style>
   <Style Selector="ProgressBar:vertical:indeterminate /template/ Border#PART_Indicator">
-      <Style.Animations>
-          <Animation Duration="0:0:3"
-                     IterationCount="Infinite"
-                     Easing="LinearEasing">
-              <KeyFrame Cue="0%">
-                  <Setter Property="TranslateTransform.Y"
-                          Value="{Binding IndeterminateStartingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
-              </KeyFrame>
-              <KeyFrame Cue="100%">
-                  <Setter Property="TranslateTransform.Y"
-                          Value="{Binding IndeterminateEndingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
-              </KeyFrame>
+    <Style.Animations>
+      <Animation Duration="0:0:3"
+                 IterationCount="Infinite"
+                 Easing="LinearEasing">
+        <KeyFrame Cue="0%">
+          <Setter Property="TranslateTransform.Y"
+                  Value="{Binding IndeterminateStartingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
+        </KeyFrame>
+        <KeyFrame Cue="100%">
+          <Setter Property="TranslateTransform.Y"
+                  Value="{Binding IndeterminateEndingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
+        </KeyFrame>
       </Animation>
-      </Style.Animations>
+    </Style.Animations>
   </Style>
 </Styles>

+ 12 - 5
src/Avalonia.X11/X11Window.cs

@@ -995,11 +995,18 @@ namespace Avalonia.X11
 
         public void SetIcon(IWindowIconImpl icon)
         {
-            var data = ((X11IconData)icon).Data;
-            fixed (void* pdata = data)
-                XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_ICON,
-                    new IntPtr((int)Atom.XA_CARDINAL), 32, PropertyMode.Replace,
-                    pdata, data.Length);
+            if (icon != null)
+            {
+                var data = ((X11IconData)icon).Data;
+                fixed (void* pdata = data)
+                    XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_ICON,
+                        new IntPtr((int)Atom.XA_CARDINAL), 32, PropertyMode.Replace,
+                        pdata, data.Length);
+            }
+            else
+            {
+                XDeleteProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_ICON);
+            }
         }
 
         public void ShowTaskbarIcon(bool value)

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

@@ -942,7 +942,7 @@ namespace Avalonia.Win32
         public void SetIcon(IWindowIconImpl icon)
         {
             var impl = (IconImpl)icon;
-            var hIcon = impl.HIcon;
+            var hIcon = impl?.HIcon ?? IntPtr.Zero;
             UnmanagedMethods.PostMessage(_hwnd, (int)UnmanagedMethods.WindowsMessage.WM_SETICON,
                 new IntPtr((int)UnmanagedMethods.Icons.ICON_BIG), hIcon);
         }

+ 0 - 17
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Attached.cs

@@ -16,19 +16,6 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal("foodefault", target.GetValue(Class2.FooProperty));
         }
 
-        [Fact]
-        public void AvaloniaProperty_Initialized_Is_Called_For_Attached_Property()
-        {
-            bool raised = false;
-
-            using (Class1.FooProperty.Initialized.Subscribe(x => raised = true))
-            {
-                new Class3();
-            }
-
-            Assert.True(raised);
-        }
-
         private class Base : AvaloniaObject
         {
         }
@@ -46,9 +33,5 @@ namespace Avalonia.Base.UnitTests
             public static readonly AttachedProperty<string> FooProperty =
                 Class1.FooProperty.AddOwner<Class2>();
         }
-
-        private class Class3 : Base
-        {
-        }
     }
 }

+ 105 - 0
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@@ -132,6 +132,111 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal("foo", target.GetValue(property));
         }
 
+        [Fact]
+        public void Completing_LocalValue_Binding_Raises_PropertyChanged()
+        {
+            var target = new Class1();
+            var source = new BehaviorSubject<BindingValue<string>>("foo");
+            var property = Class1.FooProperty;
+            var raised = 0;
+
+            target.Bind(property, source);
+            Assert.Equal("foo", target.GetValue(property));
+
+            target.PropertyChanged += (s, e) =>
+            {
+                Assert.Equal(BindingPriority.Unset, e.Priority);
+                Assert.Equal(property, e.Property);
+                Assert.Equal("foo", e.OldValue as string);
+                Assert.Equal("foodefault", e.NewValue as string);
+                ++raised;
+            };
+
+            source.OnCompleted();
+
+            Assert.Equal("foodefault", target.GetValue(property));
+            Assert.Equal(1, raised);
+        }
+
+        [Fact]
+        public void Completing_Style_Binding_Raises_PropertyChanged()
+        {
+            var target = new Class1();
+            var source = new BehaviorSubject<BindingValue<string>>("foo");
+            var property = Class1.FooProperty;
+            var raised = 0;
+
+            target.Bind(property, source, BindingPriority.Style);
+            Assert.Equal("foo", target.GetValue(property));
+
+            target.PropertyChanged += (s, e) =>
+            {
+                Assert.Equal(BindingPriority.Unset, e.Priority);
+                Assert.Equal(property, e.Property);
+                Assert.Equal("foo", e.OldValue as string);
+                Assert.Equal("foodefault", e.NewValue as string);
+                ++raised;
+            };
+
+            source.OnCompleted();
+
+            Assert.Equal("foodefault", target.GetValue(property));
+            Assert.Equal(1, raised);
+        }
+
+        [Fact]
+        public void Completing_LocalValue_Binding_With_Style_Binding_Raises_PropertyChanged()
+        {
+            var target = new Class1();
+            var source = new BehaviorSubject<BindingValue<string>>("foo");
+            var property = Class1.FooProperty;
+            var raised = 0;
+
+            target.Bind(property, new BehaviorSubject<string>("bar"), BindingPriority.Style);
+            target.Bind(property, source);
+            Assert.Equal("foo", target.GetValue(property));
+
+            target.PropertyChanged += (s, e) =>
+            {
+                Assert.Equal(BindingPriority.Style, e.Priority);
+                Assert.Equal(property, e.Property);
+                Assert.Equal("foo", e.OldValue as string);
+                Assert.Equal("bar", e.NewValue as string);
+                ++raised;
+            };
+
+            source.OnCompleted();
+
+            Assert.Equal("bar", target.GetValue(property));
+            Assert.Equal(1, raised);
+        }
+
+        [Fact]
+        public void Disposing_LocalValue_Binding_Raises_PropertyChanged()
+        {
+            var target = new Class1();
+            var source = new BehaviorSubject<BindingValue<string>>("foo");
+            var property = Class1.FooProperty;
+            var raised = 0;
+
+            var sub = target.Bind(property, source);
+            Assert.Equal("foo", target.GetValue(property));
+
+            target.PropertyChanged += (s, e) =>
+            {
+                Assert.Equal(BindingPriority.Unset, e.Priority);
+                Assert.Equal(property, e.Property);
+                Assert.Equal("foo", e.OldValue as string);
+                Assert.Equal("foodefault", e.NewValue as string);
+                ++raised;
+            };
+
+            sub.Dispose();
+
+            Assert.Equal("foodefault", target.GetValue(property));
+            Assert.Equal(1, raised);
+        }
+
         [Fact]
         public void Setting_Style_Value_Overrides_Binding_Permanently()
         {

+ 1 - 16
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs

@@ -188,6 +188,7 @@ namespace Avalonia.Base.UnitTests
             target.PropertyChanged += (s, e) =>
             {
                 Assert.Same(target, s);
+                Assert.Equal(BindingPriority.LocalValue, e.Priority);
                 Assert.Equal(Class1.FooProperty, e.Property);
                 Assert.Equal("newvalue", (string)e.OldValue);
                 Assert.Equal("unset", (string)e.NewValue);
@@ -424,22 +425,6 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal("second", target.Foo);
         }
 
-        [Fact]
-        public void Property_Notifies_Initialized()
-        {
-            bool raised = false;
-
-            Class1.FooProperty.Initialized.Subscribe(e =>
-                raised = e.Property == Class1.FooProperty &&
-                         e.OldValue == AvaloniaProperty.UnsetValue &&
-                         (string)e.NewValue == "initial" &&
-                         e.Priority == BindingPriority.Unset);
-
-            var target = new Class1();
-
-            Assert.True(raised);
-        }
-
         [Fact]
         public void Binding_Error_Reverts_To_Default_Value()
         {

+ 1 - 0
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs

@@ -30,6 +30,7 @@ namespace Avalonia.Base.UnitTests
             target.PropertyChanged += (s, e) =>
             {
                 Assert.Same(target, s);
+                Assert.Equal(BindingPriority.Unset, e.Priority);
                 Assert.Equal(Class1.FooProperty, e.Property);
                 Assert.Equal("newvalue", (string)e.OldValue);
                 Assert.Equal("foodefault", (string)e.NewValue);

+ 0 - 23
tests/Avalonia.Base.UnitTests/AvaloniaPropertyTests.cs

@@ -78,24 +78,6 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal(BindingMode.TwoWay, result.DefaultBindingMode);
         }
 
-        [Fact]
-        public void Initialized_Observable_Fired()
-        {
-            bool invoked = false;
-
-            Class1.FooProperty.Initialized.Subscribe(x =>
-            {
-                Assert.Equal(AvaloniaProperty.UnsetValue, x.OldValue);
-                Assert.Equal("default", x.NewValue);
-                Assert.Equal(BindingPriority.Unset, x.Priority);
-                invoked = true;
-            });
-
-            var target = new Class1();
-
-            Assert.True(invoked);
-        }
-
         [Fact]
         public void Changed_Observable_Fired()
         {
@@ -141,11 +123,6 @@ namespace Avalonia.Base.UnitTests
                 OverrideMetadata(typeof(T), metadata);
             }
 
-            internal override void NotifyInitialized(IAvaloniaObject o)
-            {
-                throw new NotImplementedException();
-            }
-
             internal override IDisposable RouteBind(
                 IAvaloniaObject o,
                 IObservable<BindingValue<object>> source,

+ 0 - 22
tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs

@@ -1,33 +1,12 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
-using System;
-using System.Reactive.Subjects;
-using Avalonia.Data;
 using Xunit;
 
 namespace Avalonia.Base.UnitTests
 {
     public class DirectPropertyTests
     {
-        [Fact]
-        public void Initialized_Observable_Fired()
-        {
-            bool invoked = false;
-
-            Class1.FooProperty.Initialized.Subscribe(x =>
-            {
-                Assert.Equal(AvaloniaProperty.UnsetValue, x.OldValue);
-                Assert.Equal("foo", x.NewValue);
-                Assert.Equal(BindingPriority.Unset, x.Priority);
-                invoked = true;
-            });
-
-            var target = new Class1();
-
-            Assert.True(invoked);
-        }
-
         [Fact]
         public void IsDirect_Property_Returns_True()
         {
@@ -69,7 +48,6 @@ namespace Avalonia.Base.UnitTests
             var p2 = p1.AddOwner<Class2>(o => null, (o, v) => { });
 
             Assert.Same(p1.Changed, p2.Changed);
-            Assert.Same(p1.Initialized, p2.Initialized);
         }
 
         private class Class1 : AvaloniaObject

+ 1 - 1
tests/Avalonia.Benchmarks/Base/AvaloniaObjectInitializationBenchmark.cs

@@ -6,7 +6,7 @@ namespace Avalonia.Benchmarks.Base
     [MemoryDiagnoser]
     public class AvaloniaObjectInitializationBenchmark
     {
-        [Benchmark(OperationsPerInvoke = 1000)]
+        [Benchmark]
         public Button InitializeButton()
         {
             return new Button();

+ 46 - 0
tests/Avalonia.Benchmarks/Layout/CalendarBenchmark.cs

@@ -0,0 +1,46 @@
+using System;
+using System.Runtime.CompilerServices;
+using Avalonia.Controls;
+using Avalonia.UnitTests;
+using BenchmarkDotNet.Attributes;
+
+namespace Avalonia.Benchmarks.Layout
+{
+    [MemoryDiagnoser]
+    public class CalendarBenchmark : IDisposable
+    {
+        private readonly IDisposable _app;
+        private readonly TestRoot _root;
+
+        public CalendarBenchmark()
+        {
+            _app = UnitTestApplication.Start(
+                TestServices.StyledWindow.With(
+                    renderInterface: new NullRenderingPlatform(),
+                    threadingInterface: new NullThreadingPlatform()));
+
+            _root = new TestRoot(true, null)
+            {
+                Renderer = new NullRenderer()
+            };
+
+            _root.LayoutManager.ExecuteInitialLayoutPass(_root);
+        }
+
+        [Benchmark]
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        public void CreateCalendar()
+        {
+            var calendar = new Calendar();
+
+            _root.Child = calendar;
+
+            _root.LayoutManager.ExecuteLayoutPass();
+        }
+
+        public void Dispose()
+        {
+            _app.Dispose();
+        }
+    }
+}

+ 36 - 0
tests/Avalonia.Benchmarks/NullFormattedTextImpl.cs

@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using Avalonia.Media;
+using Avalonia.Platform;
+
+namespace Avalonia.Benchmarks
+{
+    internal class NullFormattedTextImpl : IFormattedTextImpl
+    {
+        public Size Constraint { get; }
+
+        public Rect Bounds { get; }
+
+        public string Text { get; }
+
+        public IEnumerable<FormattedTextLine> GetLines()
+        {
+            throw new NotImplementedException();
+        }
+
+        public TextHitTestResult HitTestPoint(Point point)
+        {
+            throw new NotImplementedException();
+        }
+
+        public Rect HitTestTextPosition(int index)
+        {
+            throw new NotImplementedException();
+        }
+
+        public IEnumerable<Rect> HitTestTextRange(int index, int length)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 78 - 0
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Avalonia.Media;
+using Avalonia.Platform;
+using Avalonia.UnitTests;
+
+namespace Avalonia.Benchmarks
+{
+    internal class NullRenderingPlatform : IPlatformRenderInterface
+    {
+        public IFormattedTextImpl CreateFormattedText(string text, Typeface typeface, double fontSize, TextAlignment textAlignment,
+            TextWrapping wrapping, Size constraint, IReadOnlyList<FormattedTextStyleSpan> spans)
+        {
+            return new NullFormattedTextImpl();
+        }
+
+        public IGeometryImpl CreateEllipseGeometry(Rect rect)
+        {
+            throw new NotImplementedException();
+        }
+
+        public IGeometryImpl CreateLineGeometry(Point p1, Point p2)
+        {
+            throw new NotImplementedException();
+        }
+
+        public IGeometryImpl CreateRectangleGeometry(Rect rect)
+        {
+            throw new NotImplementedException();
+        }
+
+        public IStreamGeometryImpl CreateStreamGeometry()
+        {
+            return new MockStreamGeometryImpl();
+        }
+
+        public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
+        {
+            throw new NotImplementedException();
+        }
+
+        public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi)
+        {
+            throw new NotImplementedException();
+        }
+
+        public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, PixelFormat? format = null)
+        {
+            throw new NotImplementedException();
+        }
+
+        public IBitmapImpl LoadBitmap(string fileName)
+        {
+            throw new NotImplementedException();
+        }
+
+        public IBitmapImpl LoadBitmap(Stream stream)
+        {
+            throw new NotImplementedException();
+        }
+
+        public IBitmapImpl LoadBitmap(PixelFormat format, IntPtr data, PixelSize size, Vector dpi, int stride)
+        {
+            throw new NotImplementedException();
+        }
+
+        public IFontManagerImpl CreateFontManager()
+        {
+            return new MockFontManagerImpl();
+        }
+
+        public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 28 - 0
tests/Avalonia.Benchmarks/NullThreadingPlatform.cs

@@ -0,0 +1,28 @@
+using System;
+using System.Reactive.Disposables;
+using System.Threading;
+using Avalonia.Platform;
+using Avalonia.Threading;
+
+namespace Avalonia.Benchmarks
+{
+    internal class NullThreadingPlatform : IPlatformThreadingInterface
+    {
+        public void RunLoop(CancellationToken cancellationToken)
+        {
+        }
+
+        public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
+        {
+            return Disposable.Empty;
+        }
+
+        public void Signal(DispatcherPriority priority)
+        {
+        }
+
+        public bool CurrentThreadIsLoopThread => true;
+
+        public event Action<DispatcherPriority?> Signaled;
+    }
+}

+ 23 - 23
tests/Avalonia.Controls.UnitTests/CarouselTests.cs

@@ -51,10 +51,9 @@ namespace Avalonia.Controls.UnitTests
 
             Assert.Single(target.GetLogicalChildren());
 
-            var child = target.GetLogicalChildren().Single();
+            var child = GetContainerTextBlock(target.GetLogicalChildren().Single());
 
-            Assert.IsType<TextBlock>(child);
-            Assert.Equal("Foo", ((TextBlock)child).Text);
+            Assert.Equal("Foo", child.Text);
         }
 
         [Fact]
@@ -98,20 +97,18 @@ namespace Avalonia.Controls.UnitTests
 
             Assert.Equal(3, target.GetLogicalChildren().Count());
 
-            var child = target.GetLogicalChildren().First();
+            var child = GetContainerTextBlock(target.GetLogicalChildren().First());
 
-            Assert.IsType<TextBlock>(child);
-            Assert.Equal("Foo", ((TextBlock)child).Text);
+            Assert.Equal("Foo", child.Text);
 
             var newItems = items.ToList();
             newItems.RemoveAt(0);
 
             target.Items = newItems;
 
-            child = target.GetLogicalChildren().First();
+            child = GetContainerTextBlock(target.GetLogicalChildren().First());
 
-            Assert.IsType<TextBlock>(child);
-            Assert.Equal("Bar", ((TextBlock)child).Text);
+            Assert.Equal("Bar", child.Text);
         }
 
         [Fact]
@@ -136,20 +133,18 @@ namespace Avalonia.Controls.UnitTests
 
             Assert.Single(target.GetLogicalChildren());
 
-            var child = target.GetLogicalChildren().Single();
+            var child = GetContainerTextBlock(target.GetLogicalChildren().Single());
 
-            Assert.IsType<TextBlock>(child);
-            Assert.Equal("Foo", ((TextBlock)child).Text);
+            Assert.Equal("Foo", child.Text);
 
             var newItems = items.ToList();
             newItems.RemoveAt(0);
 
             target.Items = newItems;
 
-            child = target.GetLogicalChildren().Single();
+            child = GetContainerTextBlock(target.GetLogicalChildren().Single());
 
-            Assert.IsType<TextBlock>(child);
-            Assert.Equal("Bar", ((TextBlock)child).Text);
+            Assert.Equal("Bar", child.Text);
         }
 
         [Fact]
@@ -197,10 +192,9 @@ namespace Avalonia.Controls.UnitTests
 
             Assert.Equal(3, target.GetLogicalChildren().Count());
 
-            var child = target.GetLogicalChildren().First();
+            var child = GetContainerTextBlock(target.GetLogicalChildren().First());
 
-            Assert.IsType<TextBlock>(child);
-            Assert.Equal("Foo", ((TextBlock)child).Text);
+            Assert.Equal("Foo", child.Text);
 
             target.Items = null;
 
@@ -233,7 +227,7 @@ namespace Avalonia.Controls.UnitTests
 
             Assert.Equal("FooBar", target.SelectedItem);
 
-            var child = target.GetVisualDescendants().LastOrDefault();
+            var child = GetContainerTextBlock(target.GetVisualDescendants().LastOrDefault());
 
             Assert.IsType<TextBlock>(child);
             Assert.Equal("FooBar", ((TextBlock)child).Text);
@@ -261,14 +255,13 @@ namespace Avalonia.Controls.UnitTests
 
             Assert.Equal(3, target.GetLogicalChildren().Count());
 
-            var child = target.GetLogicalChildren().First();
+            var child = GetContainerTextBlock(target.GetLogicalChildren().First());
 
-            Assert.IsType<TextBlock>(child);
-            Assert.Equal("Foo", ((TextBlock)child).Text);
+            Assert.Equal("Foo", child.Text);
 
             items.RemoveAt(0);
 
-            child = target.GetLogicalChildren().First();
+            child = GetContainerTextBlock(target.GetLogicalChildren().First());
 
             Assert.IsType<TextBlock>(child);
             Assert.Equal("Bar", ((TextBlock)child).Text);
@@ -314,5 +307,12 @@ namespace Avalonia.Controls.UnitTests
                 [~CarouselPresenter.PageTransitionProperty] = control[~Carousel.PageTransitionProperty],
             }.RegisterInNameScope(scope);
         }
+
+        private static TextBlock GetContainerTextBlock(object control)
+        {
+            var contentPresenter = Assert.IsType<ContentPresenter>(control);
+            contentPresenter.UpdateChild();
+            return Assert.IsType<TextBlock>(contentPresenter.Child);
+        }
     }
 }

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

@@ -108,5 +108,36 @@ namespace Avalonia.Controls.UnitTests
                 };
             });
         }
+
+        [Fact]
+        public void Detaching_Closed_ComboBox_Keeps_Current_Focus()
+        {
+            using (UnitTestApplication.Start(TestServices.RealFocus))
+            {
+                var target = new ComboBox
+                {
+                    Items = new[] { new Canvas() },
+                    SelectedIndex = 0,
+                    Template = GetTemplate(),
+                };
+
+                var other = new Control { Focusable = true };
+
+                StackPanel panel;
+
+                var root = new TestRoot { Child = panel = new StackPanel { Children = { target, other } } };
+
+                target.ApplyTemplate();
+                target.Presenter.ApplyTemplate();
+
+                other.Focus();
+
+                Assert.True(other.IsFocused);
+
+                panel.Children.Remove(target);
+
+                Assert.True(other.IsFocused);
+            }
+        }
     }
 }

+ 28 - 4
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@@ -66,7 +66,7 @@ namespace Avalonia.Controls.UnitTests
         }
 
         [Fact]
-        public void Container_Child_Should_Have_LogicalParent_Set_To_Container()
+        public void Container_Should_Have_LogicalParent_Set_To_ItemsControl()
         {
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
@@ -87,7 +87,7 @@ namespace Avalonia.Controls.UnitTests
 
                 var container = (ContentPresenter)target.Presenter.Panel.Children[0];
 
-                Assert.Equal(container, container.Child.Parent);
+                Assert.Equal(target, container.Parent);
             }
         }
 
@@ -190,7 +190,7 @@ namespace Avalonia.Controls.UnitTests
         }
 
         [Fact]
-        public void Adding_String_Item_Should_Make_TextBlock_Appear_In_LogicalChildren()
+        public void Adding_String_Item_Should_Make_ContentPresenter_Appear_In_LogicalChildren()
         {
             var target = new ItemsControl();
             var child = new Control();
@@ -202,7 +202,7 @@ namespace Avalonia.Controls.UnitTests
 
             var logical = (ILogical)target;
             Assert.Equal(1, logical.LogicalChildren.Count);
-            Assert.IsType<TextBlock>(logical.LogicalChildren[0]);
+            Assert.IsType<ContentPresenter>(logical.LogicalChildren[0]);
         }
 
         [Fact]
@@ -575,6 +575,30 @@ namespace Avalonia.Controls.UnitTests
             };
         }
 
+        [Fact]
+        public void Detaching_Then_Reattaching_To_Logical_Tree_Twice_Does_Not_Throw()
+        {
+            // # Issue 3487
+            var target = new ItemsControl
+            {
+                Template = GetTemplate(),
+                Items = new[] { "foo", "bar" },
+                ItemTemplate = new FuncDataTemplate<string>((_, __) => new Canvas()),
+            };
+
+            var root = new TestRoot(target);
+            root.Measure(Size.Infinity);
+            root.Arrange(new Rect(root.DesiredSize));
+
+            root.Child = null;
+            root.Child = target;
+
+            target.Measure(Size.Infinity);
+
+            root.Child = null;
+            root.Child = target;
+        }
+
         private class Item
         {
             public Item(string value)

+ 27 - 1
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@@ -223,7 +223,33 @@ namespace Avalonia.Controls.UnitTests.Primitives
             }
         }
 
-        
+        [Fact]
+        public void Popup_Close_On_Closed_Popup_Should_Not_Raise_Closed_Event()
+        {
+            using (CreateServices())
+            {
+                var window = PreparedWindow();
+                var target = new Popup() { PlacementMode = PlacementMode.Pointer };
+
+                window.Content = target;
+                window.ApplyTemplate();
+                
+                int closedCount = 0;
+
+                target.Closed += (sender, args) =>
+                {
+                    closedCount++;
+                };
+
+                target.Close();
+                target.Close();
+                target.Close();
+                target.Close();
+
+                Assert.Equal(0, closedCount);
+            }
+        }
+
         [Fact]
         public void Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent()
         {

+ 22 - 0
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@@ -1202,6 +1202,28 @@ namespace Avalonia.Controls.UnitTests.Primitives
             target.MoveSelection(NavigationDirection.Next, true);
         }
 
+        [Fact]
+        public void MoveSelection_Does_Select_Disabled_Controls()
+        {
+            // Issue #3426.
+            var target = new TestSelector
+            {
+                Template = Template(),
+                Items = new[]
+                {
+                    new ListBoxItem(),
+                    new ListBoxItem { IsEnabled = false },
+                },
+                SelectedIndex = 0,
+            };
+
+            target.Measure(new Size(100, 100));
+            target.Arrange(new Rect(0, 0, 100, 100));
+            target.MoveSelection(NavigationDirection.Next, true);
+
+            Assert.Equal(0, target.SelectedIndex);
+        }
+
         [Fact]
         public void Pre_Selecting_Item_Should_Set_Selection_After_It_Was_Added_When_AlwaysSelected()
         {

+ 24 - 0
tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs

@@ -65,6 +65,30 @@ namespace Avalonia.Controls.UnitTests
             Assert.Equal(new Vector(10, 10), target.Offset);
         }
 
+        [Fact]
+        public void Test_ScrollToHome()
+        {
+            var target = new ScrollViewer();
+            target.SetValue(ScrollViewer.ExtentProperty, new Size(50, 50));
+            target.SetValue(ScrollViewer.ViewportProperty, new Size(10, 10));
+            target.Offset = new Vector(25, 25);
+            target.ScrollToHome();
+
+            Assert.Equal(new Vector(0, 0), target.Offset);
+        }
+
+        [Fact]
+        public void Test_ScrollToEnd()
+        {
+            var target = new ScrollViewer();
+            target.SetValue(ScrollViewer.ExtentProperty, new Size(50, 50));
+            target.SetValue(ScrollViewer.ViewportProperty, new Size(10, 10));
+            target.Offset = new Vector(25, 25);
+            target.ScrollToEnd();
+
+            Assert.Equal(new Vector(0, 40), target.Offset);
+        }
+
         private Control CreateTemplate(ScrollViewer control, INameScope scope)
         {
             return new Grid

+ 51 - 0
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@@ -341,11 +341,62 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void Child_Should_Be_Measured_With_Width_And_Height_If_SizeToContent_Is_Manual()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var child = new ChildControl();
+                var target = new Window 
+                { 
+                    Width = 100,
+                    Height = 50,
+                    SizeToContent = SizeToContent.Manual,
+                    Content = child 
+                };
+
+                target.Show();
+
+                Assert.Equal(new Size(100, 50), child.MeasureSize);
+            }
+        }
+
+        [Fact]
+        public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var child = new ChildControl();
+                var target = new Window
+                {
+                    Width = 100,
+                    Height = 50,
+                    SizeToContent = SizeToContent.WidthAndHeight,
+                    Content = child
+                };
+
+                target.Show();
+
+                Assert.Equal(Size.Infinity, child.MeasureSize);
+            }
+        }
+
         private IWindowImpl CreateImpl(Mock<IRenderer> renderer)
         {
             return Mock.Of<IWindowImpl>(x =>
                 x.Scaling == 1 &&
                 x.CreateRenderer(It.IsAny<IRenderRoot>()) == renderer.Object);
         }
+
+        private class ChildControl : Control
+        {
+            public Size MeasureSize { get; private set; }
+
+            protected override Size MeasureOverride(Size availableSize)
+            {
+                MeasureSize = availableSize;
+                return base.MeasureOverride(availableSize);
+            }
+        }
     }
 }

+ 33 - 0
tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs

@@ -374,5 +374,38 @@ namespace Avalonia.Layout.UnitTests
             Assert.True(control.Measured);
             Assert.True(control.IsMeasureValid);
         }
+
+        [Fact]
+        public void Calling_ExecuteLayoutPass_From_ExecuteInitialLayoutPass_Does_Not_Break_Measure()
+        {
+            // Test for issue #3550.
+            var control = new LayoutTestControl();
+            var root = new LayoutTestRoot { Child = control };
+            var count = 0;
+
+            root.LayoutManager.ExecuteInitialLayoutPass(root);
+            control.Measured = false;
+
+            control.DoMeasureOverride = (l, s) =>
+            {
+                if (count++ == 0)
+                {
+                    control.InvalidateMeasure();
+                    root.LayoutManager.ExecuteLayoutPass();
+                    return new Size(100, 100);
+                }
+                else
+                {
+                    return new Size(200, 200);
+                }
+            };
+
+            root.InvalidateMeasure();
+            control.InvalidateMeasure();
+            root.LayoutManager.ExecuteInitialLayoutPass(root);
+
+            Assert.Equal(new Size(200, 200), control.Bounds.Size);
+            Assert.Equal(new Size(200, 200), control.DesiredSize);
+        }
     }
 }

+ 52 - 0
tests/Avalonia.LeakTests/ControlTests.cs

@@ -8,8 +8,10 @@ using Avalonia.Controls;
 using Avalonia.Controls.Templates;
 using Avalonia.Diagnostics;
 using Avalonia.Layout;
+using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Rendering;
+using Avalonia.Styling;
 using Avalonia.UnitTests;
 using Avalonia.VisualTree;
 using JetBrains.dotMemoryUnit;
@@ -370,6 +372,56 @@ namespace Avalonia.LeakTests
             }
         }
 
+        [Fact]
+        public void Control_With_Style_RenderTransform_Is_Freed()
+        {
+            // # Issue #3545
+            using (Start())
+            {
+                Func<Window> run = () =>
+                {
+                    var window = new Window
+                    {
+                        Styles =
+                        {
+                            new Style(x => x.OfType<Canvas>())
+                            {
+                                Setters =
+                                {
+                                    new Setter
+                                    {
+                                        Property = Visual.RenderTransformProperty,
+                                        Value = new RotateTransform(45),
+                                    }
+                                }
+                            }
+                        },
+                        Content = new Canvas()
+                    };
+
+                    window.Show();
+
+                    // Do a layout and make sure that Canvas gets added to visual tree with
+                    // its render transform.
+                    window.LayoutManager.ExecuteInitialLayoutPass(window);
+                    var canvas = Assert.IsType<Canvas>(window.Presenter.Child);
+                    Assert.IsType<RotateTransform>(canvas.RenderTransform);
+
+                    // Clear the content and ensure the Canvas is removed.
+                    window.Content = null;
+                    window.LayoutManager.ExecuteLayoutPass();
+                    Assert.Null(window.Presenter.Child);
+
+                    return window;
+                };
+
+                var result = run();
+
+                dotMemory.Check(memory =>
+                    Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Canvas>()).ObjectsCount));
+            }
+        }
+
         private IDisposable Start()
         {
             return UnitTestApplication.Start(TestServices.StyledWindow);