From 5c407f966c86bc7766b2c93fa6635161a0a69755 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 4 Sep 2019 11:27:12 +0200 Subject: [PATCH 01/25] Added failing test for #2821. `ContentPresenter` doesn't set the logical parent of its child control after it has been removed from the logical tree and re-added. --- .../ContentControlTests.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs index 93355a22f2..ecddb322fa 100644 --- a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs @@ -331,6 +331,44 @@ 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()) + { + Setters = + { + new Setter(ContentControl.TemplateProperty, GetTemplate()), + } + } + }, + Child = target + }; + + target.Content = "Foo"; + target.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((parent, scope) => From d7357ec8769dcd5436af4159c0b2aecb54ce0c30 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 4 Sep 2019 13:10:37 +0200 Subject: [PATCH 02/25] Remove ContentControlMixin. And implement the functionality in the content controls themselves. `ContentControlMixin` was too complex and even with its complexity had bugs (such as in #2821). By moving the functionality to the content controls there is some repeated code but it's much more straightforward. --- src/Avalonia.Controls/ContentControl.cs | 32 +++- .../Mixins/ContentControlMixin.cs | 166 ------------------ .../Presenters/ContentPresenter.cs | 47 ++--- .../Presenters/IContentPresenter.cs | 13 -- .../Presenters/IContentPresenterHost.cs | 13 +- .../Primitives/HeaderedContentControl.cs | 26 ++- .../Primitives/HeaderedItemsControl.cs | 32 +++- .../HeaderedSelectingItemsControl.cs | 32 +++- src/Avalonia.Controls/TabControl.cs | 36 +++- .../AutoCompleteBoxTests.cs | 1 + .../ContentControlTests.cs | 2 + .../ContextMenuTests.cs | 12 +- .../Mixins/ContentControlMixinTests.cs | 107 ----------- .../ContentPresenterTests_InTemplate.cs | 13 ++ .../Primitives/PopupRootTests.cs | 2 + .../TabControlTests.cs | 12 +- .../TopLevelTests.cs | 3 +- .../Xaml/BindingTests.cs | 1 + .../Xaml/BindingTests_RelativeSource.cs | 3 + 19 files changed, 186 insertions(+), 367 deletions(-) delete mode 100644 src/Avalonia.Controls/Mixins/ContentControlMixin.cs delete mode 100644 tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index 16f17ae1bd..02d7890404 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/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 VerticalContentAlignmentProperty = AvaloniaProperty.Register(nameof(VerticalContentAlignment)); - /// - /// Initializes static members of the class. - /// static ContentControl() { - ContentControlMixin.Attach(ContentProperty, x => x.LogicalChildren); + ContentProperty.Changed.AddClassHandler(x => x.ContentChanged); } /// @@ -95,20 +94,39 @@ namespace Avalonia.Controls } /// - void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) + IAvaloniaList IContentPresenterHost.LogicalChildren => LogicalChildren; + + /// + bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) { - RegisterContentPresenter(presenter); + return RegisterContentPresenter(presenter); } /// /// Called when an is registered with the control. /// /// The presenter. - 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); } } } diff --git a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs deleted file mode 100644 index b826fb982e..0000000000 --- a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs +++ /dev/null @@ -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 -{ - /// - /// Adds content control functionality to control classes. - /// - /// - /// The adds behavior to a control which acts as a content - /// control such as and . It - /// keeps the control's logical children in sync with the content being displayed by the - /// control. - /// - public class ContentControlMixin - { - private static Lazy> subscriptions = - new Lazy>(() => - new ConditionalWeakTable()); - - /// - /// Initializes a new instance of the class. - /// - /// The control type. - /// The content property. - /// - /// Given an control of should return the control's - /// logical children collection. - /// - /// - /// The name of the content presenter in the control's template. - /// - public static void Attach( - AvaloniaProperty content, - Func> logicalChildrenSelector, - string presenterName = "PART_ContentPresenter") - where TControl : TemplatedControl - { - Contract.Requires(content != null); - Contract.Requires(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 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); - } - } - } - } -} diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 1072b21b1b..a5374e7c5a 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/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 _childChanging; private IDataTemplate _dataTemplate; private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper(); @@ -190,12 +190,10 @@ namespace Avalonia.Controls.Presenters set { SetValue(PaddingProperty, value); } } - /// - event EventHandler IContentPresenter.ChildChanging - { - add => _childChanging += value; - remove => _childChanging -= value; - } + /// + /// Gets the host content control. + /// + internal IContentPresenterHost Host { get; private set; } /// 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; } } } diff --git a/src/Avalonia.Controls/Presenters/IContentPresenter.cs b/src/Avalonia.Controls/Presenters/IContentPresenter.cs index 78bffec93b..31ab3a21a6 100644 --- a/src/Avalonia.Controls/Presenters/IContentPresenter.cs +++ b/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. /// object Content { get; set; } - - /// - /// Raised when property is about to change. - /// - /// - /// This event should be raised after the child has been removed from the visual tree, - /// but before the property has changed. It is intended for consumption - /// by in order to update the host control's logical - /// children. - /// - event EventHandler ChildChanging; } } diff --git a/src/Avalonia.Controls/Presenters/IContentPresenterHost.cs b/src/Avalonia.Controls/Presenters/IContentPresenterHost.cs index 3aa7e625ed..4acfba2c71 100644 --- a/src/Avalonia.Controls/Presenters/IContentPresenterHost.cs +++ b/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 /// public interface IContentPresenterHost : ITemplatedControl { + /// + /// Gets a collection describing the logical children of the host control. + /// + IAvaloniaList LogicalChildren { get; } + /// /// Registers an with a host control. /// /// The content presenter. - void RegisterContentPresenter(IContentPresenter presenter); + /// + /// True if the content presenter should add its child to the logical children of the + /// host; otherwise false. + /// + bool RegisterContentPresenter(IContentPresenter presenter); } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs index 98476c9c94..3cf50a7b80 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs +++ b/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 /// static HeaderedContentControl() { - ContentControlMixin.Attach( - HeaderProperty, - x => x.LogicalChildren, - "PART_HeaderPresenter"); + ContentProperty.Changed.AddClassHandler(x => x.HeaderChanged); } /// @@ -63,13 +61,29 @@ namespace Avalonia.Controls.Primitives } /// - 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); } } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs index bda426c23b..e0eb0b005f 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs +++ b/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 /// static HeaderedItemsControl() { - ContentControlMixin.Attach( - HeaderProperty, - x => x.LogicalChildren, - "PART_HeaderPresenter"); + HeaderProperty.Changed.AddClassHandler(x => x.HeaderChanged); } /// @@ -47,20 +46,39 @@ namespace Avalonia.Controls.Primitives } /// - void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) + IAvaloniaList IContentPresenterHost.LogicalChildren => LogicalChildren; + + /// + bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) { - RegisterContentPresenter(presenter); + return RegisterContentPresenter(presenter); } /// /// Called when an is registered with the control. /// /// The presenter. - 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); } } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs index d59be66b2b..533b643ea6 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs +++ b/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 /// static HeaderedSelectingItemsControl() { - ContentControlMixin.Attach( - HeaderProperty, - x => x.LogicalChildren, - "PART_HeaderPresenter"); + HeaderProperty.Changed.AddClassHandler(x => x.HeaderChanged); } /// @@ -47,20 +46,39 @@ namespace Avalonia.Controls.Primitives } /// - void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) + IAvaloniaList IContentPresenterHost.LogicalChildren => LogicalChildren; + + /// + bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) { - RegisterContentPresenter(presenter); + return RegisterContentPresenter(presenter); } /// /// Called when an is registered with the control. /// /// The presenter. - 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); } } } diff --git a/src/Avalonia.Controls/TabControl.cs b/src/Avalonia.Controls/TabControl.cs index fc2c118132..50bcb034ac 100644 --- a/src/Avalonia.Controls/TabControl.cs +++ b/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 /// /// A tab control that displays a tab strip along with the content of the selected tab. /// - public class TabControl : SelectingItemsControl + public class TabControl : SelectingItemsControl, IContentPresenterHost { /// /// Defines the property. @@ -68,10 +70,6 @@ namespace Avalonia.Controls SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); AffectsMeasure(TabStripPlacementProperty); - ContentControlMixin.Attach( - SelectedContentProperty, - x => x.LogicalChildren, - "PART_SelectedContentHost"); } /// @@ -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; } + + /// + IAvaloniaList IContentPresenterHost.LogicalChildren => LogicalChildren; + + /// + bool IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) + { + return RegisterContentPresenter(presenter); + } + + /// + /// Called when an is registered with the control. + /// + /// The presenter. + 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("PART_ItemsPresenter"); - - ContentPart = e.NameScope.Get("PART_SelectedContentHost"); } /// diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index ef7dc33f76..03d061e04f 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/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); } diff --git a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs index ecddb322fa..f7332415ac 100644 --- a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs +++ b/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()), Times.Once()); styler.Verify(x => x.ApplyStyles(It.IsAny()), Times.Once()); @@ -354,6 +355,7 @@ namespace Avalonia.Controls.UnitTests target.Content = "Foo"; target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); Assert.Equal(target, target.Presenter.Child.LogicalParent); diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 522afc9546..ac80fc6c7a 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/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); diff --git a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs deleted file mode 100644 index 638443e17f..0000000000 --- a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs +++ /dev/null @@ -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 Content1Property = - AvaloniaProperty.Register(nameof(Content1)); - - public static readonly StyledProperty Content2Property = - AvaloniaProperty.Register(nameof(Content2)); - - static TestControl() - { - ContentControlMixin.Attach(Content1Property, x => x.LogicalChildren, "Content_1_Presenter"); - ContentControlMixin.Attach(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); } - } - } - } -} diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs index 6ab9c345d4..6d6dfc9230 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs +++ b/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 diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index 0ebe6833d3..f75f6fcf91 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/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; diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index ee8d9cc62e..ddf7e7a0fa 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/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(target.ContentPart.Child); Assert.Equal("bar", content.Tag); diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index 901d780f16..645f87163a 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -202,8 +202,9 @@ namespace Avalonia.Controls.UnitTests target.Template = CreateTemplate(); target.Content = child; + target.ApplyTemplate(); - Assert.Throws(() => target.ApplyTemplate()); + Assert.Throws(() => target.Presenter.ApplyTemplate()); } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index b1abc9ea54..77bb215ad5 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/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); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs index 86b874f75c..f8678ee22e 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs @@ -73,6 +73,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var button = window.FindControl + + Single Multiple diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs b/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs index 8a67766c76..cdbf8fd2b6 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml.cs +++ b/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(Enumerable.Range(1, 10).Select(i => GenerateItem())); + Items = new ObservableCollection(Enumerable.Range(1, 10000).Select(i => GenerateItem())); SelectedItems = new ObservableCollection(); 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 Items { get; } + private string _selectedItem; + + public string SelectedItem + { + get { return _selectedItem; } + set { this.RaiseAndSetIfChanged(ref _selectedItem, value); } + } + + public ObservableCollection SelectedItems { get; } public ReactiveCommand AddItemCommand { get; } public ReactiveCommand RemoveItemCommand { get; } + public ReactiveCommand SelectRandomItemCommand { get; } + public SelectionMode SelectionMode { get => _selectionMode; From e9ae26e09c3aa08003e3cf3a02a7723c48d96c50 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 9 Sep 2019 20:49:46 +0100 Subject: [PATCH 05/25] fix scrolling to selected item. --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 44ae89fdbc..b53b9f73b6 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -858,7 +858,7 @@ namespace Avalonia.Controls.Primitives if (AutoScrollToSelectedItem) { - ScrollIntoView(_selectedIndex); + ScrollIntoView(_selectedItem); } } } @@ -1046,6 +1046,11 @@ namespace Avalonia.Controls.Primitives removed?.Select(x => ElementAt(Items, x)).ToArray() ?? Array.Empty()); RaiseEvent(e); } + + if(AutoScrollToSelectedItem) + { + ScrollIntoView(_selectedItem); + } } private void UpdateSelectedItems(Action action) From cce605af493864b9e996cef1441fc70ccd546c50 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 9 Sep 2019 23:51:30 +0200 Subject: [PATCH 06/25] Remove unused/bad code. Removed the code that was causing the TreeView problems described in #2944. This code was never actually called except in `TreeView` because a plain `ItemsControl` can't be used with virtualization (it uses untyped `ItemContainerGenerator` so items can't be recycled) and every other derived class derives from SelectingItemsControl which overrides this method and doesn't call the base implementation. In addition there were no unit tests covering this code. Fixes #2944 --- src/Avalonia.Controls/ItemsControl.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index 902e55bde6..0fe7291835 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -302,19 +302,6 @@ namespace Avalonia.Controls /// The details of the containers. protected virtual void OnContainersRecycled(ItemContainerEventArgs e) { - var toRemove = new List(); - - 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); } /// From d636c7647e2deb92fd0f84e40fd5cb4b92b17ffc Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 9 Sep 2019 23:14:11 +0100 Subject: [PATCH 07/25] only require a single call to ScrollIntoView --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index b53b9f73b6..336bcffa54 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -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(_selectedItem); - } } } From 9e6d3db5207d7d79d1551ec50b6c34fc9759334a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 9 Sep 2019 23:14:20 +0100 Subject: [PATCH 08/25] fix whitespace. --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 336bcffa54..340947d2e1 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -1042,7 +1042,7 @@ namespace Avalonia.Controls.Primitives RaiseEvent(e); } - if(AutoScrollToSelectedItem) + if (AutoScrollToSelectedItem) { ScrollIntoView(_selectedItem); } From 6f44dfac5b42a5240cb92e01c374369c6858de02 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 10 Sep 2019 00:18:11 +0200 Subject: [PATCH 09/25] Added unit test for #2952. --- .../Primitives/SelectingItemsControlTests.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index bc002174ec..f4f500b802 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -894,6 +894,33 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal("Qux", target.SelectedItem); } + [Fact] + public void AutoScrollToSelectedItem_Causes_Scroll_To_SelectedItem() + { + var items = new ObservableCollection + { + "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); + } + private FuncControlTemplate Template() { return new FuncControlTemplate((control, scope) => From fc33d6978caa52fbf543780ca94b71adcfa512d5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 10 Sep 2019 00:39:01 +0200 Subject: [PATCH 10/25] Fix DevTools crash. Make sure `HeaderPresenter` has its template applied before trying to get its child. --- src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs b/src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs index ddf1473b45..1326f718de 100644 --- a/src/Avalonia.Diagnostics/Views/TreePageView.xaml.cs +++ b/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; From 13333cd820b7503a2cbbf299fe813b47e930a804 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 10 Sep 2019 00:43:56 +0200 Subject: [PATCH 11/25] Avoid NRE in DefaultMenuInteractionHandler. The input can arrive after the interaction handler has already been detached. --- src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index b0dfa4185e..98f925cd0c 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -377,7 +377,7 @@ namespace Avalonia.Controls.Platform if (mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) { - Menu.Close(); + Menu?.Close(); } } From f0a9de18ac54212379ae23d231d47a32cdac5269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Komosi=C5=84ski?= Date: Tue, 10 Sep 2019 16:50:57 +0200 Subject: [PATCH 12/25] Remove lambda closure allocations. Optimize search for conversion operator. --- src/Avalonia.Base/Utilities/TypeUtilities.cs | 50 ++++++++++++++++---- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 7b76457049..d85eb4cd76 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -13,7 +13,7 @@ namespace Avalonia.Utilities /// 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; + } } } From 7f33ffaf8b6446e8ccca4c5b9f103b476f8e8960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Komosi=C5=84ski?= Date: Wed, 11 Sep 2019 10:30:49 +0200 Subject: [PATCH 13/25] Avoid allocations when running UpdateIsEffectivelyEnabled. --- src/Avalonia.Input/InputElement.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 4b4ab177b8..5b9b2676c0 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -565,9 +565,13 @@ namespace Avalonia.Input { IsEffectivelyEnabled = IsEnabledCore && (parent?.IsEffectivelyEnabled ?? true); - foreach (var child in this.GetVisualChildren().OfType()) + var children = VisualChildren; + + for (int i = 0; i < children.Count; ++i) { - child.UpdateIsEffectivelyEnabled(this); + var child = children[i] as InputElement; + + child?.UpdateIsEffectivelyEnabled(this); } } } From 7237fa979e9d8482d418735746198db19a997644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Komosi=C5=84ski?= Date: Wed, 11 Sep 2019 10:40:43 +0200 Subject: [PATCH 14/25] Add comments. --- src/Avalonia.Input/InputElement.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.Input/InputElement.cs b/src/Avalonia.Input/InputElement.cs index 5b9b2676c0..47e85416cf 100644 --- a/src/Avalonia.Input/InputElement.cs +++ b/src/Avalonia.Input/InputElement.cs @@ -565,8 +565,12 @@ namespace Avalonia.Input { IsEffectivelyEnabled = IsEnabledCore && (parent?.IsEffectivelyEnabled ?? true); + // 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) { var child = children[i] as InputElement; From 05b580317c5a71e850b620186bcf255d753ed803 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 11 Sep 2019 10:58:41 +0100 Subject: [PATCH 15/25] fix crash scrolling to view when clearing list. --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 340947d2e1..8659e6e3e5 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -1042,7 +1042,7 @@ namespace Avalonia.Controls.Primitives RaiseEvent(e); } - if (AutoScrollToSelectedItem) + if (AutoScrollToSelectedItem && _selectedItem != null) { ScrollIntoView(_selectedItem); } From 1f9bb75ed3a8ec4064984254f43827a83c9ece33 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 11 Sep 2019 13:34:02 +0200 Subject: [PATCH 16/25] Add setter for ListBox.SelectedItems. It had been erroneously omitted. --- src/Avalonia.Controls/ListBox.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index f26cd47bcb..449ca18465 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -66,7 +66,11 @@ namespace Avalonia.Controls } /// - public new IList SelectedItems => base.SelectedItems; + public new IList SelectedItems + { + get => base.SelectedItems; + set => base.SelectedItems = value; + } /// /// Gets or sets the selection mode. From 33cf4187791f840644d9867a427919298b05ca32 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 11 Sep 2019 13:34:33 +0200 Subject: [PATCH 17/25] Added failing test for #2969. --- .../Primitives/SelectingItemsControlTests.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index f4f500b802..4e4d92afdc 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -921,6 +921,26 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.True(raised); } + [Fact] + public void Can_Set_Both_SelectedItem_And_SelectedItems_During_Initialization() + { + // Issue #2969. + var target = new ListBox(); + var selectedItems = new List(); + + 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((control, scope) => From 25ebbd63be20ade9dd90a0864046b0dbcb9a7667 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 11 Sep 2019 13:35:00 +0200 Subject: [PATCH 18/25] Allow both SelectedItem and SelectedItems to be bound. Fixes #2969. --- .../Primitives/SelectingItemsControl.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 340947d2e1..2eedd29258 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/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; /// /// Initializes static members of the 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(value); + _updateSelectedItem = value; _updateSelectedIndex = int.MinValue; } } @@ -1075,9 +1075,9 @@ namespace Avalonia.Controls.Primitives { SelectedIndex = _updateSelectedIndex; } - else if (_updateSelectedItems != null) + else if (_updateSelectedItem != null) { - SelectedItems = _updateSelectedItems; + SelectedItem = _updateSelectedItem; } } From 79ecf98a46850d4378214cd205af29d195ebc416 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 11 Sep 2019 13:50:08 +0100 Subject: [PATCH 19/25] add null check to itemvirtualizer simple --- .../Presenters/ItemVirtualizerSimple.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index b8b8094582..0fcd32ba23 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -295,11 +295,14 @@ namespace Avalonia.Controls.Presenters /// 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); + } } } From 27e1b9d0f5437d109c090a9df5a4f0e4c00c7af1 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 11 Sep 2019 13:50:26 +0100 Subject: [PATCH 20/25] check selectedindex instead of _selectedItem --- src/Avalonia.Controls/Primitives/SelectingItemsControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 8659e6e3e5..d3aad42888 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -1042,7 +1042,7 @@ namespace Avalonia.Controls.Primitives RaiseEvent(e); } - if (AutoScrollToSelectedItem && _selectedItem != null) + if (AutoScrollToSelectedItem && _selectedIndex != -1) { ScrollIntoView(_selectedItem); } From 4762b8ec3392d1a4276d26278538748b2ea3f367 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 11 Sep 2019 13:58:32 +0100 Subject: [PATCH 21/25] fix whitespace. --- src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index 0fcd32ba23..cd14211075 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -295,7 +295,7 @@ namespace Avalonia.Controls.Presenters /// public override void ScrollIntoView(object item) { - if(Items != null) + if (Items != null) { var index = Items.IndexOf(item); From 69fa0f7f28ee9936757754806247015bdc7a392f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 11 Sep 2019 18:33:16 +0100 Subject: [PATCH 22/25] stackpanel copes with non-visible controls when arranging. --- src/Avalonia.Controls/StackPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index bd3441078d..9e087b7fd3 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/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) From b80aa21610bc488b68cc11f3f5803b9b7e79527b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 11 Sep 2019 18:50:58 +0100 Subject: [PATCH 23/25] add a unit test for stackpanel layout issue. --- .../StackPanelTests.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs index db113f0569..984734d414 100644 --- a/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs +++ b/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; } From 91279db308731453057205e6c54366304d67b1bb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 11 Sep 2019 18:55:08 +0100 Subject: [PATCH 24/25] apply fix to reversible stackpanel. --- src/Avalonia.Controls/Notifications/ReversibleStackPanel.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Controls/Notifications/ReversibleStackPanel.cs b/src/Avalonia.Controls/Notifications/ReversibleStackPanel.cs index 768d87c63e..9edf9848fd 100644 --- a/src/Avalonia.Controls/Notifications/ReversibleStackPanel.cs +++ b/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; From bdca92c16a0748ff67d85a7433fa3ba0e7b87a66 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 11 Sep 2019 21:29:38 +0100 Subject: [PATCH 25/25] dont handle f10 key in accesskey handler, prevents people using f10 hotkey. --- src/Avalonia.Input/AccessKeyHandler.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Avalonia.Input/AccessKeyHandler.cs b/src/Avalonia.Input/AccessKeyHandler.cs index 29768513f3..9e4b2b84e0 100644 --- a/src/Avalonia.Input/AccessKeyHandler.cs +++ b/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; } }