Преглед на файлове

Merge branch 'master' of https://github.com/AvaloniaUI/Avalonia into compiled-bindings

Jeremy Koritzinsky преди 6 години
родител
ревизия
c4fe5b957e
променени са 35 файла, в които са добавени 416 реда и са изтрити 432 реда
  1. 3 1
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  2. 20 1
      samples/ControlCatalog/Pages/ListBoxPage.xaml.cs
  3. 42 8
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  4. 25 7
      src/Avalonia.Controls/ContentControl.cs
  5. 0 13
      src/Avalonia.Controls/ItemsControl.cs
  6. 5 1
      src/Avalonia.Controls/ListBox.cs
  7. 0 166
      src/Avalonia.Controls/Mixins/ContentControlMixin.cs
  8. 5 0
      src/Avalonia.Controls/Notifications/ReversibleStackPanel.cs
  9. 1 1
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  10. 12 35
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  11. 0 13
      src/Avalonia.Controls/Presenters/IContentPresenter.cs
  12. 12 1
      src/Avalonia.Controls/Presenters/IContentPresenterHost.cs
  13. 7 4
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  14. 20 6
      src/Avalonia.Controls/Primitives/HeaderedContentControl.cs
  15. 25 7
      src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs
  16. 25 7
      src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs
  17. 10 10
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  18. 12 13
      src/Avalonia.Controls/RadioButton.cs
  19. 1 1
      src/Avalonia.Controls/StackPanel.cs
  20. 28 8
      src/Avalonia.Controls/TabControl.cs
  21. 4 1
      src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs
  22. 0 9
      src/Avalonia.Input/AccessKeyHandler.cs
  23. 10 2
      src/Avalonia.Input/InputElement.cs
  24. 1 0
      tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
  25. 40 0
      tests/Avalonia.Controls.UnitTests/ContentControlTests.cs
  26. 9 3
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  27. 0 107
      tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs
  28. 13 0
      tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs
  29. 2 0
      tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
  30. 47 0
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  31. 25 0
      tests/Avalonia.Controls.UnitTests/StackPanelTests.cs
  32. 6 6
      tests/Avalonia.Controls.UnitTests/TabControlTests.cs
  33. 2 1
      tests/Avalonia.Controls.UnitTests/TopLevelTests.cs
  34. 1 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs
  35. 3 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs

+ 3 - 1
samples/ControlCatalog/Pages/ListBoxPage.xaml

@@ -10,12 +10,14 @@
               HorizontalAlignment="Center"
               Spacing="16">
       <StackPanel Orientation="Vertical" Spacing="8">
-        <ListBox Items="{Binding Items}" SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350"></ListBox>
+        <ListBox Items="{Binding Items}" SelectedItem="{Binding SelectedItem}" AutoScrollToSelectedItem="True"  SelectedItems="{Binding SelectedItems}" SelectionMode="{Binding SelectionMode}" Width="250" Height="350"></ListBox>
 
         <Button Command="{Binding AddItemCommand}">Add</Button>
 
         <Button Command="{Binding RemoveItemCommand}">Remove</Button>
 
+        <Button Command="{Binding SelectRandomItemCommand}">Select Random Item</Button>
+
         <ComboBox SelectedIndex="{Binding SelectionMode, Mode=TwoWay}">
           <ComboBoxItem>Single</ComboBoxItem>
           <ComboBoxItem>Multiple</ComboBoxItem>

+ 20 - 1
samples/ControlCatalog/Pages/ListBoxPage.xaml.cs

@@ -1,3 +1,4 @@
+using System;
 using System.Collections.ObjectModel;
 using System.Linq;
 using System.Reactive;
@@ -27,7 +28,7 @@ namespace ControlCatalog.Pages
 
             public PageViewModel()
             {
-                Items = new ObservableCollection<string>(Enumerable.Range(1, 10).Select(i => GenerateItem()));
+                Items = new ObservableCollection<string>(Enumerable.Range(1, 10000).Select(i => GenerateItem()));
                 SelectedItems = new ObservableCollection<string>();
 
                 AddItemCommand = ReactiveCommand.Create(() => Items.Add(GenerateItem()));
@@ -39,16 +40,34 @@ namespace ControlCatalog.Pages
                         Items.Remove(SelectedItems[0]);
                     }
                 });
+
+                SelectRandomItemCommand = ReactiveCommand.Create(() =>
+                {
+                    var random = new Random();
+
+                    SelectedItem = Items[random.Next(Items.Count - 1)];
+                });
             }
 
             public ObservableCollection<string> Items { get; }
 
+            private string _selectedItem;
+
+            public string SelectedItem
+            {
+                get { return _selectedItem; }
+                set { this.RaiseAndSetIfChanged(ref _selectedItem, value); }
+            }
+
+
             public ObservableCollection<string> SelectedItems { get; }
 
             public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
 
             public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
 
+            public ReactiveCommand<Unit, Unit> SelectRandomItemCommand { get; }
+
             public SelectionMode SelectionMode
             {
                 get => _selectionMode;

+ 42 - 8
src/Avalonia.Base/Utilities/TypeUtilities.cs

@@ -13,7 +13,7 @@ namespace Avalonia.Utilities
     /// </summary>
     public static class TypeUtilities
     {
-        private static int[] Conversions =
+        private static readonly int[] Conversions =
         {
             0b101111111111101, // Boolean
             0b100001111111110, // Char
@@ -32,7 +32,7 @@ namespace Avalonia.Utilities
             0b111111111111111, // String
         };
 
-        private static int[] ImplicitConversions =
+        private static readonly int[] ImplicitConversions =
         {
             0b000000000000001, // Boolean
             0b001110111100010, // Char
@@ -51,7 +51,7 @@ namespace Avalonia.Utilities
             0b100000000000000, // String
         };
 
-        private static Type[] InbuiltTypes =
+        private static readonly Type[] InbuiltTypes =
         {
             typeof(Boolean),
             typeof(Char),
@@ -70,7 +70,7 @@ namespace Avalonia.Utilities
             typeof(String),
         };
 
-        private static readonly Type[] NumericTypes = new[]
+        private static readonly Type[] NumericTypes =
         {
             typeof(Byte),
             typeof(Decimal),
@@ -188,8 +188,7 @@ namespace Avalonia.Utilities
                 }
             }
 
-            var cast = from.GetRuntimeMethods()
-                .FirstOrDefault(m => (m.Name == "op_Implicit" || m.Name == "op_Explicit") && m.ReturnType == to);
+            var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit | OperatorType.Explicit);
 
             if (cast != null)
             {
@@ -253,8 +252,7 @@ namespace Avalonia.Utilities
                 }
             }
 
-            var cast = from.GetRuntimeMethods()
-                .FirstOrDefault(m => m.Name == "op_Implicit" && m.ReturnType == to);
+            var cast = FindTypeConversionOperatorMethod(from, to, OperatorType.Implicit);
 
             if (cast != null)
             {
@@ -335,5 +333,41 @@ namespace Avalonia.Utilities
                 return NumericTypes.Contains(type);
             }
         }
+
+        [Flags]
+        private enum OperatorType
+        {
+            Implicit = 1,
+            Explicit = 2
+        }
+
+        private static MethodInfo FindTypeConversionOperatorMethod(Type fromType, Type toType, OperatorType operatorType)
+        {
+            const string implicitName = "op_Implicit";
+            const string explicitName = "op_Explicit";
+
+            bool allowImplicit = (operatorType & OperatorType.Implicit) != 0;
+            bool allowExplicit = (operatorType & OperatorType.Explicit) != 0;
+
+            foreach (MethodInfo method in fromType.GetMethods())
+            {
+                if (!method.IsSpecialName || method.ReturnType != toType)
+                {
+                    continue;
+                }
+
+                if (allowImplicit && method.Name == implicitName)
+                {
+                    return method;
+                }
+
+                if (allowExplicit && method.Name == explicitName)
+                {
+                    return method;
+                }
+            }
+
+            return null;
+        }
     }
 }

+ 25 - 7
src/Avalonia.Controls/ContentControl.cs

@@ -1,11 +1,13 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using Avalonia.Collections;
 using Avalonia.Controls.Mixins;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Layout;
+using Avalonia.LogicalTree;
 using Avalonia.Metadata;
 
 namespace Avalonia.Controls
@@ -39,12 +41,9 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
             AvaloniaProperty.Register<ContentControl, VerticalAlignment>(nameof(VerticalContentAlignment));
 
-        /// <summary>
-        /// Initializes static members of the <see cref="ContentControl"/> class.
-        /// </summary>
         static ContentControl()
         {
-            ContentControlMixin.Attach<ContentControl>(ContentProperty, x => x.LogicalChildren);
+            ContentProperty.Changed.AddClassHandler<ContentControl>(x => x.ContentChanged);
         }
 
         /// <summary>
@@ -95,20 +94,39 @@ namespace Avalonia.Controls
         }
 
         /// <inheritdoc/>
-        void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
+        IAvaloniaList<ILogical> IContentPresenterHost.LogicalChildren => LogicalChildren;
+
+        /// <inheritdoc/>
+        bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
         {
-            RegisterContentPresenter(presenter);
+            return RegisterContentPresenter(presenter);
         }
 
         /// <summary>
         /// Called when an <see cref="IContentPresenter"/> is registered with the control.
         /// </summary>
         /// <param name="presenter">The presenter.</param>
-        protected virtual void RegisterContentPresenter(IContentPresenter presenter)
+        protected virtual bool RegisterContentPresenter(IContentPresenter presenter)
         {
             if (presenter.Name == "PART_ContentPresenter")
             {
                 Presenter = presenter;
+                return true;
+            }
+
+            return false;
+        }
+
+        private void ContentChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.OldValue is ILogical oldChild)
+            {
+                LogicalChildren.Remove(oldChild);
+            }
+
+            if (e.NewValue is ILogical newChild)
+            {
+                LogicalChildren.Add(newChild);
             }
         }
     }

+ 0 - 13
src/Avalonia.Controls/ItemsControl.cs

@@ -302,19 +302,6 @@ namespace Avalonia.Controls
         /// <param name="e">The details of the containers.</param>
         protected virtual void OnContainersRecycled(ItemContainerEventArgs e)
         {
-            var toRemove = new List<ILogical>();
-
-            foreach (var container in e.Containers)
-            {
-                // If the item is its own container, then it will be removed from the logical tree
-                // when it is removed from the Items collection.
-                if (container?.ContainerControl != container?.Item)
-                {
-                    toRemove.Add(container.ContainerControl);
-                }
-            }
-
-            LogicalChildren.RemoveAll(toRemove);
         }
 
         /// <summary>

+ 5 - 1
src/Avalonia.Controls/ListBox.cs

@@ -66,7 +66,11 @@ namespace Avalonia.Controls
         }
 
         /// <inheritdoc/>
-        public new IList SelectedItems => base.SelectedItems;
+        public new IList SelectedItems
+        {
+            get => base.SelectedItems;
+            set => base.SelectedItems = value;
+        }
 
         /// <summary>
         /// Gets or sets the selection mode.

+ 0 - 166
src/Avalonia.Controls/Mixins/ContentControlMixin.cs

@@ -1,166 +0,0 @@
-// 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.Linq;
-using System.Reactive.Disposables;
-using System.Runtime.CompilerServices;
-using Avalonia.Collections;
-using Avalonia.Controls.Presenters;
-using Avalonia.Controls.Primitives;
-using Avalonia.Interactivity;
-using Avalonia.LogicalTree;
-
-namespace Avalonia.Controls.Mixins
-{
-    /// <summary>
-    /// Adds content control functionality to control classes.
-    /// </summary>
-    /// <para>
-    /// The <see cref="ContentControlMixin"/> adds behavior to a control which acts as a content
-    /// control such as <see cref="ContentControl"/> and <see cref="HeaderedItemsControl"/>. It
-    /// keeps the control's logical children in sync with the content being displayed by the
-    /// control.
-    /// </para>
-    public class ContentControlMixin
-    {
-        private static Lazy<ConditionalWeakTable<TemplatedControl, IDisposable>> subscriptions = 
-            new Lazy<ConditionalWeakTable<TemplatedControl, IDisposable>>(() => 
-                new ConditionalWeakTable<TemplatedControl, IDisposable>());
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="SelectableMixin"/> class.
-        /// </summary>
-        /// <typeparam name="TControl">The control type.</typeparam>
-        /// <param name="content">The content property.</param>
-        /// <param name="logicalChildrenSelector">
-        /// Given an control of <typeparamref name="TControl"/> should return the control's
-        /// logical children collection.
-        /// </param>
-        /// <param name="presenterName">
-        /// The name of the content presenter in the control's template.
-        /// </param>
-        public static void Attach<TControl>(
-            AvaloniaProperty content,            
-            Func<TControl, IAvaloniaList<ILogical>> logicalChildrenSelector,
-            string presenterName = "PART_ContentPresenter")
-            where TControl : TemplatedControl
-        {
-            Contract.Requires<ArgumentNullException>(content != null);
-            Contract.Requires<ArgumentNullException>(logicalChildrenSelector != null);
-
-            void ChildChanging(object s, AvaloniaPropertyChangedEventArgs e)
-            {
-                if (s is IControl sender && sender?.TemplatedParent is TControl parent)
-                {
-                    UpdateLogicalChild(
-                        sender,
-                        logicalChildrenSelector(parent),
-                        e.OldValue,
-                        null);
-                }
-            }
-
-            void TemplateApplied(object s, RoutedEventArgs ev)
-            {
-                if (s is TControl sender)
-                {
-                    var e = (TemplateAppliedEventArgs)ev;
-                    var presenter = e.NameScope.Find(presenterName) as IContentPresenter;
-
-                    if (presenter != null)
-                    {
-                        presenter.ApplyTemplate();
-
-                        var logicalChildren = logicalChildrenSelector(sender);
-                        var subscription = new CompositeDisposable();
-
-                        presenter.ChildChanging += ChildChanging;
-                        subscription.Add(Disposable.Create(() => presenter.ChildChanging -= ChildChanging));
-
-                        subscription.Add(presenter
-                            .GetPropertyChangedObservable(ContentPresenter.ChildProperty)
-                            .Subscribe(c => UpdateLogicalChild(
-                                sender,
-                                logicalChildren,
-                                null,
-                                c.NewValue)));
-
-                        UpdateLogicalChild(
-                            sender,
-                            logicalChildren,
-                            null,
-                            presenter.GetValue(ContentPresenter.ChildProperty));
-
-                        if (subscriptions.Value.TryGetValue(sender, out IDisposable previousSubscription))
-                        {
-                            subscription = new CompositeDisposable(previousSubscription, subscription);
-                            subscriptions.Value.Remove(sender);
-                        }
-
-                        subscriptions.Value.Add(sender, subscription);
-                    }
-                }
-            }
-
-            TemplatedControl.TemplateAppliedEvent.AddClassHandler(
-                typeof(TControl),
-                TemplateApplied,
-                RoutingStrategies.Direct);
-
-            content.Changed.Subscribe(e =>
-            {
-                if (e.Sender is TControl sender)
-                {
-                    var logicalChildren = logicalChildrenSelector(sender);
-                    UpdateLogicalChild(sender, logicalChildren, e.OldValue, e.NewValue);
-                }
-            });
-
-            Control.TemplatedParentProperty.Changed.Subscribe(e =>
-            {
-                if (e.Sender is TControl sender)
-                {
-                    var logicalChild = logicalChildrenSelector(sender).FirstOrDefault() as IControl;
-                    logicalChild?.SetValue(Control.TemplatedParentProperty, sender.TemplatedParent);
-                }
-            });
-
-            TemplatedControl.TemplateProperty.Changed.Subscribe(e =>
-            {
-                if (e.Sender is TControl sender)
-                {
-                    if (subscriptions.Value.TryGetValue(sender, out IDisposable subscription))
-                    {
-                        subscription.Dispose();
-                        subscriptions.Value.Remove(sender);
-                    }
-                }
-            });
-        }
-
-        private static void UpdateLogicalChild(
-            IControl control,
-            IAvaloniaList<ILogical> logicalChildren,
-            object oldValue, 
-            object newValue)
-        {
-            if (oldValue != newValue)
-            {
-                if (oldValue is IControl child)
-                {
-                    logicalChildren.Remove(child);
-                    ((ISetInheritanceParent)child).SetParent(child.Parent);
-                }
-
-                child = newValue as IControl;
-
-                if (child != null && !logicalChildren.Contains(child))
-                {
-                    child.SetValue(Control.TemplatedParentProperty, control.TemplatedParent);
-                    logicalChildren.Add(child);
-                }
-            }
-        }
-    }
-}

+ 5 - 0
src/Avalonia.Controls/Notifications/ReversibleStackPanel.cs

@@ -39,6 +39,11 @@ namespace Avalonia.Controls
 
             foreach (Control child in children)
             {
+                if (!child.IsVisible)
+                {
+                    continue;
+                }
+
                 double childWidth = child.DesiredSize.Width;
                 double childHeight = child.DesiredSize.Height;
 

+ 1 - 1
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@@ -377,7 +377,7 @@ namespace Avalonia.Controls.Platform
 
             if (mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
             {
-                Menu.Close();
+                Menu?.Close();
             }
         }
 

+ 12 - 35
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@@ -6,6 +6,7 @@ using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Controls.Utils;
 using Avalonia.Data;
+using Avalonia.Input;
 using Avalonia.Layout;
 using Avalonia.LogicalTree;
 using Avalonia.Media;
@@ -83,7 +84,6 @@ namespace Avalonia.Controls.Presenters
 
         private IControl _child;
         private bool _createdChild;
-        EventHandler<AvaloniaPropertyChangedEventArgs> _childChanging;
         private IDataTemplate _dataTemplate;
         private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper();
 
@@ -190,12 +190,10 @@ namespace Avalonia.Controls.Presenters
             set { SetValue(PaddingProperty, value); }
         }
 
-        /// <inheritdoc/>
-        event EventHandler<AvaloniaPropertyChangedEventArgs> IContentPresenter.ChildChanging
-        {
-            add => _childChanging += value;
-            remove => _childChanging -= value;
-        }
+        /// <summary>
+        /// Gets the host content control.
+        /// </summary>
+        internal IContentPresenterHost Host { get; private set; }
 
         /// <inheritdoc/>
         public sealed override void ApplyTemplate()
@@ -222,34 +220,16 @@ namespace Avalonia.Controls.Presenters
             var content = Content;
             var oldChild = Child;
             var newChild = CreateChild();
+            var logicalChildren = Host?.LogicalChildren ?? LogicalChildren;
 
             // Remove the old child if we're not recycling it.
             if (newChild != oldChild)
             {
+
                 if (oldChild != null)
                 {
                     VisualChildren.Remove(oldChild);
-                }
-
-                if (oldChild?.Parent == this)
-                {
-                    // If we're the child's parent then the presenter isn't in a ContentControl's
-                    // template.
-                    LogicalChildren.Remove(oldChild);
-                }
-                else if (TemplatedParent != null)
-                {
-                    // If we're in a ContentControl's template then invoke ChildChanging to let
-                    // ContentControlMixin handle removing the logical child.
-                    _childChanging?.Invoke(this, new AvaloniaPropertyChangedEventArgs(
-                        this,
-                        ChildProperty,
-                        oldChild,
-                        newChild,
-                        BindingPriority.LocalValue));
-                }
-                else if (oldChild != null)
-                {
+                    logicalChildren.Remove(oldChild);
                     ((ISetInheritanceParent)oldChild).SetParent(oldChild.Parent);
                 }
             }
@@ -272,15 +252,11 @@ namespace Avalonia.Controls.Presenters
             else if (newChild != oldChild)
             {
                 ((ISetInheritanceParent)newChild).SetParent(this);
-
                 Child = newChild;
 
-                // If we're in a ContentControl's template then the child's parent will have been
-                // set by ContentControlMixin in response to Child changing. If not, then we're
-                // standalone and should make the control our own logical child.
-                if (newChild.Parent == null && TemplatedParent == null)
+                if (!logicalChildren.Contains(newChild))
                 {
-                    LogicalChildren.Add(newChild);
+                    logicalChildren.Add(newChild);
                 }
 
                 VisualChildren.Add(newChild);
@@ -459,7 +435,8 @@ namespace Avalonia.Controls.Presenters
 
         private void TemplatedParentChanged(AvaloniaPropertyChangedEventArgs e)
         {
-            (e.NewValue as IContentPresenterHost)?.RegisterContentPresenter(this);
+            var host = e.NewValue as IContentPresenterHost;
+            Host = host?.RegisterContentPresenter(this) == true ? host : null;
         }
     }
 }

+ 0 - 13
src/Avalonia.Controls/Presenters/IContentPresenter.cs

@@ -1,8 +1,6 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
-using System;
-using Avalonia.Controls.Mixins;
 using Avalonia.Controls.Primitives;
 
 namespace Avalonia.Controls.Presenters
@@ -22,16 +20,5 @@ namespace Avalonia.Controls.Presenters
         /// Gets or sets the content to be displayed by the presenter.
         /// </summary>
         object Content { get; set; }
-
-        /// <summary>
-        /// Raised when <see cref="Child"/> property is about to change.
-        /// </summary>
-        /// <remarks>
-        /// This event should be raised after the child has been removed from the visual tree,
-        /// but before the <see cref="Child"/> property has changed. It is intended for consumption
-        /// by <see cref="ContentControlMixin"/> in order to update the host control's logical
-        /// children.
-        /// </remarks>
-        event EventHandler<AvaloniaPropertyChangedEventArgs> ChildChanging;
     }
 }

+ 12 - 1
src/Avalonia.Controls/Presenters/IContentPresenterHost.cs

@@ -1,6 +1,8 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using Avalonia.Collections;
+using Avalonia.LogicalTree;
 using Avalonia.Styling;
 
 namespace Avalonia.Controls.Presenters
@@ -18,10 +20,19 @@ namespace Avalonia.Controls.Presenters
     /// </remarks>
     public interface IContentPresenterHost : ITemplatedControl
     {
+        /// <summary>
+        /// Gets a collection describing the logical children of the host control.
+        /// </summary>
+        IAvaloniaList<ILogical> LogicalChildren { get; }
+
         /// <summary>
         /// Registers an <see cref="IContentPresenter"/> with a host control.
         /// </summary>
         /// <param name="presenter">The content presenter.</param>
-        void RegisterContentPresenter(IContentPresenter presenter);
+        /// <returns>
+        /// True if the content presenter should add its child to the logical children of the
+        /// host; otherwise false.
+        /// </returns>
+        bool RegisterContentPresenter(IContentPresenter presenter);
     }
 }

+ 7 - 4
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@@ -295,11 +295,14 @@ namespace Avalonia.Controls.Presenters
         /// <inheritdoc/>
         public override void ScrollIntoView(object item)
         {
-            var index = Items.IndexOf(item);
-
-            if (index != -1)
+            if (Items != null)
             {
-                ScrollIntoView(index);
+                var index = Items.IndexOf(item);
+
+                if (index != -1)
+                {
+                    ScrollIntoView(index);
+                }
             }
         }
 

+ 20 - 6
src/Avalonia.Controls/Primitives/HeaderedContentControl.cs

@@ -4,6 +4,7 @@
 using Avalonia.Controls.Mixins;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Templates;
+using Avalonia.LogicalTree;
 
 namespace Avalonia.Controls.Primitives
 {
@@ -29,10 +30,7 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         static HeaderedContentControl()
         {
-            ContentControlMixin.Attach<HeaderedContentControl>(
-                HeaderProperty,
-                x => x.LogicalChildren,
-                "PART_HeaderPresenter");
+            ContentProperty.Changed.AddClassHandler<HeaderedContentControl>(x => x.HeaderChanged);
         }
 
         /// <summary>
@@ -63,13 +61,29 @@ namespace Avalonia.Controls.Primitives
         }
 
         /// <inheritdoc/>
-        protected override void RegisterContentPresenter(IContentPresenter presenter)
+        protected override bool RegisterContentPresenter(IContentPresenter presenter)
         {
-            base.RegisterContentPresenter(presenter);
+            var result = base.RegisterContentPresenter(presenter);
 
             if (presenter.Name == "PART_HeaderPresenter")
             {
                 HeaderPresenter = presenter;
+                result = true;
+            }
+
+            return result;
+        }
+
+        private void HeaderChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.OldValue is ILogical oldChild)
+            {
+                LogicalChildren.Remove(oldChild);
+            }
+
+            if (e.NewValue is ILogical newChild)
+            {
+                LogicalChildren.Add(newChild);
             }
         }
     }

+ 25 - 7
src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs

@@ -1,8 +1,10 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using Avalonia.Collections;
 using Avalonia.Controls.Mixins;
 using Avalonia.Controls.Presenters;
+using Avalonia.LogicalTree;
 
 namespace Avalonia.Controls.Primitives
 {
@@ -22,10 +24,7 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         static HeaderedItemsControl()
         {
-            ContentControlMixin.Attach<HeaderedItemsControl>(
-                HeaderProperty,
-                x => x.LogicalChildren,
-                "PART_HeaderPresenter");
+            HeaderProperty.Changed.AddClassHandler<HeaderedItemsControl>(x => x.HeaderChanged);
         }
 
         /// <summary>
@@ -47,20 +46,39 @@ namespace Avalonia.Controls.Primitives
         }
 
         /// <inheritdoc/>
-        void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
+        IAvaloniaList<ILogical> IContentPresenterHost.LogicalChildren => LogicalChildren;
+
+        /// <inheritdoc/>
+        bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
         {
-            RegisterContentPresenter(presenter);
+            return RegisterContentPresenter(presenter);
         }
 
         /// <summary>
         /// Called when an <see cref="IContentPresenter"/> is registered with the control.
         /// </summary>
         /// <param name="presenter">The presenter.</param>
-        protected virtual void RegisterContentPresenter(IContentPresenter presenter)
+        protected virtual bool RegisterContentPresenter(IContentPresenter presenter)
         {
             if (presenter.Name == "PART_HeaderPresenter")
             {
                 HeaderPresenter = presenter;
+                return true;
+            }
+
+            return false;
+        }
+
+        private void HeaderChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.OldValue is ILogical oldChild)
+            {
+                LogicalChildren.Remove(oldChild);
+            }
+
+            if (e.NewValue is ILogical newChild)
+            {
+                LogicalChildren.Add(newChild);
             }
         }
     }

+ 25 - 7
src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs

@@ -1,8 +1,10 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using Avalonia.Collections;
 using Avalonia.Controls.Mixins;
 using Avalonia.Controls.Presenters;
+using Avalonia.LogicalTree;
 
 namespace Avalonia.Controls.Primitives
 {
@@ -22,10 +24,7 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         static HeaderedSelectingItemsControl()
         {
-            ContentControlMixin.Attach<HeaderedSelectingItemsControl>(
-                HeaderProperty,
-                x => x.LogicalChildren,
-                "PART_HeaderPresenter");
+            HeaderProperty.Changed.AddClassHandler<HeaderedSelectingItemsControl>(x => x.HeaderChanged);
         }
 
         /// <summary>
@@ -47,20 +46,39 @@ namespace Avalonia.Controls.Primitives
         }
 
         /// <inheritdoc/>
-        void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
+        IAvaloniaList<ILogical> IContentPresenterHost.LogicalChildren => LogicalChildren;
+
+        /// <inheritdoc/>
+        bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
         {
-            RegisterContentPresenter(presenter);
+            return RegisterContentPresenter(presenter);
         }
 
         /// <summary>
         /// Called when an <see cref="IContentPresenter"/> is registered with the control.
         /// </summary>
         /// <param name="presenter">The presenter.</param>
-        protected virtual void RegisterContentPresenter(IContentPresenter presenter)
+        protected virtual bool RegisterContentPresenter(IContentPresenter presenter)
         {
             if (presenter.Name == "PART_HeaderPresenter")
             {
                 HeaderPresenter = presenter;
+                return true;
+            }
+
+            return false;
+        }
+
+        private void HeaderChanged(AvaloniaPropertyChangedEventArgs e)
+        {
+            if (e.OldValue is ILogical oldChild)
+            {
+                LogicalChildren.Remove(oldChild);
+            }
+
+            if (e.NewValue is ILogical newChild)
+            {
+                LogicalChildren.Add(newChild);
             }
         }
     }

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

@@ -112,7 +112,7 @@ namespace Avalonia.Controls.Primitives
         private bool _syncingSelectedItems;
         private int _updateCount;
         private int _updateSelectedIndex;
-        private IList _updateSelectedItems;
+        private object _updateSelectedItem;
 
         /// <summary>
         /// Initializes static members of the <see cref="SelectingItemsControl"/> class.
@@ -160,7 +160,7 @@ namespace Avalonia.Controls.Primitives
                 else
                 {
                     _updateSelectedIndex = value;
-                    _updateSelectedItems = null;
+                    _updateSelectedItem = null;
                 }
             }
         }
@@ -183,7 +183,7 @@ namespace Avalonia.Controls.Primitives
                 }
                 else
                 {
-                    _updateSelectedItems = new AvaloniaList<object>(value);
+                    _updateSelectedItem = value;
                     _updateSelectedIndex = int.MinValue;
                 }
             }
@@ -855,11 +855,6 @@ namespace Avalonia.Controls.Primitives
                     _selectedItem = ElementAt(Items, _selectedIndex);
                     RaisePropertyChanged(SelectedIndexProperty, -1, _selectedIndex, BindingPriority.LocalValue);
                     RaisePropertyChanged(SelectedItemProperty, null, _selectedItem, BindingPriority.LocalValue);
-
-                    if (AutoScrollToSelectedItem)
-                    {
-                        ScrollIntoView(_selectedIndex);
-                    }
                 }
             }
 
@@ -1046,6 +1041,11 @@ namespace Avalonia.Controls.Primitives
                     removed?.Select(x => ElementAt(Items, x)).ToArray() ?? Array.Empty<object>());
                 RaiseEvent(e);
             }
+
+            if (AutoScrollToSelectedItem && _selectedIndex != -1)
+            {
+                ScrollIntoView(_selectedItem);
+            }
         }
 
         private void UpdateSelectedItems(Action action)
@@ -1075,9 +1075,9 @@ namespace Avalonia.Controls.Primitives
             {
                 SelectedIndex = _updateSelectedIndex;
             }
-            else if (_updateSelectedItems != null)
+            else if (_updateSelectedItem != null)
             {
-                SelectedItems = _updateSelectedItems;
+                SelectedItem = _updateSelectedItem;
             }
         }
 

+ 12 - 13
src/Avalonia.Controls/RadioButton.cs

@@ -18,7 +18,7 @@ namespace Avalonia.Controls
             public static readonly RadioButtonGroupManager Default = new RadioButtonGroupManager();
             static readonly ConditionalWeakTable<IRenderRoot, RadioButtonGroupManager> s_registeredVisualRoots
                 = new ConditionalWeakTable<IRenderRoot, RadioButtonGroupManager>();
-            
+
             readonly Dictionary<string, List<WeakReference<RadioButton>>> s_registeredGroups
                 = new Dictionary<string, List<WeakReference<RadioButton>>>();
 
@@ -127,13 +127,11 @@ namespace Avalonia.Controls
         {
             if (!string.IsNullOrEmpty(GroupName))
             {
-                var manager = RadioButtonGroupManager.GetOrCreateForRoot(e.Root);
-                if (manager != _groupManager)
-                {
-                    _groupManager.Remove(this, _groupName);
-                    _groupManager = manager;
-                    manager.Add(this);
-                }
+                _groupManager?.Remove(this, _groupName);
+
+                _groupManager = RadioButtonGroupManager.GetOrCreateForRoot(e.Root);
+
+                _groupManager.Add(this);
             }
             base.OnAttachedToVisualTree(e);
         }
@@ -141,9 +139,10 @@ namespace Avalonia.Controls
         protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
         {
             base.OnDetachedFromVisualTree(e);
-            if (!string.IsNullOrEmpty(GroupName) && _groupManager != null)
+
+            if (!string.IsNullOrEmpty(GroupName))
             {
-                _groupManager.Remove(this, _groupName);
+                _groupManager?.Remove(this, _groupName);
             }
         }
 
@@ -152,9 +151,9 @@ namespace Avalonia.Controls
             string oldGroupName = GroupName;
             if (newGroupName != oldGroupName)
             {
-                if (!string.IsNullOrEmpty(oldGroupName) && _groupManager != null)
+                if (!string.IsNullOrEmpty(oldGroupName))
                 {
-                    _groupManager.Remove(this, oldGroupName);
+                    _groupManager?.Remove(this, oldGroupName);
                 }
                 _groupName = newGroupName;
                 if (!string.IsNullOrEmpty(newGroupName))
@@ -181,7 +180,7 @@ namespace Avalonia.Controls
                         .GetVisualChildren()
                         .OfType<RadioButton>()
                         .Where(x => x != this);
-                    
+
                     foreach (var sibling in siblings)
                     {
                         if (sibling.IsChecked.GetValueOrDefault())

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

@@ -251,7 +251,7 @@ namespace Avalonia.Controls
             {
                 var child = children[i];
 
-                if (child == null)
+                if (child == null || !child.IsVisible)
                 { continue; }
 
                 if (fHorizontal)

+ 28 - 8
src/Avalonia.Controls/TabControl.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System.Linq;
+using Avalonia.Collections;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Mixins;
 using Avalonia.Controls.Presenters;
@@ -9,6 +10,7 @@ using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Input;
 using Avalonia.Layout;
+using Avalonia.LogicalTree;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Controls
@@ -16,7 +18,7 @@ namespace Avalonia.Controls
     /// <summary>
     /// A tab control that displays a tab strip along with the content of the selected tab.
     /// </summary>
-    public class TabControl : SelectingItemsControl
+    public class TabControl : SelectingItemsControl, IContentPresenterHost
     {
         /// <summary>
         /// Defines the <see cref="TabStripPlacement"/> property.
@@ -68,10 +70,6 @@ namespace Avalonia.Controls
             SelectionModeProperty.OverrideDefaultValue<TabControl>(SelectionMode.AlwaysSelected);
             ItemsPanelProperty.OverrideDefaultValue<TabControl>(DefaultPanel);
             AffectsMeasure<TabControl>(TabStripPlacementProperty);
-            ContentControlMixin.Attach<TabControl>(
-                SelectedContentProperty,
-                x => x.LogicalChildren,
-                "PART_SelectedContentHost");
         }
 
         /// <summary>
@@ -136,7 +134,31 @@ namespace Avalonia.Controls
 
         internal ItemsPresenter ItemsPresenterPart { get; private set; }
 
-        internal ContentPresenter ContentPart { get; private set; }
+        internal IContentPresenter ContentPart { get; private set; }
+
+        /// <inheritdoc/>
+        IAvaloniaList<ILogical> IContentPresenterHost.LogicalChildren => LogicalChildren;
+
+        /// <inheritdoc/>
+        bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter)
+        {
+            return RegisterContentPresenter(presenter);
+        }
+
+        /// <summary>
+        /// Called when an <see cref="IContentPresenter"/> is registered with the control.
+        /// </summary>
+        /// <param name="presenter">The presenter.</param>
+        protected virtual bool RegisterContentPresenter(IContentPresenter presenter)
+        {
+            if (presenter.Name == "PART_SelectedContentHost")
+            {
+                ContentPart = presenter;
+                return true;
+            }
+
+            return false;
+        }
 
         protected override IItemContainerGenerator CreateItemContainerGenerator()
         {
@@ -148,8 +170,6 @@ namespace Avalonia.Controls
             base.OnTemplateApplied(e);
 
             ItemsPresenterPart = e.NameScope.Get<ItemsPresenter>("PART_ItemsPresenter");
-
-            ContentPart = e.NameScope.Get<ContentPresenter>("PART_SelectedContentHost");
         }
 
         /// <inheritdoc/>

+ 4 - 1
src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs

@@ -70,7 +70,10 @@ namespace Avalonia.Diagnostics.Views
         private void TreeViewItemTemplateApplied(object sender, TemplateAppliedEventArgs e)
         {
             var item = (TreeViewItem)sender;
-            var header = item.HeaderPresenter.Child;
+            var headerPresenter = item.HeaderPresenter;
+            headerPresenter.ApplyTemplate();
+
+            var header = headerPresenter.Child;
             header.PointerEnter += AddAdorner;
             header.PointerLeave += RemoveAdorner;
             item.TemplateApplied -= TreeViewItemTemplateApplied;

+ 0 - 9
src/Avalonia.Input/AccessKeyHandler.cs

@@ -231,15 +231,6 @@ namespace Avalonia.Input
                     }
 
                     break;
-
-                case Key.F10:
-                    _owner.ShowAccessKeys = _showingAccessKeys = true;
-                    if (MainMenu != null)
-                    {
-                        MainMenu.Open();
-                        e.Handled = true;
-                    }
-                    break;
             }
         }
 

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

@@ -565,9 +565,17 @@ namespace Avalonia.Input
         {
             IsEffectivelyEnabled = IsEnabledCore && (parent?.IsEffectivelyEnabled ?? true);
 
-            foreach (var child in this.GetVisualChildren().OfType<InputElement>())
+            // PERF-SENSITIVE: This is called on entire hierarchy and using foreach or LINQ
+            // will cause extra allocations and overhead.
+            
+            var children = VisualChildren;
+
+            // ReSharper disable once ForCanBeConvertedToForeach
+            for (int i = 0; i < children.Count; ++i)
             {
-                child.UpdateIsEffectivelyEnabled(this);
+                var child = children[i] as InputElement;
+
+                child?.UpdateIsEffectivelyEnabled(this);
             }
         }
     }

+ 1 - 0
tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs

@@ -984,6 +984,7 @@ namespace Avalonia.Controls.UnitTests
                 TextBox textBox = GetTextBox(control);
                 var window = new Window {Content = control};
                 window.ApplyTemplate();
+                window.Presenter.ApplyTemplate();
                 Dispatcher.UIThread.RunJobs();
                 test.Invoke(control, textBox);
             }

+ 40 - 0
tests/Avalonia.Controls.UnitTests/ContentControlTests.cs

@@ -50,6 +50,7 @@ namespace Avalonia.Controls.UnitTests
             root.Child = target;
 
             target.ApplyTemplate();
+            target.Presenter.ApplyTemplate();
 
             styler.Verify(x => x.ApplyStyles(It.IsAny<ContentControl>()), Times.Once());
             styler.Verify(x => x.ApplyStyles(It.IsAny<Border>()), Times.Once());
@@ -331,6 +332,45 @@ namespace Avalonia.Controls.UnitTests
             Assert.Null(textBlock.GetLogicalParent());
         }
 
+        [Fact]
+        public void Should_Set_Child_LogicalParent_After_Removing_And_Adding_Back_To_Logical_Tree()
+        {
+            using (UnitTestApplication.Start(TestServices.RealStyler))
+            {
+                var target = new ContentControl();
+                var root = new TestRoot
+                {
+                    Styles =
+                    {
+                        new Style(x => x.OfType<ContentControl>())
+                        {
+                            Setters =
+                            {
+                                new Setter(ContentControl.TemplateProperty, GetTemplate()),
+                            }
+                        }
+                    },
+                    Child = target
+                };
+
+                target.Content = "Foo";
+                target.ApplyTemplate();
+                target.Presenter.ApplyTemplate();
+
+                Assert.Equal(target, target.Presenter.Child.LogicalParent);
+
+                root.Child = null;
+
+                Assert.Null(target.Template);
+
+                target.Content = null;
+                root.Child = target;
+                target.Content = "Bar";
+
+                Assert.Equal(target, target.Presenter.Child.LogicalParent);
+            }
+        }
+
         private FuncControlTemplate GetTemplate()
         {
             return new FuncControlTemplate<ContentControl>((parent, scope) =>

+ 9 - 3
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@@ -27,7 +27,9 @@ namespace Avalonia.Controls.UnitTests
                     ContextMenu = sut
                 };
 
-                new Window { Content = target }.ApplyTemplate();
+                var window = new Window { Content = target };
+                window.ApplyTemplate();
+                window.Presenter.ApplyTemplate();
 
                 int openedCount = 0;
 
@@ -53,7 +55,9 @@ namespace Avalonia.Controls.UnitTests
                     ContextMenu = sut
                 };
 
-                new Window { Content = target }.ApplyTemplate();
+                var window = new Window { Content = target };
+                window.ApplyTemplate();
+                window.Presenter.ApplyTemplate();
 
                 sut.Open(target);
 
@@ -86,6 +90,7 @@ namespace Avalonia.Controls.UnitTests
 
                 var window = new Window {Content = target};
                 window.ApplyTemplate();
+                window.Presenter.ApplyTemplate();
 
                 _mouse.Click(target, MouseButton.Right);
 
@@ -115,7 +120,8 @@ namespace Avalonia.Controls.UnitTests
 
                 var window = new Window {Content = target};
                 window.ApplyTemplate();
-                
+                window.Presenter.ApplyTemplate();
+
                 _mouse.Click(target, MouseButton.Right);
 
                 Assert.True(sut.IsOpen);

+ 0 - 107
tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs

@@ -1,107 +0,0 @@
-// 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.Collections.Generic;
-using System.Linq;
-using Avalonia.Collections;
-using Avalonia.Controls.Mixins;
-using Avalonia.Controls.Presenters;
-using Avalonia.Controls.Primitives;
-using Avalonia.Controls.Templates;
-using Avalonia.LogicalTree;
-using Moq;
-using Xunit;
-
-namespace Avalonia.Controls.UnitTests.Mixins
-{
-    public class ContentControlMixinTests
-    {
-        [Fact]
-        public void Multiple_Mixin_Usages_Should_Not_Throw()
-        {
-            var target = new TestControl()
-            {
-                Template = new FuncControlTemplate((_, scope) => new Panel
-                {
-                    Children =
-                    {
-                        new ContentPresenter {Name = "Content_1_Presenter"}.RegisterInNameScope(scope),
-                        new ContentPresenter {Name = "Content_2_Presenter"}.RegisterInNameScope(scope)
-                    }
-                })
-            };
-            
-
-            var ex = Record.Exception(() => target.ApplyTemplate());
-
-            Assert.Null(ex);
-        }
-
-        [Fact]
-        public void Replacing_Template_Releases_Events()
-        {
-            var p1 = new ContentPresenter { Name = "Content_1_Presenter" };
-            var p2 = new ContentPresenter { Name = "Content_2_Presenter" };
-            var target = new TestControl
-            {
-                Template = new FuncControlTemplate((_, scope) => new Panel
-                {
-                    Children =
-                    {
-                        p1.RegisterInNameScope(scope),
-                        p2.RegisterInNameScope(scope)
-                    }
-                })
-            };
-            target.ApplyTemplate();
-
-            Control tc;
-
-            p1.Content = tc = new Control();
-            p1.UpdateChild();
-            Assert.Contains(tc, target.GetLogicalChildren());
-
-            p2.Content = tc = new Control();
-            p2.UpdateChild();
-            Assert.Contains(tc, target.GetLogicalChildren());
-
-            target.Template = null;
-
-            p1.Content = tc = new Control();
-            p1.UpdateChild();
-            Assert.DoesNotContain(tc, target.GetLogicalChildren());
-
-            p2.Content = tc = new Control();
-            p2.UpdateChild();
-            Assert.DoesNotContain(tc, target.GetLogicalChildren());
-
-        }
-
-        private class TestControl : TemplatedControl
-        {
-            public static readonly StyledProperty<object> Content1Property =
-                AvaloniaProperty.Register<TestControl, object>(nameof(Content1));
-
-            public static readonly StyledProperty<object> Content2Property =
-                AvaloniaProperty.Register<TestControl, object>(nameof(Content2));
-
-            static TestControl()
-            {
-                ContentControlMixin.Attach<TestControl>(Content1Property, x => x.LogicalChildren, "Content_1_Presenter");
-                ContentControlMixin.Attach<TestControl>(Content2Property, x => x.LogicalChildren, "Content_2_Presenter");
-            }
-
-            public object Content1
-            {
-                get { return GetValue(Content1Property); }
-                set { SetValue(Content1Property, value); }
-            }
-
-            public object Content2
-            {
-                get { return GetValue(Content2Property); }
-                set { SetValue(Content2Property, value); }
-            }
-        }
-    }
-}

+ 13 - 0
tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs

@@ -343,6 +343,19 @@ namespace Avalonia.Controls.UnitTests.Presenters
             Assert.Same(logicalParent, ((IStyledElement)child).StylingParent);
         }
 
+        [Fact]
+        public void Should_Clear_Host_When_Host_Template_Cleared()
+        {
+            var (target, host) = CreateTarget();
+
+            Assert.Same(host, target.Host);
+
+            host.Template = null;
+            host.ApplyTemplate();
+
+            Assert.Null(target.Host);
+        }
+
         (ContentPresenter presenter, ContentControl templatedParent) CreateTarget()
         {
             var templatedParent = new ContentControl

+ 2 - 0
tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs

@@ -51,6 +51,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 window.Content = target;
 
                 window.ApplyTemplate();
+                window.Presenter.ApplyTemplate();
                 target.ApplyTemplate();
                 target.Popup.Open();
 
@@ -167,6 +168,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 window.Content = target;
 
                 window.ApplyTemplate();
+                window.Presenter.ApplyTemplate();
                 target.ApplyTemplate();
                 target.Popup.Open();
                 target.PopupContent = null;

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

@@ -894,6 +894,53 @@ namespace Avalonia.Controls.UnitTests.Primitives
             Assert.Equal("Qux", target.SelectedItem);
         }
 
+        [Fact]
+        public void AutoScrollToSelectedItem_Causes_Scroll_To_SelectedItem()
+        {
+            var items = new ObservableCollection<string>
+            {
+               "Foo",
+               "Bar",
+               "Baz"
+            };
+
+            var target = new ListBox
+            {
+                Template = Template(),
+                Items = items,
+            };
+
+            target.ApplyTemplate();
+            target.Presenter.ApplyTemplate();
+
+            var raised = false;
+            target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true);
+
+            target.SelectedIndex = 2;
+
+            Assert.True(raised);
+        }
+
+        [Fact]
+        public void Can_Set_Both_SelectedItem_And_SelectedItems_During_Initialization()
+        {
+            // Issue #2969.
+            var target = new ListBox();
+            var selectedItems = new List<object>();
+
+            target.BeginInit();
+            target.Template = Template();
+            target.Items = new[] { "Foo", "Bar", "Baz" };
+            target.SelectedItems = selectedItems;
+            target.SelectedItem = "Bar";
+            target.EndInit();
+
+            Assert.Equal("Bar", target.SelectedItem);
+            Assert.Equal(1, target.SelectedIndex);
+            Assert.Same(selectedItems, target.SelectedItems);
+            Assert.Equal(new[] { "Bar" }, selectedItems);
+        }
+
         private FuncControlTemplate Template()
         {
             return new FuncControlTemplate<SelectingItemsControl>((control, scope) =>

+ 25 - 0
tests/Avalonia.Controls.UnitTests/StackPanelTests.cs

@@ -332,6 +332,31 @@ namespace Avalonia.Controls.UnitTests
             Assert.Equal(sizeWithTwoChildren, sizeWithThreeChildren);
         }
 
+        [Theory]
+        [InlineData(Orientation.Horizontal)]
+        [InlineData(Orientation.Vertical)]
+        public void Only_Arrange_Visible_Children(Orientation orientation)
+        {
+
+            var hiddenPanel = new Panel { Width = 10, Height = 10, IsVisible = false };
+            var panel = new Panel { Width = 10, Height = 10 };
+
+            var target = new StackPanel
+            {
+                Spacing = 40,
+                Orientation = orientation,
+                Children =
+                {
+                    hiddenPanel,
+                    panel
+                }
+            };
+
+            target.Measure(Size.Infinity);
+            target.Arrange(new Rect(target.DesiredSize));
+            Assert.Equal(new Rect(0, 0, 10, 10), panel.Bounds);
+        }
+
         private class TestControl : Control
         {
             public Size MeasureConstraint { get; private set; }

+ 6 - 6
tests/Avalonia.Controls.UnitTests/TabControlTests.cs

@@ -183,27 +183,27 @@ namespace Avalonia.Controls.UnitTests
 
             ApplyTemplate(target);
 
-            target.ContentPart.UpdateChild();
+            ((ContentPresenter)target.ContentPart).UpdateChild();
             var dataContext = ((TextBlock)target.ContentPart.Child).DataContext;
             Assert.Equal(items[0], dataContext);
 
             target.SelectedIndex = 1;
-            target.ContentPart.UpdateChild();
+            ((ContentPresenter)target.ContentPart).UpdateChild();
             dataContext = ((Button)target.ContentPart.Child).DataContext;
             Assert.Equal(items[1], dataContext);
 
             target.SelectedIndex = 2;
-            target.ContentPart.UpdateChild();
+            ((ContentPresenter)target.ContentPart).UpdateChild();
             dataContext = ((TextBlock)target.ContentPart.Child).DataContext;
             Assert.Equal("Base", dataContext);
 
             target.SelectedIndex = 3;
-            target.ContentPart.UpdateChild();
+            ((ContentPresenter)target.ContentPart).UpdateChild();
             dataContext = ((TextBlock)target.ContentPart.Child).DataContext;
             Assert.Equal("Qux", dataContext);
 
             target.SelectedIndex = 4;
-            target.ContentPart.UpdateChild();
+            ((ContentPresenter)target.ContentPart).UpdateChild();
             dataContext = target.ContentPart.DataContext;
             Assert.Equal("Base", dataContext);
         }
@@ -279,7 +279,7 @@ namespace Avalonia.Controls.UnitTests
             };
 
             ApplyTemplate(target);
-            target.ContentPart.UpdateChild();
+            ((ContentPresenter)target.ContentPart).UpdateChild();
 
             var content = Assert.IsType<TextBlock>(target.ContentPart.Child);
             Assert.Equal("bar", content.Tag);

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

@@ -202,8 +202,9 @@ namespace Avalonia.Controls.UnitTests
 
                 target.Template = CreateTemplate();
                 target.Content = child;
+                target.ApplyTemplate();
 
-                Assert.Throws<InvalidOperationException>(() => target.ApplyTemplate());
+                Assert.Throws<InvalidOperationException>(() => target.Presenter.ApplyTemplate());
             }
         }
 

+ 1 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs

@@ -142,6 +142,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
 
                 window.DataContext = new { Foo = "foo" };
                 window.ApplyTemplate();
+                window.Presenter.ApplyTemplate();
 
                 Assert.Equal("foo", border.DataContext);
             }

+ 3 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs

@@ -73,6 +73,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
                 var button = window.FindControl<Button>("button");
 
                 window.ApplyTemplate();
+                window.Presenter.ApplyTemplate();
 
                 Assert.Equal("border2", button.Content);
             }
@@ -169,6 +170,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
                 var button = window.FindControl<Button>("button");
 
                 window.ApplyTemplate();
+                window.Presenter.ApplyTemplate();
 
                 Assert.Equal("border1", button.Content);
             }
@@ -293,6 +295,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
                 var button = window.FindControl<Button>("button");
 
                 window.ApplyTemplate();
+                window.Presenter.ApplyTemplate();
 
                 Assert.Equal("title", button.Content);
             }