Browse Source

Merge pull request #2438 from AvaloniaUI/fixes/2420-slider-leak

Fix leak when TemplateBinding appears in style setter
pull/2487/head
danwalmsley 7 years ago
committed by GitHub
parent
commit
70e09f4dab
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      src/Avalonia.Controls/Control.cs
  2. 10
      src/Avalonia.Styling/Styling/IRequiresTemplateInSetter.cs
  3. 13
      src/Avalonia.Styling/Styling/ISetterValue.cs
  4. 8
      src/Avalonia.Styling/Styling/Setter.cs
  5. 17
      src/Markup/Avalonia.Markup/Data/TemplateBinding.cs
  6. 33
      tests/Avalonia.LeakTests/ControlTests.cs
  7. 14
      tests/Avalonia.Styling.UnitTests/SetterTests.cs

10
src/Avalonia.Controls/Control.cs

@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
@ -20,7 +21,7 @@ namespace Avalonia.Controls
/// ///
/// - A <see cref="Tag"/> property to allow user-defined data to be attached to the control. /// - A <see cref="Tag"/> property to allow user-defined data to be attached to the control.
/// </remarks> /// </remarks>
public class Control : InputElement, IControl, INamed, ISupportInitialize, IVisualBrushInitialize, IRequiresTemplateInSetter public class Control : InputElement, IControl, INamed, ISupportInitialize, IVisualBrushInitialize, ISetterValue
{ {
/// <summary> /// <summary>
/// Defines the <see cref="FocusAdorner"/> property. /// Defines the <see cref="FocusAdorner"/> property.
@ -90,6 +91,13 @@ namespace Avalonia.Controls
/// <inheritdoc/> /// <inheritdoc/>
bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
/// <inheritdoc/>
void ISetterValue.Initialize(ISetter setter)
{
throw new InvalidOperationException(
"Cannot use a control as a Setter value. Wrap the control in a <Template>.");
}
/// <inheritdoc/> /// <inheritdoc/>
void IVisualBrushInitialize.EnsureInitialized() void IVisualBrushInitialize.EnsureInitialized()
{ {

10
src/Avalonia.Styling/Styling/IRequiresTemplateInSetter.cs

@ -1,10 +0,0 @@
namespace Avalonia.Styling
{
/// <summary>
/// This is an interface for advanced scenarios to assist users in correct style development.
/// You as a user will not need to use this interface directly.
/// </summary>
public interface IRequiresTemplateInSetter
{
}
}

13
src/Avalonia.Styling/Styling/ISetterValue.cs

@ -0,0 +1,13 @@
namespace Avalonia.Styling
{
/// <summary>
/// Customizes the behavior of a class when added as a value to an <see cref="ISetter"/>.
/// </summary>
public interface ISetterValue
{
/// <summary>
/// Notifies that the object has been added as a setter value.
/// </summary>
void Initialize(ISetter setter);
}
}

8
src/Avalonia.Styling/Styling/Setter.cs

@ -65,13 +65,7 @@ namespace Avalonia.Styling
set set
{ {
if (value is IRequiresTemplateInSetter) (value as ISetterValue)?.Initialize(this);
{
throw new ArgumentException(
"Cannot assign a control to Setter.Value. Wrap the control in a <Template>.",
nameof(value));
}
_value = value; _value = value;
} }
} }

17
src/Markup/Avalonia.Markup/Data/TemplateBinding.cs

@ -3,6 +3,7 @@ using System.Globalization;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Styling;
namespace Avalonia.Data namespace Avalonia.Data
{ {
@ -12,8 +13,10 @@ namespace Avalonia.Data
public class TemplateBinding : SingleSubscriberObservableBase<object>, public class TemplateBinding : SingleSubscriberObservableBase<object>,
IBinding, IBinding,
IDescription, IDescription,
ISubject<object> ISubject<object>,
ISetterValue
{ {
private bool _isSetterValue;
private IStyledElement _target; private IStyledElement _target;
private Type _targetType; private Type _targetType;
@ -35,10 +38,11 @@ namespace Avalonia.Data
{ {
// Usually each `TemplateBinding` will only be instantiated once; in this case we can // Usually each `TemplateBinding` will only be instantiated once; in this case we can
// use the `TemplateBinding` object itself as the instanced binding in order to save // use the `TemplateBinding` object itself as the instanced binding in order to save
// allocating a new object. If the binding *is* instantiated more than once (which can // allocating a new object.
// happen if it appears in a `Setter` for example, then just make a clone and instantiate //
// that. // If the binding appears in a `Setter`, then make a clone and instantiate that because
if (_target == null) // because the setter can outlive the control and cause a leak.
if (_target == null && !_isSetterValue)
{ {
_target = (IStyledElement)target; _target = (IStyledElement)target;
_targetType = targetProperty?.PropertyType; _targetType = targetProperty?.PropertyType;
@ -106,6 +110,9 @@ namespace Avalonia.Data
} }
} }
/// <inheritdoc/>
void ISetterValue.Initialize(ISetter setter) => _isSetterValue = true;
protected override void Subscribed() protected override void Subscribed()
{ {
TemplatedParentChanged(); TemplatedParentChanged();

33
tests/Avalonia.LeakTests/ControlTests.cs

@ -308,6 +308,39 @@ namespace Avalonia.LeakTests
} }
[Fact]
public void Slider_Is_Freed()
{
using (Start())
{
Func<Window> run = () =>
{
var window = new Window
{
Content = new Slider()
};
window.Show();
// Do a layout and make sure that Slider gets added to visual tree.
window.LayoutManager.ExecuteInitialLayoutPass(window);
Assert.IsType<Slider>(window.Presenter.Child);
// Clear the content and ensure the Slider is removed.
window.Content = null;
window.LayoutManager.ExecuteLayoutPass();
Assert.Null(window.Presenter.Child);
return window;
};
var result = run();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Slider>()).ObjectsCount));
}
}
[Fact] [Fact]
public void RendererIsDisposed() public void RendererIsDisposed()
{ {

14
tests/Avalonia.Styling.UnitTests/SetterTests.cs

@ -1,17 +1,15 @@
// Copyright (c) The Avalonia Project. All rights reserved. // 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. // Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Globalization;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using Moq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data;
using Xunit;
using System;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Markup.Data; using Avalonia.Data;
using Avalonia.Markup;
using System.Globalization;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Moq;
using Xunit;
namespace Avalonia.Styling.UnitTests namespace Avalonia.Styling.UnitTests
{ {
@ -22,7 +20,7 @@ namespace Avalonia.Styling.UnitTests
{ {
var target = new Setter(); var target = new Setter();
Assert.Throws<ArgumentException>(() => target.Value = new Border()); Assert.Throws<InvalidOperationException>(() => target.Value = new Border());
} }
[Fact] [Fact]

Loading…
Cancel
Save