Sfoglia il codice sorgente

Merge pull request #3327 from AvaloniaUI/fixes/3323-resourcedictionary-resource

Fix referencing resources in merged dictionaries
Jumar Macato 6 anni fa
parent
commit
fe2d2ac9d6

+ 6 - 8
src/Avalonia.Styling/Styling/ISetStyleParent.cs → src/Avalonia.Styling/Controls/ISetResourceParent.cs

@@ -1,29 +1,27 @@
-using Avalonia.Controls;
-
-namespace Avalonia.Styling
+namespace Avalonia.Controls
 {
     /// <summary>
-    /// Defines an interface through which a <see cref="Style"/>'s parent can be set.
+    /// Defines an interface through which an <see cref="IResourceNode"/>'s parent can be set.
     /// </summary>
     /// <remarks>
     /// You should not usually need to use this interface - it is for internal use only.
     /// </remarks>
-    public interface ISetStyleParent : IStyle
+    public interface ISetResourceParent : IResourceNode
     {
         /// <summary>
-        /// Sets the style parent.
+        /// Sets the resource parent.
         /// </summary>
         /// <param name="parent">The parent.</param>
         void SetParent(IResourceNode parent);
 
         /// <summary>
-        /// Notifies the style that a change has been made to resources that apply to it.
+        /// Notifies the resource node that a change has been made to the resources in its parent.
         /// </summary>
         /// <param name="e">The event args.</param>
         /// <remarks>
         /// This method will be called automatically by the framework, you should not need to call
         /// this method yourself.
         /// </remarks>
-        void NotifyResourcesChanged(ResourcesChangedEventArgs e);
+        void ParentResourcesChanged(ResourcesChangedEventArgs e);
     }
 }

+ 60 - 2
src/Avalonia.Styling/Controls/ResourceDictionary.cs

@@ -12,8 +12,12 @@ namespace Avalonia.Controls
     /// <summary>
     /// An indexed dictionary of resources.
     /// </summary>
-    public class ResourceDictionary : AvaloniaDictionary<object, object>, IResourceDictionary
+    public class ResourceDictionary : AvaloniaDictionary<object, object>,
+        IResourceDictionary,
+        IResourceNode,
+        ISetResourceParent
     {
+        private IResourceNode _parent;
         private AvaloniaList<IResourceProvider> _mergedDictionaries;
 
         /// <summary>
@@ -39,6 +43,12 @@ namespace Avalonia.Controls
                     _mergedDictionaries.ForEachItem(
                         x =>
                         {
+                            if (x is ISetResourceParent setParent)
+                            {
+                                setParent.SetParent(this);
+                                setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
+                            }
+
                             if (x.HasResources)
                             {
                                 OnResourcesChanged();
@@ -48,11 +58,18 @@ namespace Avalonia.Controls
                         },
                         x =>
                         {
+                            if (x is ISetResourceParent setParent)
+                            {
+                                setParent.SetParent(null);
+                                setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
+                            }
+
                             if (x.HasResources)
                             {
                                 OnResourcesChanged();
                             }
 
+                            (x as ISetResourceParent)?.SetParent(null);
                             x.ResourcesChanged -= MergedDictionaryResourcesChanged;
                         },
                         () => { });
@@ -68,6 +85,27 @@ namespace Avalonia.Controls
             get => Count > 0 || (_mergedDictionaries?.Any(x => x.HasResources) ?? false);
         }
 
+        /// <inheritdoc/>
+        IResourceNode IResourceNode.ResourceParent => _parent;
+
+        /// <inheritdoc/>
+        void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
+        {
+            NotifyMergedDictionariesResourcesChanged(e);
+            ResourcesChanged?.Invoke(this, e);
+        }
+
+        /// <inheritdoc/>
+        void ISetResourceParent.SetParent(IResourceNode parent)
+        {
+            if (_parent != null && parent != null)
+            {
+                throw new InvalidOperationException("The ResourceDictionary already has a parent.");
+            }
+            
+            _parent = parent;
+        }
+
         /// <inheritdoc/>
         public bool TryGetResource(object key, out object value)
         {
@@ -95,7 +133,27 @@ namespace Avalonia.Controls
             ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
         }
 
-        private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => OnResourcesChanged();
+        private void NotifyMergedDictionariesResourcesChanged(ResourcesChangedEventArgs e)
+        {
+            if (_mergedDictionaries != null)
+            {
+                for (var i = _mergedDictionaries.Count - 1; i >= 0; --i)
+                {
+                    if (_mergedDictionaries[i] is ISetResourceParent merged)
+                    {
+                        merged.ParentResourcesChanged(e);
+                    }
+                }
+            }
+        }
+
+        private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            var ev = new ResourcesChangedEventArgs();
+            NotifyMergedDictionariesResourcesChanged(ev);
+            OnResourcesChanged();
+        }
+
         private void MergedDictionaryResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnResourcesChanged();
     }
 }

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

@@ -223,13 +223,13 @@ namespace Avalonia
                 {
                     if (_styles != null)
                     {
-                        (_styles as ISetStyleParent)?.SetParent(null);
+                        (_styles as ISetResourceParent)?.SetParent(null);
                         _styles.ResourcesChanged -= ThisResourcesChanged;
                     }
 
                     _styles = value;
 
-                    if (value is ISetStyleParent setParent && setParent.ResourceParent == null)
+                    if (value is ISetResourceParent setParent && setParent.ResourceParent == null)
                     {
                         setParent.SetParent(this);
                     }

+ 6 - 6
src/Avalonia.Styling/Styling/Style.cs

@@ -14,7 +14,7 @@ namespace Avalonia.Styling
     /// <summary>
     /// Defines a style.
     /// </summary>
-    public class Style : AvaloniaObject, IStyle, ISetStyleParent
+    public class Style : AvaloniaObject, IStyle, ISetResourceParent
     {
         private static Dictionary<IStyleable, CompositeDisposable> _applied =
             new Dictionary<IStyleable, CompositeDisposable>();
@@ -59,16 +59,16 @@ namespace Avalonia.Styling
 
                 if (_resources != null)
                 {
-                    hadResources = _resources.Count > 0;
+                    hadResources = _resources.HasResources;
                     _resources.ResourcesChanged -= ResourceDictionaryChanged;
                 }
 
                 _resources = value;
                 _resources.ResourcesChanged += ResourceDictionaryChanged;
 
-                if (hadResources || _resources.Count > 0)
+                if (hadResources || _resources.HasResources)
                 {
-                    ((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs());
+                    ((ISetResourceParent)this).ParentResourcesChanged(new ResourcesChangedEventArgs());
                 }
             }
         }
@@ -194,13 +194,13 @@ namespace Avalonia.Styling
         }
 
         /// <inheritdoc/>
-        void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e)
+        void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
         {
             ResourcesChanged?.Invoke(this, e);
         }
 
         /// <inheritdoc/>
-        void ISetStyleParent.SetParent(IResourceNode parent)
+        void ISetResourceParent.SetParent(IResourceNode parent)
         {
             if (_parent != null && parent != null)
             {

+ 10 - 10
src/Avalonia.Styling/Styling/Styles.cs

@@ -14,7 +14,7 @@ namespace Avalonia.Styling
     /// <summary>
     /// A style that consists of a number of child styles.
     /// </summary>
-    public class Styles : AvaloniaObject, IAvaloniaList<IStyle>, IStyle, ISetStyleParent
+    public class Styles : AvaloniaObject, IAvaloniaList<IStyle>, IStyle, ISetResourceParent
     {
         private IResourceNode _parent;
         private IResourceDictionary _resources;
@@ -27,10 +27,10 @@ namespace Avalonia.Styling
             _styles.ForEachItem(
                 x =>
                 {
-                    if (x.ResourceParent == null && x is ISetStyleParent setParent)
+                    if (x.ResourceParent == null && x is ISetResourceParent setParent)
                     {
                         setParent.SetParent(this);
-                        setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs());
+                        setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
                     }
 
                     if (x.HasResources)
@@ -43,10 +43,10 @@ namespace Avalonia.Styling
                 },
                 x =>
                 {
-                    if (x.ResourceParent == this && x is ISetStyleParent setParent)
+                    if (x.ResourceParent == this && x is ISetResourceParent setParent)
                     {
                         setParent.SetParent(null);
-                        setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs());
+                        setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
                     }
 
                     if (x.HasResources)
@@ -98,7 +98,7 @@ namespace Avalonia.Styling
 
                 if (hadResources || _resources.Count > 0)
                 {
-                    ((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs());
+                    ((ISetResourceParent)this).ParentResourcesChanged(new ResourcesChangedEventArgs());
                 }
             }
         }
@@ -246,7 +246,7 @@ namespace Avalonia.Styling
         IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator();
 
         /// <inheritdoc/>
-        void ISetStyleParent.SetParent(IResourceNode parent)
+        void ISetResourceParent.SetParent(IResourceNode parent)
         {
             if (_parent != null && parent != null)
             {
@@ -257,7 +257,7 @@ namespace Avalonia.Styling
         }
 
         /// <inheritdoc/>
-        void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e)
+        void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
         {
             ResourcesChanged?.Invoke(this, e);
         }
@@ -266,7 +266,7 @@ namespace Avalonia.Styling
         {
             foreach (var child in this)
             {
-                (child as ISetStyleParent)?.NotifyResourcesChanged(e);
+                (child as ISetResourceParent)?.ParentResourcesChanged(e);
             }
 
             ResourcesChanged?.Invoke(this, e);
@@ -280,7 +280,7 @@ namespace Avalonia.Styling
             {
                 if (foundSource)
                 {
-                    (child as ISetStyleParent)?.NotifyResourcesChanged(e);
+                    (child as ISetResourceParent)?.ParentResourcesChanged(e);
                 }
 
                 foundSource |= child == sender;

+ 25 - 1
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs

@@ -7,8 +7,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
     /// <summary>
     /// Loads a resource dictionary from a specified URL.
     /// </summary>
-    public class ResourceInclude :IResourceProvider
+    public class ResourceInclude : IResourceNode, ISetResourceParent
     {
+        private IResourceNode _parent;
         private Uri _baseUri;
         private IResourceDictionary _loaded;
 
@@ -26,6 +27,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
                     var loader = new AvaloniaXamlLoader();
                     _loaded = (IResourceDictionary)loader.Load(Source, _baseUri);
 
+                    (_loaded as ISetResourceParent)?.SetParent(this);
+                    _loaded.ResourcesChanged += ResourcesChanged;
+
                     if (_loaded.HasResources)
                     {
                         ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
@@ -44,12 +48,32 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
         /// <inhertidoc/>
         bool IResourceProvider.HasResources => Loaded.HasResources;
 
+        /// <inhertidoc/>
+        IResourceNode IResourceNode.ResourceParent => _parent;
+
         /// <inhertidoc/>
         bool IResourceProvider.TryGetResource(object key, out object value)
         {
             return Loaded.TryGetResource(key, out value);
         }
 
+        /// <inhertidoc/>
+        void ISetResourceParent.SetParent(IResourceNode parent)
+        {
+            if (_parent != null && parent != null)
+            {
+                throw new InvalidOperationException("The ResourceInclude already has a parent.");
+            }
+
+            _parent = parent;
+        }
+
+        /// <inhertidoc/>
+        void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
+        {
+            (_loaded as ISetResourceParent)?.ParentResourcesChanged(e);
+        }
+
         public ResourceInclude ProvideValue(IServiceProvider serviceProvider)
         {
             var tdc = (ITypeDescriptorContext)serviceProvider;

+ 5 - 5
src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs

@@ -10,7 +10,7 @@ namespace Avalonia.Markup.Xaml.Styling
     /// <summary>
     /// Includes a style from a URL.
     /// </summary>
-    public class StyleInclude : IStyle, ISetStyleParent
+    public class StyleInclude : IStyle, ISetResourceParent
     {
         private Uri _baseUri;
         private IStyle _loaded;
@@ -53,7 +53,7 @@ namespace Avalonia.Markup.Xaml.Styling
                 {
                     var loader = new AvaloniaXamlLoader();
                     _loaded = (IStyle)loader.Load(Source, _baseUri);
-                    (_loaded as ISetStyleParent)?.SetParent(this);
+                    (_loaded as ISetResourceParent)?.SetParent(this);
                 }
 
                 return _loaded;
@@ -89,13 +89,13 @@ namespace Avalonia.Markup.Xaml.Styling
         public bool TryGetResource(object key, out object value) => Loaded.TryGetResource(key, out value);
 
         /// <inheritdoc/>
-        void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e)
+        void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
         {
-            (Loaded as ISetStyleParent)?.NotifyResourcesChanged(e);
+            (Loaded as ISetResourceParent)?.ParentResourcesChanged(e);
         }
 
         /// <inheritdoc/>
-        void ISetStyleParent.SetParent(IResourceNode parent)
+        void ISetResourceParent.SetParent(IResourceNode parent)
         {
             if (_parent != null && parent != null)
             {

+ 123 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs

@@ -0,0 +1,123 @@
+// 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.
+
+using System;
+using Avalonia.Controls;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Templates;
+using Avalonia.Media;
+using Avalonia.Styling;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Markup.Xaml.UnitTests.Xaml
+{
+    public class ResourceDictionaryTests : XamlTestBase
+    {
+        [Fact]
+        public void StaticResource_Works_In_ResourceDictionary()
+        {
+            using (StyledWindow())
+            {
+                var xaml = @"
+<ResourceDictionary xmlns='https://github.com/avaloniaui'
+                    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+  <Color x:Key='Red'>Red</Color>
+  <SolidColorBrush x:Key='RedBrush' Color='{StaticResource Red}'/>
+</ResourceDictionary>";
+                var loader = new AvaloniaXamlLoader();
+                var resources = (ResourceDictionary)loader.Load(xaml);
+                var brush = (SolidColorBrush)resources["RedBrush"];
+
+                Assert.Equal(Colors.Red, brush.Color);
+            }
+        }
+
+        [Fact]
+        public void DynamicResource_Works_In_ResourceDictionary()
+        {
+            using (StyledWindow())
+            {
+                var xaml = @"
+<ResourceDictionary xmlns='https://github.com/avaloniaui'
+                    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+  <Color x:Key='Red'>Red</Color>
+  <SolidColorBrush x:Key='RedBrush' Color='{DynamicResource Red}'/>
+</ResourceDictionary>";
+                var loader = new AvaloniaXamlLoader();
+                var resources = (ResourceDictionary)loader.Load(xaml);
+                var brush = (SolidColorBrush)resources["RedBrush"];
+
+                Assert.Equal(Colors.Red, brush.Color);
+            }
+        }
+
+        [Fact]
+        public void DynamicResource_Finds_Resource_In_Parent_Dictionary()
+        {
+            var dictionaryXaml = @"
+<ResourceDictionary xmlns='https://github.com/avaloniaui'
+                    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+  <SolidColorBrush x:Key='RedBrush' Color='{DynamicResource Red}'/>
+</ResourceDictionary>";
+
+            using (StyledWindow(assets: ("test:dict.xaml", dictionaryXaml)))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Resources>
+        <ResourceDictionary>
+            <ResourceDictionary.MergedDictionaries>
+                <ResourceInclude Source='test:dict.xaml'/>
+            </ResourceDictionary.MergedDictionaries>
+        </ResourceDictionary>
+        <Color x:Key='Red'>Red</Color>
+    </Window.Resources>
+    <Button Name='button' Background='{DynamicResource RedBrush}'/>
+</Window>";
+
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var button = window.FindControl<Button>("button");
+
+                var brush = Assert.IsType<SolidColorBrush>(button.Background);
+                Assert.Equal(Colors.Red, brush.Color);
+
+                window.Resources["Red"] = Colors.Green;
+
+                Assert.Equal(Colors.Green, brush.Color);
+            }
+        }
+
+        private IDisposable StyledWindow(params (string, string)[] assets)
+        {
+            var services = TestServices.StyledWindow.With(
+                assetLoader: new MockAssetLoader(assets),
+                theme: () => new Styles
+                {
+                    WindowStyle(),
+                });
+
+            return UnitTestApplication.Start(services);
+        }
+
+        private Style WindowStyle()
+        {
+            return new Style(x => x.OfType<Window>())
+            {
+                Setters =
+                {
+                    new Setter(
+                        Window.TemplateProperty,
+                        new FuncControlTemplate<Window>((x, scope) =>
+                            new ContentPresenter
+                            {
+                                Name = "PART_ContentPresenter",
+                                [!ContentPresenter.ContentProperty] = x[!Window.ContentProperty],
+                            }.RegisterInNameScope(scope)))
+                }
+            };
+        }
+    }
+}

+ 30 - 15
tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs

@@ -3,6 +3,7 @@
 
 using System;
 using Avalonia.Controls;
+using Moq;
 using Xunit;
 
 namespace Avalonia.Styling.UnitTests
@@ -136,7 +137,7 @@ namespace Avalonia.Styling.UnitTests
         }
 
         [Fact]
-        public void ResourcesChanged_Should_Not_Be_Raised_On_Empty_MergedDictionary_Remove()
+        public void ResourcesChanged_Should_Be_Raised_On_MergedDictionary_Resource_Add()
         {
             var target = new ResourceDictionary
             {
@@ -145,31 +146,45 @@ namespace Avalonia.Styling.UnitTests
                     new ResourceDictionary(),
                 }
             };
+
             var raised = false;
 
             target.ResourcesChanged += (_, __) => raised = true;
-            target.MergedDictionaries.RemoveAt(0);
+            ((IResourceDictionary)target.MergedDictionaries[0]).Add("foo", "bar");
 
-            Assert.False(raised);
+            Assert.True(raised);
         }
 
         [Fact]
-        public void ResourcesChanged_Should_Be_Raised_On_MergedDictionary_Resource_Add()
+        public void MergedDictionary_ParentResourcesChanged_Should_Be_Called_On_Resource_Add()
         {
-            var target = new ResourceDictionary
-            {
-                MergedDictionaries =
-                {
-                    new ResourceDictionary(),
-                }
-            };
+            var target = new ResourceDictionary();
+            var merged = new Mock<ISetResourceParent>();
 
-            var raised = false;
+            target.MergedDictionaries.Add(merged.Object);
+            merged.ResetCalls();
 
-            target.ResourcesChanged += (_, __) => raised = true;
-            ((IResourceDictionary)target.MergedDictionaries[0]).Add("foo", "bar");
+            target.Add("foo", "bar");
 
-            Assert.True(raised);
+            merged.Verify(
+                x => x.ParentResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
+                Times.Once);
+        }
+
+        [Fact]
+        public void MergedDictionary_ParentResourcesChanged_Should_Be_Called_On_NotifyResourceChanged()
+        {
+            var target = new ResourceDictionary();
+            var merged = new Mock<ISetResourceParent>();
+
+            target.MergedDictionaries.Add(merged.Object);
+            merged.ResetCalls();
+
+            ((ISetResourceParent)target).ParentResourcesChanged(new ResourcesChangedEventArgs());
+
+            merged.Verify(
+                x => x.ParentResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
+                Times.Once);
         }
     }
 }