Explorar el Código

Merge branch 'master' into datagrid1

Steven Kirk hace 5 años
padre
commit
204cee0699

+ 7 - 1
src/Avalonia.Base/AvaloniaObject.cs

@@ -478,7 +478,13 @@ namespace Avalonia
             }
         }
 
-        void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry) { }
+        void IValueSink.Completed<T>(
+            StyledPropertyBase<T> property,
+            IPriorityValueEntry entry,
+            Optional<T> oldValue) 
+        {
+            ((IValueSink)this).ValueChanged(property, BindingPriority.Unset, oldValue, default);
+        }
 
         /// <summary>
         /// Called for each inherited property when the <see cref="InheritanceParent"/> changes.

+ 2 - 2
src/Avalonia.Base/PropertyStore/BindingEntry.cs

@@ -48,10 +48,10 @@ namespace Avalonia.PropertyStore
         {
             _subscription?.Dispose();
             _subscription = null;
-            _sink.Completed(Property, this);
+            _sink.Completed(Property, this, Value);
         }
 
-        public void OnCompleted() => _sink.Completed(Property, this);
+        public void OnCompleted() => _sink.Completed(Property, this, Value);
 
         public void OnError(Exception error)
         {

+ 4 - 1
src/Avalonia.Base/PropertyStore/IValueSink.cs

@@ -15,6 +15,9 @@ namespace Avalonia.PropertyStore
             Optional<T> oldValue,
             BindingValue<T> newValue);
 
-        void Completed(AvaloniaProperty property, IPriorityValueEntry entry);
+        void Completed<T>(
+            StyledPropertyBase<T> property,
+            IPriorityValueEntry entry,
+            Optional<T> oldValue);
     }
 }

+ 4 - 1
src/Avalonia.Base/PropertyStore/PriorityValue.cs

@@ -117,7 +117,10 @@ namespace Avalonia.PropertyStore
             UpdateEffectiveValue();
         }
 
-        void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry)
+        void IValueSink.Completed<TValue>(
+            StyledPropertyBase<TValue> property,
+            IPriorityValueEntry entry,
+            Optional<TValue> oldValue)
         {
             _entries.Remove((IPriorityValueEntry<T>)entry);
             UpdateEffectiveValue();

+ 7 - 2
src/Avalonia.Base/ValueStore.cs

@@ -148,7 +148,7 @@ namespace Avalonia
                         _values.Remove(property);
                         _sink.ValueChanged(
                             property,
-                            BindingPriority.LocalValue,
+                            BindingPriority.Unset,
                             old,
                             BindingValue<T>.Unset);
                     }
@@ -190,13 +190,17 @@ namespace Avalonia
             _sink.ValueChanged(property, priority, oldValue, newValue);
         }
 
-        void IValueSink.Completed(AvaloniaProperty property, IPriorityValueEntry entry)
+        void IValueSink.Completed<T>(
+            StyledPropertyBase<T> property,
+            IPriorityValueEntry entry,
+            Optional<T> oldValue)
         {
             if (_values.TryGetValue(property, out var slot))
             {
                 if (slot == entry)
                 {
                     _values.Remove(property);
+                    _sink.Completed(property, entry, oldValue);
                 }
             }
         }
@@ -228,6 +232,7 @@ namespace Avalonia
                 else
                 {
                     var priorityValue = new PriorityValue<T>(_owner, property, this, l);
+                    priorityValue.SetValue(value, priority);
                     _values.SetValue(property, priorityValue);
                 }
             }

+ 37 - 5
src/Avalonia.Controls/TextBox.cs

@@ -14,6 +14,7 @@ using Avalonia.Interactivity;
 using Avalonia.Media;
 using Avalonia.Metadata;
 using Avalonia.Data;
+using Avalonia.Layout;
 using Avalonia.Utilities;
 
 namespace Avalonia.Controls
@@ -72,6 +73,18 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<TextAlignment> TextAlignmentProperty =
             TextBlock.TextAlignmentProperty.AddOwner<TextBox>();
 
+        /// <summary>
+        /// Defines the <see cref="HorizontalAlignment"/> property.
+        /// </summary>
+        public static readonly StyledProperty<HorizontalAlignment> HorizontalContentAlignmentProperty =
+            ContentControl.HorizontalContentAlignmentProperty.AddOwner<TextBox>();
+
+        /// <summary>
+        /// Defines the <see cref="VerticalAlignment"/> property.
+        /// </summary>
+        public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
+            ContentControl.VerticalContentAlignmentProperty.AddOwner<TextBox>();
+
         public static readonly StyledProperty<TextWrapping> TextWrappingProperty =
             TextBlock.TextWrappingProperty.AddOwner<TextBox>();
 
@@ -262,6 +275,24 @@ namespace Avalonia.Controls
             }
         }
 
+        /// <summary>
+        /// Gets or sets the horizontal alignment of the content within the control.
+        /// </summary>
+        public HorizontalAlignment HorizontalContentAlignment
+        {
+            get { return GetValue(HorizontalContentAlignmentProperty); }
+            set { SetValue(HorizontalContentAlignmentProperty, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the vertical alignment of the content within the control.
+        /// </summary>
+        public VerticalAlignment VerticalContentAlignment
+        {
+            get { return GetValue(VerticalContentAlignmentProperty); }
+            set { SetValue(VerticalContentAlignmentProperty, value); }
+        }
+
         public TextAlignment TextAlignment
         {
             get { return GetValue(TextAlignmentProperty); }
@@ -316,8 +347,7 @@ namespace Avalonia.Controls
                 !AcceptsReturn &&
                 Text?.Length > 0)
             {
-                SelectionStart = 0;
-                SelectionEnd = Text.Length;
+                SelectAll();
             }
             else
             {
@@ -673,8 +703,7 @@ namespace Avalonia.Controls
                         SelectionEnd = StringUtils.NextWord(text, index);
                         break;
                     case 3:
-                        SelectionStart = 0;
-                        SelectionEnd = text.Length;
+                        SelectAll();
                         break;
                 }
             }
@@ -896,7 +925,10 @@ namespace Avalonia.Controls
             CaretIndex = caretIndex;
         }
 
-        private void SelectAll()
+        /// <summary>
+        /// Select all text in the TextBox
+        /// </summary>
+        public void SelectAll()
         {
             SelectionStart = 0;
             SelectionEnd = Text?.Length ?? 0;

+ 1 - 1
src/Avalonia.Controls/Window.cs

@@ -529,7 +529,7 @@ namespace Avalonia.Controls
         {
             var sizeToContent = SizeToContent;
             var clientSize = ClientSize;
-            Size constraint = clientSize;
+            var constraint = availableSize;
 
             if ((sizeToContent & SizeToContent.Width) != 0)
             {

+ 1 - 1
src/Avalonia.Input/IInputElement.cs

@@ -63,7 +63,7 @@ namespace Avalonia.Input
         event EventHandler<PointerReleasedEventArgs> PointerReleased;
 
         /// <summary>
-        /// Occurs when the mouse wheen is scrolled over the control.
+        /// Occurs when the mouse wheel is scrolled over the control.
         /// </summary>
         event EventHandler<PointerWheelEventArgs> PointerWheelChanged;
 

+ 7 - 4
src/Avalonia.Native/WindowImplBase.cs

@@ -162,9 +162,12 @@ namespace Avalonia.Native
 
             void IAvnWindowBaseEvents.Resized(AvnSize size)
             {
-                var s = new Size(size.Width, size.Height);
-                _parent._savedLogicalSize = s;
-                _parent.Resized?.Invoke(s);
+                if (_parent._native != null)
+                {
+                    var s = new Size(size.Width, size.Height);
+                    _parent._savedLogicalSize = s;
+                    _parent.Resized?.Invoke(s);
+                }
             }
 
             void IAvnWindowBaseEvents.PositionChanged(AvnPoint position)
@@ -317,7 +320,7 @@ namespace Avalonia.Native
             _native.SetTopMost(value);
         }
 
-        public double Scaling => _native.GetScaling();
+        public double Scaling => _native?.GetScaling() ?? 1;
 
         public Action Deactivated { get; set; }
         public Action Activated { get; set; }

+ 21 - 6
src/Avalonia.Styling/StyledElement.cs

@@ -573,9 +573,12 @@ namespace Avalonia
                     element._dataContextUpdating = true;
                     element.OnDataContextBeginUpdate();
 
-                    foreach (var child in element.LogicalChildren)
+                    var logicalChildren = element.LogicalChildren;
+                    var logicalChildrenCount = logicalChildren.Count;
+
+                    for (var i = 0; i < logicalChildrenCount; i++)
                     {
-                        if (child is StyledElement s &&
+                        if (element.LogicalChildren[i] is StyledElement s &&
                             s.InheritanceParent == element &&
                             !s.IsSet(DataContextProperty))
                         {
@@ -646,9 +649,15 @@ namespace Avalonia
                 AttachedToLogicalTree?.Invoke(this, e);
             }
 
-            foreach (var child in LogicalChildren.OfType<StyledElement>())
+            var logicalChildren = LogicalChildren;
+            var logicalChildrenCount = logicalChildren.Count;
+
+            for (var i = 0; i < logicalChildrenCount; i++)
             {
-                child.OnAttachedToLogicalTreeCore(e);
+                if (logicalChildren[i] is StyledElement child)
+                {
+                    child.OnAttachedToLogicalTreeCore(e);
+                }
             }
         }
 
@@ -661,9 +670,15 @@ namespace Avalonia
                 OnDetachedFromLogicalTree(e);
                 DetachedFromLogicalTree?.Invoke(this, e);
 
-                foreach (var child in LogicalChildren.OfType<StyledElement>())
+                var logicalChildren = LogicalChildren;
+                var logicalChildrenCount = logicalChildren.Count;
+
+                for (var i = 0; i < logicalChildrenCount; i++)
                 {
-                    child.OnDetachedFromLogicalTreeCore(e);
+                    if (logicalChildren[i] is StyledElement child)
+                    {
+                        child.OnDetachedFromLogicalTreeCore(e);
+                    }
                 }
 
 #if DEBUG

+ 2 - 2
src/Avalonia.Styling/Styling/Setter.cs

@@ -78,8 +78,6 @@ namespace Avalonia.Styling
         {
             Contract.Requires<ArgumentNullException>(control != null);
 
-            var description = style?.ToString();
-
             if (Property == null)
             {
                 throw new InvalidOperationException("Setter.Property must be set.");
@@ -107,6 +105,8 @@ namespace Avalonia.Styling
                 }
                 else
                 {
+                    var description = style?.ToString();
+
                     var activated = new ActivatedValue(activator, value, description);
                     return control.Bind(Property, activated, BindingPriority.StyleTrigger);
                 }

+ 25 - 10
src/Avalonia.Styling/Styling/Style.cs

@@ -116,13 +116,21 @@ namespace Avalonia.Styling
                 if (match.IsMatch)
                 {
                     var controlSubscriptions = GetSubscriptions(control);
-                    
-                    var subs = new CompositeDisposable(Setters.Count + Animations.Count);
 
-                    if (control is Animatable animatable)
+                    var animatable = control as Animatable;
+
+                    var setters = Setters;
+                    var settersCount = setters.Count;
+                    var animations = Animations;
+                    var animationsCount = animations.Count;
+
+                    var subs = new CompositeDisposable(settersCount + (animatable != null ? animationsCount : 0) + 1);
+
+                    if (animatable != null)
                     {
-                        foreach (var animation in Animations)
+                        for (var i = 0; i < animationsCount; i++)
                         {
+                            var animation = animations[i];
                             var obsMatch = match.Activator;
 
                             if (match.Result == SelectorMatchResult.AlwaysThisType ||
@@ -133,17 +141,19 @@ namespace Avalonia.Styling
 
                             var sub = animation.Apply(animatable, null, obsMatch);
                             subs.Add(sub);
-                        } 
+                        }
                     }
 
-                    foreach (var setter in Setters)
+                    for (var i = 0; i < settersCount; i++)
                     {
+                        var setter = setters[i];
                         var sub = setter.Apply(this, control, match.Activator);
                         subs.Add(sub);
                     }
 
+                    subs.Add(Disposable.Create((subs, Subscriptions) , state =>  state.Subscriptions.Remove(state.subs)));
+
                     controlSubscriptions.Add(subs);
-                    controlSubscriptions.Add(Disposable.Create(() => Subscriptions.Remove(subs)));
                     Subscriptions.Add(subs);
                 }
 
@@ -151,18 +161,23 @@ namespace Avalonia.Styling
             }
             else if (control == container)
             {
+                var setters = Setters;
+                var settersCount = setters.Count;
+
                 var controlSubscriptions = GetSubscriptions(control);
 
-                var subs = new CompositeDisposable(Setters.Count);
+                var subs = new CompositeDisposable(settersCount + 1);
 
-                foreach (var setter in Setters)
+                for (var i = 0; i < settersCount; i++)
                 {
+                    var setter = setters[i];
                     var sub = setter.Apply(this, control, null);
                     subs.Add(sub);
                 }
 
+                subs.Add(Disposable.Create((subs, Subscriptions), state => state.Subscriptions.Remove(state.subs)));
+
                 controlSubscriptions.Add(subs);
-                controlSubscriptions.Add(Disposable.Create(() => Subscriptions.Remove(subs)));
                 Subscriptions.Add(subs);
                 return true;
             }

+ 3 - 1
src/Avalonia.Styling/Styling/Styles.cs

@@ -239,8 +239,10 @@ namespace Avalonia.Styling
         /// <inheritdoc/>
         public bool Remove(IStyle item) => _styles.Remove(item);
 
+        public AvaloniaList<IStyle>.Enumerator GetEnumerator() => _styles.GetEnumerator();
+
         /// <inheritdoc/>
-        public IEnumerator<IStyle> GetEnumerator() => _styles.GetEnumerator();
+        IEnumerator<IStyle> IEnumerable<IStyle>.GetEnumerator() => _styles.GetEnumerator();
 
         /// <inheritdoc/>
         IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator();

+ 3 - 1
src/Avalonia.Themes.Default/TextBox.xaml

@@ -12,7 +12,9 @@
                 Background="{TemplateBinding Background}"
                 BorderBrush="{TemplateBinding BorderBrush}"
                 BorderThickness="{TemplateBinding BorderThickness}">
-          <DockPanel Margin="{TemplateBinding Padding}">
+          <DockPanel Margin="{TemplateBinding Padding}"
+                     HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
+                     VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
 
             <TextBlock Name="floatingWatermark"
                        Foreground="{DynamicResource ThemeAccentBrush}"

+ 20 - 6
src/Avalonia.Visuals/Visual.cs

@@ -386,11 +386,18 @@ namespace Avalonia
             AttachedToVisualTree?.Invoke(this, e);
             InvalidateVisual();
 
-            if (VisualChildren != null)
+            var visualChildren = VisualChildren;
+
+            if (visualChildren != null)
             {
-                foreach (Visual child in VisualChildren.OfType<Visual>())
+                var visualChildrenCount = visualChildren.Count;
+
+                for (var i = 0; i < visualChildrenCount; i++)
                 {
-                    child.OnAttachedToVisualTreeCore(e);
+                    if (visualChildren[i] is Visual child)
+                    {
+                        child.OnAttachedToVisualTreeCore(e);
+                    }
                 }
             }
         }
@@ -415,11 +422,18 @@ namespace Avalonia
             DetachedFromVisualTree?.Invoke(this, e);
             e.Root?.Renderer?.AddDirty(this);
 
-            if (VisualChildren != null)
+            var visualChildren = VisualChildren;
+
+            if (visualChildren != null)
             {
-                foreach (Visual child in VisualChildren.OfType<Visual>())
+                var visualChildrenCount = visualChildren.Count;
+
+                for (var i = 0; i < visualChildrenCount; i++)
                 {
-                    child.OnDetachedFromVisualTreeCore(e);
+                    if (visualChildren[i] is Visual child)
+                    {
+                        child.OnDetachedFromVisualTreeCore(e);
+                    }
                 }
             }
         }

+ 105 - 0
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs

@@ -132,6 +132,111 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal("foo", target.GetValue(property));
         }
 
+        [Fact]
+        public void Completing_LocalValue_Binding_Raises_PropertyChanged()
+        {
+            var target = new Class1();
+            var source = new BehaviorSubject<BindingValue<string>>("foo");
+            var property = Class1.FooProperty;
+            var raised = 0;
+
+            target.Bind(property, source);
+            Assert.Equal("foo", target.GetValue(property));
+
+            target.PropertyChanged += (s, e) =>
+            {
+                Assert.Equal(BindingPriority.Unset, e.Priority);
+                Assert.Equal(property, e.Property);
+                Assert.Equal("foo", e.OldValue as string);
+                Assert.Equal("foodefault", e.NewValue as string);
+                ++raised;
+            };
+
+            source.OnCompleted();
+
+            Assert.Equal("foodefault", target.GetValue(property));
+            Assert.Equal(1, raised);
+        }
+
+        [Fact]
+        public void Completing_Style_Binding_Raises_PropertyChanged()
+        {
+            var target = new Class1();
+            var source = new BehaviorSubject<BindingValue<string>>("foo");
+            var property = Class1.FooProperty;
+            var raised = 0;
+
+            target.Bind(property, source, BindingPriority.Style);
+            Assert.Equal("foo", target.GetValue(property));
+
+            target.PropertyChanged += (s, e) =>
+            {
+                Assert.Equal(BindingPriority.Unset, e.Priority);
+                Assert.Equal(property, e.Property);
+                Assert.Equal("foo", e.OldValue as string);
+                Assert.Equal("foodefault", e.NewValue as string);
+                ++raised;
+            };
+
+            source.OnCompleted();
+
+            Assert.Equal("foodefault", target.GetValue(property));
+            Assert.Equal(1, raised);
+        }
+
+        [Fact]
+        public void Completing_LocalValue_Binding_With_Style_Binding_Raises_PropertyChanged()
+        {
+            var target = new Class1();
+            var source = new BehaviorSubject<BindingValue<string>>("foo");
+            var property = Class1.FooProperty;
+            var raised = 0;
+
+            target.Bind(property, new BehaviorSubject<string>("bar"), BindingPriority.Style);
+            target.Bind(property, source);
+            Assert.Equal("foo", target.GetValue(property));
+
+            target.PropertyChanged += (s, e) =>
+            {
+                Assert.Equal(BindingPriority.Style, e.Priority);
+                Assert.Equal(property, e.Property);
+                Assert.Equal("foo", e.OldValue as string);
+                Assert.Equal("bar", e.NewValue as string);
+                ++raised;
+            };
+
+            source.OnCompleted();
+
+            Assert.Equal("bar", target.GetValue(property));
+            Assert.Equal(1, raised);
+        }
+
+        [Fact]
+        public void Disposing_LocalValue_Binding_Raises_PropertyChanged()
+        {
+            var target = new Class1();
+            var source = new BehaviorSubject<BindingValue<string>>("foo");
+            var property = Class1.FooProperty;
+            var raised = 0;
+
+            var sub = target.Bind(property, source);
+            Assert.Equal("foo", target.GetValue(property));
+
+            target.PropertyChanged += (s, e) =>
+            {
+                Assert.Equal(BindingPriority.Unset, e.Priority);
+                Assert.Equal(property, e.Property);
+                Assert.Equal("foo", e.OldValue as string);
+                Assert.Equal("foodefault", e.NewValue as string);
+                ++raised;
+            };
+
+            sub.Dispose();
+
+            Assert.Equal("foodefault", target.GetValue(property));
+            Assert.Equal(1, raised);
+        }
+
         [Fact]
         public void Setting_Style_Value_Overrides_Binding_Permanently()
         {

+ 1 - 0
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs

@@ -188,6 +188,7 @@ namespace Avalonia.Base.UnitTests
             target.PropertyChanged += (s, e) =>
             {
                 Assert.Same(target, s);
+                Assert.Equal(BindingPriority.LocalValue, e.Priority);
                 Assert.Equal(Class1.FooProperty, e.Property);
                 Assert.Equal("newvalue", (string)e.OldValue);
                 Assert.Equal("unset", (string)e.NewValue);

+ 12 - 0
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_SetValue.cs

@@ -30,6 +30,7 @@ namespace Avalonia.Base.UnitTests
             target.PropertyChanged += (s, e) =>
             {
                 Assert.Same(target, s);
+                Assert.Equal(BindingPriority.Unset, e.Priority);
                 Assert.Equal(Class1.FooProperty, e.Property);
                 Assert.Equal("newvalue", (string)e.OldValue);
                 Assert.Equal("foodefault", (string)e.NewValue);
@@ -239,6 +240,17 @@ namespace Avalonia.Base.UnitTests
             Assert.Equal("two", target.GetValue(Class1.FooProperty));
         }
 
+        [Fact]
+        public void SetValue_Animation_Overrides_LocalValue()
+        {
+            Class1 target = new Class1();
+
+            target.SetValue(Class1.FooProperty, "one", BindingPriority.LocalValue);
+            Assert.Equal("one", target.GetValue(Class1.FooProperty));
+            target.SetValue(Class1.FooProperty, "two", BindingPriority.Animation);
+            Assert.Equal("two", target.GetValue(Class1.FooProperty));
+        }
+
         [Fact]
         public void Setting_UnsetValue_Reverts_To_Default_Value()
         {

+ 47 - 0
tests/Avalonia.Benchmarks/Styling/StyleAttachBenchmark.cs

@@ -0,0 +1,47 @@
+using System;
+using System.Runtime.CompilerServices;
+using Avalonia.Controls;
+using Avalonia.UnitTests;
+using BenchmarkDotNet.Attributes;
+
+namespace Avalonia.Benchmarks.Styling
+{
+    [MemoryDiagnoser]
+    public class StyleAttachBenchmark : IDisposable
+    {
+        private readonly IDisposable _app;
+        private readonly TestRoot _root;
+        private readonly TextBox _control;
+
+        public StyleAttachBenchmark()
+        {
+            _app = UnitTestApplication.Start(
+                TestServices.StyledWindow.With(
+                    renderInterface: new NullRenderingPlatform(),
+                    threadingInterface: new NullThreadingPlatform()));
+
+            _root = new TestRoot(true, null)
+            {
+                Renderer = new NullRenderer(),
+            };
+
+            _control = new TextBox();
+        }
+
+        [Benchmark]
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        public void AttachTextBoxStyles()
+        {
+            var styles = UnitTestApplication.Current.Styles;
+
+            styles.Attach(_control, UnitTestApplication.Current);
+
+            styles.Detach();
+        }
+
+        public void Dispose()
+        {
+            _app.Dispose();
+        }
+    }
+}

+ 51 - 0
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@@ -341,11 +341,62 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void Child_Should_Be_Measured_With_Width_And_Height_If_SizeToContent_Is_Manual()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var child = new ChildControl();
+                var target = new Window 
+                { 
+                    Width = 100,
+                    Height = 50,
+                    SizeToContent = SizeToContent.Manual,
+                    Content = child 
+                };
+
+                target.Show();
+
+                Assert.Equal(new Size(100, 50), child.MeasureSize);
+            }
+        }
+
+        [Fact]
+        public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var child = new ChildControl();
+                var target = new Window
+                {
+                    Width = 100,
+                    Height = 50,
+                    SizeToContent = SizeToContent.WidthAndHeight,
+                    Content = child
+                };
+
+                target.Show();
+
+                Assert.Equal(Size.Infinity, child.MeasureSize);
+            }
+        }
+
         private IWindowImpl CreateImpl(Mock<IRenderer> renderer)
         {
             return Mock.Of<IWindowImpl>(x =>
                 x.Scaling == 1 &&
                 x.CreateRenderer(It.IsAny<IRenderRoot>()) == renderer.Object);
         }
+
+        private class ChildControl : Control
+        {
+            public Size MeasureSize { get; private set; }
+
+            protected override Size MeasureOverride(Size availableSize)
+            {
+                MeasureSize = availableSize;
+                return base.MeasureOverride(availableSize);
+            }
+        }
     }
 }

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

@@ -8,8 +8,10 @@ using Avalonia.Controls;
 using Avalonia.Controls.Templates;
 using Avalonia.Diagnostics;
 using Avalonia.Layout;
+using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Rendering;
+using Avalonia.Styling;
 using Avalonia.UnitTests;
 using Avalonia.VisualTree;
 using JetBrains.dotMemoryUnit;
@@ -370,6 +372,56 @@ namespace Avalonia.LeakTests
             }
         }
 
+        [Fact]
+        public void Control_With_Style_RenderTransform_Is_Freed()
+        {
+            // # Issue #3545
+            using (Start())
+            {
+                Func<Window> run = () =>
+                {
+                    var window = new Window
+                    {
+                        Styles =
+                        {
+                            new Style(x => x.OfType<Canvas>())
+                            {
+                                Setters =
+                                {
+                                    new Setter
+                                    {
+                                        Property = Visual.RenderTransformProperty,
+                                        Value = new RotateTransform(45),
+                                    }
+                                }
+                            }
+                        },
+                        Content = new Canvas()
+                    };
+
+                    window.Show();
+
+                    // Do a layout and make sure that Canvas gets added to visual tree with
+                    // its render transform.
+                    window.LayoutManager.ExecuteInitialLayoutPass(window);
+                    var canvas = Assert.IsType<Canvas>(window.Presenter.Child);
+                    Assert.IsType<RotateTransform>(canvas.RenderTransform);
+
+                    // Clear the content and ensure the Canvas 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<Canvas>()).ObjectsCount));
+            }
+        }
+
         private IDisposable Start()
         {
             return UnitTestApplication.Start(TestServices.StyledWindow);