Browse Source

Apply target's TemplatedParent to a Flyout and Tooltip, when it's opened (#8412)

* Apply target's TemplatedParent to a Flyout and Tooltip, when it's opened

* Add flyout and tooltip leak tests

* Fix Flyout_Is_Freed
Max Katz 3 years ago
parent
commit
49aad04861

+ 1 - 0
src/Avalonia.Controls/Flyouts/FlyoutBase.cs

@@ -223,6 +223,7 @@ namespace Avalonia.Controls.Primitives
             {
                 Popup.PlacementTarget = Target = placementTarget;
                 ((ISetLogicalParent)Popup).SetParent(placementTarget);
+                Popup.SetValue(StyledElement.TemplatedParentProperty, placementTarget.TemplatedParent);
             }
 
             if (Popup.Child == null)

+ 1 - 16
src/Avalonia.Controls/Primitives/Popup.cs

@@ -860,22 +860,7 @@ namespace Avalonia.Controls.Primitives
         {
             if (control != null)
             {
-                var templatedParent = TemplatedParent;
-
-                if (control.TemplatedParent == null)
-                {
-                    control.SetValue(TemplatedParentProperty, templatedParent);
-                }
-
-                control.ApplyTemplate();
-
-                if (!(control is IPresenter) && control.TemplatedParent == templatedParent)
-                {
-                    foreach (IControl child in control.VisualChildren)
-                    {
-                        SetTemplatedParentAndApplyChildTemplates(child);
-                    }
-                }
+                TemplatedControl.ApplyTemplatedParent(control, TemplatedParent);
             }
         }
 

+ 5 - 5
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@@ -285,7 +285,7 @@ namespace Avalonia.Controls.Primitives
                     Logger.TryGet(LogEventLevel.Verbose, LogArea.Control)?.Log(this, "Creating control template");
 
                     var (child, nameScope) = template.Build(this);
-                    ApplyTemplatedParent(child);
+                    ApplyTemplatedParent(child, this);
                     ((ISetLogicalParent)child).SetParent(this);
                     VisualChildren.Add(child);
                     
@@ -387,18 +387,18 @@ namespace Avalonia.Controls.Primitives
         /// Sets the TemplatedParent property for the created template children.
         /// </summary>
         /// <param name="control">The control.</param>
-        private void ApplyTemplatedParent(IControl control)
+        internal static void ApplyTemplatedParent(IStyledElement control, ITemplatedControl? templatedParent)
         {
-            control.SetValue(TemplatedParentProperty, this);
+            control.SetValue(TemplatedParentProperty, templatedParent);
 
             var children = control.LogicalChildren;
             var count = children.Count;
 
             for (var i = 0; i < count; i++)
             {
-                if (children[i] is IControl child)
+                if (children[i] is IStyledElement child)
                 {
-                    ApplyTemplatedParent(child);
+                    ApplyTemplatedParent(child, templatedParent);
                 }
             }
         }

+ 3 - 2
src/Avalonia.Controls/ToolTip.cs

@@ -271,8 +271,9 @@ namespace Avalonia.Controls
             _popupHost = OverlayPopupHost.CreatePopupHost(control, null);
             _popupHost.SetChild(this);
             ((ISetLogicalParent)_popupHost).SetParent(control);
-            
-            _popupHost.ConfigurePosition(control, GetPlacement(control), 
+            ApplyTemplatedParent(this, control.TemplatedParent);
+
+            _popupHost.ConfigurePosition(control, GetPlacement(control),
                 new Point(GetHorizontalOffset(control), GetVerticalOffset(control)));
 
             WindowManagerAddShadowHintChanged(_popupHost, false);

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

@@ -7,6 +7,7 @@ using System.Reactive.Disposables;
 using Avalonia.Collections;
 using Avalonia.Controls;
 using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Shapes;
 using Avalonia.Controls.Templates;
 using Avalonia.Data;
@@ -877,6 +878,110 @@ namespace Avalonia.LeakTests
             }
         }
 
+        [Fact]
+        public void ToolTip_Is_Freed()
+        {
+            using (Start())
+            {
+                Func<Window> run = () =>
+                {
+                    var window = new Window();
+                    var source = new Button
+                    {
+                        Template = new FuncControlTemplate<Button>((parent, _) =>
+                            new Decorator
+                            {
+                                [ToolTip.TipProperty] = new TextBlock
+                                {
+                                    [~TextBlock.TextProperty] = new TemplateBinding(ContentControl.ContentProperty)
+                                }
+                            }),
+                    };
+
+                    window.Content = source;
+                    window.Show();
+
+                    var templateChild = (Decorator)source.GetVisualChildren().Single();
+                    ToolTip.SetIsOpen(templateChild, true);
+
+                    ToolTip.SetIsOpen(templateChild, false);
+
+                    // Detach the button from the logical tree, so there is no reference to it
+                    window.Content = null;
+                    
+                    // Mock keep reference on a Popup via InvocationsCollection. So let's clear it before. 
+                    Mock.Get(window.PlatformImpl).Invocations.Clear();
+                    
+                    return window;
+                };
+
+                var result = run();
+
+                // Process all Loaded events to free control reference(s)
+                Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
+
+                dotMemory.Check(memory =>
+                {
+                    Assert.Equal(0, memory.GetObjects(where => where.Type.Is<TextBlock>()).ObjectsCount);
+                    Assert.Equal(0, memory.GetObjects(where => where.Type.Is<ToolTip>()).ObjectsCount);
+                });
+            }
+        }
+        
+        [Fact]
+        public void Flyout_Is_Freed()
+        {
+            using (Start())
+            {
+                Func<Window> run = () =>
+                {
+                    var window = new Window();
+                    var source = new Button
+                    {
+                        Template = new FuncControlTemplate<Button>((parent, _) =>
+                            new Button
+                            {
+                                Flyout = new Flyout
+                                {
+                                    Content = new TextBlock
+                                    {
+                                        [~TextBlock.TextProperty] = new TemplateBinding(ContentControl.ContentProperty)
+                                    }
+                                }
+                            }),
+                    };
+
+                    window.Content = source;
+                    window.Show();
+
+                    var templateChild = (Button)source.GetVisualChildren().Single();
+                    templateChild.Flyout!.ShowAt(templateChild);
+                    
+                    templateChild.Flyout!.Hide();
+
+                    // Detach the button from the logical tree, so there is no reference to it
+                    window.Content = null;
+
+                    // Mock keep reference on a Popup via InvocationsCollection. So let's clear it before. 
+                    Mock.Get(window.PlatformImpl).Invocations.Clear();
+                    
+                    return window;
+                };
+
+                var result = run();
+
+                // Process all Loaded events to free control reference(s)
+                Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
+
+                dotMemory.Check(memory =>
+                {
+                    Assert.Equal(0, memory.GetObjects(where => where.Type.Is<TextBlock>()).ObjectsCount);
+                    Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Flyout>()).ObjectsCount);
+                    Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Popup>()).ObjectsCount);
+                });
+            }
+        }
+        
         private FuncControlTemplate CreateWindowTemplate()
         {
             return new FuncControlTemplate<Window>((parent, scope) =>

+ 127 - 0
tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs

@@ -3,9 +3,11 @@ using System.Globalization;
 using System.Linq;
 using Avalonia.Controls;
 using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives;
 using Avalonia.Controls.Templates;
 using Avalonia.Data;
 using Avalonia.Data.Converters;
+using Avalonia.UnitTests;
 using Avalonia.VisualTree;
 using Xunit;
 
@@ -90,6 +92,131 @@ namespace Avalonia.Markup.UnitTests.Data
             Assert.Equal("bar", source.Content);
         }
 
+        [Fact]
+        public void Should_Work_Inside_Of_Tooltip()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var window = new Window();
+                var source = new Button
+                {
+                    Template = new FuncControlTemplate<Button>((parent, _) =>
+                        new Decorator
+                        {
+                            [ToolTip.TipProperty] = new TextBlock
+                            {
+                                [~TextBlock.TextProperty] = new TemplateBinding(ContentControl.ContentProperty)
+                            }
+                        }),
+                };
+
+                window.Content = source;
+                window.Show();
+                try
+                {
+                    var templateChild = (Decorator)source.GetVisualChildren().Single();
+                    ToolTip.SetIsOpen(templateChild, true);
+
+                    var target = (TextBlock)ToolTip.GetTip(templateChild)!;
+
+                    Assert.Null(target.Text);
+                    source.Content = "foo";
+                    Assert.Equal("foo", target.Text);
+                    source.Content = "bar";
+                    Assert.Equal("bar", target.Text);
+                }
+                finally
+                {
+                    window.Close();
+                }
+            }
+        }
+
+        [Fact]
+        public void Should_Work_Inside_Of_Popup()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var window = new Window();
+                var source = new Button
+                {
+                    Template = new FuncControlTemplate<Button>((parent, _) =>
+                        new Popup
+                        {
+                            Child = new TextBlock
+                            {
+                                [~TextBlock.TextProperty] = new TemplateBinding(ContentControl.ContentProperty)
+                            }
+                        }),
+                };
+
+                window.Content = source;
+                window.Show();
+                try
+                {
+                    var popup = (Popup)source.GetVisualChildren().Single();
+                    popup.IsOpen = true;
+
+                    var target = (TextBlock)popup.Child!;
+
+                    target[~TextBlock.TextProperty] = new TemplateBinding(ContentControl.ContentProperty);
+                    Assert.Null(target.Text);
+                    source.Content = "foo";
+                    Assert.Equal("foo", target.Text);
+                    source.Content = "bar";
+                    Assert.Equal("bar", target.Text);
+                }
+                finally
+                {
+                    window.Close();
+                }
+            }
+        }
+
+        [Fact]
+        public void Should_Work_Inside_Of_Flyout()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var window = new Window();
+                var source = new Button
+                {
+                    Template = new FuncControlTemplate<Button>((parent, _) =>
+                        new Button
+                        {
+                            Flyout = new Flyout
+                            {
+                                Content = new TextBlock
+                                {
+                                    [~TextBlock.TextProperty] = new TemplateBinding(ContentControl.ContentProperty)
+                                }
+                            }
+                        }),
+                };
+
+                window.Content = source;
+                window.Show();
+                try
+                {
+                    var templateChild = (Button)source.GetVisualChildren().Single();
+                    templateChild.Flyout!.ShowAt(templateChild);
+
+                    var target = (TextBlock)((Flyout)templateChild.Flyout).Content!;
+
+                    target[~TextBlock.TextProperty] = new TemplateBinding(ContentControl.ContentProperty);
+                    Assert.Null(target.Text);
+                    source.Content = "foo";
+                    Assert.Equal("foo", target.Text);
+                    source.Content = "bar";
+                    Assert.Equal("bar", target.Text);
+                }
+                finally
+                {
+                    window.Close();
+                }
+            }
+        }
+
         private class PrefixConverter : IValueConverter
         {
             public object Convert(object value, Type targetType, object parameter, CultureInfo culture)