Browse Source

Detach styles when control removed from visual tree.

Steven Kirk 10 years ago
parent
commit
4fa3c98ca3

+ 7 - 0
src/Perspex.Controls/ItemsControl.cs

@@ -152,6 +152,13 @@ namespace Perspex.Controls
             Presenter = nameScope.Find<IItemsPresenter>("PART_ItemsPresenter");
         }
 
+        /// <inheritdoc/>
+        protected override void OnTemplateChanged(PerspexPropertyChangedEventArgs e)
+        {
+            base.OnTemplateChanged(e);
+            ItemContainerGenerator.Clear();
+        }
+
         /// <summary>
         /// Caled when the <see cref="Items"/> property changes.
         /// </summary>

+ 11 - 6
src/Perspex.Controls/Primitives/TemplatedControl.cs

@@ -81,12 +81,7 @@ namespace Perspex.Controls.Primitives
         /// </summary>
         static TemplatedControl()
         {
-            TemplateProperty.Changed.Subscribe(e =>
-            {
-                var templatedControl = (TemplatedControl)e.Sender;
-                templatedControl._templateApplied = false;
-                templatedControl.InvalidateMeasure();
-            });
+            TemplateProperty.Changed.AddClassHandler<TemplatedControl>(x => x.OnTemplateChanged);
         }
 
         /// <summary>
@@ -224,6 +219,16 @@ namespace Perspex.Controls.Primitives
         {
         }
 
+        /// <summary>
+        /// Called when the <see cref="Template"/> property changes.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        protected virtual void OnTemplateChanged(PerspexPropertyChangedEventArgs e)
+        {
+            _templateApplied = false;
+            InvalidateMeasure();
+        }
+
         /// <summary>
         /// Sets the TemplatedParent property for a control created from the control template and
         /// applies the templates of nested templated controls. Also adds each control to its name

+ 2 - 2
src/Perspex.Layout/LayoutManager.cs

@@ -226,12 +226,12 @@ namespace Perspex.Layout
                         {
                             var parent = item.Control.GetVisualParent<ILayoutable>();
 
-                            while (parent.PreviousMeasure == null)
+                            while (parent != null && parent.PreviousMeasure == null)
                             {
                                 parent = parent.GetVisualParent<ILayoutable>();
                             }
 
-                            if (parent.GetVisualRoot() == Root)
+                            if (parent != null && parent.GetVisualRoot() == Root)
                             {
                                 parent.Measure(parent.PreviousMeasure.Value, true);
                             }

+ 3 - 1
src/Perspex.Styling/Properties/AssemblyInfo.cs

@@ -2,7 +2,9 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System.Reflection;
+using System.Runtime.CompilerServices;
 using Perspex.Metadata;
 
 [assembly: AssemblyTitle("Perspex.Styling")]
-[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Styling")]
+[assembly: XmlnsDefinition("https://github.com/perspex", "Perspex.Styling")]
+[assembly: InternalsVisibleTo("Perspex.Styling.UnitTests")]

+ 13 - 1
src/Perspex.Styling/Styling/Style.cs

@@ -56,9 +56,21 @@ namespace Perspex.Styling
 
                 if (match.ImmediateResult != false)
                 {
+                    var visual = control as IVisual;
+                    var activator = match.ObservableResult ?? 
+                        Observable.Never<bool>().StartWith(true);
+
+                    if (visual != null)
+                    {
+                        var detached = Observable.FromEventPattern<VisualTreeAttachmentEventArgs>(
+                            x => visual.DetachedFromVisualTree += x,
+                            x => visual.DetachedFromVisualTree -= x);
+                        activator = activator.TakeUntil(detached);
+                    }
+
                     foreach (var setter in Setters)
                     {
-                        setter.Apply(this, control, match.ObservableResult);
+                        setter.Apply(this, control, activator);
                     }
                 }
             }

+ 7 - 6
src/Perspex.Styling/Styling/StyleBinding.cs

@@ -61,7 +61,8 @@ namespace Perspex.Styling
         /// </summary>
         public object ActivatedValue
         {
-            get; }
+            get;
+        }
 
         /// <summary>
         /// Gets a description of the binding.
@@ -90,16 +91,16 @@ namespace Perspex.Styling
 
             if (Source == null)
             {
-                return _activator.Subscribe(
-                    active => observer.OnNext(active ? ActivatedValue : PerspexProperty.UnsetValue),
-                    observer.OnError,
-                    observer.OnCompleted);
+                return _activator
+                    .Select(active => active ? ActivatedValue : PerspexProperty.UnsetValue)
+                    .Subscribe(observer);
             }
             else
             {
                 return _activator
                     .CombineLatest(Source, (x, y) => new { Active = x, Value = y })
-                    .Subscribe(x => observer.OnNext(x.Active ? x.Value : PerspexProperty.UnsetValue));
+                    .Select(x => x.Active ? x.Value : PerspexProperty.UnsetValue)
+                    .Subscribe(observer);
             }
         }
     }

+ 2 - 0
tests/Perspex.Styling.UnitTests/Perspex.Styling.UnitTests.csproj

@@ -85,10 +85,12 @@
     <Compile Include="SelectorTests_OfType.cs" />
     <Compile Include="SelectorTests_Template.cs" />
     <Compile Include="StyleActivatorTests.cs" />
+    <Compile Include="StyleBindingTests.cs" />
     <Compile Include="StyleTests.cs" />
     <Compile Include="TestControlBase.cs" />
     <Compile Include="TestObservable.cs" />
     <Compile Include="TestObserver.cs" />
+    <Compile Include="TestRoot.cs" />
     <Compile Include="TestSelectors.cs" />
     <Compile Include="TestSubject.cs" />
     <Compile Include="TestTemplatedControl.cs" />

+ 79 - 0
tests/Perspex.Styling.UnitTests/StyleBindingTests.cs

@@ -0,0 +1,79 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+using System.Reactive.Subjects;
+using Xunit;
+
+namespace Perspex.Styling.UnitTests
+{
+    public class StyleBindingTests
+    {
+        [Fact]
+        public async void Should_Produce_UnsetValue_On_Activator_False()
+        {
+            var activator = new BehaviorSubject<bool>(false);
+            var target = new StyleBinding(activator, 1, string.Empty);
+            var result = await target.Take(1);
+
+            Assert.Equal(PerspexProperty.UnsetValue, result);
+        }
+
+        [Fact]
+        public async void Should_Produce_Value_On_Activator_True()
+        {
+            var activator = new BehaviorSubject<bool>(true);
+            var target = new StyleBinding(activator, 1, string.Empty);
+            var result = await target.Take(1);
+
+            Assert.Equal(1, result);
+        }
+
+        [Fact]
+        public void Should_Change_Value_On_Activator_Change()
+        {
+            var activator = new BehaviorSubject<bool>(false);
+            var target = new StyleBinding(activator, 1, string.Empty);
+            var result = new List<object>();
+
+            target.Subscribe(x => result.Add(x));
+
+            activator.OnNext(true);
+            activator.OnNext(false);
+
+            Assert.Equal(new[] { PerspexProperty.UnsetValue, 1, PerspexProperty.UnsetValue }, result);
+        }
+
+        [Fact]
+        public void Should_Change_Value_With_Source_Observable()
+        {
+            var activator = new BehaviorSubject<bool>(false);
+            var source = new BehaviorSubject<object>(1);
+            var target = new StyleBinding(activator, source, string.Empty);
+            var result = new List<object>();
+
+            target.Subscribe(x => result.Add(x));
+
+            activator.OnNext(true);
+            source.OnNext(2);
+            activator.OnNext(false);
+
+            Assert.Equal(new[] { PerspexProperty.UnsetValue, 1, 2, PerspexProperty.UnsetValue }, result);
+        }
+
+        [Fact]
+        public void Should_Complete_When_Activator_Completes()
+        {
+            var activator = new BehaviorSubject<bool>(false);
+            var target = new StyleBinding(activator, 1, string.Empty);
+            var completed = false;
+
+            target.Subscribe(_ => { }, () => completed = true);
+            activator.OnCompleted();
+
+            Assert.True(completed);
+        }
+    }
+}

+ 26 - 1
tests/Perspex.Styling.UnitTests/StyleTests.cs

@@ -166,7 +166,7 @@ namespace Perspex.Styling.UnitTests
         {
             var source = new BehaviorSubject<string>("Foo");
 
-            Style style = new Style(x => x.OfType<Class1>().Class("foo"))
+            var style = new Style(x => x.OfType<Class1>().Class("foo"))
             {
                 Setters = new[]
                 {
@@ -187,6 +187,31 @@ namespace Perspex.Styling.UnitTests
             Assert.Equal("foodefault", target.Foo);
         }
 
+        [Fact]
+        public void Style_Should_Detach_When_Removed_From_Visual_Tree()
+        {
+            Border border;
+
+            var style = new Style(x => x.OfType<Border>())
+            {
+                Setters = new[]
+                {
+                    new Setter(Border.BorderThicknessProperty, 4),
+                }
+            };
+
+            var root = new TestRoot
+            {
+                Child = border = new Border(),
+            };
+
+            style.Attach(border, null);
+
+            Assert.Equal(4, border.BorderThickness);
+            root.Child = null;
+            Assert.Equal(0, border.BorderThickness);
+        }
+
         private class Class1 : Control
         {
             public static readonly PerspexProperty<string> FooProperty =

+ 34 - 0
tests/Perspex.Styling.UnitTests/TestRoot.cs

@@ -0,0 +1,34 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using Moq;
+using Perspex.Controls;
+using Perspex.Layout;
+using Perspex.Platform;
+using Perspex.Rendering;
+
+namespace Perspex.Styling.UnitTests
+{
+    internal class TestRoot : Decorator, ILayoutRoot, IRenderRoot
+    {
+        public Size ClientSize => new Size(100, 100);
+
+        public ILayoutManager LayoutManager => new Mock<ILayoutManager>().Object;
+
+        public IRenderTarget RenderTarget
+        {
+            get { throw new NotImplementedException(); }
+        }
+
+        public IRenderQueueManager RenderQueueManager
+        {
+            get { throw new NotImplementedException(); }
+        }
+
+        public Point TranslatePointToScreen(Point p)
+        {
+            return new Point();
+        }
+    }
+}