Browse Source

Refactored resources.

Split resource nodes into two types:

- `IResourceHost` represents controls and `Application`
- `IResourceProvider` represents resource dictionaries and styles, these are owned by `IResourceHost`s

Dynamic resources are now always resolved from an `IResourceHost`: if an `IResourceProvider` doesn't have a host then a dynamic resource in the resource provider will not be resolved. Resource providers no longer have a `ResourcesChanged`  event, instead they notify their `IResourceHost` owner of a resource change by calling the `NotifyHostedResourcesChanged` method.
pull/3957/head
Steven Kirk 6 years ago
parent
commit
42859fae89
  1. 58
      src/Avalonia.Controls/Application.cs
  2. 4
      src/Avalonia.Controls/TopLevel.cs
  3. 4
      src/Avalonia.Styling/Controls/IResourceDictionary.cs
  4. 32
      src/Avalonia.Styling/Controls/IResourceHost.cs
  5. 28
      src/Avalonia.Styling/Controls/IResourceNode.cs
  6. 41
      src/Avalonia.Styling/Controls/IResourceProvider.cs
  7. 27
      src/Avalonia.Styling/Controls/ISetResourceParent.cs
  8. 164
      src/Avalonia.Styling/Controls/ResourceDictionary.cs
  9. 103
      src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs
  10. 8
      src/Avalonia.Styling/IStyledElement.cs
  11. 57
      src/Avalonia.Styling/StyledElement.cs
  12. 2
      src/Avalonia.Styling/Styling/IStyle.cs
  13. 2
      src/Avalonia.Styling/Styling/IStyleHost.cs
  14. 71
      src/Avalonia.Styling/Styling/Style.cs
  15. 165
      src/Avalonia.Styling/Styling/Styles.cs
  16. 32
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs
  17. 52
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs
  18. 18
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs
  19. 66
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
  20. 19
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs
  21. 152
      tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs
  22. 28
      tests/Avalonia.Styling.UnitTests/StyleTests.cs
  23. 59
      tests/Avalonia.Styling.UnitTests/StyledElementTests.cs
  24. 17
      tests/Avalonia.Styling.UnitTests/StyledElementTests_Resources.cs
  25. 111
      tests/Avalonia.Styling.UnitTests/StylesTests.cs

58
src/Avalonia.Controls/Application.cs

@ -30,7 +30,7 @@ namespace Avalonia
/// method. /// method.
/// - Tracks the lifetime of the application. /// - Tracks the lifetime of the application.
/// </remarks> /// </remarks>
public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceNode public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost
{ {
/// <summary> /// <summary>
/// The application-global data templates. /// The application-global data templates.
@ -129,26 +129,13 @@ namespace Avalonia
/// </summary> /// </summary>
public IResourceDictionary Resources public IResourceDictionary Resources
{ {
get => _resources ?? (Resources = new ResourceDictionary()); get => _resources ??= new ResourceDictionary(this);
set set
{ {
Contract.Requires<ArgumentNullException>(value != null); value = value ?? throw new ArgumentNullException(nameof(value));
_resources?.RemoveOwner(this);
var hadResources = false;
if (_resources != null)
{
hadResources = _resources.Count > 0;
_resources.ResourcesChanged -= ThisResourcesChanged;
}
_resources = value; _resources = value;
_resources.ResourcesChanged += ThisResourcesChanged; _resources.AddOwner(this);
if (hadResources || _resources.Count > 0)
{
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
}
} }
} }
@ -161,23 +148,15 @@ namespace Avalonia
/// <remarks> /// <remarks>
/// Global styles apply to all windows in the application. /// Global styles apply to all windows in the application.
/// </remarks> /// </remarks>
public Styles Styles public Styles Styles => _styles ??= new Styles(this);
{
get
{
if (_styles == null)
{
_styles = new Styles(this);
_styles.ResourcesChanged += ThisResourcesChanged;
}
return _styles;
}
}
/// <inheritdoc/> /// <inheritdoc/>
bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
/// <inheritdoc/>
bool IResourceNode.HasResources => (_resources?.HasResources ?? false) ||
(((IResourceNode?)_styles)?.HasResources ?? false);
/// <summary> /// <summary>
/// Gets the styling parent of the application, which is null. /// Gets the styling parent of the application, which is null.
/// </summary> /// </summary>
@ -185,13 +164,7 @@ namespace Avalonia
/// <inheritdoc/> /// <inheritdoc/>
bool IStyleHost.IsStylesInitialized => _styles != null; bool IStyleHost.IsStylesInitialized => _styles != null;
/// <inheritdoc/>
bool IResourceProvider.HasResources => _resources?.Count > 0;
/// <inheritdoc/>
IResourceNode IResourceNode.ResourceParent => null;
/// <summary> /// <summary>
/// Application lifetime, use it for things like setting the main window and exiting the app from code /// Application lifetime, use it for things like setting the main window and exiting the app from code
/// Currently supported lifetimes are: /// Currently supported lifetimes are:
@ -219,13 +192,18 @@ namespace Avalonia
public virtual void Initialize() { } public virtual void Initialize() { }
/// <inheritdoc/> /// <inheritdoc/>
bool IResourceProvider.TryGetResource(object key, out object value) bool IResourceNode.TryGetResource(object key, out object value)
{ {
value = null; value = null;
return (_resources?.TryGetResource(key, out value) ?? false) || return (_resources?.TryGetResource(key, out value) ?? false) ||
Styles.TryGetResource(key, out value); Styles.TryGetResource(key, out value);
} }
void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e)
{
ResourcesChanged?.Invoke(this, e);
}
void IStyleHost.StylesAdded(IReadOnlyList<IStyle> styles) void IStyleHost.StylesAdded(IReadOnlyList<IStyle> styles)
{ {
_stylesAdded?.Invoke(styles); _stylesAdded?.Invoke(styles);
@ -282,8 +260,6 @@ namespace Avalonia
try try
{ {
_notifyingResourcesChanged = true; _notifyingResourcesChanged = true;
(_resources as ISetResourceParent)?.ParentResourcesChanged(e);
(_styles as ISetResourceParent)?.ParentResourcesChanged(e);
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
} }
finally finally

4
src/Avalonia.Controls/TopLevel.cs

@ -127,11 +127,11 @@ namespace Avalonia.Controls
x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty<Cursor>()) x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty<Cursor>())
.Switch().Subscribe(cursor => PlatformImpl?.SetCursor(cursor?.PlatformCursor)); .Switch().Subscribe(cursor => PlatformImpl?.SetCursor(cursor?.PlatformCursor));
if (((IStyleHost)this).StylingParent is IResourceProvider applicationResources) if (((IStyleHost)this).StylingParent is IResourceHost applicationResources)
{ {
WeakSubscriptionManager.Subscribe( WeakSubscriptionManager.Subscribe(
applicationResources, applicationResources,
nameof(IResourceProvider.ResourcesChanged), nameof(IResourceHost.ResourcesChanged),
this); this);
} }
} }

4
src/Avalonia.Styling/Controls/IResourceDictionary.cs

@ -1,11 +1,13 @@
using System.Collections.Generic; using System.Collections.Generic;
#nullable enable
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// An indexed dictionary of resources. /// An indexed dictionary of resources.
/// </summary> /// </summary>
public interface IResourceDictionary : IResourceProvider, IDictionary<object, object> public interface IResourceDictionary : IResourceProvider, IDictionary<object, object?>
{ {
/// <summary> /// <summary>
/// Gets a collection of child resource dictionaries. /// Gets a collection of child resource dictionaries.

32
src/Avalonia.Styling/Controls/IResourceHost.cs

@ -0,0 +1,32 @@
using System;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
/// Represents an element which hosts resources.
/// </summary>
/// <remarks>
/// This interface is implemented by <see cref="StyledElement"/> and `Application`.
/// </remarks>
public interface IResourceHost : IResourceNode
{
/// <summary>
/// Raised when the resources change on the element or an ancestor of the element.
/// </summary>
event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
/// <summary>
/// Notifies the resource host that one or more of its hosted resources has changed.
/// </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. It is called when the resources hosted by this element have
/// changed, and is usually called by a resource dictionary or style hosted by the element
/// in response to a resource being added or removed.
/// </remarks>
void NotifyHostedResourcesChanged(ResourcesChangedEventArgs e);
}
}

28
src/Avalonia.Styling/Controls/IResourceNode.cs

@ -1,15 +1,35 @@
using System; using System;
#nullable enable
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Represents resource provider in a tree. /// Represents an object that can be queried for resources.
/// </summary> /// </summary>
public interface IResourceNode : IResourceProvider /// <remarks>
/// The interface represents a common interface for both controls that host resources
/// (<see cref="IResourceHost"/>) and resource providers such as <see cref="ResourceDictionary"/>
/// (see <see cref="IResourceProvider"/>).
/// </remarks>
public interface IResourceNode
{ {
/// <summary> /// <summary>
/// Gets the parent resource node, if any. /// Gets a value indicating whether the object has resources.
/// </summary>
bool HasResources { get; }
/// <summary>
/// Tries to find a resource within the object.
/// </summary> /// </summary>
IResourceNode ResourceParent { get; } /// <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(object key, out object? value);
} }
} }

41
src/Avalonia.Styling/Controls/IResourceProvider.cs

@ -1,33 +1,42 @@
using System; using System;
using Avalonia.Styling;
#nullable enable
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// Represents an object that can be queried for resources. /// Represents an object that can be queried for resources but does not appear in the logical tree.
/// </summary> /// </summary>
public interface IResourceProvider /// <remarks>
/// This interface is implemented by <see cref="ResourceDictionary"/>, <see cref="Style"/> and
/// <see cref="Styles"/>
/// </remarks>
public interface IResourceProvider : IResourceNode
{ {
/// <summary> /// <summary>
/// Raised when resources in the provider are changed. /// Gets the owner of the resource provider.
/// </summary>
/// <remarks>
/// If multiple owners are added, returns the first.
/// </remarks>
IResourceHost? Owner { get; }
/// <summary>
/// Raised when the <see cref="Owner"/> of the resource provider changes.
/// </summary> /// </summary>
event EventHandler<ResourcesChangedEventArgs> ResourcesChanged; event EventHandler OwnerChanged;
/// <summary> /// <summary>
/// Gets a value indicating whether the element has resources. /// Adds an owner to the resource provider.
/// </summary> /// </summary>
bool HasResources { get; } /// <param name="owner">The owner.</param>
void AddOwner(IResourceHost owner);
/// <summary> /// <summary>
/// Tries to find a resource within the provider. /// Removes a resource provider owner.
/// </summary> /// </summary>
/// <param name="key">The resource key.</param> /// <param name="owner">The owner.</param>
/// <param name="value"> void RemoveOwner(IResourceHost owner);
/// 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(object key, out object value);
} }
} }

27
src/Avalonia.Styling/Controls/ISetResourceParent.cs

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

164
src/Avalonia.Styling/Controls/ResourceDictionary.cs

@ -1,21 +1,20 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Metadata;
#nullable enable
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
/// <summary> /// <summary>
/// An indexed dictionary of resources. /// An indexed dictionary of resources.
/// </summary> /// </summary>
public class ResourceDictionary : AvaloniaDictionary<object, object>, public class ResourceDictionary : AvaloniaDictionary<object, object?>, IResourceDictionary
IResourceDictionary,
IResourceNode,
ISetResourceParent
{ {
private IResourceNode _parent; private IResourceHost? _owner;
private AvaloniaList<IResourceProvider> _mergedDictionaries; private AvaloniaList<IResourceProvider>? _mergedDictionaries;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ResourceDictionary"/> class. /// Initializes a new instance of the <see cref="ResourceDictionary"/> class.
@ -25,10 +24,28 @@ namespace Avalonia.Controls
CollectionChanged += OnCollectionChanged; CollectionChanged += OnCollectionChanged;
} }
/// <inheritdoc/> /// <summary>
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged; /// Initializes a new instance of the <see cref="ResourceDictionary"/> class.
/// </summary>
public ResourceDictionary(IResourceHost owner)
: this()
{
Owner = owner;
}
public IResourceHost? Owner
{
get => _owner;
private set
{
if (_owner != value)
{
_owner = value;
OwnerChanged?.Invoke(this, EventArgs.Empty);
}
}
}
/// <inheritdoc/>
public IList<IResourceProvider> MergedDictionaries public IList<IResourceProvider> MergedDictionaries
{ {
get get
@ -40,71 +57,51 @@ namespace Avalonia.Controls
_mergedDictionaries.ForEachItem( _mergedDictionaries.ForEachItem(
x => x =>
{ {
if (x is ISetResourceParent setParent) if (Owner is object)
{
setParent.SetParent(this);
setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
}
if (x.HasResources)
{ {
OnResourcesChanged(); x.AddOwner(Owner);
} }
x.ResourcesChanged += MergedDictionaryResourcesChanged;
}, },
x => x =>
{ {
if (x is ISetResourceParent setParent) if (Owner is object)
{
setParent.SetParent(null);
setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
}
if (x.HasResources)
{ {
OnResourcesChanged(); x.RemoveOwner(Owner);
} }
}, null);
(x as ISetResourceParent)?.SetParent(null);
x.ResourcesChanged -= MergedDictionaryResourcesChanged;
},
() => { });
} }
return _mergedDictionaries; return _mergedDictionaries;
} }
} }
/// <inheritdoc/> bool IResourceNode.HasResources
bool IResourceProvider.HasResources
{ {
get => Count > 0 || (_mergedDictionaries?.Any(x => x.HasResources) ?? false); get
} {
if (Count > 0)
/// <inheritdoc/> {
IResourceNode IResourceNode.ResourceParent => _parent; return true;
}
/// <inheritdoc/> if (_mergedDictionaries?.Count > 0)
void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) {
{ foreach (var i in _mergedDictionaries)
NotifyMergedDictionariesResourcesChanged(e); {
ResourcesChanged?.Invoke(this, e); if (i.HasResources)
} {
return true;
}
}
}
/// <inheritdoc/> return false;
void ISetResourceParent.SetParent(IResourceNode parent)
{
if (_parent != null && parent != null)
{
throw new InvalidOperationException("The ResourceDictionary already has a parent.");
} }
_parent = parent;
} }
/// <inheritdoc/> public event EventHandler? OwnerChanged;
public bool TryGetResource(object key, out object value)
public bool TryGetResource(object key, out object? value)
{ {
if (TryGetValue(key, out value)) if (TryGetValue(key, out value))
{ {
@ -125,32 +122,63 @@ namespace Avalonia.Controls
return false; return false;
} }
private void OnResourcesChanged() void IResourceProvider.AddOwner(IResourceHost owner)
{ {
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); owner = owner ?? throw new ArgumentNullException(nameof(owner));
if (Owner != null)
{
throw new InvalidOperationException("The ResourceDictionary already has a parent.");
}
Owner = owner;
var hasResources = Count > 0;
if (_mergedDictionaries is object)
{
foreach (var i in _mergedDictionaries)
{
i.AddOwner(owner);
hasResources |= i.HasResources;
}
}
if (hasResources)
{
owner.NotifyHostedResourcesChanged(new ResourcesChangedEventArgs());
}
} }
private void NotifyMergedDictionariesResourcesChanged(ResourcesChangedEventArgs e) void IResourceProvider.RemoveOwner(IResourceHost owner)
{ {
if (_mergedDictionaries != null) owner = owner ?? throw new ArgumentNullException(nameof(owner));
if (Owner == owner)
{ {
for (var i = _mergedDictionaries.Count - 1; i >= 0; --i) Owner = null;
var hasResources = Count > 0;
if (_mergedDictionaries is object)
{ {
if (_mergedDictionaries[i] is ISetResourceParent merged) foreach (var i in _mergedDictionaries)
{ {
merged.ParentResourcesChanged(e); i.RemoveOwner(owner);
hasResources |= i.HasResources;
} }
} }
if (hasResources)
{
owner.NotifyHostedResourcesChanged(new ResourcesChangedEventArgs());
}
} }
} }
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{ {
var ev = new ResourcesChangedEventArgs(); Owner?.NotifyHostedResourcesChanged(new ResourcesChangedEventArgs());
NotifyMergedDictionariesResourcesChanged(ev);
OnResourcesChanged();
} }
private void MergedDictionaryResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnResourcesChanged();
} }
} }

103
src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs

@ -1,6 +1,9 @@
using System; using System;
using Avalonia.LogicalTree;
using Avalonia.Reactive; using Avalonia.Reactive;
#nullable enable
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
public static class ResourceNodeExtensions public static class ResourceNodeExtensions
@ -11,8 +14,11 @@ namespace Avalonia.Controls
/// <param name="control">The control.</param> /// <param name="control">The control.</param>
/// <param name="key">The resource key.</param> /// <param name="key">The resource key.</param>
/// <returns>The resource, or <see cref="AvaloniaProperty.UnsetValue"/> if not found.</returns> /// <returns>The resource, or <see cref="AvaloniaProperty.UnsetValue"/> if not found.</returns>
public static object FindResource(this IResourceNode control, object key) public static object? FindResource(this IResourceHost control, object key)
{ {
control = control ?? throw new ArgumentNullException(nameof(control));
key = key ?? throw new ArgumentNullException(nameof(key));
if (control.TryFindResource(key, out var value)) if (control.TryFindResource(key, out var value))
{ {
return value; return value;
@ -28,16 +34,16 @@ namespace Avalonia.Controls
/// <param name="key">The resource key.</param> /// <param name="key">The resource key.</param>
/// <param name="value">On return, contains the resource if found, otherwise null.</param> /// <param name="value">On return, contains the resource if found, otherwise null.</param>
/// <returns>True if the resource was found; otherwise false.</returns> /// <returns>True if the resource was found; otherwise false.</returns>
public static bool TryFindResource(this IResourceNode control, object key, out object value) public static bool TryFindResource(this IResourceHost control, object key, out object? value)
{ {
Contract.Requires<ArgumentNullException>(control != null); control = control ?? throw new ArgumentNullException(nameof(control));
Contract.Requires<ArgumentNullException>(key != null); key = key ?? throw new ArgumentNullException(nameof(key));
var current = control; IResourceHost? current = control;
while (current != null) while (current != null)
{ {
if (current is IResourceNode host) if (current is IResourceHost host)
{ {
if (host.TryGetResource(key, out value)) if (host.TryGetResource(key, out value))
{ {
@ -45,24 +51,35 @@ namespace Avalonia.Controls
} }
} }
current = current.ResourceParent; current = (current as IStyledElement)?.StylingParent as IResourceHost;
} }
value = null; value = null;
return false; return false;
} }
public static IObservable<object> GetResourceObservable(this IResourceNode target, object key) public static IObservable<object?> GetResourceObservable(this IStyledElement control, object key)
{
control = control ?? throw new ArgumentNullException(nameof(control));
key = key ?? throw new ArgumentNullException(nameof(key));
return new ResourceObservable(control, key);
}
public static IObservable<object?> GetResourceObservable(this IResourceProvider resourceProvider, object key)
{ {
return new ResourceObservable(target, key); resourceProvider = resourceProvider ?? throw new ArgumentNullException(nameof(resourceProvider));
key = key ?? throw new ArgumentNullException(nameof(key));
return new FloatingResourceObservable(resourceProvider, key);
} }
private class ResourceObservable : LightweightObservableBase<object> private class ResourceObservable : LightweightObservableBase<object?>
{ {
private readonly IResourceNode _target; private readonly IStyledElement _target;
private readonly object _key; private readonly object _key;
public ResourceObservable(IResourceNode target, object key) public ResourceObservable(IStyledElement target, object key)
{ {
_target = target; _target = target;
_key = key; _key = key;
@ -78,7 +95,7 @@ namespace Avalonia.Controls
_target.ResourcesChanged -= ResourcesChanged; _target.ResourcesChanged -= ResourcesChanged;
} }
protected override void Subscribed(IObserver<object> observer, bool first) protected override void Subscribed(IObserver<object?> observer, bool first)
{ {
observer.OnNext(_target.FindResource(_key)); observer.OnNext(_target.FindResource(_key));
} }
@ -88,5 +105,65 @@ namespace Avalonia.Controls
PublishNext(_target.FindResource(_key)); PublishNext(_target.FindResource(_key));
} }
} }
private class FloatingResourceObservable : LightweightObservableBase<object?>
{
private readonly IResourceProvider _target;
private readonly object _key;
private IResourceHost? _owner;
public FloatingResourceObservable(IResourceProvider target, object key)
{
_target = target;
_key = key;
}
protected override void Initialize()
{
_target.OwnerChanged += OwnerChanged;
_owner = _target.Owner;
}
protected override void Deinitialize()
{
_target.OwnerChanged -= OwnerChanged;
_owner = null;
}
protected override void Subscribed(IObserver<object?> observer, bool first)
{
if (_target.Owner is object)
{
observer.OnNext(_target.Owner?.FindResource(_key));
}
}
private void PublishNext()
{
PublishNext(_target.Owner?.FindResource(_key));
}
private void OwnerChanged(object sender, EventArgs e)
{
if (_owner is object)
{
_owner.ResourcesChanged -= ResourcesChanged;
}
_owner = _target.Owner;
if (_owner is object)
{
_owner.ResourcesChanged += ResourcesChanged;
}
PublishNext();
}
private void ResourcesChanged(object sender, ResourcesChangedEventArgs e)
{
PublishNext();
}
}
} }
} }

8
src/Avalonia.Styling/IStyledElement.cs

@ -9,8 +9,7 @@ namespace Avalonia
IStyleable, IStyleable,
IStyleHost, IStyleHost,
ILogical, ILogical,
IResourceProvider, IResourceHost,
IResourceNode,
IDataContextProvider IDataContextProvider
{ {
/// <summary> /// <summary>
@ -18,6 +17,11 @@ namespace Avalonia
/// </summary> /// </summary>
event EventHandler Initialized; event EventHandler Initialized;
/// <summary>
/// Raised when resources on the element are changed.
/// </summary>
event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
/// <summary> /// <summary>
/// Gets a value that indicates whether the element has finished initialization. /// Gets a value that indicates whether the element has finished initialization.
/// </summary> /// </summary>

57
src/Avalonia.Styling/StyledElement.cs

@ -212,7 +212,6 @@ namespace Avalonia
if (_styles is null) if (_styles is null)
{ {
_styles = new Styles(this); _styles = new Styles(this);
_styles.ResourcesChanged += ThisResourcesChanged;
NotifyResourcesChanged(new ResourcesChangedEventArgs()); NotifyResourcesChanged(new ResourcesChangedEventArgs());
} }
@ -225,32 +224,13 @@ namespace Avalonia
/// </summary> /// </summary>
public IResourceDictionary Resources public IResourceDictionary Resources
{ {
get => _resources ?? (Resources = new ResourceDictionary()); get => _resources ??= new ResourceDictionary(this);
set set
{ {
value = value ?? throw new ArgumentNullException(nameof(value)); value = value ?? throw new ArgumentNullException(nameof(value));
_resources?.RemoveOwner(this);
var hadResources = false;
if (_resources != null)
{
(_resources as ISetResourceParent)?.SetParent(null);
hadResources = _resources.Count > 0;
_resources.ResourcesChanged -= ThisResourcesChanged;
}
_resources = value; _resources = value;
_resources.ResourcesChanged += ThisResourcesChanged; _resources.AddOwner(this);
if (value is ISetResourceParent setParent && setParent.ResourceParent == null)
{
setParent.SetParent(this);
}
if (hadResources || _resources.Count > 0)
{
NotifyResourcesChanged(new ResourcesChangedEventArgs());
}
} }
} }
@ -312,10 +292,8 @@ namespace Avalonia
IAvaloniaReadOnlyList<ILogical> ILogical.LogicalChildren => LogicalChildren; IAvaloniaReadOnlyList<ILogical> ILogical.LogicalChildren => LogicalChildren;
/// <inheritdoc/> /// <inheritdoc/>
bool IResourceProvider.HasResources => _resources?.Count > 0 || Styles.HasResources; bool IResourceNode.HasResources => (_resources?.HasResources ?? false) ||
(((IResourceNode?)_styles)?.HasResources ?? false);
/// <inheritdoc/>
IResourceNode? IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode;
/// <inheritdoc/> /// <inheritdoc/>
IAvaloniaReadOnlyList<string> IStyleable.Classes => Classes; IAvaloniaReadOnlyList<string> IStyleable.Classes => Classes;
@ -407,7 +385,10 @@ namespace Avalonia
void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e); void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e);
/// <inheritdoc/> /// <inheritdoc/>
bool IResourceProvider.TryGetResource(object key, out object? value) void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e);
/// <inheritdoc/>
bool IResourceNode.TryGetResource(object key, out object? value)
{ {
value = null; value = null;
return (_resources?.TryGetResource(key, out value) ?? false) || return (_resources?.TryGetResource(key, out value) ?? false) ||
@ -446,6 +427,7 @@ namespace Avalonia
{ {
old.ResourcesChanged -= ThisResourcesChanged; old.ResourcesChanged -= ThisResourcesChanged;
} }
if (Parent != null) if (Parent != null)
{ {
Parent.ResourcesChanged += ThisResourcesChanged; Parent.ResourcesChanged += ThisResourcesChanged;
@ -804,24 +786,9 @@ namespace Avalonia
} }
} }
private void NotifyResourcesChanged(ResourcesChangedEventArgs e) private void NotifyResourcesChanged(ResourcesChangedEventArgs? e = null)
{ {
if (_notifyingResourcesChanged) ResourcesChanged?.Invoke(this, e ?? new ResourcesChangedEventArgs());
{
return;
}
try
{
_notifyingResourcesChanged = true;
(_resources as ISetResourceParent)?.ParentResourcesChanged(e);
(_styles as ISetResourceParent)?.ParentResourcesChanged(e);
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
}
finally
{
_notifyingResourcesChanged = false;
}
} }
private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e) private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e)

2
src/Avalonia.Styling/Styling/IStyle.cs

@ -8,7 +8,7 @@ namespace Avalonia.Styling
/// <summary> /// <summary>
/// Defines the interface for styles. /// Defines the interface for styles.
/// </summary> /// </summary>
public interface IStyle : IResourceNode public interface IStyle
{ {
/// <summary> /// <summary>
/// Gets a collection of child styles. /// Gets a collection of child styles.

2
src/Avalonia.Styling/Styling/IStyleHost.cs

@ -27,7 +27,7 @@ namespace Avalonia.Styling
/// <summary> /// <summary>
/// Gets the parent style host element. /// Gets the parent style host element.
/// </summary> /// </summary>
IStyleHost StylingParent { get; } IStyleHost? StylingParent { get; }
/// <summary> /// <summary>
/// Called when styles are added to <see cref="Styles"/> or a nested styles collection. /// Called when styles are added to <see cref="Styles"/> or a nested styles collection.

71
src/Avalonia.Styling/Styling/Style.cs

@ -11,9 +11,9 @@ namespace Avalonia.Styling
/// <summary> /// <summary>
/// Defines a style. /// Defines a style.
/// </summary> /// </summary>
public class Style : AvaloniaObject, IStyle, ISetResourceParent public class Style : AvaloniaObject, IStyle, IResourceProvider
{ {
private IResourceNode? _parent; private IResourceHost? _owner;
private IResourceDictionary? _resources; private IResourceDictionary? _resources;
private List<ISetter>? _setters; private List<ISetter>? _setters;
private List<IAnimation>? _animations; private List<IAnimation>? _animations;
@ -34,8 +34,18 @@ namespace Avalonia.Styling
Selector = selector(null); Selector = selector(null);
} }
/// <inheritdoc/> public IResourceHost? Owner
public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged; {
get => _owner;
private set
{
if (_owner != value)
{
_owner = value;
OwnerChanged?.Invoke(this, EventArgs.Empty);
}
}
}
/// <summary> /// <summary>
/// Gets or sets a dictionary of style resources. /// Gets or sets a dictionary of style resources.
@ -47,20 +57,18 @@ namespace Avalonia.Styling
{ {
value = value ?? throw new ArgumentNullException(nameof(value)); value = value ?? throw new ArgumentNullException(nameof(value));
var hadResources = false; var hadResources = _resources?.HasResources ?? false;
if (_resources != null)
{
hadResources = _resources.HasResources;
_resources.ResourcesChanged -= ResourceDictionaryChanged;
}
_resources = value; _resources = value;
_resources.ResourcesChanged += ResourceDictionaryChanged;
if (hadResources || _resources.HasResources) if (Owner is object)
{ {
((ISetResourceParent)this).ParentResourcesChanged(new ResourcesChangedEventArgs()); _resources.AddOwner(Owner);
if (hadResources || _resources.HasResources)
{
Owner.NotifyHostedResourcesChanged(new ResourcesChangedEventArgs());
}
} }
} }
} }
@ -81,15 +89,11 @@ namespace Avalonia.Styling
/// </summary> /// </summary>
public IList<IAnimation> Animations => _animations ??= new List<IAnimation>(); public IList<IAnimation> Animations => _animations ??= new List<IAnimation>();
/// <inheritdoc/> bool IResourceNode.HasResources => _resources?.Count > 0;
IResourceNode? IResourceNode.ResourceParent => _parent;
/// <inheritdoc/>
bool IResourceProvider.HasResources => _resources?.Count > 0;
IReadOnlyList<IStyle> IStyle.Children => Array.Empty<IStyle>(); IReadOnlyList<IStyle> IStyle.Children => Array.Empty<IStyle>();
/// <inheritdoc/> public event EventHandler? OwnerChanged;
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
{ {
target = target ?? throw new ArgumentNullException(nameof(target)); target = target ?? throw new ArgumentNullException(nameof(target));
@ -107,7 +111,6 @@ namespace Avalonia.Styling
return match.Result; return match.Result;
} }
/// <inheritdoc/>
public bool TryGetResource(object key, out object? result) public bool TryGetResource(object key, out object? result)
{ {
result = null; result = null;
@ -130,26 +133,28 @@ namespace Avalonia.Styling
} }
} }
/// <inheritdoc/> void IResourceProvider.AddOwner(IResourceHost owner)
void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
{ {
ResourcesChanged?.Invoke(this, e); owner = owner ?? throw new ArgumentNullException(nameof(owner));
}
/// <inheritdoc/> if (Owner != null)
void ISetResourceParent.SetParent(IResourceNode parent)
{
if (_parent != null && parent != null)
{ {
throw new InvalidOperationException("The Style already has a parent."); throw new InvalidOperationException("The Style already has a parent.");
} }
_parent = parent; Owner = owner;
_resources?.AddOwner(owner);
} }
private void ResourceDictionaryChanged(object sender, ResourcesChangedEventArgs e) void IResourceProvider.RemoveOwner(IResourceHost owner)
{ {
ResourcesChanged?.Invoke(this, e); owner = owner ?? throw new ArgumentNullException(nameof(owner));
if (Owner == owner)
{
Owner = null;
_resources?.RemoveOwner(owner);
}
} }
} }
} }

165
src/Avalonia.Styling/Styling/Styles.cs

@ -2,7 +2,6 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
@ -13,10 +12,13 @@ namespace Avalonia.Styling
/// <summary> /// <summary>
/// A style that consists of a number of child styles. /// A style that consists of a number of child styles.
/// </summary> /// </summary>
public class Styles : AvaloniaObject, IAvaloniaList<IStyle>, IStyle, ISetResourceParent public class Styles : AvaloniaObject,
IAvaloniaList<IStyle>,
IStyle,
IResourceProvider
{ {
private readonly AvaloniaList<IStyle> _styles = new AvaloniaList<IStyle>(); private readonly AvaloniaList<IStyle> _styles = new AvaloniaList<IStyle>();
private IResourceNode? _parent; private IResourceHost? _owner;
private IResourceDictionary? _resources; private IResourceDictionary? _resources;
private Dictionary<Type, List<IStyle>?>? _cache; private Dictionary<Type, List<IStyle>?>? _cache;
private bool _notifyingResourcesChanged; private bool _notifyingResourcesChanged;
@ -27,22 +29,29 @@ namespace Avalonia.Styling
_styles.CollectionChanged += OnCollectionChanged; _styles.CollectionChanged += OnCollectionChanged;
} }
public Styles(IResourceNode parent) public Styles(IResourceHost owner)
: this() : this()
{ {
_parent = parent; Owner = owner;
} }
public event NotifyCollectionChangedEventHandler? CollectionChanged; public event NotifyCollectionChangedEventHandler? CollectionChanged;
public event EventHandler? OwnerChanged;
/// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged;
/// <inheritdoc/>
public int Count => _styles.Count; public int Count => _styles.Count;
/// <inheritdoc/> public IResourceHost? Owner
public bool HasResources => _resources?.Count > 0 || this.Any(x => x.HasResources); {
get => _owner;
private set
{
if (_owner != value)
{
_owner = value;
OwnerChanged?.Invoke(this, EventArgs.Empty);
}
}
}
/// <summary> /// <summary>
/// Gets or sets a dictionary of style resources. /// Gets or sets a dictionary of style resources.
@ -54,43 +63,53 @@ namespace Avalonia.Styling
{ {
value = value ?? throw new ArgumentNullException(nameof(Resources)); value = value ?? throw new ArgumentNullException(nameof(Resources));
var hadResources = false; if (Owner is object)
if (_resources != null)
{ {
hadResources = _resources.Count > 0; _resources?.RemoveOwner(Owner);
_resources.ResourcesChanged -= NotifyResourcesChanged;
} }
_resources = value; _resources = value;
_resources.ResourcesChanged += NotifyResourcesChanged;
if (hadResources || _resources.Count > 0) if (Owner is object)
{ {
((ISetResourceParent)this).ParentResourcesChanged(new ResourcesChangedEventArgs()); _resources.AddOwner(Owner);
} }
} }
} }
/// <inheritdoc/>
IResourceNode? IResourceNode.ResourceParent => _parent;
/// <inheritdoc/>
bool ICollection<IStyle>.IsReadOnly => false; bool ICollection<IStyle>.IsReadOnly => false;
/// <inheritdoc/> bool IResourceNode.HasResources
{
get
{
if (Count > 0)
{
return true;
}
foreach (var i in this)
{
if (i is IResourceProvider p && p.HasResources)
{
return true;
}
}
return false;
}
}
IStyle IReadOnlyList<IStyle>.this[int index] => _styles[index]; IStyle IReadOnlyList<IStyle>.this[int index] => _styles[index];
IReadOnlyList<IStyle> IStyle.Children => this; IReadOnlyList<IStyle> IStyle.Children => this;
/// <inheritdoc/>
public IStyle this[int index] public IStyle this[int index]
{ {
get => _styles[index]; get => _styles[index];
set => _styles[index] = value; set => _styles[index] = value;
} }
/// <inheritdoc/>
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
{ {
_cache ??= new Dictionary<Type, List<IStyle>?>(); _cache ??= new Dictionary<Type, List<IStyle>?>();
@ -142,7 +161,7 @@ namespace Avalonia.Styling
for (var i = Count - 1; i >= 0; --i) for (var i = Count - 1; i >= 0; --i)
{ {
if (this[i].TryGetResource(key, out value)) if (this[i] is IResourceProvider p && p.TryGetResource(key, out value))
{ {
return true; return true;
} }
@ -203,20 +222,29 @@ namespace Avalonia.Styling
IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator();
/// <inheritdoc/> /// <inheritdoc/>
void ISetResourceParent.SetParent(IResourceNode parent) void IResourceProvider.AddOwner(IResourceHost owner)
{ {
if (_parent != null && parent != null) owner = owner ?? throw new ArgumentNullException(nameof(owner));
if (Owner != null)
{ {
throw new InvalidOperationException("The Style already has a parent."); throw new InvalidOperationException("The Styles already has a owner.");
} }
_parent = parent; Owner = owner;
_resources?.AddOwner(owner);
} }
/// <inheritdoc/> /// <inheritdoc/>
void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) void IResourceProvider.RemoveOwner(IResourceHost owner)
{ {
NotifyResourcesChanged(e); owner = owner ?? throw new ArgumentNullException(nameof(owner));
if (Owner == owner)
{
Owner = null;
_resources?.RemoveOwner(owner);
}
} }
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
@ -241,22 +269,15 @@ namespace Avalonia.Styling
{ {
var style = (IStyle)items[i]; var style = (IStyle)items[i];
if (style.ResourceParent == null && style is ISetResourceParent setParent) if (Owner is object && style is IResourceProvider resourceProvider)
{ {
setParent.SetParent(this); resourceProvider.AddOwner(Owner);
setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
} }
if (style.HasResources)
{
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
}
style.ResourcesChanged += NotifyResourcesChanged;
_cache = null; _cache = null;
} }
GetHost()?.StylesAdded(ToReadOnlyList<IStyle>(items)); (Owner as IStyleHost)?.StylesAdded(ToReadOnlyList<IStyle>(items));
} }
void Remove(IList items) void Remove(IList items)
@ -265,22 +286,15 @@ namespace Avalonia.Styling
{ {
var style = (IStyle)items[i]; var style = (IStyle)items[i];
if (style.ResourceParent == this && style is ISetResourceParent setParent) if (Owner is object && style is IResourceProvider resourceProvider)
{ {
setParent.SetParent(null); resourceProvider.RemoveOwner(Owner);
setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
} }
if (style.HasResources)
{
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
}
style.ResourcesChanged -= NotifyResourcesChanged;
_cache = null; _cache = null;
} }
GetHost()?.StylesRemoved(ToReadOnlyList<IStyle>(items)); (Owner as IStyleHost)?.StylesRemoved(ToReadOnlyList<IStyle>(items));
} }
switch (e.Action) switch (e.Action)
@ -301,50 +315,5 @@ namespace Avalonia.Styling
CollectionChanged?.Invoke(this, e); CollectionChanged?.Invoke(this, e);
} }
private IStyleHost? GetHost()
{
var node = _parent;
while (node != null)
{
if (node is IStyleHost host)
{
return host;
}
node = node.ResourceParent;
}
return null;
}
private void NotifyResourcesChanged(object sender, ResourcesChangedEventArgs e)
{
NotifyResourcesChanged(e);
}
private void NotifyResourcesChanged(ResourcesChangedEventArgs e)
{
if (_notifyingResourcesChanged)
{
return;
}
try
{
_notifyingResourcesChanged = true;
foreach (var child in this)
{
(child as ISetResourceParent)?.ParentResourcesChanged(e);
}
ResourcesChanged?.Invoke(this, e);
}
finally
{
_notifyingResourcesChanged = false;
}
}
} }
} }

32
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs

@ -1,15 +1,15 @@
using System; using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data; using Avalonia.Data;
#nullable enable
namespace Avalonia.Markup.Xaml.MarkupExtensions namespace Avalonia.Markup.Xaml.MarkupExtensions
{ {
public class DynamicResourceExtension : IBinding public class DynamicResourceExtension : IBinding
{ {
private IResourceNode _anchor; private IStyledElement? _anchor;
private IResourceProvider? _resourceProvider;
public DynamicResourceExtension() public DynamicResourceExtension()
{ {
@ -20,32 +20,46 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
ResourceKey = resourceKey; ResourceKey = resourceKey;
} }
public object ResourceKey { get; set; } public object? ResourceKey { get; set; }
public IBinding ProvideValue(IServiceProvider serviceProvider) public IBinding ProvideValue(IServiceProvider serviceProvider)
{ {
var provideTarget = serviceProvider.GetService<IProvideValueTarget>(); var provideTarget = serviceProvider.GetService<IProvideValueTarget>();
if (!(provideTarget.TargetObject is IResourceNode)) if (!(provideTarget.TargetObject is IStyledElement))
{ {
_anchor = serviceProvider.GetFirstParent<IResourceNode>(); _anchor = serviceProvider.GetFirstParent<IStyledElement>();
if (_anchor is null)
{
_resourceProvider = serviceProvider.GetFirstParent<IResourceProvider>();
}
} }
return this; return this;
} }
InstancedBinding IBinding.Initiate( InstancedBinding? IBinding.Initiate(
IAvaloniaObject target, IAvaloniaObject target,
AvaloniaProperty targetProperty, AvaloniaProperty targetProperty,
object anchor, object anchor,
bool enableDataValidation) bool enableDataValidation)
{ {
var control = target as IResourceNode ?? _anchor; if (ResourceKey is null)
{
return null;
}
var control = target as IStyledElement ?? _anchor as IStyledElement;
if (control != null) if (control != null)
{ {
return InstancedBinding.OneWay(control.GetResourceObservable(ResourceKey)); return InstancedBinding.OneWay(control.GetResourceObservable(ResourceKey));
} }
else if (_resourceProvider is object)
{
return InstancedBinding.OneWay(_resourceProvider.GetResourceObservable(ResourceKey));
}
return null; return null;
} }

52
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs

@ -2,18 +2,17 @@
using System.ComponentModel; using System.ComponentModel;
using Avalonia.Controls; using Avalonia.Controls;
#nullable enable
namespace Avalonia.Markup.Xaml.MarkupExtensions namespace Avalonia.Markup.Xaml.MarkupExtensions
{ {
/// <summary> /// <summary>
/// Loads a resource dictionary from a specified URL. /// Loads a resource dictionary from a specified URL.
/// </summary> /// </summary>
public class ResourceInclude : IResourceNode, ISetResourceParent public class ResourceInclude : IResourceProvider
{ {
private IResourceNode _parent; private Uri? _baseUri;
private Uri _baseUri; private IResourceDictionary? _loaded;
private IResourceDictionary _loaded;
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
/// <summary> /// <summary>
/// Gets the loaded resource dictionary. /// Gets the loaded resource dictionary.
@ -26,53 +25,34 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
{ {
var loader = new AvaloniaXamlLoader(); var loader = new AvaloniaXamlLoader();
_loaded = (IResourceDictionary)loader.Load(Source, _baseUri); _loaded = (IResourceDictionary)loader.Load(Source, _baseUri);
(_loaded as ISetResourceParent)?.SetParent(this);
_loaded.ResourcesChanged += ResourcesChanged;
if (_loaded.HasResources)
{
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
}
} }
return _loaded; return _loaded;
} }
} }
public IResourceHost? Owner => Loaded.Owner;
/// <summary> /// <summary>
/// Gets or sets the source URL. /// Gets or sets the source URL.
/// </summary> /// </summary>
public Uri Source { get; set; } public Uri? Source { get; set; }
/// <inhertidoc/>
bool IResourceProvider.HasResources => Loaded.HasResources;
/// <inhertidoc/> bool IResourceNode.HasResources => Loaded.HasResources;
IResourceNode IResourceNode.ResourceParent => _parent;
/// <inhertidoc/> public event EventHandler OwnerChanged
bool IResourceProvider.TryGetResource(object key, out object value)
{ {
return Loaded.TryGetResource(key, out value); add => Loaded.OwnerChanged += value;
remove => Loaded.OwnerChanged -= value;
} }
/// <inhertidoc/> bool IResourceNode.TryGetResource(object key, out object? value)
void ISetResourceParent.SetParent(IResourceNode parent)
{ {
if (_parent != null && parent != null) return Loaded.TryGetResource(key, out value);
{
throw new InvalidOperationException("The ResourceInclude already has a parent.");
}
_parent = parent;
} }
/// <inhertidoc/> void IResourceProvider.AddOwner(IResourceHost owner) => Loaded.AddOwner(owner);
void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) void IResourceProvider.RemoveOwner(IResourceHost owner) => Loaded.RemoveOwner(owner);
{
(_loaded as ISetResourceParent)?.ParentResourcesChanged(e);
}
public ResourceInclude ProvideValue(IServiceProvider serviceProvider) public ResourceInclude ProvideValue(IServiceProvider serviceProvider)
{ {

18
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs

@ -4,6 +4,7 @@ using System.ComponentModel;
using System.Reflection; using System.Reflection;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Data; using Avalonia.Markup.Data;
using Avalonia.Markup.Xaml.XamlIl.Runtime;
namespace Avalonia.Markup.Xaml.MarkupExtensions namespace Avalonia.Markup.Xaml.MarkupExtensions
{ {
@ -22,15 +23,22 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
public object ProvideValue(IServiceProvider serviceProvider) public object ProvideValue(IServiceProvider serviceProvider)
{ {
// Look upwards though the ambient context for IResourceProviders which might be able var stack = serviceProvider.GetService<IAvaloniaXamlIlParentStackProvider>();
// to give us the resource.
foreach (var resourceProvider in serviceProvider.GetParents<IResourceNode>()) // Look upwards though the ambient context for IResourceHosts and IResourceProviders
// which might be able to give us the resource.
foreach (var e in stack.Parents)
{ {
if (resourceProvider.TryGetResource(ResourceKey, out var value)) object value;
if (e is IResourceHost host && host.TryGetResource(ResourceKey, out value))
{
return value;
}
else if (e is IResourceProvider provider && provider.TryGetResource(ResourceKey, out value))
{ {
return value; return value;
} }
} }
// The resource still hasn't been found, so add a delayed one-time binding. // The resource still hasn't been found, so add a delayed one-time binding.

66
src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs

@ -10,32 +10,30 @@ namespace Avalonia.Markup.Xaml.Styling
/// <summary> /// <summary>
/// Includes a style from a URL. /// Includes a style from a URL.
/// </summary> /// </summary>
public class StyleInclude : IStyle, ISetResourceParent public class StyleInclude : IStyle, IResourceProvider
{ {
private Uri _baseUri; private readonly Uri _baseUri;
private IStyle[]? _loaded; private IStyle[]? _loaded;
private IResourceNode? _parent;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="StyleInclude"/> class. /// Initializes a new instance of the <see cref="StyleInclude"/> class.
/// </summary> /// </summary>
/// <param name="baseUri"></param> /// <param name="baseUri">The base URL for the XAML context.</param>
public StyleInclude(Uri baseUri) public StyleInclude(Uri baseUri)
{ {
_baseUri = baseUri; _baseUri = baseUri;
} }
/// <summary>
/// Initializes a new instance of the <see cref="StyleInclude"/> class.
/// </summary>
/// <param name="serviceProvider">The XAML service provider.</param>
public StyleInclude(IServiceProvider serviceProvider) public StyleInclude(IServiceProvider serviceProvider)
{ {
_baseUri = serviceProvider.GetContextBaseUri(); _baseUri = serviceProvider.GetContextBaseUri();
} }
/// <inheritdoc/> public IResourceHost? Owner => (Loaded as IResourceProvider)?.Owner;
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged
{
add {}
remove {}
}
/// <summary> /// <summary>
/// Gets or sets the source URL. /// Gets or sets the source URL.
@ -53,7 +51,6 @@ namespace Avalonia.Markup.Xaml.Styling
{ {
var loader = new AvaloniaXamlLoader(); var loader = new AvaloniaXamlLoader();
var loaded = (IStyle)loader.Load(Source, _baseUri); var loaded = (IStyle)loader.Load(Source, _baseUri);
(loaded as ISetResourceParent)?.SetParent(this);
_loaded = new[] { loaded }; _loaded = new[] { loaded };
} }
@ -61,35 +58,42 @@ namespace Avalonia.Markup.Xaml.Styling
} }
} }
/// <inheritdoc/> bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false;
bool IResourceProvider.HasResources => Loaded.HasResources;
/// <inheritdoc/>
IResourceNode? IResourceNode.ResourceParent => _parent;
IReadOnlyList<IStyle> IStyle.Children => _loaded ?? Array.Empty<IStyle>(); IReadOnlyList<IStyle> IStyle.Children => _loaded ?? Array.Empty<IStyle>();
/// <inheritdoc/> public event EventHandler OwnerChanged
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host);
/// <inheritdoc/>
public bool TryGetResource(object key, out object? value) => Loaded.TryGetResource(key, out value);
/// <inheritdoc/>
void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
{ {
(Loaded as ISetResourceParent)?.ParentResourcesChanged(e); add
{
if (Loaded is IResourceProvider rp)
{
rp.OwnerChanged += value;
}
}
remove
{
if (Loaded is IResourceProvider rp)
{
rp.OwnerChanged -= value;
}
}
} }
/// <inheritdoc/> public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host);
void ISetResourceParent.SetParent(IResourceNode parent)
public bool TryGetResource(object key, out object? value)
{ {
if (_parent != null && parent != null) if (Loaded is IResourceProvider p)
{ {
throw new InvalidOperationException("The Style already has a parent."); return p.TryGetResource(key, out value);
} }
_parent = parent; value = null;
return false;
} }
void IResourceProvider.AddOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.AddOwner(owner);
void IResourceProvider.RemoveOwner(IResourceHost owner) => (Loaded as IResourceProvider)?.RemoveOwner(owner);
} }
} }

19
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs

@ -30,25 +30,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
} }
} }
[Fact]
public void DynamicResource_Works_In_ResourceDictionary()
{
using (StyledWindow())
{
var xaml = @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Color x:Key='Red'>Red</Color>
<SolidColorBrush x:Key='RedBrush' Color='{DynamicResource Red}'/>
</ResourceDictionary>";
var loader = new AvaloniaXamlLoader();
var resources = (ResourceDictionary)loader.Load(xaml);
var brush = (SolidColorBrush)resources["RedBrush"];
Assert.Equal(Colors.Red, brush.Color);
}
}
[Fact] [Fact]
public void DynamicResource_Finds_Resource_In_Parent_Dictionary() public void DynamicResource_Finds_Resource_In_Parent_Dictionary()
{ {

152
tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs

@ -7,6 +7,20 @@ namespace Avalonia.Styling.UnitTests
{ {
public class ResourceDictionaryTests public class ResourceDictionaryTests
{ {
[Fact]
public void Cannot_Add_Null_Key()
{
var target = new ResourceDictionary();
Assert.Throws<ArgumentNullException>(() => target.Add(null, "null"));
}
[Fact]
public void Can_Add_Null_Value()
{
var target = new ResourceDictionary();
target.Add("null", null);
}
[Fact] [Fact]
public void TryGetResource_Should_Find_Resource() public void TryGetResource_Should_Find_Resource()
{ {
@ -77,66 +91,97 @@ namespace Avalonia.Styling.UnitTests
} }
[Fact] [Fact]
public void ResourcesChanged_Should_Be_Raised_On_Resource_Add() public void NotifyHostedResourcesChanged_Should_Be_Called_On_AddOwner()
{ {
var target = new ResourceDictionary(); var host = new Mock<IResourceHost>();
var raised = false; var target = new ResourceDictionary { { "foo", "bar" } };
((IResourceProvider)target).AddOwner(host.Object);
target.ResourcesChanged += (_, __) => raised = true; host.Verify(x => x.NotifyHostedResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()));
}
[Fact]
public void NotifyHostedResourcesChanged_Should_Be_Called_On_RemoveOwner()
{
var host = new Mock<IResourceHost>();
var target = new ResourceDictionary { { "foo", "bar" } };
((IResourceProvider)target).AddOwner(host.Object);
host.ResetCalls();
((IResourceProvider)target).RemoveOwner(host.Object);
host.Verify(x => x.NotifyHostedResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()));
}
[Fact]
public void NotifyHostedResourcesChanged_Should_Be_Called_On_Resource_Add()
{
var host = new Mock<IResourceHost>();
var target = new ResourceDictionary(host.Object);
host.ResetCalls();
target.Add("foo", "bar"); target.Add("foo", "bar");
Assert.True(raised); host.Verify(x => x.NotifyHostedResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()));
} }
[Fact] [Fact]
public void ResourcesChanged_Should_Be_Raised_On_MergedDictionary_Add() public void NotifyHostedResourcesChanged_Should_Be_Called_On_MergedDictionary_Add()
{ {
var target = new ResourceDictionary(); var host = new Mock<IResourceHost>();
var raised = false; var target = new ResourceDictionary(host.Object);
target.ResourcesChanged += (_, __) => raised = true; host.ResetCalls();
target.MergedDictionaries.Add(new ResourceDictionary target.MergedDictionaries.Add(new ResourceDictionary
{ {
{ "foo", "bar" }, { "foo", "bar" },
}); });
Assert.True(raised); host.Verify(
x => x.NotifyHostedResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
Times.Once);
} }
[Fact] [Fact]
public void ResourcesChanged_Should_Not_Be_Raised_On_Empty_MergedDictionary_Add() public void NotifyHostedResourcesChanged_Should_Not_Be_Called_On_Empty_MergedDictionary_Add()
{ {
var target = new ResourceDictionary(); var host = new Mock<IResourceHost>();
var raised = false; var target = new ResourceDictionary(host.Object);
target.ResourcesChanged += (_, __) => raised = true; host.ResetCalls();
target.MergedDictionaries.Add(new ResourceDictionary()); target.MergedDictionaries.Add(new ResourceDictionary());
Assert.False(raised); host.Verify(
x => x.NotifyHostedResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
Times.Never);
} }
[Fact] [Fact]
public void ResourcesChanged_Should_Be_Raised_On_MergedDictionary_Remove() public void NotifyHostedResourcesChanged_Should_Be_Called_On_MergedDictionary_Remove()
{ {
var target = new ResourceDictionary var host = new Mock<IResourceHost>();
var target = new ResourceDictionary(host.Object)
{ {
MergedDictionaries = MergedDictionaries =
{ {
new ResourceDictionary { { "foo", "bar" } }, new ResourceDictionary { { "foo", "bar" } },
} }
}; };
var raised = false;
target.ResourcesChanged += (_, __) => raised = true; host.ResetCalls();
target.MergedDictionaries.RemoveAt(0); target.MergedDictionaries.RemoveAt(0);
Assert.True(raised); host.Verify(
x => x.NotifyHostedResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
Times.Once);
} }
[Fact] [Fact]
public void ResourcesChanged_Should_Be_Raised_On_MergedDictionary_Resource_Add() public void NotifyHostedResourcesChanged_Should_Be_Called_On_MergedDictionary_Resource_Add()
{ {
var target = new ResourceDictionary var host = new Mock<IResourceHost>();
var target = new ResourceDictionary(host.Object)
{ {
MergedDictionaries = MergedDictionaries =
{ {
@ -144,44 +189,63 @@ namespace Avalonia.Styling.UnitTests
} }
}; };
var raised = false; host.ResetCalls();
target.ResourcesChanged += (_, __) => raised = true;
((IResourceDictionary)target.MergedDictionaries[0]).Add("foo", "bar"); ((IResourceDictionary)target.MergedDictionaries[0]).Add("foo", "bar");
Assert.True(raised); host.Verify(
x => x.NotifyHostedResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
Times.Once);
} }
[Fact] [Fact]
public void MergedDictionary_ParentResourcesChanged_Should_Be_Called_On_Resource_Add() public void Sets_Added_MergedDictionary_Owner()
{ {
var target = new ResourceDictionary(); var host = new Mock<IResourceHost>();
var merged = new Mock<ISetResourceParent>();
target.MergedDictionaries.Add(merged.Object); var target = new ResourceDictionary(host.Object);
merged.ResetCalls(); target.MergedDictionaries.Add(new ResourceDictionary());
target.Add("foo", "bar"); Assert.Same(host.Object, target.Owner);
Assert.Same(host.Object, ((ResourceDictionary)target.MergedDictionaries[0]).Owner);
}
merged.Verify( [Fact]
x => x.ParentResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()), public void AddOwner_Sets_MergedDictionary_Owner()
Times.Once); {
var host = new Mock<IResourceHost>();
var target = new ResourceDictionary
{
MergedDictionaries =
{
new ResourceDictionary(),
}
};
((IResourceProvider)target).AddOwner(host.Object);
Assert.Same(host.Object, target.Owner);
Assert.Same(host.Object, ((ResourceDictionary)target.MergedDictionaries[0]).Owner);
} }
[Fact] [Fact]
public void MergedDictionary_ParentResourcesChanged_Should_Be_Called_On_NotifyResourceChanged() public void RemoveOwner_Clears_MergedDictionary_Owner()
{ {
var target = new ResourceDictionary(); var host = new Mock<IResourceHost>();
var merged = new Mock<ISetResourceParent>();
target.MergedDictionaries.Add(merged.Object); var target = new ResourceDictionary(host.Object)
merged.ResetCalls(); {
MergedDictionaries =
{
new ResourceDictionary(),
}
};
((ISetResourceParent)target).ParentResourcesChanged(new ResourcesChangedEventArgs()); ((IResourceProvider)target).RemoveOwner(host.Object);
merged.Verify( Assert.Null(target.Owner);
x => x.ParentResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()), Assert.Null(((ResourceDictionary)target.MergedDictionaries[0]).Owner);
Times.Once);
} }
} }
} }

28
tests/Avalonia.Styling.UnitTests/StyleTests.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Moq;
using Xunit; using Xunit;
namespace Avalonia.Styling.UnitTests namespace Avalonia.Styling.UnitTests
@ -420,6 +421,33 @@ namespace Avalonia.Styling.UnitTests
} }
} }
[Fact]
public void Should_Set_Owner_On_Assigned_Resources()
{
var host = new Mock<IResourceHost>();
var target = new Style();
((IResourceProvider)target).AddOwner(host.Object);
var resources = new Mock<IResourceDictionary>();
target.Resources = resources.Object;
resources.Verify(x => x.AddOwner(host.Object), Times.Once);
}
[Fact]
public void Should_Set_Owner_On_Assigned_Resources_2()
{
var host = new Mock<IResourceHost>();
var target = new Style();
var resources = new Mock<IResourceDictionary>();
target.Resources = resources.Object;
host.ResetCalls();
((IResourceProvider)target).AddOwner(host.Object);
resources.Verify(x => x.AddOwner(host.Object), Times.Once);
}
private class Class1 : Control private class Class1 : Control
{ {
public static readonly StyledProperty<string> FooProperty = public static readonly StyledProperty<string> FooProperty =

59
tests/Avalonia.Styling.UnitTests/StyledElementTests.cs

@ -492,20 +492,20 @@ namespace Avalonia.Styling.UnitTests
} }
[Fact] [Fact]
public void Resources_Parent_Is_Set() public void Resources_Owner_Is_Set()
{ {
var target = new TestControl(); var target = new TestControl();
Assert.Same(target, ((IResourceNode)target.Resources).ResourceParent); Assert.Same(target, ((ResourceDictionary)target.Resources).Owner);
} }
[Fact] [Fact]
public void Assigned_Resources_Parent_Is_Set() public void Assigned_Resources_Parent_Is_Set()
{ {
var resources = new ResourceDictionary(); var resources = new Mock<IResourceDictionary>();
var target = new TestControl { Resources = resources }; var target = new TestControl { Resources = resources.Object };
Assert.Same(target, ((IResourceNode)resources).ResourceParent); resources.Verify(x => x.AddOwner(target));
} }
[Fact] [Fact]
@ -522,58 +522,25 @@ namespace Avalonia.Styling.UnitTests
} }
[Fact] [Fact]
public void Changing_Parent_Notifies_Resources_ParentResourcesChanged() public void Styles_Owner_Is_Set()
{
var resources = new Mock<IResourceDictionary>();
var setResourceParent = resources.As<ISetResourceParent>();
var target = new TestControl { Resources = resources.Object };
var parent = new Decorator { Resources = { { "foo", "bar" } } };
setResourceParent.ResetCalls();
parent.Child = target;
setResourceParent.Verify(x =>
x.ParentResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
Times.Once);
}
[Fact]
public void Styles_Parent_Is_Set()
{ {
var target = new TestControl(); var target = new TestControl();
Assert.Same(target, ((IResourceNode)target.Styles).ResourceParent); Assert.Same(target, target.Styles.Owner);
} }
[Fact] [Fact]
public void Changing_Parent_Notifies_Styles_ParentResourcesChanged() public void Changing_Parent_Raises_ResourcesChanged()
{ {
var style = new Mock<IStyle>(); var target = new TestControl();
var setResourceParent = style.As<ISetResourceParent>();
var target = new TestControl { Styles = { style.Object } };
var parent = new Decorator { Resources = { { "foo", "bar" } } }; var parent = new Decorator { Resources = { { "foo", "bar" } } };
var raised = 0;
setResourceParent.ResetCalls(); target.ResourcesChanged += (s, e) => ++raised;
parent.Child = target;
setResourceParent.Verify(x =>
x.ParentResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
Times.Once);
}
[Fact]
public void Changing_Resources_Notifies_Styles()
{
var style = new Mock<IStyle>();
var setResourceParent = style.As<ISetResourceParent>();
var target = new TestControl { Styles = { style.Object } };
setResourceParent.ResetCalls(); parent.Child = target;
target.Resources.Add("foo", "bar");
setResourceParent.Verify(x => Assert.Equal(1, raised);
x.ParentResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
Times.Once);
} }
[Fact] [Fact]

17
tests/Avalonia.Styling.UnitTests/StyledElementTests_Resources.cs

@ -217,23 +217,6 @@ namespace Avalonia.Controls.UnitTests
Assert.True(raisedOnChild); Assert.True(raisedOnChild);
} }
[Fact]
public void Setting_Logical_Parent_Raises_Style_ResourcesChanged()
{
var style = new Style(x => x.OfType<Canvas>());
var parent = new ContentControl();
var child = new StyledElement { Styles = { style } };
((ISetLogicalParent)child).SetParent(parent);
var raised = false;
style.ResourcesChanged += (_, __) => raised = true;
parent.Resources.Add("foo", "bar");
Assert.True(raised);
}
private IControlTemplate ContentControlTemplate() private IControlTemplate ContentControlTemplate()
{ {
return new FuncControlTemplate<ContentControl>((x, scope) => return new FuncControlTemplate<ContentControl>((x, scope) =>

111
tests/Avalonia.Styling.UnitTests/StylesTests.cs

@ -8,103 +8,59 @@ namespace Avalonia.Styling.UnitTests
public class StylesTests public class StylesTests
{ {
[Fact] [Fact]
public void Adding_Style_With_Resources_Should_Raise_ResourceChanged() public void Adding_Style_Should_Set_Owner()
{ {
var style = new Style var host = new Mock<IResourceHost>();
{ var target = new Styles(host.Object);
Resources = { { "foo", "bar" } }, var style = new Mock<IStyle>();
}; var rp = style.As<IResourceProvider>();
var target = new Styles(); host.ResetCalls();
var raised = false; target.Add(style.Object);
target.ResourcesChanged += (_, __) => raised = true; rp.Verify(x => x.AddOwner(host.Object));
target.Add(style);
Assert.True(raised);
} }
[Fact] [Fact]
public void Removing_Style_With_Resources_Should_Raise_ResourceChanged() public void Removing_Style_Should_Clear_Owner()
{ {
var target = new Styles var host = new Mock<IResourceHost>();
{ var target = new Styles(host.Object);
new Style var style = new Mock<IStyle>();
{ var rp = style.As<IResourceProvider>();
Resources = { { "foo", "bar" } },
}
};
var raised = false; host.ResetCalls();
target.Add(style.Object);
target.Remove(style.Object);
target.ResourcesChanged += (_, __) => raised = true; rp.Verify(x => x.RemoveOwner(host.Object));
target.Clear();
Assert.True(raised);
} }
[Fact] [Fact]
public void Adding_Style_Without_Resources_Should_Not_Raise_ResourceChanged() public void Should_Set_Owner_On_Assigned_Resources()
{ {
var style = new Style(); var host = new Mock<IResourceHost>();
var target = new Styles(); var target = new Style();
var raised = false; ((IResourceProvider)target).AddOwner(host.Object);
target.ResourcesChanged += (_, __) => raised = true; var resources = new Mock<IResourceDictionary>();
target.Add(style); target.Resources = resources.Object;
Assert.False(raised); resources.Verify(x => x.AddOwner(host.Object), Times.Once);
} }
[Fact] [Fact]
public void Adding_Resource_Should_Raise_Child_ResourceChanged() public void Should_Set_Owner_On_Assigned_Resources_2()
{ {
Style child; var host = new Mock<IResourceHost>();
var target = new Styles 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_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 ParentResourcesChanged_Should_Be_Propagated_To_Children()
{
var childStyle = new Mock<IStyle>();
var setResourceParent = childStyle.As<ISetResourceParent>();
var target = new Styles { childStyle.Object };
setResourceParent.ResetCalls(); var resources = new Mock<IResourceDictionary>();
((ISetResourceParent)target).ParentResourcesChanged(new ResourcesChangedEventArgs()); target.Resources = resources.Object;
setResourceParent.Verify(x => x.ParentResourcesChanged( host.ResetCalls();
It.IsAny<ResourcesChangedEventArgs>()), ((IResourceProvider)target).AddOwner(host.Object);
Times.Once); resources.Verify(x => x.AddOwner(host.Object), Times.Once);
} }
[Fact] [Fact]
@ -124,8 +80,7 @@ namespace Avalonia.Styling.UnitTests
} }
}; };
var result = target.FindResource("foo"); Assert.True(target.TryGetResource("foo", out var result));
Assert.Equal("bar", result); Assert.Equal("bar", result);
} }
} }

Loading…
Cancel
Save