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
pull/8440/head
Max Katz 4 years ago
committed by GitHub
parent
commit
49aad04861
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  2. 17
      src/Avalonia.Controls/Primitives/Popup.cs
  3. 10
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  4. 5
      src/Avalonia.Controls/ToolTip.cs
  5. 105
      tests/Avalonia.LeakTests/ControlTests.cs
  6. 127
      tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs

1
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)

17
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);
}
}

10
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);
}
}
}

5
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
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
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)

Loading…
Cancel
Save