Browse Source

Merge pull request #1130 from AvaloniaUI/control-resources

Control.Resources, StaticResource and DynamicResource
Steven Kirk 8 years ago
parent
commit
ec9af9cefa
49 changed files with 2848 additions and 349 deletions
  1. 1 0
      .ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject
  2. 9 0
      .ncrunch/Avalonia.Markup.UnitTests.netcoreapp1.1.v3.ncrunchproject
  3. 1 0
      .ncrunch/Avalonia.Skia.RenderTests.v3.ncrunchproject
  4. 1 1
      samples/ControlCatalog/MainWindow.xaml.cs
  5. 3 0
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  6. 34 20
      src/Avalonia.Base/Collections/AvaloniaDictionary.cs
  7. 48 3
      src/Avalonia.Controls/Application.cs
  8. 112 22
      src/Avalonia.Controls/Control.cs
  9. 1 0
      src/Avalonia.Controls/IControl.cs
  10. 1 1
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  11. 22 2
      src/Avalonia.Controls/TopLevel.cs
  12. 1 0
      src/Avalonia.Styling/Avalonia.Styling.csproj
  13. 19 0
      src/Avalonia.Styling/Controls/IResourceDictionary.cs
  14. 15 0
      src/Avalonia.Styling/Controls/IResourceNode.cs
  15. 33 0
      src/Avalonia.Styling/Controls/IResourceProvider.cs
  16. 101 0
      src/Avalonia.Styling/Controls/ResourceDictionary.cs
  17. 65 0
      src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs
  18. 11 0
      src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs
  19. 11 0
      src/Avalonia.Styling/LogicalTree/ILogical.cs
  20. 2 0
      src/Avalonia.Styling/Properties/AssemblyInfo.cs
  21. 32 0
      src/Avalonia.Styling/Styling/ISetStyleParent.cs
  22. 3 10
      src/Avalonia.Styling/Styling/IStyle.cs
  23. 54 38
      src/Avalonia.Styling/Styling/Style.cs
  24. 0 42
      src/Avalonia.Styling/Styling/StyleExtensions.cs
  25. 0 90
      src/Avalonia.Styling/Styling/StyleResources.cs
  26. 135 14
      src/Avalonia.Styling/Styling/Styles.cs
  27. 3 0
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  28. 78 7
      src/Markup/Avalonia.Markup.Xaml/Data/DelayedBinding.cs
  29. 63 0
      src/Markup/Avalonia.Markup.Xaml/Data/ResourceInclude.cs
  30. 5 2
      src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs
  31. 71 0
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs
  32. 84 0
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs
  33. 30 11
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
  34. 217 0
      tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs
  35. 17 0
      tests/Avalonia.Controls.UnitTests/TopLevelTests.cs
  36. 4 0
      tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
  37. 55 0
      tests/Avalonia.Markup.Xaml.UnitTests/Data/ResourceIncludeTests.cs
  38. 660 0
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs
  39. 476 0
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs
  40. 20 0
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/TestValueConverter.cs
  41. 3 3
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
  42. 7 5
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  43. 175 0
      tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs
  44. 0 78
      tests/Avalonia.Styling.UnitTests/ResourceTests.cs
  45. 5 0
      tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs
  46. 5 0
      tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs
  47. 115 0
      tests/Avalonia.Styling.UnitTests/StylesTests.cs
  48. 36 0
      tests/Avalonia.UnitTests/MockAssetLoader.cs
  49. 4 0
      tests/Avalonia.UnitTests/TestRoot.cs

+ 1 - 0
.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject

@@ -1,6 +1,7 @@
 <ProjectConfiguration>
   <Settings>
     <DefaultTestTimeout>1000</DefaultTestTimeout>
+    <IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
     <PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
   </Settings>
 </ProjectConfiguration>

+ 9 - 0
.ncrunch/Avalonia.Markup.UnitTests.netcoreapp1.1.v3.ncrunchproject

@@ -0,0 +1,9 @@
+<ProjectConfiguration>
+  <Settings>
+    <IgnoredTests>
+      <NamedTestSelector>
+        <TestName>Avalonia.Markup.UnitTests.Data.Plugins.DataAnnotationsValidationPluginTests.Produces_Aggregate_BindingNotificationsx</TestName>
+      </NamedTestSelector>
+    </IgnoredTests>
+  </Settings>
+</ProjectConfiguration>

+ 1 - 0
.ncrunch/Avalonia.Skia.RenderTests.v3.ncrunchproject

@@ -1,6 +1,7 @@
 <ProjectConfiguration>
   <Settings>
     <DefaultTestTimeout>1000</DefaultTestTimeout>
+    <IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
     <PreviouslyBuiltSuccessfully>True</PreviouslyBuiltSuccessfully>
   </Settings>
 </ProjectConfiguration>

+ 1 - 1
samples/ControlCatalog/MainWindow.xaml.cs

@@ -20,7 +20,7 @@ namespace ControlCatalog
             // so we must refer to this resource DLL statically. For
             // now I am doing that here. But we need a better solution!!
             var theme = new Avalonia.Themes.Default.DefaultTheme();
-            theme.FindResource("Button");
+            theme.TryGetResource("Button", out _);
             AvaloniaXamlLoader.Load(this);
         }
     }

+ 3 - 0
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@@ -47,6 +47,9 @@ namespace Avalonia
         {
             Dictionary<int, AvaloniaProperty> inner;
 
+            // Ensure the type's static ctor has been run.
+            RuntimeHelpers.RunClassConstructor(ownerType.TypeHandle);
+
             if (_attached.TryGetValue(ownerType, out inner))
             {
                 return inner.Values;

+ 34 - 20
src/Avalonia.Base/Collections/AvaloniaDictionary.cs

@@ -16,6 +16,7 @@ namespace Avalonia.Collections
     /// <typeparam name="TKey">The type of the dictionary key.</typeparam>
     /// <typeparam name="TValue">The type of the dictionary value.</typeparam>
     public class AvaloniaDictionary<TKey, TValue> : IDictionary<TKey, TValue>,
+        IDictionary,
         INotifyCollectionChanged,
         INotifyPropertyChanged
     {
@@ -51,6 +52,16 @@ namespace Avalonia.Collections
         /// <inheritdoc/>
         public ICollection<TValue> Values => _inner.Values;
 
+        bool IDictionary.IsFixedSize => ((IDictionary)_inner).IsFixedSize;
+
+        ICollection IDictionary.Keys => ((IDictionary)_inner).Keys;
+
+        ICollection IDictionary.Values => ((IDictionary)_inner).Values;
+
+        bool ICollection.IsSynchronized => ((IDictionary)_inner).IsSynchronized;
+
+        object ICollection.SyncRoot => ((IDictionary)_inner).SyncRoot;
+
         /// <summary>
         /// Gets or sets the named resource.
         /// </summary>
@@ -89,6 +100,8 @@ namespace Avalonia.Collections
             }
         }
 
+        object IDictionary.this[object key] { get => ((IDictionary)_inner)[key]; set => ((IDictionary)_inner)[key] = value; }
+
         /// <inheritdoc/>
         public void Add(TKey key, TValue value)
         {
@@ -118,10 +131,7 @@ namespace Avalonia.Collections
         }
 
         /// <inheritdoc/>
-        public bool ContainsKey(TKey key)
-        {
-            return _inner.ContainsKey(key);
-        }
+        public bool ContainsKey(TKey key) => _inner.ContainsKey(key);
 
         /// <inheritdoc/>
         public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
@@ -130,21 +140,16 @@ namespace Avalonia.Collections
         }
 
         /// <inheritdoc/>
-        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
-        {
-            return _inner.GetEnumerator();
-        }
+        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => _inner.GetEnumerator();
 
         /// <inheritdoc/>
         public bool Remove(TKey key)
         {
-            TValue value;
-
-            if (_inner.TryGetValue(key, out value))
+            if (_inner.TryGetValue(key, out TValue value))
             {
                 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count"));
                 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[{key}]"));
-                
+
                 if (CollectionChanged != null)
                 {
                     var e = new NotifyCollectionChangedEventArgs(
@@ -163,16 +168,13 @@ namespace Avalonia.Collections
         }
 
         /// <inheritdoc/>
-        public bool TryGetValue(TKey key, out TValue value)
-        {
-            return _inner.TryGetValue(key, out value);
-        }
+        public bool TryGetValue(TKey key, out TValue value) => _inner.TryGetValue(key, out value);
 
         /// <inheritdoc/>
-        IEnumerator IEnumerable.GetEnumerator()
-        {
-            return _inner.GetEnumerator();
-        }
+        IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator();
+
+        /// <inheritdoc/>
+        void ICollection.CopyTo(Array array, int index) => ((ICollection)_inner).CopyTo(array, index);
 
         /// <inheritdoc/>
         void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
@@ -192,6 +194,18 @@ namespace Avalonia.Collections
             return Remove(item.Key);
         }
 
+        /// <inheritdoc/>
+        void IDictionary.Add(object key, object value) => Add((TKey)key, (TValue)value);
+
+        /// <inheritdoc/>
+        bool IDictionary.Contains(object key) => ((IDictionary) _inner).Contains(key);
+
+        /// <inheritdoc/>
+        IDictionaryEnumerator IDictionary.GetEnumerator() => ((IDictionary)_inner).GetEnumerator();
+
+        /// <inheritdoc/>
+        void IDictionary.Remove(object key) => Remove((TKey)key);
+
         private void NotifyAdd(TKey key, TValue value)
         {
             PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count"));

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

@@ -29,7 +29,7 @@ namespace Avalonia
     /// method.
     /// - Tracks the lifetime of the application.
     /// </remarks>
-    public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IApplicationLifecycle
+    public class Application : IApplicationLifecycle, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode
     {
         /// <summary>
         /// The application-global data templates.
@@ -40,6 +40,7 @@ namespace Avalonia
             new Lazy<IClipboard>(() => (IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard)));
         private readonly Styler _styler = new Styler();
         private Styles _styles;
+        private IResourceDictionary _resources;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="Application"/> class.
@@ -49,6 +50,9 @@ namespace Avalonia
             OnExit += OnExiting;
         }
 
+        /// <inheritdoc/>
+        public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
+
         /// <summary>
         /// Gets the current instance of the <see cref="Application"/> class.
         /// </summary>
@@ -97,6 +101,34 @@ namespace Avalonia
         /// </summary>
         public IClipboard Clipboard => _clipboard.Value;
 
+        /// <summary>
+        /// Gets the application's global resource dictionary.
+        /// </summary>
+        public IResourceDictionary Resources
+        {
+            get => _resources ?? (Resources = new ResourceDictionary());
+            set
+            {
+                Contract.Requires<ArgumentNullException>(value != null);
+
+                var hadResources = false;
+
+                if (_resources != null)
+                {
+                    hadResources = _resources.Count > 0;
+                    _resources.ResourcesChanged -= ResourcesChanged;
+                }
+
+                _resources = value;
+                _resources.ResourcesChanged += ResourcesChanged;
+
+                if (hadResources || _resources.Count > 0)
+                {
+                    ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
+                }
+            }
+        }
+
         /// <summary>
         /// Gets the application's global styles.
         /// </summary>
@@ -119,6 +151,12 @@ namespace Avalonia
         /// <inheritdoc/>
         bool IStyleHost.IsStylesInitialized => _styles != null;
 
+        /// <inheritdoc/>
+        bool IResourceProvider.HasResources => _resources?.Count > 0;
+
+        /// <inheritdoc/>
+        IResourceNode IResourceNode.ResourceParent => null;
+
         /// <summary>
         /// Initializes the application by loading XAML etc.
         /// </summary>
@@ -145,13 +183,20 @@ namespace Avalonia
         {
             OnExit?.Invoke(this, EventArgs.Empty);
         }
-        
+
+        /// <inheritdoc/>
+        bool IResourceProvider.TryGetResource(string key, out object value)
+        {
+            value = null;
+            return (_resources?.TryGetResource(key, out value) ?? false) ||
+                   Styles.TryGetResource(key, out value);
+        }
+
         /// <summary>
         /// Sent when the application is exiting.
         /// </summary>
         public event EventHandler OnExit;
 
-
         /// <summary>
         /// Called when the application is exiting.
         /// </summary>

+ 112 - 22
src/Avalonia.Controls/Control.cs

@@ -97,8 +97,9 @@ namespace Avalonia.Controls
         private bool _isAttachedToLogicalTree;
         private IAvaloniaList<ILogical> _logicalChildren;
         private INameScope _nameScope;
-        private bool _styled;
+        private IResourceDictionary _resources;
         private Styles _styles;
+        private bool _styled;
         private Subject<IStyleable> _styleDetach = new Subject<IStyleable>();
 
         /// <summary>
@@ -153,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>
@@ -262,7 +268,32 @@ namespace Avalonia.Controls
         /// each control may in addition define its own styles which are applied to the control
         /// itself and its children.
         /// </remarks>
-        public Styles Styles => _styles ?? (_styles = new Styles());
+        public Styles Styles
+        {
+            get { return _styles ?? (Styles = new Styles()); }
+            set
+            {
+                Contract.Requires<ArgumentNullException>(value != null);
+
+                if (_styles != value)
+                {
+                    if (_styles != null)
+                    {
+                        (_styles as ISetStyleParent)?.SetParent(null);
+                        _styles.ResourcesChanged -= ThisResourcesChanged;
+                    }
+
+                    _styles = value;
+
+                    if (value is ISetStyleParent setParent && setParent.ResourceParent == null)
+                    {
+                        setParent.SetParent(this);
+                    } 
+
+                    _styles.ResourcesChanged += ThisResourcesChanged;
+                }
+            }
+        }
 
         /// <summary>
         /// Gets the control's logical parent.
@@ -278,6 +309,34 @@ namespace Avalonia.Controls
             set { SetValue(ContextMenuProperty, value); }
         }
 
+        /// <summary>
+        /// Gets or sets the control's resource dictionary.
+        /// </summary>
+        public IResourceDictionary Resources
+        {
+            get => _resources ?? (Resources = new ResourceDictionary());
+            set
+            {
+                Contract.Requires<ArgumentNullException>(value != null);
+
+                var hadResources = false;
+
+                if (_resources != null)
+                {
+                    hadResources = _resources.Count > 0;
+                    _resources.ResourcesChanged -= ThisResourcesChanged;
+                }
+
+                _resources = value;
+                _resources.ResourcesChanged += ThisResourcesChanged;
+
+                if (hadResources || _resources.Count > 0)
+                {
+                    ((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs());
+                }
+            }
+        }
+
         /// <summary>
         /// Gets or sets a user-defined object attached to the control.
         /// </summary>
@@ -296,9 +355,35 @@ 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;
+            }
+        }
+
         /// <inheritdoc/>
         bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
 
+        /// <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>
@@ -314,6 +399,12 @@ namespace Avalonia.Controls
         /// </summary>
         IAvaloniaReadOnlyList<ILogical> ILogical.LogicalChildren => LogicalChildren;
 
+        /// <inheritdoc/>
+        bool IResourceProvider.HasResources => _resources?.Count > 0 || Styles.HasResources;
+
+        /// <inheritdoc/>
+        IResourceNode IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode;
+
         /// <inheritdoc/>
         IAvaloniaReadOnlyList<string> IStyleable.Classes => Classes;
 
@@ -390,31 +481,24 @@ 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)
+        {
+            value = null;
+            return (_resources?.TryGetResource(key, out value) ?? false) ||
+                   (_styles?.TryGetResource(key, out value) ?? false);
+        }
 
         /// <summary>
         /// Sets the control's logical parent.
@@ -450,6 +534,7 @@ namespace Avalonia.Controls
                 }
 
                 _parent = (IControl)parent;
+                ((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs());
 
                 if (_parent is IStyleRoot || _parent?.IsAttachedToLogicalTree == true || this is IStyleRoot)
                 {
@@ -841,5 +926,10 @@ namespace Avalonia.Controls
                 }
             }
         }
+
+        private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e)
+        {
+            ((ILogical)this).NotifyResourcesChanged(e);
+        }
     }
 }

+ 1 - 0
src/Avalonia.Controls/IControl.cs

@@ -20,6 +20,7 @@ namespace Avalonia.Controls
         ILayoutable,
         IInputElement,
         INamed,
+        IResourceNode,
         IStyleable,
         IStyleHost
     {

+ 1 - 1
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -115,7 +115,7 @@ namespace Avalonia.Controls.Presenters
 
                 if (_highlightBrush == null)
                 {
-                    _highlightBrush = (IBrush)this.FindStyleResource("HighlightBrush");
+                    _highlightBrush = (IBrush)this.FindResource("HighlightBrush");
                 }
 
                 foreach (var rect in rects)

+ 22 - 2
src/Avalonia.Controls/TopLevel.cs

@@ -10,9 +10,11 @@ using Avalonia.Input;
 using Avalonia.Input.Raw;
 using Avalonia.Layout;
 using Avalonia.Logging;
+using Avalonia.LogicalTree;
 using Avalonia.Platform;
 using Avalonia.Rendering;
 using Avalonia.Styling;
+using Avalonia.Utilities;
 using Avalonia.VisualTree;
 using JetBrains.Annotations;
 
@@ -26,7 +28,13 @@ namespace Avalonia.Controls
     /// It handles scheduling layout, styling and rendering as well as
     /// tracking the widget's <see cref="ClientSize"/>.
     /// </remarks>
-    public abstract class TopLevel : ContentControl, IInputRoot, ILayoutRoot, IRenderRoot, ICloseable, IStyleRoot
+    public abstract class TopLevel : ContentControl,
+        IInputRoot,
+        ILayoutRoot,
+        IRenderRoot,
+        ICloseable,
+        IStyleRoot,
+        IWeakSubscriber<ResourcesChangedEventArgs>
     {
         /// <summary>
         /// Defines the <see cref="ClientSize"/> property.
@@ -100,7 +108,6 @@ namespace Avalonia.Controls
             impl.Resized = HandleResized;
             impl.ScalingChanged = HandleScalingChanged;
 
-
             _keyboardNavigationHandler?.SetOwner(this);
             _accessKeyHandler?.SetOwner(this);
             styler?.ApplyStyles(this);
@@ -116,6 +123,14 @@ namespace Avalonia.Controls
             {
                 _applicationLifecycle.OnExit += OnApplicationExiting;
             }
+
+            if (((IStyleHost)this).StylingParent is IResourceProvider applicationResources)
+            {
+                WeakSubscriptionManager.Subscribe(
+                    applicationResources,
+                    nameof(IResourceProvider.ResourcesChanged),
+                    this);
+            }
         }
 
         /// <summary>
@@ -165,6 +180,11 @@ namespace Avalonia.Controls
         /// <inheritdoc/>
         IMouseDevice IInputRoot.MouseDevice => PlatformImpl?.MouseDevice;
 
+        void IWeakSubscriber<ResourcesChangedEventArgs>.OnEvent(object sender, ResourcesChangedEventArgs e)
+        {
+            ((ILogical)this).NotifyResourcesChanged(e);
+        }
+
         /// <summary>
         /// Gets or sets a value indicating whether access keys are shown in the window.
         /// </summary>

+ 1 - 0
src/Avalonia.Styling/Avalonia.Styling.csproj

@@ -2,6 +2,7 @@
   <PropertyGroup>
     <TargetFramework>netstandard2.0</TargetFramework>
     <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
+    <RootNamespace>Avalonia</RootNamespace>
   </PropertyGroup>
   <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
     <DebugSymbols>true</DebugSymbols>

+ 19 - 0
src/Avalonia.Styling/Controls/IResourceDictionary.cs

@@ -0,0 +1,19 @@
+// 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.Generic;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// An indexed dictionary of resources.
+    /// </summary>
+    public interface IResourceDictionary : IResourceProvider, IDictionary<object, object>
+    {
+        /// <summary>
+        /// Gets a collection of child resource dictionaries.
+        /// </summary>
+        IList<IResourceProvider> MergedDictionaries { get; }
+    }
+}

+ 15 - 0
src/Avalonia.Styling/Controls/IResourceNode.cs

@@ -0,0 +1,15 @@
+using System;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Represents resource provider in a tree.
+    /// </summary>
+    public interface IResourceNode : IResourceProvider
+    {
+        /// <summary>
+        /// Gets the parent resource node, if any.
+        /// </summary>
+        IResourceNode ResourceParent { get; }
+    }
+}

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

@@ -0,0 +1,33 @@
+using System;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// Represents an object that can be queried for resources.
+    /// </summary>
+    public interface IResourceProvider
+    {
+        /// <summary>
+        /// Raised when resources in the provider are changed.
+        /// </summary>
+        event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
+
+        /// <summary>
+        /// Gets a value indicating whether the element has resources.
+        /// </summary>
+        bool HasResources { get; }
+
+        /// <summary>
+        /// Tries to find a resource within the provider.
+        /// </summary>
+        /// <param name="key">The resource key.</param>
+        /// <param name="value">
+        /// When this method returns, contains the value associated with the specified key,
+        /// if the key is found; otherwise, null.
+        /// </param>
+        /// <returns>
+        /// True if the resource if found, otherwise false.
+        /// </returns>
+        bool TryGetResource(string key, out object value);
+    }
+}

+ 101 - 0
src/Avalonia.Styling/Controls/ResourceDictionary.cs

@@ -0,0 +1,101 @@
+// 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.Generic;
+using System.Collections.Specialized;
+using System.Linq;
+using Avalonia.Collections;
+
+namespace Avalonia.Controls
+{
+    /// <summary>
+    /// An indexed dictionary of resources.
+    /// </summary>
+    public class ResourceDictionary : AvaloniaDictionary<object, object>, IResourceDictionary
+    {
+        private AvaloniaList<IResourceProvider> _mergedDictionaries;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ResourceDictionary"/> class.
+        /// </summary>
+        public ResourceDictionary()
+        {
+            CollectionChanged += OnCollectionChanged;
+        }
+
+        /// <inheritdoc/>
+        public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
+
+        /// <inheritdoc/>
+        public IList<IResourceProvider> MergedDictionaries
+        {
+            get
+            {
+                if (_mergedDictionaries == null)
+                {
+                    _mergedDictionaries = new AvaloniaList<IResourceProvider>();
+                    _mergedDictionaries.ResetBehavior = ResetBehavior.Remove;
+                    _mergedDictionaries.ForEachItem(
+                        x =>
+                        {
+                            if (x.HasResources)
+                            {
+                                OnResourcesChanged();
+                            }
+
+                            x.ResourcesChanged += MergedDictionaryResourcesChanged;
+                        },
+                        x =>
+                        {
+                            if (x.HasResources)
+                            {
+                                OnResourcesChanged();
+                            }
+
+                            x.ResourcesChanged -= MergedDictionaryResourcesChanged;
+                        },
+                        () => { });
+                }
+
+                return _mergedDictionaries;
+            }
+        }
+
+        /// <inheritdoc/>
+        bool IResourceProvider.HasResources
+        {
+            get => Count > 0 || (_mergedDictionaries?.Any(x => x.HasResources) ?? false);
+        }
+
+        /// <inheritdoc/>
+        public bool TryGetResource(string key, out object value)
+        {
+            if (TryGetValue(key, out value))
+            {
+                return true;
+            }
+
+            if (_mergedDictionaries != null)
+            {
+                for (var i = _mergedDictionaries.Count - 1; i >= 0; --i)
+                {
+                    if (_mergedDictionaries[i].TryGetResource(key, out value))
+                    {
+                        return true;
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        private void OnResourcesChanged()
+        {
+            ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
+        }
+
+        private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => OnResourcesChanged();
+        private void MergedDictionaryResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnResourcesChanged();
+    }
+}

+ 65 - 0
src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs

@@ -0,0 +1,65 @@
+using System;
+using System.Reactive;
+using System.Reactive.Linq;
+
+namespace Avalonia.Controls
+{
+    public static class ResourceProviderExtensions
+    {
+        /// <summary>
+        /// Finds the specified resource by searching up the logical tree and then global styles.
+        /// </summary>
+        /// <param name="control">The control.</param>
+        /// <param name="key">The resource key.</param>
+        /// <returns>The resource, or <see cref="AvaloniaProperty.UnsetValue"/> if not found.</returns>
+        public static object FindResource(this IResourceNode control, string key)
+        {
+            if (control.TryFindResource(key, out var value))
+            {
+                return value;
+            }
+
+            return AvaloniaProperty.UnsetValue;
+        }
+
+        /// <summary>
+        /// Tries to the specified resource by searching up the logical tree and then global styles.
+        /// </summary>
+        /// <param name="control">The control.</param>
+        /// <param name="key">The resource key.</param>
+        /// <param name="value">On return, contains the resource if found, otherwise null.</param>
+        /// <returns>True if the resource was found; otherwise false.</returns>
+        public static bool TryFindResource(this IResourceNode control, string key, out object value)
+        {
+            Contract.Requires<ArgumentNullException>(control != null);
+            Contract.Requires<ArgumentNullException>(key != null);
+
+            var current = control;
+
+            while (current != null)
+            {
+                if (current is IResourceNode host)
+                {
+                    if (host.TryGetResource(key, out value))
+                    {
+                        return true;
+                    }
+                }
+
+                current = current.ResourceParent;
+            }
+
+            value = null;
+            return false;
+        }
+
+        public static IObservable<object> GetResourceObservable(this IResourceNode target, string key)
+        {
+            return Observable.FromEventPattern<ResourcesChangedEventArgs>(
+                x => target.ResourcesChanged += x,
+                x => target.ResourcesChanged -= x)
+                .StartWith((EventPattern<ResourcesChangedEventArgs>)null)
+                .Select(x => target.FindResource(key));
+        }
+    }
+}

+ 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 resources that apply to it.
+        /// </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);
     }
 }

+ 2 - 0
src/Avalonia.Styling/Properties/AssemblyInfo.cs

@@ -6,5 +6,7 @@ using System.Runtime.CompilerServices;
 using Avalonia.Metadata;
 
 [assembly: AssemblyTitle("Avalonia.Styling")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.LogicalTree")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Styling")]
 [assembly: InternalsVisibleTo("Avalonia.Styling.UnitTests")]

+ 32 - 0
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
+{
+    /// <summary>
+    /// Defines an interface through which a <see cref="Style"/>'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
+    {
+        /// <summary>
+        /// Sets the style 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.
+        /// </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);
+    }
+}

+ 3 - 10
src/Avalonia.Styling/Styling/IStyle.cs

@@ -1,12 +1,14 @@
 // 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 Avalonia.Controls;
+
 namespace Avalonia.Styling
 {
     /// <summary>
     /// Defines the interface for styles.
     /// </summary>
-    public interface IStyle
+    public interface IStyle : IResourceNode
     {
         /// <summary>
         /// Attaches the style to a control if the style's selector matches.
@@ -16,14 +18,5 @@ namespace Avalonia.Styling
         /// The control that contains this style. May be null.
         /// </param>
         void Attach(IStyleable control, IStyleHost container);
-
-        /// <summary>
-        /// Tries to find a named resource within the style.
-        /// </summary>
-        /// <param name="name">The resource name.</param>
-        /// <returns>
-        /// The resource if found, otherwise <see cref="AvaloniaProperty.UnsetValue"/>.
-        /// </returns>
-        object FindResource(string name);
     }
 }

+ 54 - 38
src/Avalonia.Styling/Styling/Style.cs

@@ -3,7 +3,9 @@
 
 using System;
 using System.Collections.Generic;
+using System.Collections.Specialized;
 using System.Reactive.Linq;
+using Avalonia.Controls;
 using Avalonia.Metadata;
 
 namespace Avalonia.Styling
@@ -11,12 +13,12 @@ namespace Avalonia.Styling
     /// <summary>
     /// Defines a style.
     /// </summary>
-    public class Style : IStyle
+    public class Style : IStyle, ISetStyleParent
     {
         private static Dictionary<IStyleable, List<IDisposable>> _applied =
             new Dictionary<IStyleable, List<IDisposable>>();
-
-        private StyleResources _resources;
+        private IResourceNode _parent;
+        private IResourceDictionary _resources;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="Style"/> class.
@@ -34,33 +36,33 @@ namespace Avalonia.Styling
             Selector = selector(null);
         }
 
+        /// <inheritdoc/>
+        public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
+
         /// <summary>
         /// Gets or sets a dictionary of style resources.
         /// </summary>
-        public StyleResources Resources
+        public IResourceDictionary Resources
         {
-            get
+            get => _resources ?? (Resources = new ResourceDictionary());
+            set
             {
-                if (_resources == null)
+                Contract.Requires<ArgumentNullException>(value != null);
+
+                var hadResources = false;
+
+                if (_resources != null)
                 {
-                    _resources = new StyleResources();
+                    hadResources = _resources.Count > 0;
+                    _resources.ResourcesChanged -= ResourceDictionaryChanged;
                 }
 
-                return _resources;
-            }
+                _resources = value;
+                _resources.ResourcesChanged += ResourceDictionaryChanged;
 
-            set
-            {
-                
-                var resources = Resources;
-                if (!Equals(resources, value))
+                if (hadResources || _resources.Count > 0)
                 {
-                    foreach (var i in value)
-                    {
-                        resources[i.Key] = i.Value;
-                        //resources.Add(i.Key, i.Value);
-                        //(resources as IDictionary<string,object>).Add(i);
-                    }
+                    ((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs());
                 }
             }
         }
@@ -76,6 +78,12 @@ namespace Avalonia.Styling
         [Content]
         public IList<ISetter> Setters { get; set; } = new List<ISetter>();
 
+        /// <inheritdoc/>
+        IResourceNode IResourceNode.ResourceParent => _parent;
+
+        /// <inheritdoc/>
+        bool IResourceProvider.HasResources => _resources?.Count > 0;
+
         /// <summary>
         /// Attaches the style to a control if the style's selector matches.
         /// </summary>
@@ -112,25 +120,11 @@ namespace Avalonia.Styling
             }
         }
 
-        /// <summary>
-        /// Tries to find a named resource within the style.
-        /// </summary>
-        /// <param name="name">The resource name.</param>
-        /// <returns>
-        /// The resource if found, otherwise <see cref="AvaloniaProperty.UnsetValue"/>.
-        /// </returns>
-        public object FindResource(string name)
+        /// <inheritdoc/>
+        public bool TryGetResource(string key, out object result)
         {
-            object result = null;
-
-            if (_resources?.TryGetValue(name, out result) == true)
-            {
-                return result;
-            }
-            else
-            {
-                return AvaloniaProperty.UnsetValue;
-            }
+            result = null;
+            return _resources?.TryGetResource(key, out result) ?? false;
         }
 
         /// <summary>
@@ -149,6 +143,23 @@ namespace Avalonia.Styling
             }
         }
 
+        /// <inheritdoc/>
+        void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e)
+        {
+            ResourcesChanged?.Invoke(this, e);
+        }
+
+        /// <inheritdoc/>
+        void ISetStyleParent.SetParent(IResourceNode parent)
+        {
+            if (_parent != null && parent != null)
+            {
+                throw new InvalidOperationException("The Style already has a parent.");
+            }
+
+            _parent = parent;
+        }
+
         private static List<IDisposable> GetSubscriptions(IStyleable control)
         {
             List<IDisposable> subscriptions;
@@ -179,5 +190,10 @@ namespace Avalonia.Styling
 
             _applied.Remove(control);
         }
+
+        private void ResourceDictionaryChanged(object sender, ResourcesChangedEventArgs e)
+        {
+            ResourcesChanged?.Invoke(this, e);
+        }
     }
 }

+ 0 - 42
src/Avalonia.Styling/Styling/StyleExtensions.cs

@@ -1,42 +0,0 @@
-// 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.Styling
-{
-    public static class StyleExtensions
-    {
-        /// <summary>
-        /// Tries to find a named style resource.
-        /// </summary>
-        /// <param name="control">The control from which to find the resource.</param>
-        /// <param name="name">The resource name.</param>
-        /// <returns>
-        /// The resource if found, otherwise <see cref="AvaloniaProperty.UnsetValue"/>.
-        /// </returns>
-        public static object FindStyleResource(this IStyleHost control, string name)
-        {
-            Contract.Requires<ArgumentNullException>(control != null);
-            Contract.Requires<ArgumentNullException>(name != null);
-            Contract.Requires<ArgumentException>(!string.IsNullOrWhiteSpace(name));
-
-            while (control != null)
-            {
-                if (control.IsStylesInitialized)
-                {
-                    var result = control.Styles.FindResource(name);
-
-                    if (result != AvaloniaProperty.UnsetValue)
-                    {
-                        return result;
-                    }
-                }
-
-                control = control.StylingParent;
-            }
-
-            return AvaloniaProperty.UnsetValue;
-        }
-    }
-}

+ 0 - 90
src/Avalonia.Styling/Styling/StyleResources.cs

@@ -1,90 +0,0 @@
-using System;
-using System.Collections;
-using System.Collections.Generic;
-
-namespace Avalonia.Styling
-{
-    /// <summary>
-    /// Holds resources for a <see cref="Style"/>.
-    /// </summary>
-    public class StyleResources : IDictionary<string, object>, IDictionary
-    {
-        private Dictionary<string, object> _inner = new Dictionary<string, object>();
-
-        public object this[string key]
-        {
-            get { return _inner[key]; }
-            set { _inner[key] = value; }
-        }
-
-        public int Count => _inner.Count;
-
-        ICollection<string> IDictionary<string, object>.Keys => _inner.Keys;
-
-        ICollection<object> IDictionary<string, object>.Values => _inner.Values;
-
-        bool ICollection<KeyValuePair<string, object>>.IsReadOnly => false;
-
-        object IDictionary.this[object key]
-        {
-            get { return ((IDictionary)_inner)[key]; }
-            set { ((IDictionary)_inner)[key] = value; }
-        }
-
-        ICollection IDictionary.Keys => _inner.Keys;
-
-        ICollection IDictionary.Values => _inner.Values;
-
-        bool ICollection.IsSynchronized => false;
-
-        object ICollection.SyncRoot => ((IDictionary)_inner).SyncRoot;
-
-        bool IDictionary.IsFixedSize => false;
-
-        bool IDictionary.IsReadOnly => false;
-
-        public void Add(string key, object value) => _inner.Add(key, value);
-
-        public void Clear() => _inner.Clear();
-
-        public bool ContainsKey(string key) => _inner.ContainsKey(key);
-
-        public bool Remove(string key) => _inner.Remove(key);
-
-        public IEnumerator<KeyValuePair<string, object>> GetEnumerator() => _inner.GetEnumerator();
-
-        public bool TryGetValue(string key, out object value) => _inner.TryGetValue(key, out value);
-
-        bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
-        {
-            return ((IDictionary<string, object>)_inner).Contains(item);
-        }
-
-        void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
-        {
-            ((IDictionary<string, object>)_inner).Add(item);
-        }
-
-        void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
-        {
-            ((IDictionary<string, object>)_inner).CopyTo(array, arrayIndex);
-        }
-
-        bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
-        {
-            return ((IDictionary<string, object>)_inner).Remove(item);
-        }
-
-        void ICollection.CopyTo(Array array, int index) => ((IDictionary)_inner).CopyTo(array, index);
-
-        IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator();
-
-        IDictionaryEnumerator IDictionary.GetEnumerator() => ((IDictionary)_inner).GetEnumerator();
-
-        void IDictionary.Add(object key, object value) => ((IDictionary)_inner).Add(key, value);
-
-        bool IDictionary.Contains(object key) => ((IDictionary)_inner).Contains(key);
-
-        void IDictionary.Remove(object key) => ((IDictionary)_inner).Remove(key);
-    }
-}

+ 135 - 14
src/Avalonia.Styling/Styling/Styles.cs

@@ -1,16 +1,95 @@
 // 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.Linq;
 using Avalonia.Collections;
+using Avalonia.Controls;
 
 namespace Avalonia.Styling
 {
     /// <summary>
     /// A style that consists of a number of child styles.
     /// </summary>
-    public class Styles : AvaloniaList<IStyle>, IStyle
+    public class Styles : AvaloniaList<IStyle>, IStyle, ISetStyleParent
     {
+        private IResourceNode _parent;
+        private IResourceDictionary _resources;
+
+        public Styles()
+        {
+            ResetBehavior = ResetBehavior.Remove;
+            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());
+                    }
+
+                    x.ResourcesChanged += SubResourceChanged;
+                },
+                x =>
+                {
+                    if (x.ResourceParent == this && x is ISetStyleParent setParent)
+                    {
+                        setParent.SetParent(null);
+                        setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs());
+                    }
+
+                    if (x.HasResources)
+                    {
+                        ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
+                    }
+
+                    x.ResourcesChanged -= SubResourceChanged;
+                },
+                () => { });
+        }
+
+        /// <inheritdoc/>
+        public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
+
+        /// <inheritdoc/>
+        public bool HasResources => _resources?.Count > 0 || this.Any(x => x.HasResources);
+
+        /// <summary>
+        /// Gets or sets a dictionary of style resources.
+        /// </summary>
+        public IResourceDictionary Resources
+        {
+            get => _resources ?? (Resources = new ResourceDictionary());
+            set
+            {
+                Contract.Requires<ArgumentNullException>(value != null);
+
+                var hadResources = false;
+
+                if (_resources != null)
+                {
+                    hadResources = _resources.Count > 0;
+                    _resources.ResourcesChanged -= ResourceDictionaryChanged;
+                }
+
+                _resources = value;
+                _resources.ResourcesChanged += ResourceDictionaryChanged;
+
+                if (hadResources || _resources.Count > 0)
+                {
+                    ((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs());
+                }
+            }
+        }
+
+        /// <inheritdoc/>
+        IResourceNode IResourceNode.ResourceParent => _parent;
+
         /// <summary>
         /// Attaches the style to a control if the style's selector matches.
         /// </summary>
@@ -26,26 +105,68 @@ namespace Avalonia.Styling
             }
         }
 
-        /// <summary>
-        /// Tries to find a named resource within the style.
-        /// </summary>
-        /// <param name="name">The resource name.</param>
-        /// <returns>
-        /// The resource if found, otherwise <see cref="AvaloniaProperty.UnsetValue"/>.
-        /// </returns>
-        public object FindResource(string name)
+        /// <inheritdoc/>
+        public bool TryGetResource(string key, out object value)
+        {
+            if (_resources != null && _resources.TryGetValue(key, out value))
+            {
+                return true;
+            }
+
+            for (var i = Count - 1; i >= 0; --i)
+            {
+                if (this[i].TryGetResource(key, out value))
+                {
+                    return true;
+                }
+            }
+
+            value = null;
+            return false;
+        }
+
+        /// <inheritdoc/>
+        void ISetStyleParent.SetParent(IResourceNode parent)
+        {
+            if (_parent != null && parent != null)
+            {
+                throw new InvalidOperationException("The Style already has a parent.");
+            }
+
+            _parent = parent;
+        }
+
+        /// <inheritdoc/>
+        void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e)
+        {
+            ResourcesChanged?.Invoke(this, e);
+        }
+
+        private void ResourceDictionaryChanged(object sender, ResourcesChangedEventArgs e)
         {
-            foreach (var style in this.Reverse())
+            foreach (var child in this)
             {
-                var result = style.FindResource(name);
+                (child as ISetStyleParent)?.NotifyResourcesChanged(e);
+            }
+
+            ResourcesChanged?.Invoke(this, e);
+        }
 
-                if (result != AvaloniaProperty.UnsetValue)
+        private void SubResourceChanged(object sender, ResourcesChangedEventArgs e)
+        {
+            var foundSource = false;
+
+            foreach (var child in this)
+            {
+                if (foundSource)
                 {
-                    return result;
+                    (child as ISetStyleParent)?.NotifyResourcesChanged(e);
                 }
+
+                foundSource |= child == sender;
             }
 
-            return AvaloniaProperty.UnsetValue;
+            ResourcesChanged?.Invoke(this, e);
         }
     }
 }

+ 3 - 0
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@@ -34,6 +34,9 @@
         <Compile Include="Converters\MatrixTypeConverter.cs" />
         <Compile Include="Converters\RectTypeConverter.cs" />
         <Compile Include="Converters\SetterValueTypeConverter.cs" />
+        <Compile Include="Data\ResourceInclude.cs" />
+        <Compile Include="MarkupExtensions\DynamicResourceExtension.cs" />
+        <Compile Include="MarkupExtensions\StaticResourceExtension.cs" />
         <Compile Include="MarkupExtensions\StyleIncludeExtension.cs" />
         <Compile Include="PortableXaml\AvaloniaXamlContext.cs" />
         <Compile Include="PortableXaml\XamlBinding.cs" />

+ 78 - 7
src/Markup/Avalonia.Markup.Xaml/Data/DelayedBinding.cs

@@ -3,9 +3,11 @@
 
 using System;
 using System.Collections.Generic;
+using System.Reflection;
 using System.Runtime.CompilerServices;
 using Avalonia.Controls;
 using Avalonia.Data;
+using Avalonia.Logging;
 
 namespace Avalonia.Markup.Xaml.Data
 {
@@ -50,7 +52,36 @@ namespace Avalonia.Markup.Xaml.Data
                     target.Initialized += ApplyBindings;
                 }
 
-                bindings.Add(new Entry(binding, property));
+                bindings.Add(new BindingEntry(property, binding));
+            }
+        }
+
+        /// <summary>
+        /// Adds a delayed value to a control.
+        /// </summary>
+        /// <param name="target">The control.</param>
+        /// <param name="property">The property on the control to bind to.</param>
+        /// <param name="value">A function which returns the value.</param>
+        public static void Add(IControl target, PropertyInfo property, Func<IControl, object> value)
+        {
+            if (target.IsInitialized)
+            {
+                property.SetValue(target, value(target));
+            }
+            else
+            {
+                List<Entry> bindings;
+
+                if (!_entries.TryGetValue(target, out bindings))
+                {
+                    bindings = new List<Entry>();
+                    _entries.Add(target, bindings);
+
+                    // TODO: Make this a weak event listener.
+                    target.Initialized += ApplyBindings;
+                }
+
+                bindings.Add(new ClrPropertyValueEntry(property, value));
             }
         }
 
@@ -60,13 +91,13 @@ namespace Avalonia.Markup.Xaml.Data
         /// <param name="control">The control.</param>
         public static void ApplyBindings(IControl control)
         {
-            List<Entry> bindings;
+            List<Entry> entries;
 
-            if (_entries.TryGetValue(control, out bindings))
+            if (_entries.TryGetValue(control, out entries))
             {
-                foreach (var binding in bindings)
+                foreach (var entry in entries)
                 {
-                    control.Bind(binding.Property, binding.Binding);
+                    entry.Apply(control);
                 }
 
                 _entries.Remove(control);
@@ -80,9 +111,14 @@ namespace Avalonia.Markup.Xaml.Data
             target.Initialized -= ApplyBindings;
         }
 
-        private class Entry
+        private abstract class Entry
+        {
+            public abstract void Apply(IControl control);
+        }
+
+        private class BindingEntry : Entry
         {
-            public Entry(IBinding binding, AvaloniaProperty property)
+            public BindingEntry(AvaloniaProperty property, IBinding binding)
             {
                 Binding = binding;
                 Property = property;
@@ -90,6 +126,41 @@ namespace Avalonia.Markup.Xaml.Data
 
             public IBinding Binding { get; }
             public AvaloniaProperty Property { get; }
+
+            public override void Apply(IControl control)
+            {
+                control.Bind(Property, Binding);
+            }
+        }
+
+        private class ClrPropertyValueEntry : Entry
+        {
+            public ClrPropertyValueEntry(PropertyInfo property, Func<IControl, object> value)
+            {
+                Property = property;
+                Value = value;
+            }
+
+            public PropertyInfo Property { get; }
+            public Func<IControl, object> Value { get; }
+
+            public override void Apply(IControl control)
+            {
+                try
+                {
+                    Property.SetValue(control, Value(control));
+                }
+                catch (Exception e)
+                {
+                    Logger.Error(
+                        LogArea.Property,
+                        control,
+                        "Error setting {Property} on {Target}: {Exception}",
+                        Property.Name,
+                        control,
+                        e);
+                }
+            }
         }
     }
 }

+ 63 - 0
src/Markup/Avalonia.Markup.Xaml/Data/ResourceInclude.cs

@@ -0,0 +1,63 @@
+using System;
+using System.ComponentModel;
+using Avalonia.Controls;
+using Portable.Xaml.ComponentModel;
+using Portable.Xaml.Markup;
+
+namespace Avalonia.Markup.Xaml.Data
+{
+    /// <summary>
+    /// Loads a resource dictionary from a specified URL.
+    /// </summary>
+    public class ResourceInclude : MarkupExtension, IResourceProvider
+    {
+        private Uri _baseUri;
+        private IResourceDictionary _loaded;
+
+        public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
+
+        /// <summary>
+        /// Gets the loaded resource dictionary.
+        /// </summary>
+        public IResourceDictionary Loaded
+        {
+            get
+            {
+                if (_loaded == null)
+                {
+                    var loader = new AvaloniaXamlLoader();
+                    _loaded = (IResourceDictionary)loader.Load(Source, _baseUri);
+
+                    if (_loaded.HasResources)
+                    {
+                        ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
+                    }
+                }
+
+                return _loaded;
+            }
+        }
+
+        /// <summary>
+        /// Gets or sets the source URL.
+        /// </summary>
+        public Uri Source { get; set; }
+
+        /// <inhertidoc/>
+        bool IResourceProvider.HasResources => Loaded.HasResources;
+
+        /// <inhertidoc/>
+        bool IResourceProvider.TryGetResource(string key, out object value)
+        {
+            return Loaded.TryGetResource(key, out value);
+        }
+
+        /// <inhertidoc/>
+        public override object ProvideValue(IServiceProvider serviceProvider)
+        {
+            var tdc = (ITypeDescriptorContext)serviceProvider;
+            _baseUri = tdc?.GetBaseUri();
+            return this;
+        }
+    }
+}

+ 5 - 2
src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs

@@ -46,11 +46,14 @@ namespace Avalonia.Markup.Xaml.Data
 
             if (host != null)
             {
-                resource = host.FindStyleResource(Name);
+                resource = host.FindResource(Name);
             }
             else if (style != null)
             {
-                resource = style.FindResource(Name);
+                if (!style.TryGetResource(Name, out resource))
+                {
+                    resource = AvaloniaProperty.UnsetValue;
+                }
             }
 
             if (resource != AvaloniaProperty.UnsetValue)

+ 71 - 0
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs

@@ -0,0 +1,71 @@
+// 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.ComponentModel;
+using System.Linq;
+using System.Reactive.Linq;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Portable.Xaml;
+using Portable.Xaml.ComponentModel;
+using Portable.Xaml.Markup;
+
+namespace Avalonia.Markup.Xaml.MarkupExtensions
+{
+    public class DynamicResourceExtension : MarkupExtension, IBinding
+    {
+        private IResourceNode _anchor;
+
+        public DynamicResourceExtension()
+        {
+        }
+
+        public DynamicResourceExtension(string resourceKey)
+        {
+            ResourceKey = resourceKey;
+        }
+
+        public string ResourceKey { get; set; }
+
+        public override object ProvideValue(IServiceProvider serviceProvider)
+        {
+            var context = (ITypeDescriptorContext)serviceProvider;
+            var provideTarget = context.GetService<IProvideValueTarget>();
+
+            if (!(provideTarget.TargetObject is IResourceNode))
+            {
+                _anchor = GetAnchor<IResourceNode>(context);
+            }
+
+            return this;
+        }
+
+        InstancedBinding IBinding.Initiate(
+            IAvaloniaObject target,
+            AvaloniaProperty targetProperty,
+            object anchor,
+            bool enableDataValidation)
+        {
+            var control = target as IResourceNode ?? _anchor;
+
+            if (control != null)
+            {
+                return new InstancedBinding(control.GetResourceObservable(ResourceKey));
+            }
+
+            return null;
+        }
+
+        private T GetAnchor<T>(ITypeDescriptorContext context) where T : class
+        {
+            var schemaContext = context.GetService<IXamlSchemaContextProvider>().SchemaContext;
+            var ambientProvider = context.GetService<IAmbientProvider>();
+            var xamlType = schemaContext.GetXamlType(typeof(T));
+
+            // We override XamlType.CanAssignTo in BindingXamlType so the results we get back
+            // from GetAllAmbientValues aren't necessarily of the correct type.
+            return ambientProvider.GetAllAmbientValues(xamlType).OfType<T>().FirstOrDefault();
+        }
+    }
+}

+ 84 - 0
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs

@@ -0,0 +1,84 @@
+// 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.Generic;
+using System.ComponentModel;
+using System.Reflection;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml.Data;
+using Portable.Xaml;
+using Portable.Xaml.ComponentModel;
+using Portable.Xaml.Markup;
+
+namespace Avalonia.Markup.Xaml.MarkupExtensions
+{
+    public class StaticResourceExtension : MarkupExtension
+    {
+        public StaticResourceExtension()
+        {
+        }
+
+        public StaticResourceExtension(string resourceKey)
+        {
+            ResourceKey = resourceKey;
+        }
+
+        public string ResourceKey { get; set; }
+
+        public override object ProvideValue(IServiceProvider serviceProvider)
+        {
+            var context = (ITypeDescriptorContext)serviceProvider;
+            var schemaContext = context.GetService<IXamlSchemaContextProvider>().SchemaContext;
+            var ambientProvider = context.GetService<IAmbientProvider>();
+            var resourceProviderType = schemaContext.GetXamlType(typeof(IResourceNode));
+            var ambientValues = ambientProvider.GetAllAmbientValues(resourceProviderType);
+
+            // Look upwards though the ambient context for IResourceProviders which might be able
+            // to give us the resource.
+            //
+            // TODO: If we're in a template then only the ambient values since the root of the
+            // template wil be included here. We need some way to get hold of the parent ambient
+            // context and search that. See the test:
+            //
+            //   StaticResource_Can_Be_Assigned_To_Property_In_ControlTemplate_In_Styles_File
+            //
+            foreach (var ambientValue in ambientValues)
+            {
+                // We override XamlType.CanAssignTo in BindingXamlType so the results we get back
+                // from GetAllAmbientValues aren't necessarily of the correct type.
+                if (ambientValue is IResourceNode resourceProvider)
+                {
+                    if (resourceProvider is IControl control && control.StylingParent != null)
+                    {
+                        // If we've got to a control that has a StylingParent then it's probably
+                        // a top level control and its StylingParent is pointing to the global
+                        // styles. If this is case just do a FindResource on it.
+                        return control.FindResource(ResourceKey);
+                    }
+                    else if (resourceProvider.TryGetResource(ResourceKey, out var value))
+                    {
+                        return value;
+                    }
+                }
+            }
+
+            // The resource still hasn't been found, so add a delayed one-time binding.
+            var provideTarget = context.GetService<IProvideValueTarget>();
+
+            if (provideTarget.TargetObject is IControl target &&
+                provideTarget.TargetProperty is PropertyInfo property)
+            {
+                DelayedBinding.Add(target, property, GetValue);
+                return AvaloniaProperty.UnsetValue;
+            }
+
+            throw new KeyNotFoundException($"Static resource '{ResourceKey}' not found.");
+        }
+
+        private object GetValue(IControl control)
+        {
+            return control.FindResource(ResourceKey);
+        }
+    }
+}

+ 30 - 11
src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs

@@ -3,27 +3,31 @@
 
 using Avalonia.Styling;
 using System;
+using Avalonia.Controls;
 
 namespace Avalonia.Markup.Xaml.Styling
 {
     /// <summary>
     /// Includes a style from a URL.
     /// </summary>
-    public class StyleInclude : IStyle
+    public class StyleInclude : IStyle, ISetStyleParent
     {
         private Uri _baseUri;
         private IStyle _loaded;
+        private IResourceNode _parent;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="StyleInclude"/> class.
         /// </summary>
         /// <param name="baseUri"></param>
-
         public StyleInclude(Uri baseUri)
         {
             _baseUri = baseUri;
         }
 
+        /// <inheritdoc/>
+        public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
+
         /// <summary>
         /// Gets or sets the source URL.
         /// </summary>
@@ -40,12 +44,19 @@ namespace Avalonia.Markup.Xaml.Styling
                 {
                     var loader = new AvaloniaXamlLoader();
                     _loaded = (IStyle)loader.Load(Source, _baseUri);
+                    (_loaded as ISetStyleParent)?.SetParent(this);
                 }
 
                 return _loaded;
             }
         }
 
+        /// <inheritdoc/>
+        bool IResourceProvider.HasResources => Loaded.HasResources;
+
+        /// <inheritdoc/>
+        IResourceNode IResourceNode.ResourceParent => _parent;
+
         /// <inheritdoc/>
         public void Attach(IStyleable control, IStyleHost container)
         {
@@ -55,16 +66,24 @@ namespace Avalonia.Markup.Xaml.Styling
             }
         }
 
-        /// <summary>
-        /// Tries to find a named resource within the style.
-        /// </summary>
-        /// <param name="name">The resource name.</param>
-        /// <returns>
-        /// The resource if found, otherwise <see cref="AvaloniaProperty.UnsetValue"/>.
-        /// </returns>
-        public object FindResource(string name)
+        /// <inheritdoc/>
+        public bool TryGetResource(string key, out object value) => Loaded.TryGetResource(key, out value);
+
+        /// <inheritdoc/>
+        void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e)
         {
-            return Loaded.FindResource(name);
+            (Loaded as ISetStyleParent)?.NotifyResourcesChanged(e);
+        }
+
+        /// <inheritdoc/>
+        void ISetStyleParent.SetParent(IResourceNode parent)
+        {
+            if (_parent != null && parent != null)
+            {
+                throw new InvalidOperationException("The Style already has a parent.");
+            }
+
+            _parent = parent;
         }
     }
 }

+ 217 - 0
tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs

@@ -0,0 +1,217 @@
+// 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.Presenters;
+using Avalonia.Controls.Templates;
+using Avalonia.Styling;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Controls.UnitTests
+{
+    public class ControlTests_Resources
+    {
+        [Fact]
+        public void FindResource_Should_Find_Control_Resource()
+        {
+            var target = new Control
+            {
+                Resources =
+                {
+                    { "foo", "foo-value" },
+                }
+            };
+
+            Assert.Equal("foo-value", target.FindResource("foo"));
+        }
+
+        [Fact]
+        public void FindResource_Should_Find_Control_Resource_In_Parent()
+        {
+            Control target;
+
+            var root = new Decorator
+            {
+                Resources =
+                {
+                    { "foo", "foo-value" },
+                },
+                Child = target = new Control(),
+            };
+
+            Assert.Equal("foo-value", target.FindResource("foo"));
+        }
+
+        [Fact]
+        public void FindResource_Should_Find_Application_Resource()
+        {
+            Control target;
+
+            var app = new Application
+            {
+                Resources =
+                {
+                    { "foo", "foo-value" },
+                },
+            };
+
+            var root = new TestRoot
+            {
+                Child = target = new Control(),
+                StylingParent = app,
+            };
+
+            Assert.Equal("foo-value", target.FindResource("foo"));
+        }
+
+        [Fact]
+        public void FindResource_Should_Find_Style_Resource()
+        {
+            var target = new Control
+            {
+                Styles =
+                {
+                    new Style
+                    {
+                        Resources =
+                        {
+                            { "foo", "foo-value" },
+                        }
+                    }
+                },
+                Resources =
+                {
+                    { "bar", "bar-value" },
+                },
+            };
+
+            Assert.Equal("foo-value", target.FindResource("foo"));
+        }
+
+        [Fact]
+        public void FindResource_Should_Find_Styles_Resource()
+        {
+            var target = new Control
+            {
+                Styles =
+                {
+                    new Styles
+                    {
+                        Resources =
+                        {
+                            { "foo", "foo-value" },
+                        }
+                    }
+                },
+                Resources =
+                {
+                    { "bar", "bar-value" },
+                },
+            };
+
+            Assert.Equal("foo-value", target.FindResource("foo"));
+        }
+
+        [Fact]
+        public void FindResource_Should_Find_Application_Style_Resource()
+        {
+            Control target;
+
+            var app = new Application
+            {
+                Styles =
+                {
+                    new Style
+                    {
+                        Resources =
+                        {
+                            { "foo", "foo-value" },
+                        },
+                    }
+                },
+                Resources =
+                {
+                    { "bar", "bar-value" },
+                },
+            };
+
+            var root = new TestRoot
+            {
+                Child = target = new Control(),
+                StylingParent = app,
+            };
+
+            Assert.Equal("foo-value", target.FindResource("foo"));
+        }
+
+        [Fact]
+        public void Adding_Resource_Should_Call_Raise_ResourceChanged_On_Logical_Children()
+        {
+            Border child;
+
+            var target = new ContentControl
+            {
+                Content = child = new Border(),
+                Template = ContentControlTemplate(),
+            };
+
+            var raisedOnTarget = false;
+            var raisedOnPresenter = false;
+            var raisedOnChild = false;
+
+            target.Measure(Size.Infinity);
+            target.ResourcesChanged += (_, __) => raisedOnTarget = true;
+            target.Presenter.ResourcesChanged += (_, __) => raisedOnPresenter = true;
+            child.ResourcesChanged += (_, __) => raisedOnChild = true;
+
+            target.Resources.Add("foo", "bar");
+
+            Assert.True(raisedOnTarget);
+            Assert.False(raisedOnPresenter);
+            Assert.True(raisedOnChild);
+        }
+
+        [Fact]
+        public void Adding_Resource_To_Styles_Should_Raise_ResourceChanged()
+        {
+            var target = new Decorator();
+            var raised = false;
+
+            target.ResourcesChanged += (_, __) => raised = true;
+            target.Styles.Resources.Add("foo", "bar");
+
+            Assert.True(raised);
+        }
+
+        [Fact]
+        public void Adding_Resource_To_Nested_Style_Should_Raise_ResourceChanged()
+        {
+            Style style;
+            var target = new Decorator
+            {
+                Styles =
+                {
+                    (style = new Style()),
+                }
+            };
+
+            var raised = false;
+
+            target.ResourcesChanged += (_, __) => raised = true;
+            style.Resources.Add("foo", "bar");
+
+            Assert.True(raised);
+        }
+
+        private IControlTemplate ContentControlTemplate()
+        {
+            return new FuncControlTemplate<ContentControl>(x =>
+                new ContentPresenter
+                {
+                    Name = "PART_ContentPresenter",
+                    [!ContentPresenter.ContentProperty] = x[!ContentControl.ContentProperty],
+                });
+        }
+    }
+}

+ 17 - 0
tests/Avalonia.Controls.UnitTests/TopLevelTests.cs

@@ -219,6 +219,23 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        [Fact]
+        public void Adding_Resource_To_Application_Should_Raise_ResourcesChanged()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var impl = new Mock<ITopLevelImpl>();
+                impl.SetupAllProperties();
+                var target = new TestTopLevel(impl.Object);
+                var raised = false;
+
+                target.ResourcesChanged += (_, __) => raised = true;
+                Application.Current.Resources.Add("foo", "bar");
+
+                Assert.True(raised);
+            }
+        }
+
         private FuncControlTemplate<TestTopLevel> CreateTemplate()
         {
             return new FuncControlTemplate<TestTopLevel>(x =>

+ 4 - 0
tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs

@@ -162,6 +162,10 @@ namespace Avalonia.Layout.UnitTests
         private void RegisterServices()
         {
             var globalStyles = new Mock<IGlobalStyles>();
+            var globalStylesResources = globalStyles.As<IResourceNode>();
+            var outObj = (object)10;
+            globalStylesResources.Setup(x => x.TryGetResource("FontSizeNormal", out outObj)).Returns(true);
+
             var renderInterface = new Mock<IPlatformRenderInterface>();
             renderInterface.Setup(x =>
                 x.CreateFormattedText(

+ 55 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Data/ResourceIncludeTests.cs

@@ -0,0 +1,55 @@
+using System;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Markup.Xaml.UnitTests.Data
+{
+    public class ResourceIncludeTests
+    {
+        public class StaticResourceExtensionTests
+        {
+            [Fact]
+            public void ResourceInclude_Loads_ResourceDictionary()
+            {
+                var includeXaml = @"
+<ResourceDictionary xmlns='https://github.com/avaloniaui'
+                    xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
+</ResourceDictionary>
+";
+                using (StartWithResources(("test:include.xaml", includeXaml)))
+                {
+                    var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Resources>
+        <ResourceDictionary>
+            <ResourceDictionary.MergedDictionaries>
+                <ResourceInclude Source='test:include.xaml'/>
+            </ResourceDictionary.MergedDictionaries>
+        </ResourceDictionary>
+    </UserControl.Resources>
+
+    <Border Name='border' Background='{StaticResource brush}'/>
+</UserControl>";
+
+                    var loader = new AvaloniaXamlLoader();
+                    var userControl = (UserControl)loader.Load(xaml);
+                    var border = userControl.FindControl<Border>("border");
+
+                    var brush = (SolidColorBrush)border.Background;
+                    Assert.Equal(0xff506070, brush.Color.ToUint32());
+                }
+            }
+
+            private IDisposable StartWithResources(params (string, string)[] assets)
+            {
+                var assetLoader = new MockAssetLoader(assets);
+                var services = new TestServices(assetLoader: assetLoader);
+                return UnitTestApplication.Start(services);
+            }
+        }
+    }
+}

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

@@ -0,0 +1,660 @@
+// 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.Linq;
+using Avalonia.Controls;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Templates;
+using Avalonia.Markup.Xaml.Data;
+using Avalonia.Media;
+using Avalonia.Styling;
+using Avalonia.UnitTests;
+using Avalonia.VisualTree;
+using Xunit;
+
+namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
+{
+    public class DynamicResourceExtensionTests
+    {
+        [Fact]
+        public void DynamicResource_Can_Be_Assigned_To_Property()
+        {
+            var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Resources>
+        <SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
+    </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(0xff506070, brush.Color.ToUint32());
+        }
+
+        [Fact]
+        public void DynamicResource_Can_Be_Assigned_To_Attached_Property()
+        {
+            var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Resources>
+        <x:Int32 x:Key='col'>5</x:Int32>
+    </UserControl.Resources>
+
+    <Border Name='border' Grid.Column='{DynamicResource col}'/>
+</UserControl>";
+
+            var loader = new AvaloniaXamlLoader();
+            var userControl = (UserControl)loader.Load(xaml);
+            var border = userControl.FindControl<Border>("border");
+
+            DelayedBinding.ApplyBindings(border);
+
+            Assert.Equal(5, Grid.GetColumn(border));
+        }
+
+        [Fact]
+        public void DynamicResource_From_Style_Can_Be_Assigned_To_Property()
+        {
+            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'>#ff506070</SolidColorBrush>
+            </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(0xff506070, brush.Color.ToUint32());
+        }
+
+        [Fact]
+        public void DynamicResource_From_MergedDictionary_Can_Be_Assigned_To_Property()
+        {
+            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'>#ff506070</SolidColorBrush>
+                </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(0xff506070, brush.Color.ToUint32());
+        }
+
+        [Fact]
+        public void DynamicResource_From_MergedDictionary_In_Style_Can_Be_Assigned_To_Property()
+        {
+            var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Styles>
+        <Style>
+            <Style.Resources>
+                <ResourceDictionary>
+                    <ResourceDictionary.MergedDictionaries>
+                        <ResourceDictionary>
+                            <SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
+                        </ResourceDictionary>
+                    </ResourceDictionary.MergedDictionaries>
+                </ResourceDictionary>
+            </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(0xff506070, brush.Color.ToUint32());
+        }
+
+        [Fact]
+        public void DynamicResource_From_Application_Can_Be_Assigned_To_Property_In_Window()
+        {
+            using (StyledWindow())
+            {
+                Application.Current.Resources.Add("brush", new SolidColorBrush(0xff506070));
+
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Border Name='border' Background='{DynamicResource brush}'/>
+</Window>";
+
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var border = window.FindControl<Border>("border");
+
+                var brush = (SolidColorBrush)border.Background;
+                Assert.Equal(0xff506070, brush.Color.ToUint32());
+            }
+        }
+
+        [Fact]
+        public void DynamicResource_From_Application_Can_Be_Assigned_To_Property_In_UserControl()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                Application.Current.Resources.Add("brush", new SolidColorBrush(0xff506070));
+
+                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");
+
+                // We don't actually know where the global styles are until we attach the control
+                // to a window, as Window has StylingParent set to Application.
+                var window = new Window { Content = userControl };
+                window.Show();
+
+                var brush = (SolidColorBrush)border.Background;
+                Assert.Equal(0xff506070, brush.Color.ToUint32());
+            }
+        }
+
+        [Fact]
+        public void DynamicResource_Can_Be_Assigned_To_Setter()
+        {
+            using (StyledWindow())
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Resources>
+        <SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
+    </Window.Resources>
+    <Window.Styles>
+        <Style Selector='Button'>
+            <Setter Property='Background' Value='{DynamicResource brush}'/>
+        </Style>
+    </Window.Styles>
+    <Button Name='button'/>
+</Window>";
+
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var button = window.FindControl<Button>("button");
+                var brush = (SolidColorBrush)button.Background;
+
+                Assert.Equal(0xff506070, brush.Color.ToUint32());
+            }
+        }
+
+        [Fact]
+        public void DynamicResource_From_Style_Can_Be_Assigned_To_Setter()
+        {
+            using (StyledWindow())
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Styles>
+        <Style>
+            <Style.Resources>
+                <SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
+            </Style.Resources>
+        </Style>
+        <Style Selector='Button'>
+            <Setter Property='Background' Value='{DynamicResource brush}'/>
+        </Style>
+    </Window.Styles>
+    <Button Name='button'/>
+</Window>";
+
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var button = window.FindControl<Button>("button");
+                var brush = (SolidColorBrush)button.Background;
+
+                Assert.Equal(0xff506070, brush.Color.ToUint32());
+            }
+        }
+
+        [Fact]
+        public void DynamicResource_Can_Be_Assigned_To_Setter_In_Styles_File()
+        {
+            var styleXaml = @"
+<Styles xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Styles.Resources>
+        <SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
+    </Styles.Resources>
+
+    <Style Selector='Border'>
+        <Setter Property='Background' Value='{DynamicResource brush}'/>
+    </Style>
+</Styles>";
+
+            using (StyledWindow(assets: ("test:style.xaml", styleXaml)))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Styles>
+        <StyleInclude Source='test:style.xaml'/>
+    </Window.Styles>
+    <Border Name='border'/>
+</Window>";
+
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var border = window.FindControl<Border>("border");
+                var brush = (SolidColorBrush)border.Background;
+
+                Assert.Equal(0xff506070, brush.Color.ToUint32());
+            }
+        }
+
+        [Fact]
+        public void DynamicResource_Can_Be_Assigned_To_Property_In_ControlTemplate_In_Styles_File()
+        {
+            var styleXaml = @"
+<Styles xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Styles.Resources>
+        <SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
+    </Styles.Resources>
+
+    <Style Selector='Button'>
+        <Setter Property='Template'>
+            <ControlTemplate>
+                <Border Name='border' Background='{DynamicResource brush}'/>
+            </ControlTemplate>
+        </Setter>
+    </Style>
+</Styles>";
+
+            using (StyledWindow(assets: ("test:style.xaml", styleXaml)))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Styles>
+        <StyleInclude Source='test:style.xaml'/>
+    </Window.Styles>
+    <Button Name='button'/>
+</Window>";
+
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var button = window.FindControl<Button>("button");
+
+                window.Show();
+
+                var border = (Border)button.GetVisualChildren().Single();
+                var brush = (SolidColorBrush)border.Background;
+
+                Assert.Equal(0xff506070, brush.Color.ToUint32());
+            }
+        }
+
+        [Fact]
+        public void DynamicResource_Can_Be_Assigned_To_Resource_Property()
+        {
+            var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Resources>
+        <Color x:Key='color'>#ff506070</Color>
+        <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(0xff506070, brush.Color.ToUint32());
+        }
+
+
+        [Fact]
+        public void DynamicResource_Can_Be_Assigned_To_ItemTemplate_Property()
+        {
+            var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Resources>
+        <DataTemplate x:Key='PurpleData'>
+          <TextBlock Text='{Binding Name}' Background='Purple'/>
+        </DataTemplate>
+    </UserControl.Resources>
+
+    <ListBox Name='listBox' ItemTemplate='{DynamicResource PurpleData}'/>
+</UserControl>";
+
+            var loader = new AvaloniaXamlLoader();
+            var userControl = (UserControl)loader.Load(xaml);
+            var listBox = userControl.FindControl<ListBox>("listBox");
+
+            DelayedBinding.ApplyBindings(listBox);
+
+            Assert.NotNull(listBox.ItemTemplate);
+        }
+
+        [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_Tracks_Added_MergedResource()
+        {
+            var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Resources>
+        <ResourceDictionary>
+            <ResourceDictionary.MergedDictionaries>
+                <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);
+
+            Assert.Null(border.Background);
+
+            ((IResourceDictionary)userControl.Resources.MergedDictionaries[0]).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_MergedResource_Dictionary()
+        {
+            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);
+
+            var dictionary = new ResourceDictionary
+            {
+                { "brush", new SolidColorBrush(0xff506070) },
+            };
+
+            userControl.Resources.MergedDictionaries.Add(dictionary);
+
+            var brush = (SolidColorBrush)border.Background;
+            Assert.NotNull(brush);
+            Assert.Equal(0xff506070, brush.Color.ToUint32());
+        }
+
+        [Fact]
+        public void DynamicResource_Tracks_Added_Style_MergedResource_Dictionary()
+        {
+            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);
+
+            var dictionary = new ResourceDictionary
+            {
+                { "brush", new SolidColorBrush(0xff506070) },
+            };
+
+            ((Style)userControl.Styles[0]).Resources.MergedDictionaries.Add(dictionary);
+
+            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()
+        {
+            var style1Xaml = @"
+<Style xmlns='https://github.com/avaloniaui'
+       xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+  <Style.Resources>
+    <Color x:Key='Red'>Red</Color>
+  </Style.Resources>
+</Style>";
+            var style2Xaml = @"
+<Style xmlns='https://github.com/avaloniaui'
+       xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+  <Style.Resources>
+    <SolidColorBrush x:Key='RedBrush' Color='{DynamicResource Red}'/>
+  </Style.Resources>
+</Style>";
+            using (StyledWindow(
+                ("test:style1.xaml", style1Xaml), 
+                ("test:style2.xaml", style2Xaml)))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Styles>
+        <StyleInclude Source='test:style1.xaml'/>
+        <StyleInclude Source='test:style2.xaml'/>
+    </Window.Styles>
+    <Border Name='border' Background='{DynamicResource RedBrush}'/>
+</Window>";
+
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var border = window.FindControl<Border>("border");
+                var borderBrush = (ISolidColorBrush)border.Background;
+
+                Assert.NotNull(borderBrush);
+                Assert.Equal(0xffff0000, borderBrush.Color.ToUint32());
+            }
+        }
+
+        [Fact]
+        public void Control_Property_Is_Updated_When_Parent_Is_Changed()
+        {
+            var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Resources>
+        <SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
+    </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(0xff506070, brush.Color.ToUint32());
+
+            userControl.Content = null;
+
+            Assert.Null(border.Background);
+
+            userControl.Content = border;
+
+            brush = (SolidColorBrush)border.Background;
+            Assert.Equal(0xff506070, brush.Color.ToUint32());
+        }
+
+        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 =>
+                            new ContentPresenter
+                            {
+                                Name = "PART_ContentPresenter",
+                                [!ContentPresenter.ContentProperty] = x[!Window.ContentProperty],
+                            }))
+                }
+            };
+        }
+    }
+}

+ 476 - 0
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs

@@ -0,0 +1,476 @@
+// 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.Linq;
+using Avalonia.Controls;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Templates;
+using Avalonia.Media;
+using Avalonia.Styling;
+using Avalonia.UnitTests;
+using Avalonia.VisualTree;
+using Xunit;
+
+namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
+{
+    public class StaticResourceExtensionTests
+    {
+        [Fact]
+        public void StaticResource_Can_Be_Assigned_To_Property()
+        {
+            var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Resources>
+        <SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
+    </UserControl.Resources>
+
+    <Border Name='border' Background='{StaticResource brush}'/>
+</UserControl>";
+
+            var loader = new AvaloniaXamlLoader();
+            var userControl = (UserControl)loader.Load(xaml);
+            var border = userControl.FindControl<Border>("border");
+
+            var brush = (SolidColorBrush)border.Background;
+            Assert.Equal(0xff506070, brush.Color.ToUint32());
+        }
+
+        [Fact]
+        public void StaticResource_Can_Be_Assigned_To_Attached_Property()
+        {
+            var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Resources>
+        <x:Int32 x:Key='col'>5</x:Int32>
+    </UserControl.Resources>
+
+    <Border Name='border' Grid.Column='{StaticResource col}'/>
+</UserControl>";
+
+            var loader = new AvaloniaXamlLoader();
+            var userControl = (UserControl)loader.Load(xaml);
+            var border = userControl.FindControl<Border>("border");
+
+            Assert.Equal(5, Grid.GetColumn(border));
+        }
+
+        [Fact]
+        public void StaticResource_From_Style_Can_Be_Assigned_To_Property()
+        {
+            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'>#ff506070</SolidColorBrush>
+            </Style.Resources>
+        </Style>
+    </UserControl.Styles>
+
+    <Border Name='border' Background='{StaticResource brush}'/>
+</UserControl>";
+
+            var loader = new AvaloniaXamlLoader();
+            var userControl = (UserControl)loader.Load(xaml);
+            var border = userControl.FindControl<Border>("border");
+
+            var brush = (SolidColorBrush)border.Background;
+            Assert.Equal(0xff506070, brush.Color.ToUint32());
+        }
+
+        [Fact]
+        public void StaticResource_From_Application_Can_Be_Assigned_To_Property_In_Window()
+        {
+            using (StyledWindow())
+            {
+                Application.Current.Resources.Add("brush", new SolidColorBrush(0xff506070));
+
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Border Name='border' Background='{StaticResource brush}'/>
+</Window>";
+
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var border = window.FindControl<Border>("border");
+
+                var brush = (SolidColorBrush)border.Background;
+                Assert.Equal(0xff506070, brush.Color.ToUint32());
+            }
+        }
+
+        [Fact]
+        public void StaticResource_From_MergedDictionary_Can_Be_Assigned_To_Property()
+        {
+            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'>#ff506070</SolidColorBrush>
+                </ResourceDictionary>
+            </ResourceDictionary.MergedDictionaries>
+        </ResourceDictionary>
+    </UserControl.Resources>
+
+    <Border Name='border' Background='{StaticResource brush}'/>
+</UserControl>";
+
+            var loader = new AvaloniaXamlLoader();
+            var userControl = (UserControl)loader.Load(xaml);
+            var border = userControl.FindControl<Border>("border");
+
+            var brush = (SolidColorBrush)border.Background;
+            Assert.Equal(0xff506070, brush.Color.ToUint32());
+        }
+
+        [Fact]
+        public void StaticResource_From_MergedDictionary_In_Style_Can_Be_Assigned_To_Property()
+        {
+            var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Styles>
+        <Style>
+            <Style.Resources>
+                <ResourceDictionary>
+                    <ResourceDictionary.MergedDictionaries>
+                        <ResourceDictionary>
+                            <SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
+                        </ResourceDictionary>
+                    </ResourceDictionary.MergedDictionaries>
+                </ResourceDictionary>
+            </Style.Resources>
+        </Style>
+    </UserControl.Styles>
+
+    <Border Name='border' Background='{StaticResource brush}'/>
+</UserControl>";
+
+            var loader = new AvaloniaXamlLoader();
+            var userControl = (UserControl)loader.Load(xaml);
+            var border = userControl.FindControl<Border>("border");
+
+            var brush = (SolidColorBrush)border.Background;
+            Assert.Equal(0xff506070, brush.Color.ToUint32());
+        }
+
+        [Fact]
+        public void StaticResource_From_Application_Can_Be_Assigned_To_Property_In_UserControl()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                Application.Current.Resources.Add("brush", new SolidColorBrush(0xff506070));
+
+                var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Border Name='border' Background='{StaticResource brush}'/>
+</UserControl>";
+
+                var loader = new AvaloniaXamlLoader();
+                var userControl = (UserControl)loader.Load(xaml);
+                var border = userControl.FindControl<Border>("border");
+
+                // We don't actually know where the global styles are until we attach the control
+                // to a window, as Window has StylingParent set to Application.
+                var window = new Window { Content = userControl };
+                window.Show();
+
+                var brush = (SolidColorBrush)border.Background;
+                Assert.Equal(0xff506070, brush.Color.ToUint32());
+            }
+        }
+
+        [Fact]
+        public void StaticResource_Can_Be_Assigned_To_Setter()
+        {
+            using (StyledWindow())
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Resources>
+        <SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
+    </Window.Resources>
+    <Window.Styles>
+        <Style Selector='Button'>
+            <Setter Property='Background' Value='{StaticResource brush}'/>
+        </Style>
+    </Window.Styles>
+    <Button Name='button'/>
+</Window>";
+
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var button = window.FindControl<Button>("button");
+                var brush = (SolidColorBrush)button.Background;
+
+                Assert.Equal(0xff506070, brush.Color.ToUint32());
+            }
+        }
+
+        [Fact]
+        public void StaticResource_From_Style_Can_Be_Assigned_To_Setter()
+        {
+            using (StyledWindow())
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Styles>
+        <Style>
+            <Style.Resources>
+                <SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
+            </Style.Resources>
+        </Style>
+        <Style Selector='Button'>
+            <Setter Property='Background' Value='{StaticResource brush}'/>
+        </Style>
+    </Window.Styles>
+    <Button Name='button'/>
+</Window>";
+
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var button = window.FindControl<Button>("button");
+                var brush = (SolidColorBrush)button.Background;
+
+                Assert.Equal(0xff506070, brush.Color.ToUint32());
+            }
+        }
+
+        [Fact]
+        public void StaticResource_Can_Be_Assigned_To_Setter_In_Styles_File()
+        {
+            var styleXaml = @"
+<Styles xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Styles.Resources>
+        <SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
+    </Styles.Resources>
+
+    <Style Selector='Border'>
+        <Setter Property='Background' Value='{StaticResource brush}'/>
+    </Style>
+</Styles>";
+
+            using (StyledWindow(assets: ("test:style.xaml", styleXaml)))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Styles>
+        <StyleInclude Source='test:style.xaml'/>
+    </Window.Styles>
+    <Border Name='border'/>
+</Window>";
+
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var border = window.FindControl<Border>("border");
+                var brush = (SolidColorBrush)border.Background;
+
+                Assert.Equal(0xff506070, brush.Color.ToUint32());
+            }
+        }
+
+        [Fact]
+        public void StaticResource_Can_Be_Assigned_To_Resource_Property()
+        {
+            var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Resources>
+        <Color x:Key='color'>#ff506070</Color>
+        <SolidColorBrush x:Key='brush' Color='{StaticResource color}'/>
+    </UserControl.Resources>
+
+    <Border Name='border' Background='{StaticResource brush}'/>
+</UserControl>";
+
+            var loader = new AvaloniaXamlLoader();
+            var userControl = (UserControl)loader.Load(xaml);
+            var border = userControl.FindControl<Border>("border");
+
+            var brush = (SolidColorBrush)border.Background;
+            Assert.Equal(0xff506070, brush.Color.ToUint32());
+        }
+
+        [Fact]
+        public void StaticResource_Can_Be_Assigned_To_Resource_Property_In_Styles_File()
+        {
+            var xaml = @"
+<Styles xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Styles.Resources>
+        <Color x:Key='color'>#ff506070</Color>
+        <SolidColorBrush x:Key='brush' Color='{StaticResource color}'/>
+    </Styles.Resources>
+</Styles>";
+
+            var loader = new AvaloniaXamlLoader();
+            var styles = (Styles)loader.Load(xaml);
+            var brush = (SolidColorBrush)styles.Resources["brush"];
+
+            Assert.Equal(0xff506070, brush.Color.ToUint32());
+        }
+
+        [Fact(Skip = "Not yet supported by Portable.Xaml")]
+        public void StaticResource_Can_Be_Assigned_To_Property_In_ControlTemplate_In_Styles_File()
+        {
+            var styleXaml = @"
+<Styles xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Styles.Resources>
+        <SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
+    </Styles.Resources>
+
+    <Style Selector='Button'>
+        <Setter Property='Template'>
+            <ControlTemplate>
+                <Border Name='border' Background='{StaticResource brush}'/>
+            </ControlTemplate>
+        </Setter>
+    </Style>
+</Styles>";
+
+            using (StyledWindow(assets: ("test:style.xaml", styleXaml)))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Styles>
+        <StyleInclude Source='test:style.xaml'/>
+    </Window.Styles>
+    <Button Name='button'/>
+</Window>";
+
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var button = window.FindControl<Button>("button");
+
+                window.Show();
+
+                var border = (Border)button.GetVisualChildren().Single();
+                var brush = (SolidColorBrush)border.Background;
+
+                // To make this work we somehow need to be able to get hold of the parent ambient
+                // context from Portable.Xaml. See TODO in StaticResourceExtension.
+                Assert.Equal(0xff506070, brush.Color.ToUint32());
+            }
+        }
+
+        [Fact]
+        public void StaticResource_Can_Be_Assigned_To_ItemTemplate_Property()
+        {
+            var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Resources>
+        <DataTemplate x:Key='PurpleData'>
+          <TextBlock Text='{Binding Name}' Background='Purple'/>
+        </DataTemplate>
+    </UserControl.Resources>
+
+    <ListBox Name='listBox' ItemTemplate='{StaticResource PurpleData}'/>
+</UserControl>";
+
+            var loader = new AvaloniaXamlLoader();
+            var userControl = (UserControl)loader.Load(xaml);
+            var listBox = userControl.FindControl<ListBox>("listBox");
+
+            Assert.NotNull(listBox.ItemTemplate);
+        }
+
+        [Fact]
+        public void StaticResource_Can_Be_Assigned_To_Converter()
+        {
+            using (StyledWindow())
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+             xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'>
+    <Window.Resources>
+        <local:TestValueConverter x:Key='converter' Append='bar'/>
+    </Window.Resources>
+
+    <TextBlock Name='textBlock' Text='{Binding Converter={StaticResource converter}}'/>
+</Window>";
+
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var textBlock = window.FindControl<TextBlock>("textBlock");
+
+                window.DataContext = "foo";
+                window.ApplyTemplate();
+
+                Assert.Equal("foobar", textBlock.Text);
+            }
+        }
+
+        [Fact]
+        public void Control_Property_Is_Not_Updated_When_Parent_Is_Changed()
+        {
+            var xaml = @"
+<UserControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <UserControl.Resources>
+        <SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
+    </UserControl.Resources>
+
+    <Border Name='border' Background='{StaticResource brush}'/>
+</UserControl>";
+
+            var loader = new AvaloniaXamlLoader();
+            var userControl = (UserControl)loader.Load(xaml);
+            var border = userControl.FindControl<Border>("border");
+
+            var brush = (SolidColorBrush)border.Background;
+            Assert.Equal(0xff506070, brush.Color.ToUint32());
+
+            userControl.Content = null;
+
+            brush = (SolidColorBrush)border.Background;
+            Assert.Equal(0xff506070, brush.Color.ToUint32());
+        }
+
+        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 =>
+                            new ContentPresenter
+                            {
+                                Name = "PART_ContentPresenter",
+                                [!ContentPresenter.ContentProperty] = x[!Window.ContentProperty],
+                            }))
+                }
+            };
+        }
+    }
+}

+ 20 - 0
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/TestValueConverter.cs

@@ -0,0 +1,20 @@
+using System;
+using System.Globalization;
+
+namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
+{
+    public class TestValueConverter : IValueConverter
+    {
+        public string Append { get; set; }
+
+        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            return value.ToString() + Append;
+        }
+
+        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 3 - 3
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs

@@ -480,13 +480,13 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
 
             Assert.True(style.Resources.Count > 0);
 
-            var brush = style.FindResource("Brush") as SolidColorBrush;
+            style.TryGetResource("Brush", out var brush);
 
             Assert.NotNull(brush);
 
-            Assert.Equal(Colors.White, brush.Color);
+            Assert.Equal(Colors.White, ((SolidColorBrush)brush).Color);
 
-            var d = (double)style.FindResource("Double");
+            style.TryGetResource("Double", out var d);
 
             Assert.Equal(10.0, d);
         }

+ 7 - 5
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@@ -141,7 +141,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
 
                 var loader = new AvaloniaXamlLoader();
                 var window = (Window)loader.Load(xaml);
-                var brush = (ISolidColorBrush)window.FindStyleResource("brush");
+                var brush = (ISolidColorBrush)window.FindResource("brush");
                 var button = window.FindControl<Button>("button");
 
                 DelayedBinding.ApplyBindings(button);
@@ -169,9 +169,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
 
             var loader = new AvaloniaXamlLoader();
             var styles = (Styles)loader.Load(xaml);
-            var brush = (ISolidColorBrush)styles.FindResource("brush");
 
-            Assert.Equal(0xff506070, brush.Color.ToUint32());
+            styles.TryGetResource("brush", out var brush);
+
+            Assert.Equal(0xff506070, ((SolidColorBrush)brush).Color.ToUint32());
         }
 
         [Fact]
@@ -194,9 +195,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
 
             var loader = new AvaloniaXamlLoader();
             var styles = (Styles)loader.Load(xaml);
-            var brush = (ISolidColorBrush)styles.FindResource("brush");
 
-            Assert.Equal(0xff506070, brush.Color.ToUint32());
+            styles.TryGetResource("brush", out var brush);
+
+            Assert.Equal(0xff506070, ((SolidColorBrush)brush).Color.ToUint32());
         }
 
         [Fact(Skip = "TODO: Issue #492")]

+ 175 - 0
tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs

@@ -0,0 +1,175 @@
+// 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 Xunit;
+
+namespace Avalonia.Styling.UnitTests
+{
+    public class ResourceDictionaryTests
+    {
+        [Fact]
+        public void TryGetResource_Should_Find_Resource()
+        {
+            var target = new ResourceDictionary
+            {
+                { "foo", "bar" },
+            };
+
+            Assert.True(target.TryGetResource("foo", out var result));
+            Assert.Equal("bar", result);
+        }
+
+        [Fact]
+        public void TryGetResource_Should_Find_Resource_From_Merged_Dictionary()
+        {
+            var target = new ResourceDictionary
+            {
+                MergedDictionaries =
+                {
+                    new ResourceDictionary
+                    {
+                        { "foo", "bar" },
+                    }
+                }
+            };
+
+            Assert.True(target.TryGetResource("foo", out var result));
+            Assert.Equal("bar", result);
+        }
+
+        [Fact]
+        public void TryGetResource_Should_Find_Resource_From_Itself_Before_Merged_Dictionary()
+        {
+            var target = new ResourceDictionary
+            {
+                { "foo", "bar" },
+            };
+
+            target.MergedDictionaries.Add(new ResourceDictionary
+            {
+                { "foo", "baz" },
+            });
+
+            Assert.True(target.TryGetResource("foo", out var result));
+            Assert.Equal("bar", result);
+        }
+
+        [Fact]
+        public void TryGetResource_Should_Find_Resource_From_Later_Merged_Dictionary()
+        {
+            var target = new ResourceDictionary
+            {
+                MergedDictionaries =
+                {
+                    new ResourceDictionary
+                    {
+                        { "foo", "bar" },
+                    },
+                    new ResourceDictionary
+                    {
+                        { "foo", "baz" },
+                    }
+                }
+            };
+
+            Assert.True(target.TryGetResource("foo", out var result));
+            Assert.Equal("baz", result);
+        }
+
+        [Fact]
+        public void ResourcesChanged_Should_Be_Raised_On_Resource_Add()
+        {
+            var target = new ResourceDictionary();
+            var raised = false;
+
+            target.ResourcesChanged += (_, __) => raised = true;
+            target.Add("foo", "bar");
+
+            Assert.True(raised);
+        }
+
+        [Fact]
+        public void ResourcesChanged_Should_Be_Raised_On_MergedDictionary_Add()
+        {
+            var target = new ResourceDictionary();
+            var raised = false;
+
+            target.ResourcesChanged += (_, __) => raised = true;
+            target.MergedDictionaries.Add(new ResourceDictionary
+            {
+                { "foo", "bar" },
+            });
+
+            Assert.True(raised);
+        }
+
+        [Fact]
+        public void ResourcesChanged_Should_Not_Be_Raised_On_Empty_MergedDictionary_Add()
+        {
+            var target = new ResourceDictionary();
+            var raised = false;
+
+            target.ResourcesChanged += (_, __) => raised = true;
+            target.MergedDictionaries.Add(new ResourceDictionary());
+
+            Assert.False(raised);
+        }
+
+        [Fact]
+        public void ResourcesChanged_Should_Be_Raised_On_MergedDictionary_Remove()
+        {
+            var target = new ResourceDictionary
+            {
+                MergedDictionaries =
+                {
+                    new ResourceDictionary { { "foo", "bar" } },
+                }
+            };
+            var raised = false;
+
+            target.ResourcesChanged += (_, __) => raised = true;
+            target.MergedDictionaries.RemoveAt(0);
+
+            Assert.True(raised);
+        }
+
+        [Fact]
+        public void ResourcesChanged_Should_Not_Be_Raised_On_Empty_MergedDictionary_Remove()
+        {
+            var target = new ResourceDictionary
+            {
+                MergedDictionaries =
+                {
+                    new ResourceDictionary(),
+                }
+            };
+            var raised = false;
+
+            target.ResourcesChanged += (_, __) => raised = true;
+            target.MergedDictionaries.RemoveAt(0);
+
+            Assert.False(raised);
+        }
+
+        [Fact]
+        public void ResourcesChanged_Should_Be_Raised_On_MergedDictionary_Resource_Add()
+        {
+            var target = new ResourceDictionary
+            {
+                MergedDictionaries =
+                {
+                    new ResourceDictionary(),
+                }
+            };
+
+            var raised = false;
+
+            target.ResourcesChanged += (_, __) => raised = true;
+            ((IResourceDictionary)target.MergedDictionaries[0]).Add("foo", "bar");
+
+            Assert.True(raised);
+        }
+    }
+}

+ 0 - 78
tests/Avalonia.Styling.UnitTests/ResourceTests.cs

@@ -1,78 +0,0 @@
-// 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.Collections.Generic;
-using Avalonia.Controls;
-using Xunit;
-
-namespace Avalonia.Styling.UnitTests
-{
-    public class ResourceTests
-    {
-        [Fact]
-        public void FindStyleResource_Should_Find_Correct_Resource()
-        {
-            Border target;
-
-            var tree = new Decorator
-            {
-                Styles =
-                {
-                    new Style
-                    {
-                        Resources = new StyleResources
-                        {
-                            { "Foo", "foo resource" },
-                            { "Bar", "overridden" },
-                        }
-                    }
-                },
-                Child = target = new Border
-                {
-                    Styles =
-                    {
-                        new Style
-                        {
-                            Resources = new StyleResources
-                            {
-                                { "Bar", "again overridden" },
-                            }
-                        },
-                        new Style
-                        {
-                            Resources = new StyleResources
-                            {
-                                { "Bar", "bar resource" },
-                            }
-                        }
-                    }
-                }
-            };
-
-            Assert.Equal("foo resource", target.FindStyleResource("Foo"));
-            Assert.Equal("bar resource", target.FindStyleResource("Bar"));
-        }
-
-        [Fact]
-        public void FindStyleResource_Should_Return_UnsetValue_For_Not_Found()
-        {
-            Border target;
-
-            var tree = target = new Border
-            {
-                Styles =
-                {
-                    new Style
-                    {
-                        Resources = new StyleResources
-                        {
-                            { "Foo", "foo" },
-                        }
-                    },
-                }
-            };
-
-            Assert.Equal(AvaloniaProperty.UnsetValue, target.FindStyleResource("Baz"));
-        }
-    }
-}

+ 5 - 0
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

+ 5 - 0
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

+ 115 - 0
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);
+        }
+    }
+}

+ 36 - 0
tests/Avalonia.UnitTests/MockAssetLoader.cs

@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Platform;
+
+namespace Avalonia.UnitTests
+{
+    public class MockAssetLoader : IAssetLoader
+    {
+        private Dictionary<Uri, string> _assets;
+
+        public MockAssetLoader(params (string, string)[] assets)
+        {
+            _assets = assets.ToDictionary(x => new Uri(x.Item1, UriKind.RelativeOrAbsolute), x => x.Item2);
+        }
+
+        public bool Exists(Uri uri, Uri baseUri = null)
+        {
+            return _assets.ContainsKey(uri);
+        }
+
+        public Stream Open(Uri uri, Uri baseUri = null)
+        {
+            return new MemoryStream(Encoding.UTF8.GetBytes(_assets[uri]));
+        }
+
+        public void SetDefaultAssembly(Assembly asm)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}

+ 4 - 0
tests/Avalonia.UnitTests/TestRoot.cs

@@ -63,6 +63,10 @@ namespace Avalonia.UnitTests
 
         public bool ShowAccessKeys { get; set; }
 
+        public IStyleHost StylingParent { get; set; }
+
+        IStyleHost IStyleHost.StylingParent => StylingParent;
+
         public IRenderTarget CreateRenderTarget() => _renderTarget;
 
         public void Invalidate(Rect rect)