Przeglądaj źródła

Merge pull request #1031 from AvaloniaUI/fixes/1029-fix-tooltip-crash

Fix exception in ToolTip.
Steven Kirk 8 lat temu
rodzic
commit
2f301b2bcd

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

@@ -118,6 +118,7 @@ namespace Avalonia.Controls
         public Control()
         {
             _nameScope = this as INameScope;
+            _isAttachedToLogicalTree = this is IStyleRoot;
         }
 
         /// <summary>
@@ -369,6 +370,12 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <inheritdoc/>
+        void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
+        {
+            this.OnAttachedToLogicalTreeCore(e);
+        }
+
         /// <inheritdoc/>
         void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
@@ -418,7 +425,7 @@ namespace Avalonia.Controls
 
                 if (_isAttachedToLogicalTree)
                 {
-                    var oldRoot = FindStyleRoot(old);
+                    var oldRoot = FindStyleRoot(old) ?? this as IStyleRoot;
 
                     if (oldRoot == null)
                     {
@@ -436,7 +443,7 @@ namespace Avalonia.Controls
 
                 _parent = (IControl)parent;
 
-                if (_parent is IStyleRoot || _parent?.IsAttachedToLogicalTree == true)
+                if (_parent is IStyleRoot || _parent?.IsAttachedToLogicalTree == true || this is IStyleRoot)
                 {
                     var newRoot = FindStyleRoot(this);
 

+ 11 - 0
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@@ -285,6 +285,17 @@ namespace Avalonia.Controls.Primitives
             return this;
         }
 
+        /// <inheritdoc/>
+        protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
+        {
+            if (VisualChildren.Count > 0)
+            {
+                ((ILogical)VisualChildren[0]).NotifyAttachedToLogicalTree(e);
+            }
+
+            base.OnAttachedToLogicalTree(e);
+        }
+
         /// <inheritdoc/>
         protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
         {

+ 26 - 14
src/Avalonia.Controls/ToolTip.cs

@@ -105,16 +105,21 @@ namespace Avalonia.Controls
         {
             if (control != null && control.IsVisible && control.GetVisualRoot() != null)
             {
-                if (s_popup != null)
-                {
-                    throw new AvaloniaInternalException("Previous ToolTip not disposed.");
-                }
                 var cp = (control.GetVisualRoot() as IInputRoot)?.MouseDevice?.GetPosition(control);
                 var position = control.PointToScreen(cp ?? new Point(0, 0)) + new Vector(0, 22);
 
-                s_popup = new PopupRoot();
+                if (s_popup == null)
+                {
+                    s_popup = new PopupRoot();
+                    s_popup.Content = new ToolTip();
+                }
+                else
+                {
+                    ((ISetLogicalParent)s_popup).SetParent(null);
+                }
+
                 ((ISetLogicalParent)s_popup).SetParent(control);
-                s_popup.Content = new ToolTip { Content = GetTip(control) };
+                ((ToolTip)s_popup.Content).Content = GetTip(control);
                 s_popup.Position = position;
                 s_popup.Show();
 
@@ -146,16 +151,23 @@ namespace Avalonia.Controls
             {
                 if (s_popup != null)
                 {
-                    // Clear the ToolTip's Content in case it has control content: this will
-                    // reset its visual parent allowing it to be used again.
-                    ((ToolTip)s_popup.Content).Content = null;
-
-                    // Dispose of the popup.
-                    s_popup.Dispose();
-                    s_popup = null;
+                    DisposeTooltip();
+                    s_show.OnNext(null);
                 }
+            }
+        }
+
+        private static void DisposeTooltip()
+        {
+            if (s_popup != null)
+            {
+                // Clear the ToolTip's Content in case it has control content: this will
+                // reset its visual parent allowing it to be used again.
+                ((ToolTip)s_popup.Content).Content = null;
 
-                s_show.OnNext(null);
+                // Dispose of the popup.
+                s_popup.Dispose();
+                s_popup = null;
             }
         }
     }

+ 10 - 0
src/Avalonia.Styling/LogicalTree/ILogical.cs

@@ -36,6 +36,16 @@ namespace Avalonia.LogicalTree
         /// </summary>
         IAvaloniaReadOnlyList<ILogical> LogicalChildren { get; }
 
+        /// <summary>
+        /// Notifies the control that it is being attached to a rooted logical tree.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        /// <remarks>
+        /// This method will be called automatically by the framework, you should not need to call
+        /// this method yourself.
+        /// </remarks>
+        void NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e);
+
         /// <summary>
         /// Notifies the control that it is being detached from a rooted logical tree.
         /// </summary>

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

@@ -0,0 +1,109 @@
+// 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.Presenters;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
+using Avalonia.LogicalTree;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests.Primitives
+{
+    public class PopupRootTests
+    {
+        [Fact]
+        public void PopupRoot_IsAttachedToLogicalTree_Is_True()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var target = CreateTarget();
+
+                Assert.True(((ILogical)target).IsAttachedToLogicalTree);
+            }
+        }
+
+        [Fact]
+        public void Templated_Child_IsAttachedToLogicalTree_Is_True()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var target = CreateTarget();
+
+                Assert.True(target.Presenter.IsAttachedToLogicalTree);
+            }
+        }
+
+        [Fact]
+        public void Attaching_PopupRoot_To_Parent_Logical_Tree_Raises_DetachedFromLogicalTree_And_AttachedToLogicalTree()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var child = new Decorator();
+                var target = CreateTarget();
+                var window = new Window();
+                var detachedCount = 0;
+                var attachedCount = 0;
+
+                target.Content = child;
+
+                target.DetachedFromLogicalTree += (s, e) => ++detachedCount;
+                child.DetachedFromLogicalTree += (s, e) => ++detachedCount;
+                target.AttachedToLogicalTree += (s, e) => ++attachedCount;
+                child.AttachedToLogicalTree += (s, e) => ++attachedCount;
+
+                ((ISetLogicalParent)target).SetParent(window);
+
+                Assert.Equal(2, detachedCount);
+                Assert.Equal(2, attachedCount);
+            }
+        }
+
+        [Fact]
+        public void Detaching_PopupRoot_From_Parent_Logical_Tree_Raises_DetachedFromLogicalTree_And_AttachedToLogicalTree()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var child = new Decorator();
+                var target = CreateTarget();
+                var window = new Window();
+                var detachedCount = 0;
+                var attachedCount = 0;
+
+                target.Content = child;
+                ((ISetLogicalParent)target).SetParent(window);
+
+                target.DetachedFromLogicalTree += (s, e) => ++detachedCount;
+                child.DetachedFromLogicalTree += (s, e) => ++detachedCount;
+                target.AttachedToLogicalTree += (s, e) => ++attachedCount;
+                child.AttachedToLogicalTree += (s, e) => ++attachedCount;
+
+                ((ISetLogicalParent)target).SetParent(null);
+
+                // Despite being detached from the parent logical tree, we're still attached to a
+                // logical tree as PopupRoot itself is a logical tree root.
+                Assert.True(((ILogical)target).IsAttachedToLogicalTree);
+                Assert.True(((ILogical)child).IsAttachedToLogicalTree);
+                Assert.Equal(2, detachedCount);
+                Assert.Equal(2, attachedCount);
+            }
+        }
+
+        private PopupRoot CreateTarget()
+        {
+            var result = new PopupRoot
+            {
+                Template = new FuncControlTemplate<PopupRoot>(_ =>
+                    new ContentPresenter
+                    {
+                        Name = "PART_ContentPresenter",
+                    }),
+            };
+
+            result.ApplyTemplate();
+
+            return result;
+        }
+    }
+}

+ 28 - 0
tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs

@@ -527,6 +527,34 @@ namespace Avalonia.Controls.UnitTests.Primitives
             }
         }
 
+        [Fact]
+        public void Moving_To_New_LogicalTree_Should_Detach_Attach_Template_Child()
+        {
+            using (UnitTestApplication.Start(TestServices.RealStyler))
+            {
+                TestTemplatedControl target;
+                var root = new TestRoot
+                {
+                    Child = target = new TestTemplatedControl
+                    {
+                        Template = new FuncControlTemplate(_ => new Decorator()),
+                    }
+                };
+
+                Assert.NotNull(target.Template);
+                target.ApplyTemplate();
+
+                var templateChild = (ILogical)target.GetVisualChildren().Single();
+                Assert.True(templateChild.IsAttachedToLogicalTree);
+
+                root.Child = null;
+                Assert.False(templateChild.IsAttachedToLogicalTree);
+
+                var newRoot = new TestRoot { Child = target };
+                Assert.True(templateChild.IsAttachedToLogicalTree);
+            }
+        }
+
         private static IControl ScrollingContentControlTemplate(ContentControl control)
         {
             return new Border

+ 14 - 5
tests/Avalonia.Controls.UnitTests/TopLevelTests.cs

@@ -2,24 +2,33 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
-using System.Reactive;
-using System.Reactive.Subjects;
-using Moq;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Templates;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
 using Avalonia.Layout;
+using Avalonia.LogicalTree;
 using Avalonia.Platform;
-using Avalonia.Rendering;
-using Avalonia.Styling;
 using Avalonia.UnitTests;
+using Moq;
 using Xunit;
 
 namespace Avalonia.Controls.UnitTests
 {
     public class TopLevelTests
     {
+        [Fact]
+        public void IsAttachedToLogicalTree_Is_True()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var impl = new Mock<ITopLevelImpl>();
+                var target = new TestTopLevel(impl.Object);
+
+                Assert.True(((ILogical)target).IsAttachedToLogicalTree);
+            }
+        }
+
         [Fact]
         public void ClientSize_Should_Be_Set_On_Construction()
         {

+ 5 - 0
tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs

@@ -145,6 +145,11 @@ namespace Avalonia.Styling.UnitTests
                 throw new NotImplementedException();
             }
 
+            public void NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
+            {
+                throw new NotImplementedException();
+            }
+
             public void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
             {
                 throw new NotImplementedException();

+ 5 - 0
tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs

@@ -175,6 +175,11 @@ namespace Avalonia.Styling.UnitTests
                 throw new NotImplementedException();
             }
 
+            public void NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
+            {
+                throw new NotImplementedException();
+            }
+
             public void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
             {
                 throw new NotImplementedException();