Browse Source

Lazily evaluate <Template>s in setters.

Part of fixing #5027.
Steven Kirk 5 years ago
parent
commit
72a43174e1

+ 1 - 1
src/Avalonia.Styling/Styling/PropertySetterInstance.cs

@@ -7,7 +7,7 @@ using Avalonia.Reactive;
 namespace Avalonia.Styling
 {
     /// <summary>
-    /// A <see cref="Setter"/> which has been instance on a control.
+    /// A <see cref="Setter"/> which has been instanced on a control.
     /// </summary>
     /// <typeparam name="T">The target property type.</typeparam>
     internal class PropertySetterInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>,

+ 128 - 0
src/Avalonia.Styling/Styling/PropertySetterLazyInstance.cs

@@ -0,0 +1,128 @@
+using System;
+using Avalonia.Data;
+using Avalonia.Reactive;
+
+#nullable enable
+
+namespace Avalonia.Styling
+{
+    /// <summary>
+    /// A <see cref="Setter"/> which has been instanced on a control and whose value is lazily
+    /// evaluated.
+    /// </summary>
+    /// <typeparam name="T">The target property type.</typeparam>
+    internal class PropertySetterLazyInstance<T> : SingleSubscriberObservableBase<BindingValue<T>>,
+        ISetterInstance
+    {
+        private readonly IStyleable _target;
+        private readonly StyledPropertyBase<T>? _styledProperty;
+        private readonly DirectPropertyBase<T>? _directProperty;
+        private readonly Func<T> _valueFactory;
+        private BindingValue<T> _value;
+        private IDisposable? _subscription;
+        private bool _isActive;
+
+        public PropertySetterLazyInstance(
+            IStyleable target,
+            StyledPropertyBase<T> property,
+            Func<T> valueFactory)
+        {
+            _target = target;
+            _styledProperty = property;
+            _valueFactory = valueFactory;
+        }
+
+        public PropertySetterLazyInstance(
+            IStyleable target,
+            DirectPropertyBase<T> property,
+            Func<T> valueFactory)
+        {
+            _target = target;
+            _directProperty = property;
+            _valueFactory = valueFactory;
+        }
+
+        public void Start(bool hasActivator)
+        {
+            _isActive = !hasActivator;
+
+            if (_styledProperty is object)
+            {
+                var priority = hasActivator ? BindingPriority.StyleTrigger : BindingPriority.Style;
+                _subscription = _target.Bind(_styledProperty, this, priority);
+            }
+            else
+            {
+                _subscription = _target.Bind(_directProperty, this);
+            }
+        }
+
+        public void Activate()
+        {
+            if (!_isActive)
+            {
+                _isActive = true;
+                PublishNext();
+            }
+        }
+
+        public void Deactivate()
+        {
+            if (_isActive)
+            {
+                _isActive = false;
+                PublishNext();
+            }
+        }
+
+        public override void Dispose()
+        {
+            if (_subscription is object)
+            {
+                var sub = _subscription;
+                _subscription = null;
+                sub.Dispose();
+            }
+            else if (_isActive)
+            {
+                if (_styledProperty is object)
+                {
+                    _target.ClearValue(_styledProperty);
+                }
+                else
+                {
+                    _target.ClearValue(_directProperty);
+                }
+            }
+
+            base.Dispose();
+        }
+
+        protected override void Subscribed() => PublishNext();
+        protected override void Unsubscribed() { }
+
+        private T GetValue()
+        {
+            if (_value.HasValue)
+            {
+                return _value.Value;
+            }
+
+            _value = _valueFactory();
+            return _value.Value;
+        }
+
+        private void PublishNext()
+        {
+            if (_isActive)
+            {
+                GetValue();
+                PublishNext(_value);
+            }
+            else
+            {
+                PublishNext(default);
+            }
+        }
+    }
+}

+ 15 - 9
src/Avalonia.Styling/Styling/Setter.cs

@@ -68,18 +68,10 @@ namespace Avalonia.Styling
                 throw new InvalidOperationException("Setter.Property must be set.");
             }
 
-            var value = Value;
-
-            if (value is ITemplate template &&
-                !typeof(ITemplate).IsAssignableFrom(Property.PropertyType))
-            {
-                value = template.Build();
-            }
-
             var data = new SetterVisitorData
             {
                 target = target,
-                value = value,
+                value = Value,
             };
 
             Property.Accept(this, ref data);
@@ -97,6 +89,13 @@ namespace Avalonia.Styling
                     property,
                     binding);
             }
+            else if (data.value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(property.PropertyType))
+            {
+                data.result = new PropertySetterLazyInstance<T>(
+                    data.target,
+                    property,
+                    () => (T)template.Build());
+            }
             else
             {
                 data.result = new PropertySetterInstance<T>(
@@ -117,6 +116,13 @@ namespace Avalonia.Styling
                     property,
                     binding);
             }
+            else if (data.value is ITemplate template && !typeof(ITemplate).IsAssignableFrom(property.PropertyType))
+            {
+                data.result = new PropertySetterLazyInstance<T>(
+                    data.target,
+                    property,
+                    () => (T)template.Build());
+            }
             else
             {
                 data.result = new PropertySetterInstance<T>(

+ 2 - 0
tests/Avalonia.Styling.UnitTests/StyleTests.cs

@@ -284,7 +284,9 @@ namespace Avalonia.Styling.UnitTests
             };
 
             var target = new Class1();
+            target.BeginInit();
             styles.TryAttach(target, null);
+            target.EndInit();
 
             Assert.NotNull(target.Child);
             Assert.Equal(1, instantiationCount);