From 513efe99f7740fdb59836494f9b4f6ff415b0964 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 23 Aug 2017 19:24:26 +0200 Subject: [PATCH] Start making DynamicResource react to changes. --- src/Avalonia.Controls/Application.cs | 3 + src/Avalonia.Controls/Control.cs | 100 +++++++++++++----- .../Controls/IResourceProvider.cs | 5 + .../Controls/ResourceDictionary.cs | 6 +- .../Controls/ResourcesChangedEventArgs.cs | 11 ++ src/Avalonia.Styling/LogicalTree/ILogical.cs | 11 ++ src/Avalonia.Styling/Styling/Style.cs | 13 ++- src/Avalonia.Styling/Styling/Styles.cs | 27 ++++- .../DynamicResourceExtension.cs | 11 +- .../Styling/StyleInclude.cs | 3 + .../DynamicResourceExtensionTests.cs | 76 +++++++++++++ 11 files changed, 236 insertions(+), 30 deletions(-) create mode 100644 src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 4b7b7be900..fda97e259c 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -49,6 +49,9 @@ namespace Avalonia OnExit += OnExiting; } + /// + public event EventHandler ResourcesChanged; + /// /// Gets the current instance of the class. /// diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index b9e5d1c609..81acf1a204 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -154,6 +154,11 @@ namespace Avalonia.Controls /// public event EventHandler Initialized; + /// + /// Occurs when a resource in this control or a parent control has changed. + /// + public event EventHandler ResourcesChanged; + /// /// Gets or sets the name of the control. /// @@ -269,8 +274,22 @@ namespace Avalonia.Controls /// public Styles Styles { - get { return _styles ?? (_styles = new Styles()); } - set { _styles = value; } + get { return _styles ?? (Styles = new Styles()); } + set + { + Contract.Requires(value != null); + + if (_styles != value) + { + if (_styles != null) + { + _styles.ResourcesChanged -= StyleResourcesChanged; + } + + _styles = value; + _styles.ResourcesChanged += StyleResourcesChanged; + } + } } /// @@ -290,7 +309,19 @@ namespace Avalonia.Controls /// /// Gets or sets the control's resource dictionary. /// - public IResourceDictionary Resources => _resources ?? (_resources = new ResourceDictionary()); + public IResourceDictionary Resources + { + get + { + if (_resources == null) + { + _resources = new ResourceDictionary(); + _resources.CollectionChanged += ResourceDictionaryChanged; + } + + return _resources; + } + } /// /// Gets or sets a user-defined object attached to the control. @@ -310,6 +341,32 @@ namespace Avalonia.Controls internal set { SetValue(TemplatedParentProperty, value); } } + /// + /// Gets the control's logical children. + /// + protected IAvaloniaList LogicalChildren + { + get + { + if (_logicalChildren == null) + { + var list = new AvaloniaList(); + list.ResetBehavior = ResetBehavior.Remove; + list.Validate = ValidateLogicalChild; + list.CollectionChanged += LogicalChildrenCollectionChanged; + _logicalChildren = list; + } + + return _logicalChildren; + } + } + + /// + /// Gets the collection in a form that allows adding and removing + /// pseudoclasses. + /// + protected IPseudoClasses PseudoClasses => Classes; + /// /// Gets a value indicating whether the element is attached to a rooted logical tree. /// @@ -398,32 +455,17 @@ namespace Avalonia.Controls this.OnDetachedFromLogicalTreeCore(e); } - /// - /// Gets the control's logical children. - /// - protected IAvaloniaList LogicalChildren + /// + void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) { - get - { - if (_logicalChildren == null) - { - var list = new AvaloniaList(); - list.ResetBehavior = ResetBehavior.Remove; - list.Validate = ValidateLogicalChild; - list.CollectionChanged += LogicalChildrenCollectionChanged; - _logicalChildren = list; - } + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - return _logicalChildren; + foreach (var child in LogicalChildren) + { + child.NotifyResourcesChanged(e); } } - /// - /// Gets the collection in a form that allows adding and removing - /// pseudoclasses. - /// - protected IPseudoClasses PseudoClasses => Classes; - /// bool IResourceProvider.TryGetResource(string key, out object value) { @@ -857,5 +899,15 @@ namespace Avalonia.Controls } } } + + private void ResourceDictionaryChanged(object sender, NotifyCollectionChangedEventArgs e) + { + ((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); + } + + private void StyleResourcesChanged(object sender, ResourcesChangedEventArgs e) + { + ((ILogical)this).NotifyResourcesChanged(e); + } } } diff --git a/src/Avalonia.Styling/Controls/IResourceProvider.cs b/src/Avalonia.Styling/Controls/IResourceProvider.cs index 3e47bf9092..9308b7a18d 100644 --- a/src/Avalonia.Styling/Controls/IResourceProvider.cs +++ b/src/Avalonia.Styling/Controls/IResourceProvider.cs @@ -7,6 +7,11 @@ namespace Avalonia.Controls /// public interface IResourceProvider { + /// + /// Raised when the resources in the element are changed. + /// + event EventHandler ResourcesChanged; + /// /// Tries to find a resource within the element. /// diff --git a/src/Avalonia.Styling/Controls/ResourceDictionary.cs b/src/Avalonia.Styling/Controls/ResourceDictionary.cs index 1e35afa771..bd08680a42 100644 --- a/src/Avalonia.Styling/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Styling/Controls/ResourceDictionary.cs @@ -1,4 +1,7 @@ -using System; +// 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 System.Collections; using Avalonia.Collections; @@ -9,6 +12,7 @@ namespace Avalonia.Controls /// public class ResourceDictionary : AvaloniaDictionary, IResourceDictionary, IDictionary { + /// public bool TryGetResource(string key, out object value) => TryGetValue(key, out value); } } diff --git a/src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs b/src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs new file mode 100644 index 0000000000..68c7a58ab9 --- /dev/null +++ b/src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs @@ -0,0 +1,11 @@ +// 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; + +namespace Avalonia.Controls +{ + public class ResourcesChangedEventArgs : EventArgs + { + } +} diff --git a/src/Avalonia.Styling/LogicalTree/ILogical.cs b/src/Avalonia.Styling/LogicalTree/ILogical.cs index 006a9f5cc1..a6e804567d 100644 --- a/src/Avalonia.Styling/LogicalTree/ILogical.cs +++ b/src/Avalonia.Styling/LogicalTree/ILogical.cs @@ -3,6 +3,7 @@ using System; using Avalonia.Collections; +using Avalonia.Controls; namespace Avalonia.LogicalTree { @@ -55,5 +56,15 @@ namespace Avalonia.LogicalTree /// this method yourself. /// void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e); + + /// + /// Notifies the control that a change has been made to its resources. + /// + /// 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 5dc0409cf0..1ea99a42ac 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Metadata; @@ -16,8 +17,7 @@ namespace Avalonia.Styling { private static Dictionary> _applied = new Dictionary>(); - - private IResourceDictionary _resources; + private ResourceDictionary _resources; /// /// Initializes a new instance of the class. @@ -35,6 +35,9 @@ namespace Avalonia.Styling Selector = selector(null); } + /// + public event EventHandler ResourcesChanged; + /// /// Gets or sets a dictionary of style resources. /// @@ -45,6 +48,7 @@ namespace Avalonia.Styling if (_resources == null) { _resources = new ResourceDictionary(); + _resources.CollectionChanged += ResourceDictionaryChanged; } return _resources; @@ -151,5 +155,10 @@ namespace Avalonia.Styling _applied.Remove(control); } + + private void ResourceDictionaryChanged(object sender, NotifyCollectionChangedEventArgs e) + { + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + } } } diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index 690b636b1e..4ce41f6f46 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -1,6 +1,8 @@ // 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 System.Collections.Specialized; using System.Linq; using Avalonia.Collections; using Avalonia.Controls; @@ -12,7 +14,19 @@ namespace Avalonia.Styling /// public class Styles : AvaloniaList, IStyle { - private IResourceDictionary _resources; + private ResourceDictionary _resources; + + public Styles() + { + ResetBehavior = ResetBehavior.Remove; + this.ForEachItem( + x => x.ResourcesChanged += SubResourceChanged, + x => x.ResourcesChanged -= SubResourceChanged, + () => { }); + } + + /// + public event EventHandler ResourcesChanged; /// /// Gets or sets a dictionary of style resources. @@ -24,6 +38,7 @@ namespace Avalonia.Styling if (_resources == null) { _resources = new ResourceDictionary(); + _resources.CollectionChanged += ResourceDictionaryChanged; } return _resources; @@ -64,5 +79,15 @@ namespace Avalonia.Styling value = null; return false; } + + private void ResourceDictionaryChanged(object sender, NotifyCollectionChangedEventArgs e) + { + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + } + + private void SubResourceChanged(object sender, ResourcesChangedEventArgs e) + { + 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 4bb0913a2e..7921510944 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -3,6 +3,8 @@ using System; using System.ComponentModel; +using System.Reactive; +using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Data; using Portable.Xaml; @@ -49,8 +51,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (control != null) { - var resource = control.FindResource(ResourceKey); - return new InstancedBinding(resource); + 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 null; diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index a8fecdc6e1..556e2324cd 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -25,6 +25,9 @@ namespace Avalonia.Markup.Xaml.Styling _baseUri = baseUri; } + /// + public event EventHandler ResourcesChanged; + /// /// Gets or sets the source URL. /// diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index a697e18610..46a90635b2 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -276,6 +276,82 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Equal(0xff506070, brush.Color.ToUint32()); } + [Fact] + public void DynamicResource_Tracks_Added_Resource() + { + var xaml = @" + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + DelayedBinding.ApplyBindings(border); + + Assert.Null(border.Background); + + userControl.Resources.Add("brush", new SolidColorBrush(0xff506070)); + + var brush = (SolidColorBrush)border.Background; + Assert.NotNull(brush); + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + + [Fact] + public void DynamicResource_Tracks_Added_Style_Resource() + { + var xaml = @" + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + DelayedBinding.ApplyBindings(border); + + Assert.Null(border.Background); + + userControl.Styles.Resources.Add("brush", new SolidColorBrush(0xff506070)); + + var brush = (SolidColorBrush)border.Background; + Assert.NotNull(brush); + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + + [Fact] + public void DynamicResource_Tracks_Added_Nested_Style_Resource() + { + var xaml = @" + + + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + DelayedBinding.ApplyBindings(border); + + Assert.Null(border.Background); + + ((Style)userControl.Styles[0]).Resources.Add("brush", new SolidColorBrush(0xff506070)); + + var brush = (SolidColorBrush)border.Background; + Assert.NotNull(brush); + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + [Fact] public void DynamicResource_Can_Be_Found_Across_Xaml_Style_Files() {