Browse Source

Merge pull request #3597 from AvaloniaUI/fixes/3590-nested-resources

Fix DynamicResource in Style resources
Steven Kirk 5 years ago
parent
commit
0a579a2fa9

+ 35 - 2
src/Avalonia.Controls/Application.cs

@@ -44,6 +44,7 @@ namespace Avalonia
         private readonly Styler _styler = new Styler();
         private Styles _styles;
         private IResourceDictionary _resources;
+        private bool _notifyingResourcesChanged;
 
         /// <summary>
         /// Defines the <see cref="DataContext"/> property.
@@ -160,7 +161,19 @@ namespace Avalonia
         /// <remarks>
         /// Global styles apply to all windows in the application.
         /// </remarks>
-        public Styles Styles => _styles ?? (_styles = new Styles());
+        public Styles Styles
+        {
+            get
+            {
+                if (_styles == null)
+                {
+                    _styles = new Styles(this);
+                    _styles.ResourcesChanged += ThisResourcesChanged;
+                }
+
+                return _styles;
+            }
+        }
 
         /// <inheritdoc/>
         bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
@@ -233,9 +246,29 @@ namespace Avalonia
             
         }
 
+        private void NotifyResourcesChanged(ResourcesChangedEventArgs e)
+        {
+            if (_notifyingResourcesChanged)
+            {
+                return;
+            }
+
+            try
+            {
+                _notifyingResourcesChanged = true;
+                (_resources as ISetResourceParent)?.ParentResourcesChanged(e);
+                (_styles as ISetResourceParent)?.ParentResourcesChanged(e);
+                ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
+            }
+            finally
+            {
+                _notifyingResourcesChanged = false;
+            }
+        }
+
         private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e)
         {
-            ResourcesChanged?.Invoke(this, e);
+            NotifyResourcesChanged(e);
         }
 
         private string _name;

+ 37 - 25
src/Avalonia.Styling/StyledElement.cs

@@ -67,6 +67,7 @@ namespace Avalonia
         private Subject<IStyleable> _styleDetach = new Subject<IStyleable>();
         private ITemplatedControl _templatedParent;
         private bool _dataContextUpdating;
+        private bool _notifyingResourcesChanged;
 
         /// <summary>
         /// Initializes static members of the <see cref="StyledElement"/> class.
@@ -214,28 +215,15 @@ namespace Avalonia
         /// </remarks>
         public Styles Styles
         {
-            get { return _styles ?? (Styles = new Styles()); }
-            set
+            get
             {
-                Contract.Requires<ArgumentNullException>(value != null);
-
-                if (_styles != value)
+                if (_styles == null)
                 {
-                    if (_styles != null)
-                    {
-                        (_styles as ISetResourceParent)?.SetParent(null);
-                        _styles.ResourcesChanged -= ThisResourcesChanged;
-                    }
-
-                    _styles = value;
-
-                    if (value is ISetResourceParent setParent && setParent.ResourceParent == null)
-                    {
-                        setParent.SetParent(this);
-                    }
-
+                    _styles = new Styles(this);
                     _styles.ResourcesChanged += ThisResourcesChanged;
                 }
+
+                return _styles;
             }
         }
 
@@ -253,6 +241,7 @@ namespace Avalonia
 
                 if (_resources != null)
                 {
+                    (_resources as ISetResourceParent)?.SetParent(null);
                     hadResources = _resources.Count > 0;
                     _resources.ResourcesChanged -= ThisResourcesChanged;
                 }
@@ -260,9 +249,14 @@ namespace Avalonia
                 _resources = value;
                 _resources.ResourcesChanged += ThisResourcesChanged;
 
+                if (value is ISetResourceParent setParent && setParent.ResourceParent == null)
+                {
+                    setParent.SetParent(this);
+                }
+
                 if (hadResources || _resources.Count > 0)
                 {
-                    ((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs());
+                    NotifyResourcesChanged(new ResourcesChangedEventArgs());
                 }
             }
         }
@@ -407,10 +401,7 @@ namespace Avalonia
         }
 
         /// <inheritdoc/>
-        void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e)
-        {
-            ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
-        }
+        void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e);
 
         /// <inheritdoc/>
         bool IResourceProvider.TryGetResource(object key, out object value)
@@ -456,7 +447,8 @@ namespace Avalonia
                 {
                     Parent.ResourcesChanged += ThisResourcesChanged;
                 }
-                ((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs());
+                
+                NotifyResourcesChanged(new ResourcesChangedEventArgs());
 
                 if (Parent is ILogicalRoot || Parent?.IsAttachedToLogicalTree == true || this is ILogicalRoot)
                 {
@@ -721,9 +713,29 @@ namespace Avalonia
             }
         }
 
+        private void NotifyResourcesChanged(ResourcesChangedEventArgs e)
+        {
+            if (_notifyingResourcesChanged)
+            {
+                return;
+            }
+
+            try
+            {
+                _notifyingResourcesChanged = true;
+                (_resources as ISetResourceParent)?.ParentResourcesChanged(e);
+                (_styles as ISetResourceParent)?.ParentResourcesChanged(e);
+                ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
+            }
+            finally
+            {
+                _notifyingResourcesChanged = false;
+            }
+        }
+
         private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e)
         {
-            ((ILogical)this).NotifyResourcesChanged(e);
+            NotifyResourcesChanged(e);
         }
     }
 }

+ 27 - 19
src/Avalonia.Styling/Styling/Styles.cs

@@ -20,6 +20,7 @@ namespace Avalonia.Styling
         private IResourceDictionary _resources;
         private AvaloniaList<IStyle> _styles = new AvaloniaList<IStyle>();
         private Dictionary<Type, List<IStyle>> _cache;
+        private bool _notifyingResourcesChanged;
 
         public Styles()
         {
@@ -38,7 +39,7 @@ namespace Avalonia.Styling
                         ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
                     }
 
-                    x.ResourcesChanged += SubResourceChanged;
+                    x.ResourcesChanged += NotifyResourcesChanged;
                     _cache = null;
                 },
                 x =>
@@ -54,12 +55,18 @@ namespace Avalonia.Styling
                         ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
                     }
 
-                    x.ResourcesChanged -= SubResourceChanged;
+                    x.ResourcesChanged -= NotifyResourcesChanged;
                     _cache = null;
                 },
                 () => { });
         }
 
+        public Styles(IResourceNode parent)
+            : this()
+        {
+            _parent = parent;
+        }
+
         public event NotifyCollectionChangedEventHandler CollectionChanged
         {
             add => _styles.CollectionChanged += value;
@@ -90,11 +97,11 @@ namespace Avalonia.Styling
                 if (_resources != null)
                 {
                     hadResources = _resources.Count > 0;
-                    _resources.ResourcesChanged -= ResourceDictionaryChanged;
+                    _resources.ResourcesChanged -= NotifyResourcesChanged;
                 }
 
                 _resources = value;
-                _resources.ResourcesChanged += ResourceDictionaryChanged;
+                _resources.ResourcesChanged += NotifyResourcesChanged;
 
                 if (hadResources || _resources.Count > 0)
                 {
@@ -261,34 +268,35 @@ namespace Avalonia.Styling
         /// <inheritdoc/>
         void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
         {
-            ResourcesChanged?.Invoke(this, e);
+            NotifyResourcesChanged(e);
         }
 
-        private void ResourceDictionaryChanged(object sender, ResourcesChangedEventArgs e)
+        private void NotifyResourcesChanged(object sender, ResourcesChangedEventArgs e)
         {
-            foreach (var child in this)
-            {
-                (child as ISetResourceParent)?.ParentResourcesChanged(e);
-            }
-
-            ResourcesChanged?.Invoke(this, e);
+            NotifyResourcesChanged(e);
         }
 
-        private void SubResourceChanged(object sender, ResourcesChangedEventArgs e)
+        private void NotifyResourcesChanged(ResourcesChangedEventArgs e)
         {
-            var foundSource = false;
+            if (_notifyingResourcesChanged)
+            {
+                return;
+            }
 
-            foreach (var child in this)
+            try
             {
-                if (foundSource)
+                _notifyingResourcesChanged = true;
+                foreach (var child in this)
                 {
                     (child as ISetResourceParent)?.ParentResourcesChanged(e);
                 }
 
-                foundSource |= child == sender;
+                ResourcesChanged?.Invoke(this, e);
+            }
+            finally
+            {
+                _notifyingResourcesChanged = false;
             }
-
-            ResourcesChanged?.Invoke(this, e);
         }
     }
 }

+ 131 - 0
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs

@@ -627,6 +627,137 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
             Assert.Equal(0xff506070, brush.Color.ToUint32());
         }
 
+        [Fact]
+        public void Resource_With_DynamicResource_Is_Updated_When_Added_To_Parent()
+        {
+            var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Resources>
+        <SolidColorBrush x:Key='brush' Color='{DynamicResource color}'/>
+    </UserControl.Resources>
+
+    <Border Name='border' Background='{DynamicResource brush}'/>
+</UserControl>";
+
+            var loader = new AvaloniaXamlLoader();
+            var userControl = (UserControl)loader.Load(xaml);
+            var border = userControl.FindControl<Border>("border");
+
+            DelayedBinding.ApplyBindings(border);
+
+            var brush = (SolidColorBrush)border.Background;
+            Assert.Equal(0u, brush.Color.ToUint32());
+
+            brush.GetObservable(SolidColorBrush.ColorProperty).Subscribe(_ => { });
+
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var window = new Window
+                {
+                    Resources =
+                    {
+                        { "color", Colors.Red }
+                    },
+                    Content = userControl,
+                };
+
+                window.Show();
+
+                Assert.Equal(Colors.Red, brush.Color);
+            }
+        }
+
+        [Fact]
+        public void MergedDictionary_Resource_With_DynamicResource_Is_Updated_When_Added_To_Parent()
+        {
+            var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Resources>
+        <ResourceDictionary>
+            <ResourceDictionary.MergedDictionaries>
+                <ResourceDictionary>
+                    <SolidColorBrush x:Key='brush' Color='{DynamicResource color}'/>
+                </ResourceDictionary>
+            </ResourceDictionary.MergedDictionaries>
+        </ResourceDictionary>
+    </UserControl.Resources>
+
+    <Border Name='border' Background='{DynamicResource brush}'/>
+</UserControl>";
+
+            var loader = new AvaloniaXamlLoader();
+            var userControl = (UserControl)loader.Load(xaml);
+            var border = userControl.FindControl<Border>("border");
+
+            DelayedBinding.ApplyBindings(border);
+
+            var brush = (SolidColorBrush)border.Background;
+            Assert.Equal(0u, brush.Color.ToUint32());
+
+            brush.GetObservable(SolidColorBrush.ColorProperty).Subscribe(_ => { });
+
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var window = new Window
+                {
+                    Resources =
+                    {
+                        { "color", Colors.Red }
+                    },
+                    Content = userControl,
+                };
+
+                window.Show();
+
+                Assert.Equal(Colors.Red, brush.Color);
+            }
+        }
+
+        [Fact]
+        public void Style_Resource_With_DynamicResource_Is_Updated_When_Added_To_Parent()
+        {
+            var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Styles>
+        <Style>
+            <Style.Resources>
+                <SolidColorBrush x:Key='brush' Color='{DynamicResource color}'/>
+            </Style.Resources>
+        </Style>
+    </UserControl.Styles>
+
+    <Border Name='border' Background='{DynamicResource brush}'/>
+</UserControl>";
+
+            var loader = new AvaloniaXamlLoader();
+            var userControl = (UserControl)loader.Load(xaml);
+            var border = userControl.FindControl<Border>("border");
+
+            DelayedBinding.ApplyBindings(border);
+
+            var brush = (SolidColorBrush)border.Background;
+            Assert.Equal(0u, brush.Color.ToUint32());
+
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var window = new Window
+                {
+                    Resources =
+                    {
+                        { "color", Colors.Red }
+                    },
+                    Content = userControl,
+                };
+
+                window.Show();
+
+                Assert.Equal(Colors.Red, brush.Color);
+            }
+        }
+
         private IDisposable StyledWindow(params (string, string)[] assets)
         {
             var services = TestServices.StyledWindow.With(

+ 85 - 0
tests/Avalonia.Styling.UnitTests/StyledElementTests.cs

@@ -489,6 +489,91 @@ namespace Avalonia.Styling.UnitTests
                 called);
         }
 
+        [Fact]
+        public void Resources_Parent_Is_Set()
+        {
+            var target = new TestControl();
+
+            Assert.Same(target, ((IResourceNode)target.Resources).ResourceParent);
+        }
+
+        [Fact]
+        public void Assigned_Resources_Parent_Is_Set()
+        {
+            var resources = new ResourceDictionary();
+            var target = new TestControl { Resources = resources };
+
+            Assert.Same(target, ((IResourceNode)resources).ResourceParent);
+        }
+
+        [Fact]
+        public void Assigning_Resources_Raises_ResourcesChanged()
+        {
+            var resources = new ResourceDictionary { { "foo", "bar" } };
+            var target = new TestControl();
+            var raised = 0;
+
+            target.ResourcesChanged += (s, e) => ++raised;
+            target.Resources = resources;
+
+            Assert.Equal(1, raised);
+        }
+
+        [Fact]
+        public void Changing_Parent_Notifies_Resources_ParentResourcesChanged()
+        {
+            var resources = new Mock<IResourceDictionary>();
+            var setResourceParent = resources.As<ISetResourceParent>();
+            var target = new TestControl { Resources = resources.Object };
+            var parent = new Decorator { Resources = { { "foo", "bar" } } };
+
+            setResourceParent.ResetCalls();
+            parent.Child = target;
+
+            setResourceParent.Verify(x =>
+                x.ParentResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
+                Times.Once);
+        }
+
+        [Fact]
+        public void Styles_Parent_Is_Set()
+        {
+            var target = new TestControl();
+
+            Assert.Same(target, ((IResourceNode)target.Styles).ResourceParent);
+        }
+
+        [Fact]
+        public void Changing_Parent_Notifies_Styles_ParentResourcesChanged()
+        {
+            var style = new Mock<IStyle>();
+            var setResourceParent = style.As<ISetResourceParent>();
+            var target = new TestControl { Styles = { style.Object } };
+            var parent = new Decorator { Resources = { { "foo", "bar" } } };
+
+            setResourceParent.ResetCalls();
+            parent.Child = target;
+
+            setResourceParent.Verify(x =>
+                x.ParentResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
+                Times.Once);
+        }
+
+        [Fact]
+        public void Changing_Resources_Notifies_Styles()
+        {
+            var style = new Mock<IStyle>();
+            var setResourceParent = style.As<ISetResourceParent>();
+            var target = new TestControl { Styles = { style.Object } };
+
+            setResourceParent.ResetCalls();
+            target.Resources.Add("foo", "bar");
+
+            setResourceParent.Verify(x =>
+                x.ParentResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
+                Times.Once);
+        }
+
         private interface IDataContextEvents
         {
             event EventHandler DataContextBeginUpdate;

+ 18 - 1
tests/Avalonia.Styling.UnitTests/StyledElementTests_Resources.cs

@@ -205,7 +205,7 @@ namespace Avalonia.Controls.UnitTests
         }
 
         [Fact]
-        public void Setting_Logical_Parent_Subscribes_To_Parents_ResourceChanged_Event()
+        public void Setting_Logical_Parent_Raises_Child_ResourcesChanged()
         {
             var parent = new ContentControl();
             var child = new StyledElement();
@@ -220,6 +220,23 @@ namespace Avalonia.Controls.UnitTests
             Assert.True(raisedOnChild);
         }
 
+        [Fact]
+        public void Setting_Logical_Parent_Raises_Style_ResourcesChanged()
+        {
+            var style = new Style(x => x.OfType<Canvas>());
+            var parent = new ContentControl();
+            var child = new StyledElement { Styles = { style } };
+
+            ((ISetLogicalParent)child).SetParent(parent);
+            var raised = false;
+
+            style.ResourcesChanged += (_, __) => raised = true;
+
+            parent.Resources.Add("foo", "bar");
+
+            Assert.True(raised);
+        }
+
         private IControlTemplate ContentControlTemplate()
         {
             return new FuncControlTemplate<ContentControl>((x, scope) =>

+ 11 - 15
tests/Avalonia.Styling.UnitTests/StylesTests.cs

@@ -3,6 +3,7 @@
 
 using System;
 using Avalonia.Controls;
+using Moq;
 using Xunit;
 
 namespace Avalonia.Styling.UnitTests
@@ -76,7 +77,7 @@ namespace Avalonia.Styling.UnitTests
         }
 
         [Fact]
-        public void Adding_Resource_To_Younger_Sibling_Style_Should_Raise_ResourceChanged()
+        public void Adding_Resource_To_Sibling_Style_Should_Raise_ResourceChanged()
         {
             Style style1;
             Style style2;
@@ -95,25 +96,20 @@ namespace Avalonia.Styling.UnitTests
         }
 
         [Fact]
-        public void Adding_Resource_To_Older_Sibling_Style_Should_Raise_ResourceChanged()
+        public void ParentResourcesChanged_Should_Be_Propagated_To_Children()
         {
-            Style style1;
-            Style style2;
-            var target = new Styles
-            {
-                (style1 = new Style()),
-                (style2 = new Style()),
-            };
-
-            var raised = false;
+            var childStyle = new Mock<IStyle>();
+            var setResourceParent = childStyle.As<ISetResourceParent>();
+            var target = new Styles { childStyle.Object };
 
-            style1.ResourcesChanged += (_, __) => raised = true;
-            style2.Resources.Add("foo", "bar");
+            setResourceParent.ResetCalls();
+            ((ISetResourceParent)target).ParentResourcesChanged(new ResourcesChangedEventArgs());
 
-            Assert.False(raised);
+            setResourceParent.Verify(x => x.ParentResourcesChanged(
+                It.IsAny<ResourcesChangedEventArgs>()),
+                Times.Once);
         }
 
-
         [Fact]
         public void Finds_Resource_In_Merged_Dictionary()
         {