Browse Source

Start making DynamicResource react to changes.

Steven Kirk 8 years ago
parent
commit
513efe99f7

+ 3 - 0
src/Avalonia.Controls/Application.cs

@@ -49,6 +49,9 @@ namespace Avalonia
             OnExit += OnExiting;
         }
 
+        /// <inheritdoc/>
+        public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
+
         /// <summary>
         /// Gets the current instance of the <see cref="Application"/> class.
         /// </summary>

+ 76 - 24
src/Avalonia.Controls/Control.cs

@@ -154,6 +154,11 @@ namespace Avalonia.Controls
         /// </remarks>
         public event EventHandler Initialized;
 
+        /// <summary>
+        /// Occurs when a resource in this control or a parent control has changed.
+        /// </summary>
+        public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
+
         /// <summary>
         /// Gets or sets the name of the control.
         /// </summary>
@@ -269,8 +274,22 @@ namespace Avalonia.Controls
         /// </remarks>
         public Styles Styles
         {
-            get { return _styles ?? (_styles = new Styles()); }
-            set { _styles = value; }
+            get { return _styles ?? (Styles = new Styles()); }
+            set
+            {
+                Contract.Requires<ArgumentNullException>(value != null);
+
+                if (_styles != value)
+                {
+                    if (_styles != null)
+                    {
+                        _styles.ResourcesChanged -= StyleResourcesChanged;
+                    }
+
+                    _styles = value;
+                    _styles.ResourcesChanged += StyleResourcesChanged;
+                }
+            }
         }
 
         /// <summary>
@@ -290,7 +309,19 @@ namespace Avalonia.Controls
         /// <summary>
         /// Gets or sets the control's resource dictionary.
         /// </summary>
-        public IResourceDictionary Resources => _resources ?? (_resources = new ResourceDictionary());
+        public IResourceDictionary Resources
+        {
+            get
+            {
+                if (_resources == null)
+                {
+                    _resources = new ResourceDictionary();
+                    _resources.CollectionChanged += ResourceDictionaryChanged;
+                }
+
+                return _resources;
+            }
+        }
 
         /// <summary>
         /// Gets or sets a user-defined object attached to the control.
@@ -310,6 +341,32 @@ namespace Avalonia.Controls
             internal set { SetValue(TemplatedParentProperty, value); }
         }
 
+        /// <summary>
+        /// Gets the control's logical children.
+        /// </summary>
+        protected IAvaloniaList<ILogical> LogicalChildren
+        {
+            get
+            {
+                if (_logicalChildren == null)
+                {
+                    var list = new AvaloniaList<ILogical>();
+                    list.ResetBehavior = ResetBehavior.Remove;
+                    list.Validate = ValidateLogicalChild;
+                    list.CollectionChanged += LogicalChildrenCollectionChanged;
+                    _logicalChildren = list;
+                }
+
+                return _logicalChildren;
+            }
+        }
+
+        /// <summary>
+        /// Gets the <see cref="Classes"/> collection in a form that allows adding and removing
+        /// pseudoclasses.
+        /// </summary>
+        protected IPseudoClasses PseudoClasses => Classes;
+
         /// <summary>
         /// Gets a value indicating whether the element is attached to a rooted logical tree.
         /// </summary>
@@ -398,32 +455,17 @@ namespace Avalonia.Controls
             this.OnDetachedFromLogicalTreeCore(e);
         }
 
-        /// <summary>
-        /// Gets the control's logical children.
-        /// </summary>
-        protected IAvaloniaList<ILogical> LogicalChildren
+        /// <inheritdoc/>
+        void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e)
         {
-            get
-            {
-                if (_logicalChildren == null)
-                {
-                    var list = new AvaloniaList<ILogical>();
-                    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);
             }
         }
 
-        /// <summary>
-        /// Gets the <see cref="Classes"/> collection in a form that allows adding and removing
-        /// pseudoclasses.
-        /// </summary>
-        protected IPseudoClasses PseudoClasses => Classes;
-
         /// <inheritdoc/>
         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);
+        }
     }
 }

+ 5 - 0
src/Avalonia.Styling/Controls/IResourceProvider.cs

@@ -7,6 +7,11 @@ namespace Avalonia.Controls
     /// </summary>
     public interface IResourceProvider
     {
+        /// <summary>
+        /// Raised when the resources in the element are changed.
+        /// </summary>
+        event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
+
         /// <summary>
         /// Tries to find a resource within the element.
         /// </summary>

+ 5 - 1
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
     /// </summary>
     public class ResourceDictionary : AvaloniaDictionary<string, object>, IResourceDictionary, IDictionary
     {
+        /// <inheritdoc/>
         public bool TryGetResource(string key, out object value) => TryGetValue(key, out value);
     }
 }

+ 11 - 0
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
+    {
+    }
+}

+ 11 - 0
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.
         /// </remarks>
         void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e);
+
+        /// <summary>
+        /// Notifies the control that a change has been made to its resources.
+        /// </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);
     }
 }

+ 11 - 2
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<IStyleable, List<IDisposable>> _applied =
             new Dictionary<IStyleable, List<IDisposable>>();
-
-        private IResourceDictionary _resources;
+        private ResourceDictionary _resources;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="Style"/> class.
@@ -35,6 +35,9 @@ namespace Avalonia.Styling
             Selector = selector(null);
         }
 
+        /// <inheritdoc/>
+        public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
+
         /// <summary>
         /// Gets or sets a dictionary of style resources.
         /// </summary>
@@ -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());
+        }
     }
 }

+ 26 - 1
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
     /// </summary>
     public class Styles : AvaloniaList<IStyle>, IStyle
     {
-        private IResourceDictionary _resources;
+        private ResourceDictionary _resources;
+
+        public Styles()
+        {
+            ResetBehavior = ResetBehavior.Remove;
+            this.ForEachItem(
+                x => x.ResourcesChanged += SubResourceChanged,
+                x => x.ResourcesChanged -= SubResourceChanged,
+                () => { });
+        }
+
+        /// <inheritdoc/>
+        public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
 
         /// <summary>
         /// 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);
+        }
     }
 }

+ 9 - 2
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<ResourcesChangedEventArgs>(
+                    x => control.ResourcesChanged += x,
+                    x => control.ResourcesChanged -= x)
+                    .StartWith((EventPattern<ResourcesChangedEventArgs>)null)
+                    .Select(x => control.FindResource(ResourceKey));
+
+                return new InstancedBinding(o);
             }
 
             return null;

+ 3 - 0
src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs

@@ -25,6 +25,9 @@ namespace Avalonia.Markup.Xaml.Styling
             _baseUri = baseUri;
         }
 
+        /// <inheritdoc/>
+        public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
+
         /// <summary>
         /// Gets or sets the source URL.
         /// </summary>

+ 76 - 0
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 = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <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);
+
+            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 = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <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);
+
+            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 = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Styles>
+        <Style>
+        </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);
+
+            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()
         {