From 0e155bd2d4925800ad6fefe32cb4d6e891ba4d4a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 24 Aug 2017 02:50:49 +0200 Subject: [PATCH] Make dynamic resources work in more situations. Fixes #492 in a fashion: `DynamicResource` now works for this scenario. --- src/Avalonia.Controls/Application.cs | 3 + src/Avalonia.Controls/Control.cs | 10 ++ src/Avalonia.Controls/ControlExtensions.cs | 29 ----- .../Controls/IResourceProvider.cs | 5 + .../Controls/ResourceProviderExtensions.cs | 49 ++++++++ src/Avalonia.Styling/LogicalTree/ILogical.cs | 2 +- .../Styling/ISetStyleParent.cs | 32 +++++ src/Avalonia.Styling/Styling/Style.cs | 23 +++- src/Avalonia.Styling/Styling/Styles.cs | 56 ++++++++- .../DynamicResourceExtension.cs | 16 +-- .../Styling/StyleInclude.cs | 25 +++- .../ControlTests_Resources.cs | 38 ------ .../DynamicResourceExtensionTests.cs | 4 - .../SelectorTests_Child.cs | 5 + .../SelectorTests_Descendent.cs | 5 + .../Avalonia.Styling.UnitTests/StylesTests.cs | 115 ++++++++++++++++++ 16 files changed, 329 insertions(+), 88 deletions(-) create mode 100644 src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs create mode 100644 src/Avalonia.Styling/Styling/ISetStyleParent.cs create mode 100644 tests/Avalonia.Styling.UnitTests/StylesTests.cs diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 4533855b73..ce15c0b9e3 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -128,6 +128,9 @@ namespace Avalonia /// bool IResourceProvider.HasResources => _resources?.Count > 0; + /// + IResourceProvider IResourceProvider.ResourceParent => null; + /// /// Initializes the application by loading XAML etc. /// diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index c93742b7f6..a40b59d60a 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -283,10 +283,17 @@ namespace Avalonia.Controls { if (_styles != null) { + (_styles as ISetStyleParent)?.SetParent(null); _styles.ResourcesChanged -= StyleResourcesChanged; } _styles = value; + + if (value is ISetStyleParent setParent && setParent.ResourceParent == null) + { + setParent.SetParent(this); + } + _styles.ResourcesChanged += StyleResourcesChanged; } } @@ -385,6 +392,9 @@ namespace Avalonia.Controls /// bool IResourceProvider.HasResources => _resources?.Count > 0 || Styles.HasResources; + /// + IResourceProvider IResourceProvider.ResourceParent => ((IStyleHost)this).StylingParent as IResourceProvider; + /// IAvaloniaReadOnlyList IStyleable.Classes => Classes; diff --git a/src/Avalonia.Controls/ControlExtensions.cs b/src/Avalonia.Controls/ControlExtensions.cs index 87c774dfcf..60a940627f 100644 --- a/src/Avalonia.Controls/ControlExtensions.cs +++ b/src/Avalonia.Controls/ControlExtensions.cs @@ -81,35 +81,6 @@ namespace Avalonia.Controls .FirstOrDefault(x => x != null); } - /// - /// Finds the specified resource by searching up the logical tree and then global styles. - /// - /// The control. - /// The resource key. - /// The resource, or if not found. - public static object FindResource(this IControl control, string key) - { - Contract.Requires(control != null); - Contract.Requires(key != null); - - var current = control as IStyleHost; - - while (current != null) - { - if (current is IResourceProvider host) - { - if (host.TryGetResource(key, out var value)) - { - return value; - } - } - - current = current.StylingParent; - } - - return AvaloniaProperty.UnsetValue; - } - /// /// Adds or removes a pseudoclass depending on a boolean value. /// diff --git a/src/Avalonia.Styling/Controls/IResourceProvider.cs b/src/Avalonia.Styling/Controls/IResourceProvider.cs index e3ea2e1aab..180476b2e4 100644 --- a/src/Avalonia.Styling/Controls/IResourceProvider.cs +++ b/src/Avalonia.Styling/Controls/IResourceProvider.cs @@ -17,6 +17,11 @@ namespace Avalonia.Controls /// bool HasResources { get; } + /// + /// Gets the parent resource provider, if any. + /// + IResourceProvider ResourceParent { get; } + /// /// Tries to find a resource within the element. /// diff --git a/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs b/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs new file mode 100644 index 0000000000..45e16438d0 --- /dev/null +++ b/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Reactive; +using System.Reactive.Linq; +using System.Text; + +namespace Avalonia.Controls +{ + public static class ResourceProviderExtensions + { + /// + /// Finds the specified resource by searching up the logical tree and then global styles. + /// + /// The control. + /// The resource key. + /// The resource, or if not found. + public static object FindResource(this IResourceProvider control, string key) + { + Contract.Requires(control != null); + Contract.Requires(key != null); + + var current = control; + + while (current != null) + { + if (current is IResourceProvider host) + { + if (host.TryGetResource(key, out var value)) + { + return value; + } + } + + current = current.ResourceParent; + } + + return AvaloniaProperty.UnsetValue; + } + + public static IObservable GetResourceObservable(this IResourceProvider target, string key) + { + return Observable.FromEventPattern( + x => target.ResourcesChanged += x, + x => target.ResourcesChanged -= x) + .StartWith((EventPattern)null) + .Select(x => target.FindResource(key)); + } + } +} diff --git a/src/Avalonia.Styling/LogicalTree/ILogical.cs b/src/Avalonia.Styling/LogicalTree/ILogical.cs index a6e804567d..8ee3c9ea4f 100644 --- a/src/Avalonia.Styling/LogicalTree/ILogical.cs +++ b/src/Avalonia.Styling/LogicalTree/ILogical.cs @@ -58,7 +58,7 @@ namespace Avalonia.LogicalTree void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e); /// - /// Notifies the control that a change has been made to its resources. + /// Notifies the control that a change has been made to resources that apply to it. /// /// The event args. /// diff --git a/src/Avalonia.Styling/Styling/ISetStyleParent.cs b/src/Avalonia.Styling/Styling/ISetStyleParent.cs new file mode 100644 index 0000000000..da5b34798e --- /dev/null +++ b/src/Avalonia.Styling/Styling/ISetStyleParent.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Controls; + +namespace Avalonia.Styling +{ + /// + /// Defines an interface through which a 's parent can be set. + /// + /// + /// You should not usually need to use this interface - it is for internal use only. + /// + public interface ISetStyleParent : IStyle + { + /// + /// Sets the style parent. + /// + /// The parent. + void SetParent(IResourceProvider parent); + + /// + /// Notifies the style that a change has been made to resources that apply to it. + /// + /// The event args. + /// + /// This method will be called automatically by the framework, you should not need to call + /// this method yourself. + /// + void NotifyResourcesChanged(ResourcesChangedEventArgs e); + } +} diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index b095f38035..54b53868da 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -13,10 +13,11 @@ namespace Avalonia.Styling /// /// Defines a style. /// - public class Style : IStyle + public class Style : IStyle, ISetStyleParent { private static Dictionary> _applied = new Dictionary>(); + private IResourceProvider _parent; private ResourceDictionary _resources; /// @@ -69,6 +70,9 @@ namespace Avalonia.Styling /// bool IResourceProvider.HasResources => _resources?.Count > 0; + /// + IResourceProvider IResourceProvider.ResourceParent => _parent; + /// /// Attaches the style to a control if the style's selector matches. /// @@ -128,6 +132,23 @@ namespace Avalonia.Styling } } + /// + void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) + { + ResourcesChanged?.Invoke(this, e); + } + + /// + void ISetStyleParent.SetParent(IResourceProvider parent) + { + if (_parent != null && parent != null) + { + throw new InvalidOperationException("The Style already has a parent."); + } + + _parent = parent; + } + private static List GetSubscriptions(IStyleable control) { List subscriptions; diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index 47647d726e..43a542d460 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -12,8 +12,9 @@ namespace Avalonia.Styling /// /// A style that consists of a number of child styles. /// - public class Styles : AvaloniaList, IStyle + public class Styles : AvaloniaList, IStyle, ISetStyleParent { + private IResourceProvider _parent; private ResourceDictionary _resources; public Styles() @@ -22,6 +23,12 @@ namespace Avalonia.Styling this.ForEachItem( x => { + if (x.ResourceParent == null && x is ISetStyleParent setParent) + { + setParent.SetParent(this); + setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs()); + } + if (x.HasResources) { ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); @@ -31,6 +38,12 @@ namespace Avalonia.Styling }, x => { + if (x.ResourceParent == this && x is ISetStyleParent setParent) + { + setParent.SetParent(null); + setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs()); + } + if (x.HasResources) { ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); @@ -64,6 +77,9 @@ namespace Avalonia.Styling } } + /// + IResourceProvider IResourceProvider.ResourceParent => _parent; + /// /// Attaches the style to a control if the style's selector matches. /// @@ -99,13 +115,49 @@ namespace Avalonia.Styling return false; } + /// + void ISetStyleParent.SetParent(IResourceProvider parent) + { + if (_parent != null && parent != null) + { + throw new InvalidOperationException("The Style already has a parent."); + } + + _parent = parent; + } + + /// + void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) + { + ResourcesChanged?.Invoke(this, e); + } + private void ResourceDictionaryChanged(object sender, NotifyCollectionChangedEventArgs e) { - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + var ev = new ResourcesChangedEventArgs(); + + foreach (var child in this) + { + (child as ISetStyleParent)?.NotifyResourcesChanged(ev); + } + + ResourcesChanged?.Invoke(this, ev); } private void SubResourceChanged(object sender, ResourcesChangedEventArgs e) { + var foundSource = false; + + foreach (var child in this) + { + if (foundSource) + { + (child as ISetStyleParent)?.NotifyResourcesChanged(e); + } + + foundSource |= child == sender; + } + ResourcesChanged?.Invoke(this, e); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index 7921510944..905bf16d1f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -15,7 +15,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { public class DynamicResourceExtension : MarkupExtension, IBinding { - private IControl _anchor; + private IResourceProvider _anchor; public DynamicResourceExtension() { @@ -33,9 +33,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions var context = (ITypeDescriptorContext)serviceProvider; var provideTarget = context.GetService(); - if (!(provideTarget.TargetObject is IControl)) + if (!(provideTarget.TargetObject is IResourceProvider)) { - _anchor = GetAnchor(context); + _anchor = GetAnchor(context); } return this; @@ -47,17 +47,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions object anchor, bool enableDataValidation) { - var control = target as IControl ?? _anchor as IControl; + var control = target as IResourceProvider ?? _anchor; if (control != null) { - var o = Observable.FromEventPattern( - x => control.ResourcesChanged += x, - x => control.ResourcesChanged -= x) - .StartWith((EventPattern)null) - .Select(x => control.FindResource(ResourceKey)); - - return new InstancedBinding(o); + return new InstancedBinding(control.GetResourceObservable(ResourceKey)); } return null; diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 8e571af4a0..a121cd8c1b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -10,16 +10,16 @@ namespace Avalonia.Markup.Xaml.Styling /// /// Includes a style from a URL. /// - public class StyleInclude : IStyle + public class StyleInclude : IStyle, ISetStyleParent { private Uri _baseUri; private IStyle _loaded; + private IResourceProvider _parent; /// /// Initializes a new instance of the class. /// /// - public StyleInclude(Uri baseUri) { _baseUri = baseUri; @@ -44,6 +44,7 @@ namespace Avalonia.Markup.Xaml.Styling { var loader = new AvaloniaXamlLoader(); _loaded = (IStyle)loader.Load(Source, _baseUri); + (_loaded as ISetStyleParent)?.SetParent(this); } return _loaded; @@ -53,6 +54,9 @@ namespace Avalonia.Markup.Xaml.Styling /// bool IResourceProvider.HasResources => Loaded.HasResources; + /// + IResourceProvider IResourceProvider.ResourceParent => _parent; + /// public void Attach(IStyleable control, IStyleHost container) { @@ -64,5 +68,22 @@ namespace Avalonia.Markup.Xaml.Styling /// public bool TryGetResource(string key, out object value) => Loaded.TryGetResource(key, out value); + + /// + void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) + { + (Loaded as ISetStyleParent)?.NotifyResourcesChanged(e); + } + + /// + void ISetStyleParent.SetParent(IResourceProvider parent) + { + if (_parent != null && parent != null) + { + throw new InvalidOperationException("The Style already has a parent."); + } + + _parent = parent; + } } } \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs b/tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs index 561cad87c7..6a6cb48001 100644 --- a/tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs +++ b/tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs @@ -204,44 +204,6 @@ namespace Avalonia.Controls.UnitTests Assert.True(raised); } - [Fact] - public void Adding_Style_With_Resource_Should_Raise_ResourceChanged() - { - Style style = new Style - { - Resources = { { "foo", "bar" } }, - }; - - var target = new Decorator(); - var raised = false; - - target.ResourcesChanged += (_, __) => raised = true; - target.Styles.Add(style); - - Assert.True(raised); - } - - [Fact] - public void Removing_Style_With_Resource_Should_Raise_ResourceChanged() - { - var target = new Decorator - { - Styles = - { - new Style - { - Resources = { { "foo", "bar" } }, - } - } - }; - var raised = false; - - target.ResourcesChanged += (_, __) => raised = true; - target.Styles.Clear(); - - Assert.True(raised); - } - private IControlTemplate ContentControlTemplate() { return new FuncControlTemplate(x => diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index 46a90635b2..a8e93b29f0 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -360,8 +360,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'> Red - Green - Blue "; var style2Xaml = @" @@ -369,8 +367,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'> - - "; using (StyledWindow( diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs index 50b4828e73..352efe5358 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs @@ -154,6 +154,11 @@ namespace Avalonia.Styling.UnitTests { throw new NotImplementedException(); } + + public void NotifyResourcesChanged(ResourcesChangedEventArgs e) + { + throw new NotImplementedException(); + } } public class TestLogical1 : TestLogical diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs index 7cf8c3dd1c..c413904c8f 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs @@ -184,6 +184,11 @@ namespace Avalonia.Styling.UnitTests { throw new NotImplementedException(); } + + public void NotifyResourcesChanged(ResourcesChangedEventArgs e) + { + throw new NotImplementedException(); + } } public class TestLogical1 : TestLogical diff --git a/tests/Avalonia.Styling.UnitTests/StylesTests.cs b/tests/Avalonia.Styling.UnitTests/StylesTests.cs new file mode 100644 index 0000000000..c033dad0c6 --- /dev/null +++ b/tests/Avalonia.Styling.UnitTests/StylesTests.cs @@ -0,0 +1,115 @@ +// 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 Xunit; + +namespace Avalonia.Styling.UnitTests +{ + public class StylesTests + { + [Fact] + public void Adding_Style_With_Resources_Should_Raise_ResourceChanged() + { + var style = new Style + { + Resources = { { "foo", "bar" } }, + }; + + var target = new Styles(); + var raised = false; + + target.ResourcesChanged += (_, __) => raised = true; + target.Add(style); + + Assert.True(raised); + } + + [Fact] + public void Removing_Style_With_Resources_Should_Raise_ResourceChanged() + { + var target = new Styles + { + new Style + { + Resources = { { "foo", "bar" } }, + } + }; + + var raised = false; + + target.ResourcesChanged += (_, __) => raised = true; + target.Clear(); + + Assert.True(raised); + } + + [Fact] + public void Adding_Style_Without_Resources_Should_Not_Raise_ResourceChanged() + { + var style = new Style(); + var target = new Styles(); + var raised = false; + + target.ResourcesChanged += (_, __) => raised = true; + target.Add(style); + + Assert.False(raised); + } + + [Fact] + public void Adding_Resource_Should_Raise_Child_ResourceChanged() + { + Style child; + var target = new Styles + { + (child = new Style()), + }; + + var raised = false; + + child.ResourcesChanged += (_, __) => raised = true; + target.Resources.Add("foo", "bar"); + + Assert.True(raised); + } + + [Fact] + public void Adding_Resource_To_Younger_Sibling_Style_Should_Raise_ResourceChanged() + { + Style style1; + Style style2; + var target = new Styles + { + (style1 = new Style()), + (style2 = new Style()), + }; + + var raised = false; + + style2.ResourcesChanged += (_, __) => raised = true; + style1.Resources.Add("foo", "bar"); + + Assert.True(raised); + } + + [Fact] + public void Adding_Resource_To_Older_Sibling_Style_Should_Raise_ResourceChanged() + { + Style style1; + Style style2; + var target = new Styles + { + (style1 = new Style()), + (style2 = new Style()), + }; + + var raised = false; + + style1.ResourcesChanged += (_, __) => raised = true; + style2.Resources.Add("foo", "bar"); + + Assert.False(raised); + } + } +}