diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 02f47e07b4..f0fe98a4b7 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -30,7 +30,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceNode + public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost { /// /// The application-global data templates. @@ -129,26 +129,13 @@ namespace Avalonia /// public IResourceDictionary Resources { - get => _resources ?? (Resources = new ResourceDictionary()); + get => _resources ??= new ResourceDictionary(this); set { - Contract.Requires(value != null); - - var hadResources = false; - - if (_resources != null) - { - hadResources = _resources.Count > 0; - _resources.ResourcesChanged -= ThisResourcesChanged; - } - + value = value ?? throw new ArgumentNullException(nameof(value)); + _resources?.RemoveOwner(this); _resources = value; - _resources.ResourcesChanged += ThisResourcesChanged; - - if (hadResources || _resources.Count > 0) - { - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - } + _resources.AddOwner(this); } } @@ -161,23 +148,15 @@ namespace Avalonia /// /// Global styles apply to all windows in the application. /// - public Styles Styles - { - get - { - if (_styles == null) - { - _styles = new Styles(this); - _styles.ResourcesChanged += ThisResourcesChanged; - } - - return _styles; - } - } + public Styles Styles => _styles ??= new Styles(this); /// bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; + /// + bool IResourceNode.HasResources => (_resources?.HasResources ?? false) || + (((IResourceNode?)_styles)?.HasResources ?? false); + /// /// Gets the styling parent of the application, which is null. /// @@ -185,13 +164,7 @@ namespace Avalonia /// bool IStyleHost.IsStylesInitialized => _styles != null; - - /// - bool IResourceProvider.HasResources => _resources?.Count > 0; - - /// - IResourceNode IResourceNode.ResourceParent => null; - + /// /// Application lifetime, use it for things like setting the main window and exiting the app from code /// Currently supported lifetimes are: @@ -219,13 +192,18 @@ namespace Avalonia public virtual void Initialize() { } /// - bool IResourceProvider.TryGetResource(object key, out object value) + bool IResourceNode.TryGetResource(object key, out object value) { value = null; return (_resources?.TryGetResource(key, out value) ?? false) || Styles.TryGetResource(key, out value); } + void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e) + { + ResourcesChanged?.Invoke(this, e); + } + void IStyleHost.StylesAdded(IReadOnlyList styles) { _stylesAdded?.Invoke(styles); @@ -282,8 +260,6 @@ namespace Avalonia try { _notifyingResourcesChanged = true; - (_resources as ISetResourceParent)?.ParentResourcesChanged(e); - (_styles as ISetResourceParent)?.ParentResourcesChanged(e); ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); } finally diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index d17f3b0423..b6eee290ce 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -127,11 +127,11 @@ namespace Avalonia.Controls x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty()) .Switch().Subscribe(cursor => PlatformImpl?.SetCursor(cursor?.PlatformCursor)); - if (((IStyleHost)this).StylingParent is IResourceProvider applicationResources) + if (((IStyleHost)this).StylingParent is IResourceHost applicationResources) { WeakSubscriptionManager.Subscribe( applicationResources, - nameof(IResourceProvider.ResourcesChanged), + nameof(IResourceHost.ResourcesChanged), this); } } diff --git a/src/Avalonia.Styling/Controls/IResourceDictionary.cs b/src/Avalonia.Styling/Controls/IResourceDictionary.cs index 240f9b08bf..3a68dde31e 100644 --- a/src/Avalonia.Styling/Controls/IResourceDictionary.cs +++ b/src/Avalonia.Styling/Controls/IResourceDictionary.cs @@ -1,11 +1,13 @@ using System.Collections.Generic; +#nullable enable + namespace Avalonia.Controls { /// /// An indexed dictionary of resources. /// - public interface IResourceDictionary : IResourceProvider, IDictionary + public interface IResourceDictionary : IResourceProvider, IDictionary { /// /// Gets a collection of child resource dictionaries. diff --git a/src/Avalonia.Styling/Controls/IResourceHost.cs b/src/Avalonia.Styling/Controls/IResourceHost.cs new file mode 100644 index 0000000000..fed9359d94 --- /dev/null +++ b/src/Avalonia.Styling/Controls/IResourceHost.cs @@ -0,0 +1,32 @@ +using System; + +#nullable enable + +namespace Avalonia.Controls +{ + /// + /// Represents an element which hosts resources. + /// + /// + /// This interface is implemented by and `Application`. + /// + public interface IResourceHost : IResourceNode + { + /// + /// Raised when the resources change on the element or an ancestor of the element. + /// + event EventHandler ResourcesChanged; + + /// + /// Notifies the resource host that one or more of its hosted resources has changed. + /// + /// The event args. + /// + /// 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. + /// + void NotifyHostedResourcesChanged(ResourcesChangedEventArgs e); + } +} diff --git a/src/Avalonia.Styling/Controls/IResourceNode.cs b/src/Avalonia.Styling/Controls/IResourceNode.cs index b6a6cdc3e3..73bfeaf161 100644 --- a/src/Avalonia.Styling/Controls/IResourceNode.cs +++ b/src/Avalonia.Styling/Controls/IResourceNode.cs @@ -1,15 +1,35 @@ using System; +#nullable enable + namespace Avalonia.Controls { /// - /// Represents resource provider in a tree. + /// Represents an object that can be queried for resources. /// - public interface IResourceNode : IResourceProvider + /// + /// The interface represents a common interface for both controls that host resources + /// () and resource providers such as + /// (see ). + /// + public interface IResourceNode { /// - /// Gets the parent resource node, if any. + /// Gets a value indicating whether the object has resources. + /// + bool HasResources { get; } + + /// + /// Tries to find a resource within the object. /// - IResourceNode ResourceParent { get; } + /// The resource key. + /// + /// When this method returns, contains the value associated with the specified key, + /// if the key is found; otherwise, null. + /// + /// + /// True if the resource if found, otherwise false. + /// + bool TryGetResource(object key, out object? value); } } diff --git a/src/Avalonia.Styling/Controls/IResourceProvider.cs b/src/Avalonia.Styling/Controls/IResourceProvider.cs index cbaacee012..27a8c2da68 100644 --- a/src/Avalonia.Styling/Controls/IResourceProvider.cs +++ b/src/Avalonia.Styling/Controls/IResourceProvider.cs @@ -1,33 +1,42 @@ using System; +using Avalonia.Styling; + +#nullable enable namespace Avalonia.Controls { /// - /// 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. /// - public interface IResourceProvider + /// + /// This interface is implemented by , and + /// + /// + public interface IResourceProvider : IResourceNode { /// - /// Raised when resources in the provider are changed. + /// Gets the owner of the resource provider. + /// + /// + /// If multiple owners are added, returns the first. + /// + IResourceHost? Owner { get; } + + /// + /// Raised when the of the resource provider changes. /// - event EventHandler ResourcesChanged; + event EventHandler OwnerChanged; /// - /// Gets a value indicating whether the element has resources. + /// Adds an owner to the resource provider. /// - bool HasResources { get; } + /// The owner. + void AddOwner(IResourceHost owner); /// - /// Tries to find a resource within the provider. + /// Removes a resource provider owner. /// - /// The resource key. - /// - /// When this method returns, contains the value associated with the specified key, - /// if the key is found; otherwise, null. - /// - /// - /// True if the resource if found, otherwise false. - /// - bool TryGetResource(object key, out object value); + /// The owner. + void RemoveOwner(IResourceHost owner); } } diff --git a/src/Avalonia.Styling/Controls/ISetResourceParent.cs b/src/Avalonia.Styling/Controls/ISetResourceParent.cs deleted file mode 100644 index a1264adc34..0000000000 --- a/src/Avalonia.Styling/Controls/ISetResourceParent.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace Avalonia.Controls -{ - /// - /// Defines an interface through which an 's parent can be set. - /// - /// - /// You should not usually need to use this interface - it is for internal use only. - /// - public interface ISetResourceParent : IResourceNode - { - /// - /// Sets the resource parent. - /// - /// The parent. - void SetParent(IResourceNode parent); - - /// - /// Notifies the resource node that a change has been made to the resources in its parent. - /// - /// The event args. - /// - /// This method will be called automatically by the framework, you should not need to call - /// this method yourself. - /// - void ParentResourcesChanged(ResourcesChangedEventArgs e); - } -} diff --git a/src/Avalonia.Styling/Controls/ResourceDictionary.cs b/src/Avalonia.Styling/Controls/ResourceDictionary.cs index ff84ab0b73..cedd116e92 100644 --- a/src/Avalonia.Styling/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Styling/Controls/ResourceDictionary.cs @@ -1,21 +1,20 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using Avalonia.Collections; +using Avalonia.Metadata; + +#nullable enable namespace Avalonia.Controls { /// /// An indexed dictionary of resources. /// - public class ResourceDictionary : AvaloniaDictionary, - IResourceDictionary, - IResourceNode, - ISetResourceParent + public class ResourceDictionary : AvaloniaDictionary, IResourceDictionary { - private IResourceNode _parent; - private AvaloniaList _mergedDictionaries; + private IResourceHost? _owner; + private AvaloniaList? _mergedDictionaries; /// /// Initializes a new instance of the class. @@ -25,10 +24,28 @@ namespace Avalonia.Controls CollectionChanged += OnCollectionChanged; } - /// - public event EventHandler ResourcesChanged; + /// + /// Initializes a new instance of the class. + /// + public ResourceDictionary(IResourceHost owner) + : this() + { + Owner = owner; + } + + public IResourceHost? Owner + { + get => _owner; + private set + { + if (_owner != value) + { + _owner = value; + OwnerChanged?.Invoke(this, EventArgs.Empty); + } + } + } - /// public IList MergedDictionaries { get @@ -40,71 +57,51 @@ namespace Avalonia.Controls _mergedDictionaries.ForEachItem( x => { - if (x is ISetResourceParent setParent) - { - setParent.SetParent(this); - setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); - } - - if (x.HasResources) + if (Owner is object) { - OnResourcesChanged(); + x.AddOwner(Owner); } - - x.ResourcesChanged += MergedDictionaryResourcesChanged; }, x => { - if (x is ISetResourceParent setParent) - { - setParent.SetParent(null); - setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); - } - - if (x.HasResources) + if (Owner is object) { - OnResourcesChanged(); + x.RemoveOwner(Owner); } - - (x as ISetResourceParent)?.SetParent(null); - x.ResourcesChanged -= MergedDictionaryResourcesChanged; - }, - () => { }); + }, null); } return _mergedDictionaries; } } - /// - bool IResourceProvider.HasResources + bool IResourceNode.HasResources { - get => Count > 0 || (_mergedDictionaries?.Any(x => x.HasResources) ?? false); - } - - /// - IResourceNode IResourceNode.ResourceParent => _parent; + get + { + if (Count > 0) + { + return true; + } - /// - void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) - { - NotifyMergedDictionariesResourcesChanged(e); - ResourcesChanged?.Invoke(this, e); - } + if (_mergedDictionaries?.Count > 0) + { + foreach (var i in _mergedDictionaries) + { + if (i.HasResources) + { + return true; + } + } + } - /// - void ISetResourceParent.SetParent(IResourceNode parent) - { - if (_parent != null && parent != null) - { - throw new InvalidOperationException("The ResourceDictionary already has a parent."); + return false; } - - _parent = parent; } - /// - public bool TryGetResource(object key, out object value) + public event EventHandler? OwnerChanged; + + public bool TryGetResource(object key, out object? value) { if (TryGetValue(key, out value)) { @@ -125,32 +122,63 @@ namespace Avalonia.Controls 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) { - var ev = new ResourcesChangedEventArgs(); - NotifyMergedDictionariesResourcesChanged(ev); - OnResourcesChanged(); + Owner?.NotifyHostedResourcesChanged(new ResourcesChangedEventArgs()); } - - private void MergedDictionaryResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnResourcesChanged(); } } diff --git a/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs b/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs index 19a16f86c4..7771629e0d 100644 --- a/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs +++ b/src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs @@ -1,6 +1,9 @@ using System; +using Avalonia.LogicalTree; using Avalonia.Reactive; +#nullable enable + namespace Avalonia.Controls { public static class ResourceNodeExtensions @@ -11,8 +14,11 @@ namespace Avalonia.Controls /// The control. /// The resource key. /// The resource, or if not found. - 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)) { return value; @@ -28,16 +34,16 @@ namespace Avalonia.Controls /// The resource key. /// On return, contains the resource if found, otherwise null. /// True if the resource was found; otherwise false. - 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(control != null); - Contract.Requires(key != null); + control = control ?? throw new ArgumentNullException(nameof(control)); + key = key ?? throw new ArgumentNullException(nameof(key)); - var current = control; + IResourceHost? current = control; while (current != null) { - if (current is IResourceNode host) + if (current is IResourceHost host) { 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; return false; } - public static IObservable GetResourceObservable(this IResourceNode target, object key) + public static IObservable 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 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 + private class ResourceObservable : LightweightObservableBase { - private readonly IResourceNode _target; + private readonly IStyledElement _target; private readonly object _key; - public ResourceObservable(IResourceNode target, object key) + public ResourceObservable(IStyledElement target, object key) { _target = target; _key = key; @@ -78,7 +95,7 @@ namespace Avalonia.Controls _target.ResourcesChanged -= ResourcesChanged; } - protected override void Subscribed(IObserver observer, bool first) + protected override void Subscribed(IObserver observer, bool first) { observer.OnNext(_target.FindResource(_key)); } @@ -88,5 +105,65 @@ namespace Avalonia.Controls PublishNext(_target.FindResource(_key)); } } + + private class FloatingResourceObservable : LightweightObservableBase + { + 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 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(); + } + } } } diff --git a/src/Avalonia.Styling/IStyledElement.cs b/src/Avalonia.Styling/IStyledElement.cs index d4d0f179c3..610c743a3e 100644 --- a/src/Avalonia.Styling/IStyledElement.cs +++ b/src/Avalonia.Styling/IStyledElement.cs @@ -9,8 +9,7 @@ namespace Avalonia IStyleable, IStyleHost, ILogical, - IResourceProvider, - IResourceNode, + IResourceHost, IDataContextProvider { /// @@ -18,6 +17,11 @@ namespace Avalonia /// event EventHandler Initialized; + /// + /// Raised when resources on the element are changed. + /// + event EventHandler ResourcesChanged; + /// /// Gets a value that indicates whether the element has finished initialization. /// diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 1a4bc5b3a3..febb8ec817 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -212,7 +212,6 @@ namespace Avalonia if (_styles is null) { _styles = new Styles(this); - _styles.ResourcesChanged += ThisResourcesChanged; NotifyResourcesChanged(new ResourcesChangedEventArgs()); } @@ -225,32 +224,13 @@ namespace Avalonia /// public IResourceDictionary Resources { - get => _resources ?? (Resources = new ResourceDictionary()); + get => _resources ??= new ResourceDictionary(this); set { value = value ?? throw new ArgumentNullException(nameof(value)); - - var hadResources = false; - - if (_resources != null) - { - (_resources as ISetResourceParent)?.SetParent(null); - hadResources = _resources.Count > 0; - _resources.ResourcesChanged -= ThisResourcesChanged; - } - + _resources?.RemoveOwner(this); _resources = value; - _resources.ResourcesChanged += ThisResourcesChanged; - - if (value is ISetResourceParent setParent && setParent.ResourceParent == null) - { - setParent.SetParent(this); - } - - if (hadResources || _resources.Count > 0) - { - NotifyResourcesChanged(new ResourcesChangedEventArgs()); - } + _resources.AddOwner(this); } } @@ -312,10 +292,8 @@ namespace Avalonia IAvaloniaReadOnlyList ILogical.LogicalChildren => LogicalChildren; /// - bool IResourceProvider.HasResources => _resources?.Count > 0 || Styles.HasResources; - - /// - IResourceNode? IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode; + bool IResourceNode.HasResources => (_resources?.HasResources ?? false) || + (((IResourceNode?)_styles)?.HasResources ?? false); /// IAvaloniaReadOnlyList IStyleable.Classes => Classes; @@ -407,7 +385,10 @@ namespace Avalonia void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e); /// - bool IResourceProvider.TryGetResource(object key, out object? value) + void IResourceHost.NotifyHostedResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e); + + /// + bool IResourceNode.TryGetResource(object key, out object? value) { value = null; return (_resources?.TryGetResource(key, out value) ?? false) || @@ -446,6 +427,7 @@ namespace Avalonia { old.ResourcesChanged -= ThisResourcesChanged; } + if (Parent != null) { Parent.ResourcesChanged += ThisResourcesChanged; @@ -804,24 +786,9 @@ namespace Avalonia } } - private void NotifyResourcesChanged(ResourcesChangedEventArgs e) + private void NotifyResourcesChanged(ResourcesChangedEventArgs? e = null) { - if (_notifyingResourcesChanged) - { - return; - } - - try - { - _notifyingResourcesChanged = true; - (_resources as ISetResourceParent)?.ParentResourcesChanged(e); - (_styles as ISetResourceParent)?.ParentResourcesChanged(e); - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - } - finally - { - _notifyingResourcesChanged = false; - } + ResourcesChanged?.Invoke(this, e ?? new ResourcesChangedEventArgs()); } private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e) diff --git a/src/Avalonia.Styling/Styling/IStyle.cs b/src/Avalonia.Styling/Styling/IStyle.cs index 738a69cb88..78fbe0f2b5 100644 --- a/src/Avalonia.Styling/Styling/IStyle.cs +++ b/src/Avalonia.Styling/Styling/IStyle.cs @@ -8,7 +8,7 @@ namespace Avalonia.Styling /// /// Defines the interface for styles. /// - public interface IStyle : IResourceNode + public interface IStyle { /// /// Gets a collection of child styles. diff --git a/src/Avalonia.Styling/Styling/IStyleHost.cs b/src/Avalonia.Styling/Styling/IStyleHost.cs index 6953940a71..360b40d9a1 100644 --- a/src/Avalonia.Styling/Styling/IStyleHost.cs +++ b/src/Avalonia.Styling/Styling/IStyleHost.cs @@ -27,7 +27,7 @@ namespace Avalonia.Styling /// /// Gets the parent style host element. /// - IStyleHost StylingParent { get; } + IStyleHost? StylingParent { get; } /// /// Called when styles are added to or a nested styles collection. diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index de0506163e..ae866ebf46 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -11,9 +11,9 @@ namespace Avalonia.Styling /// /// Defines a style. /// - public class Style : AvaloniaObject, IStyle, ISetResourceParent + public class Style : AvaloniaObject, IStyle, IResourceProvider { - private IResourceNode? _parent; + private IResourceHost? _owner; private IResourceDictionary? _resources; private List? _setters; private List? _animations; @@ -34,8 +34,18 @@ namespace Avalonia.Styling Selector = selector(null); } - /// - public event EventHandler? ResourcesChanged; + public IResourceHost? Owner + { + get => _owner; + private set + { + if (_owner != value) + { + _owner = value; + OwnerChanged?.Invoke(this, EventArgs.Empty); + } + } + } /// /// Gets or sets a dictionary of style resources. @@ -47,20 +57,18 @@ namespace Avalonia.Styling { value = value ?? throw new ArgumentNullException(nameof(value)); - var hadResources = false; - - if (_resources != null) - { - hadResources = _resources.HasResources; - _resources.ResourcesChanged -= ResourceDictionaryChanged; - } + var hadResources = _resources?.HasResources ?? false; _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 /// public IList Animations => _animations ??= new List(); - /// - IResourceNode? IResourceNode.ResourceParent => _parent; - - /// - bool IResourceProvider.HasResources => _resources?.Count > 0; - + bool IResourceNode.HasResources => _resources?.Count > 0; IReadOnlyList IStyle.Children => Array.Empty(); - /// + public event EventHandler? OwnerChanged; + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) { target = target ?? throw new ArgumentNullException(nameof(target)); @@ -107,7 +111,6 @@ namespace Avalonia.Styling return match.Result; } - /// public bool TryGetResource(object key, out object? result) { result = null; @@ -130,26 +133,28 @@ namespace Avalonia.Styling } } - /// - void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) + void IResourceProvider.AddOwner(IResourceHost owner) { - ResourcesChanged?.Invoke(this, e); - } + owner = owner ?? throw new ArgumentNullException(nameof(owner)); - /// - void ISetResourceParent.SetParent(IResourceNode parent) - { - if (_parent != null && parent != null) + if (Owner != null) { 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); + } } } } diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index ac355fd8b3..1058b863f0 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -2,7 +2,6 @@ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using Avalonia.Collections; using Avalonia.Controls; @@ -13,10 +12,13 @@ namespace Avalonia.Styling /// /// A style that consists of a number of child styles. /// - public class Styles : AvaloniaObject, IAvaloniaList, IStyle, ISetResourceParent + public class Styles : AvaloniaObject, + IAvaloniaList, + IStyle, + IResourceProvider { private readonly AvaloniaList _styles = new AvaloniaList(); - private IResourceNode? _parent; + private IResourceHost? _owner; private IResourceDictionary? _resources; private Dictionary?>? _cache; private bool _notifyingResourcesChanged; @@ -27,22 +29,29 @@ namespace Avalonia.Styling _styles.CollectionChanged += OnCollectionChanged; } - public Styles(IResourceNode parent) + public Styles(IResourceHost owner) : this() { - _parent = parent; + Owner = owner; } public event NotifyCollectionChangedEventHandler? CollectionChanged; + public event EventHandler? OwnerChanged; - /// - public event EventHandler? ResourcesChanged; - - /// public int Count => _styles.Count; - /// - public bool HasResources => _resources?.Count > 0 || this.Any(x => x.HasResources); + public IResourceHost? Owner + { + get => _owner; + private set + { + if (_owner != value) + { + _owner = value; + OwnerChanged?.Invoke(this, EventArgs.Empty); + } + } + } /// /// Gets or sets a dictionary of style resources. @@ -54,43 +63,53 @@ namespace Avalonia.Styling { value = value ?? throw new ArgumentNullException(nameof(Resources)); - var hadResources = false; - - if (_resources != null) + if (Owner is object) { - hadResources = _resources.Count > 0; - _resources.ResourcesChanged -= NotifyResourcesChanged; + _resources?.RemoveOwner(Owner); } _resources = value; - _resources.ResourcesChanged += NotifyResourcesChanged; - if (hadResources || _resources.Count > 0) + if (Owner is object) { - ((ISetResourceParent)this).ParentResourcesChanged(new ResourcesChangedEventArgs()); + _resources.AddOwner(Owner); } } } - /// - IResourceNode? IResourceNode.ResourceParent => _parent; - - /// bool ICollection.IsReadOnly => false; - /// + 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.this[int index] => _styles[index]; IReadOnlyList IStyle.Children => this; - /// public IStyle this[int index] { get => _styles[index]; set => _styles[index] = value; } - /// public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) { _cache ??= new Dictionary?>(); @@ -142,7 +161,7 @@ namespace Avalonia.Styling 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; } @@ -203,20 +222,29 @@ namespace Avalonia.Styling IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator(); /// - 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); } /// - 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) @@ -241,22 +269,15 @@ namespace Avalonia.Styling { var style = (IStyle)items[i]; - if (style.ResourceParent == null && style is ISetResourceParent setParent) + if (Owner is object && style is IResourceProvider resourceProvider) { - setParent.SetParent(this); - setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); + resourceProvider.AddOwner(Owner); } - if (style.HasResources) - { - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - } - - style.ResourcesChanged += NotifyResourcesChanged; _cache = null; } - GetHost()?.StylesAdded(ToReadOnlyList(items)); + (Owner as IStyleHost)?.StylesAdded(ToReadOnlyList(items)); } void Remove(IList items) @@ -265,22 +286,15 @@ namespace Avalonia.Styling { var style = (IStyle)items[i]; - if (style.ResourceParent == this && style is ISetResourceParent setParent) + if (Owner is object && style is IResourceProvider resourceProvider) { - setParent.SetParent(null); - setParent.ParentResourcesChanged(new ResourcesChangedEventArgs()); + resourceProvider.RemoveOwner(Owner); } - if (style.HasResources) - { - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - } - - style.ResourcesChanged -= NotifyResourcesChanged; _cache = null; } - GetHost()?.StylesRemoved(ToReadOnlyList(items)); + (Owner as IStyleHost)?.StylesRemoved(ToReadOnlyList(items)); } switch (e.Action) @@ -301,50 +315,5 @@ namespace Avalonia.Styling 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; - } - } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index 1e493ee2e8..d0d4aae9b8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -1,15 +1,15 @@ using System; -using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; using Avalonia.Controls; using Avalonia.Data; +#nullable enable + namespace Avalonia.Markup.Xaml.MarkupExtensions { public class DynamicResourceExtension : IBinding { - private IResourceNode _anchor; + private IStyledElement? _anchor; + private IResourceProvider? _resourceProvider; public DynamicResourceExtension() { @@ -20,32 +20,46 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions ResourceKey = resourceKey; } - public object ResourceKey { get; set; } + public object? ResourceKey { get; set; } public IBinding ProvideValue(IServiceProvider serviceProvider) { var provideTarget = serviceProvider.GetService(); - if (!(provideTarget.TargetObject is IResourceNode)) + if (!(provideTarget.TargetObject is IStyledElement)) { - _anchor = serviceProvider.GetFirstParent(); + _anchor = serviceProvider.GetFirstParent(); + + if (_anchor is null) + { + _resourceProvider = serviceProvider.GetFirstParent(); + } } return this; } - InstancedBinding IBinding.Initiate( + InstancedBinding? IBinding.Initiate( IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor, 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) { return InstancedBinding.OneWay(control.GetResourceObservable(ResourceKey)); } + else if (_resourceProvider is object) + { + return InstancedBinding.OneWay(_resourceProvider.GetResourceObservable(ResourceKey)); + } return null; } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs index 0d56942645..87e2ba3a2e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs @@ -2,18 +2,17 @@ using System.ComponentModel; using Avalonia.Controls; +#nullable enable + namespace Avalonia.Markup.Xaml.MarkupExtensions { /// /// Loads a resource dictionary from a specified URL. /// - public class ResourceInclude : IResourceNode, ISetResourceParent + public class ResourceInclude : IResourceProvider { - private IResourceNode _parent; - private Uri _baseUri; - private IResourceDictionary _loaded; - - public event EventHandler ResourcesChanged; + private Uri? _baseUri; + private IResourceDictionary? _loaded; /// /// Gets the loaded resource dictionary. @@ -26,53 +25,34 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { var loader = new AvaloniaXamlLoader(); _loaded = (IResourceDictionary)loader.Load(Source, _baseUri); - - (_loaded as ISetResourceParent)?.SetParent(this); - _loaded.ResourcesChanged += ResourcesChanged; - - if (_loaded.HasResources) - { - ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - } } return _loaded; } } + public IResourceHost? Owner => Loaded.Owner; + /// /// Gets or sets the source URL. /// - public Uri Source { get; set; } - - /// - bool IResourceProvider.HasResources => Loaded.HasResources; + public Uri? Source { get; set; } - /// - IResourceNode IResourceNode.ResourceParent => _parent; + bool IResourceNode.HasResources => Loaded.HasResources; - /// - bool IResourceProvider.TryGetResource(object key, out object value) + public event EventHandler OwnerChanged { - return Loaded.TryGetResource(key, out value); + add => Loaded.OwnerChanged += value; + remove => Loaded.OwnerChanged -= value; } - /// - void ISetResourceParent.SetParent(IResourceNode parent) + bool IResourceNode.TryGetResource(object key, out object? value) { - if (_parent != null && parent != null) - { - throw new InvalidOperationException("The ResourceInclude already has a parent."); - } - - _parent = parent; + return Loaded.TryGetResource(key, out value); } - /// - void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) - { - (_loaded as ISetResourceParent)?.ParentResourcesChanged(e); - } + void IResourceProvider.AddOwner(IResourceHost owner) => Loaded.AddOwner(owner); + void IResourceProvider.RemoveOwner(IResourceHost owner) => Loaded.RemoveOwner(owner); public ResourceInclude ProvideValue(IServiceProvider serviceProvider) { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs index b34123a5de..6d9fac638c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Reflection; using Avalonia.Controls; using Avalonia.Markup.Data; +using Avalonia.Markup.Xaml.XamlIl.Runtime; namespace Avalonia.Markup.Xaml.MarkupExtensions { @@ -22,15 +23,22 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public object ProvideValue(IServiceProvider serviceProvider) { - // Look upwards though the ambient context for IResourceProviders which might be able - // to give us the resource. - foreach (var resourceProvider in serviceProvider.GetParents()) + var stack = serviceProvider.GetService(); + + // 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; } - } // The resource still hasn't been found, so add a delayed one-time binding. diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 3d3d9bc08e..2b39263ee9 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -10,32 +10,30 @@ namespace Avalonia.Markup.Xaml.Styling /// /// Includes a style from a URL. /// - public class StyleInclude : IStyle, ISetResourceParent + public class StyleInclude : IStyle, IResourceProvider { - private Uri _baseUri; + private readonly Uri _baseUri; private IStyle[]? _loaded; - private IResourceNode? _parent; /// /// Initializes a new instance of the class. /// - /// + /// The base URL for the XAML context. public StyleInclude(Uri baseUri) { _baseUri = baseUri; } + /// + /// Initializes a new instance of the class. + /// + /// The XAML service provider. public StyleInclude(IServiceProvider serviceProvider) { _baseUri = serviceProvider.GetContextBaseUri(); } - - /// - public event EventHandler ResourcesChanged - { - add {} - remove {} - } + + public IResourceHost? Owner => (Loaded as IResourceProvider)?.Owner; /// /// Gets or sets the source URL. @@ -53,7 +51,6 @@ namespace Avalonia.Markup.Xaml.Styling { var loader = new AvaloniaXamlLoader(); var loaded = (IStyle)loader.Load(Source, _baseUri); - (loaded as ISetResourceParent)?.SetParent(this); _loaded = new[] { loaded }; } @@ -61,35 +58,42 @@ namespace Avalonia.Markup.Xaml.Styling } } - /// - bool IResourceProvider.HasResources => Loaded.HasResources; - - /// - IResourceNode? IResourceNode.ResourceParent => _parent; + bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false; IReadOnlyList IStyle.Children => _loaded ?? Array.Empty(); - /// - public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host); - - /// - public bool TryGetResource(object key, out object? value) => Loaded.TryGetResource(key, out value); - - /// - void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e) + public event EventHandler OwnerChanged { - (Loaded as ISetResourceParent)?.ParentResourcesChanged(e); + add + { + if (Loaded is IResourceProvider rp) + { + rp.OwnerChanged += value; + } + } + remove + { + if (Loaded is IResourceProvider rp) + { + rp.OwnerChanged -= value; + } + } } - /// - void ISetResourceParent.SetParent(IResourceNode parent) + public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host) => Loaded.TryAttach(target, host); + + 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); } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs index e5fd5ed37e..1c82e46b3c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs +++ b/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 = @" - - Red - -"; - var loader = new AvaloniaXamlLoader(); - var resources = (ResourceDictionary)loader.Load(xaml); - var brush = (SolidColorBrush)resources["RedBrush"]; - - Assert.Equal(Colors.Red, brush.Color); - } - } - [Fact] public void DynamicResource_Finds_Resource_In_Parent_Dictionary() { diff --git a/tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs b/tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs index c66952f428..84b0d09b61 100644 --- a/tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs +++ b/tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs @@ -7,6 +7,20 @@ namespace Avalonia.Styling.UnitTests { public class ResourceDictionaryTests { + [Fact] + public void Cannot_Add_Null_Key() + { + var target = new ResourceDictionary(); + Assert.Throws(() => target.Add(null, "null")); + } + + [Fact] + public void Can_Add_Null_Value() + { + var target = new ResourceDictionary(); + target.Add("null", null); + } + [Fact] public void TryGetResource_Should_Find_Resource() { @@ -77,66 +91,97 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public void ResourcesChanged_Should_Be_Raised_On_Resource_Add() + public void NotifyHostedResourcesChanged_Should_Be_Called_On_AddOwner() { - var target = new ResourceDictionary(); - var raised = false; + var host = new Mock(); + var target = new ResourceDictionary { { "foo", "bar" } }; + + ((IResourceProvider)target).AddOwner(host.Object); - target.ResourcesChanged += (_, __) => raised = true; + host.Verify(x => x.NotifyHostedResourcesChanged(It.IsAny())); + } + + [Fact] + public void NotifyHostedResourcesChanged_Should_Be_Called_On_RemoveOwner() + { + var host = new Mock(); + 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())); + } + + [Fact] + public void NotifyHostedResourcesChanged_Should_Be_Called_On_Resource_Add() + { + var host = new Mock(); + var target = new ResourceDictionary(host.Object); + + host.ResetCalls(); target.Add("foo", "bar"); - Assert.True(raised); + host.Verify(x => x.NotifyHostedResourcesChanged(It.IsAny())); } [Fact] - public void ResourcesChanged_Should_Be_Raised_On_MergedDictionary_Add() + public void NotifyHostedResourcesChanged_Should_Be_Called_On_MergedDictionary_Add() { - var target = new ResourceDictionary(); - var raised = false; + var host = new Mock(); + var target = new ResourceDictionary(host.Object); - target.ResourcesChanged += (_, __) => raised = true; + host.ResetCalls(); target.MergedDictionaries.Add(new ResourceDictionary { { "foo", "bar" }, }); - Assert.True(raised); + host.Verify( + x => x.NotifyHostedResourcesChanged(It.IsAny()), + Times.Once); } [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 raised = false; + var host = new Mock(); + var target = new ResourceDictionary(host.Object); - target.ResourcesChanged += (_, __) => raised = true; + host.ResetCalls(); target.MergedDictionaries.Add(new ResourceDictionary()); - Assert.False(raised); + host.Verify( + x => x.NotifyHostedResourcesChanged(It.IsAny()), + Times.Never); } [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(); + var target = new ResourceDictionary(host.Object) { MergedDictionaries = { new ResourceDictionary { { "foo", "bar" } }, } }; - var raised = false; - target.ResourcesChanged += (_, __) => raised = true; + host.ResetCalls(); target.MergedDictionaries.RemoveAt(0); - Assert.True(raised); + host.Verify( + x => x.NotifyHostedResourcesChanged(It.IsAny()), + Times.Once); } [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(); + var target = new ResourceDictionary(host.Object) { MergedDictionaries = { @@ -144,44 +189,63 @@ namespace Avalonia.Styling.UnitTests } }; - var raised = false; - - target.ResourcesChanged += (_, __) => raised = true; + host.ResetCalls(); ((IResourceDictionary)target.MergedDictionaries[0]).Add("foo", "bar"); - Assert.True(raised); + host.Verify( + x => x.NotifyHostedResourcesChanged(It.IsAny()), + Times.Once); } [Fact] - public void MergedDictionary_ParentResourcesChanged_Should_Be_Called_On_Resource_Add() + public void Sets_Added_MergedDictionary_Owner() { - var target = new ResourceDictionary(); - var merged = new Mock(); + var host = new Mock(); - target.MergedDictionaries.Add(merged.Object); - merged.ResetCalls(); + var target = new ResourceDictionary(host.Object); + 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( - x => x.ParentResourcesChanged(It.IsAny()), - Times.Once); + [Fact] + public void AddOwner_Sets_MergedDictionary_Owner() + { + var host = new Mock(); + + 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] - public void MergedDictionary_ParentResourcesChanged_Should_Be_Called_On_NotifyResourceChanged() + public void RemoveOwner_Clears_MergedDictionary_Owner() { - var target = new ResourceDictionary(); - var merged = new Mock(); + var host = new Mock(); - target.MergedDictionaries.Add(merged.Object); - merged.ResetCalls(); + var target = new ResourceDictionary(host.Object) + { + MergedDictionaries = + { + new ResourceDictionary(), + } + }; - ((ISetResourceParent)target).ParentResourcesChanged(new ResourcesChangedEventArgs()); + ((IResourceProvider)target).RemoveOwner(host.Object); - merged.Verify( - x => x.ParentResourcesChanged(It.IsAny()), - Times.Once); + Assert.Null(target.Owner); + Assert.Null(((ResourceDictionary)target.MergedDictionaries[0]).Owner); } + } } diff --git a/tests/Avalonia.Styling.UnitTests/StyleTests.cs b/tests/Avalonia.Styling.UnitTests/StyleTests.cs index 086986a52e..3e2bd68cf0 100644 --- a/tests/Avalonia.Styling.UnitTests/StyleTests.cs +++ b/tests/Avalonia.Styling.UnitTests/StyleTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Data; using Avalonia.UnitTests; +using Moq; using Xunit; 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(); + var target = new Style(); + ((IResourceProvider)target).AddOwner(host.Object); + + var resources = new Mock(); + 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(); + var target = new Style(); + + var resources = new Mock(); + target.Resources = resources.Object; + + host.ResetCalls(); + ((IResourceProvider)target).AddOwner(host.Object); + resources.Verify(x => x.AddOwner(host.Object), Times.Once); + } + private class Class1 : Control { public static readonly StyledProperty FooProperty = diff --git a/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs b/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs index e336fbf838..857815f5b4 100644 --- a/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs +++ b/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs @@ -492,20 +492,20 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public void Resources_Parent_Is_Set() + public void Resources_Owner_Is_Set() { var target = new TestControl(); - Assert.Same(target, ((IResourceNode)target.Resources).ResourceParent); + Assert.Same(target, ((ResourceDictionary)target.Resources).Owner); } [Fact] public void Assigned_Resources_Parent_Is_Set() { - var resources = new ResourceDictionary(); - var target = new TestControl { Resources = resources }; + var resources = new Mock(); + var target = new TestControl { Resources = resources.Object }; - Assert.Same(target, ((IResourceNode)resources).ResourceParent); + resources.Verify(x => x.AddOwner(target)); } [Fact] @@ -522,58 +522,25 @@ namespace Avalonia.Styling.UnitTests } [Fact] - public void Changing_Parent_Notifies_Resources_ParentResourcesChanged() - { - var resources = new Mock(); - var setResourceParent = resources.As(); - 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()), - Times.Once); - } - - [Fact] - public void Styles_Parent_Is_Set() + public void Styles_Owner_Is_Set() { var target = new TestControl(); - Assert.Same(target, ((IResourceNode)target.Styles).ResourceParent); + Assert.Same(target, target.Styles.Owner); } [Fact] - public void Changing_Parent_Notifies_Styles_ParentResourcesChanged() + public void Changing_Parent_Raises_ResourcesChanged() { - var style = new Mock(); - var setResourceParent = style.As(); - var target = new TestControl { Styles = { style.Object } }; + var target = new TestControl(); var parent = new Decorator { Resources = { { "foo", "bar" } } }; + var raised = 0; - setResourceParent.ResetCalls(); - parent.Child = target; - - setResourceParent.Verify(x => - x.ParentResourcesChanged(It.IsAny()), - Times.Once); - } - - [Fact] - public void Changing_Resources_Notifies_Styles() - { - var style = new Mock(); - var setResourceParent = style.As(); - var target = new TestControl { Styles = { style.Object } }; + target.ResourcesChanged += (s, e) => ++raised; - setResourceParent.ResetCalls(); - target.Resources.Add("foo", "bar"); + parent.Child = target; - setResourceParent.Verify(x => - x.ParentResourcesChanged(It.IsAny()), - Times.Once); + Assert.Equal(1, raised); } [Fact] diff --git a/tests/Avalonia.Styling.UnitTests/StyledElementTests_Resources.cs b/tests/Avalonia.Styling.UnitTests/StyledElementTests_Resources.cs index 4b86c00cc2..8a79a867d0 100644 --- a/tests/Avalonia.Styling.UnitTests/StyledElementTests_Resources.cs +++ b/tests/Avalonia.Styling.UnitTests/StyledElementTests_Resources.cs @@ -217,23 +217,6 @@ namespace Avalonia.Controls.UnitTests Assert.True(raisedOnChild); } - [Fact] - public void Setting_Logical_Parent_Raises_Style_ResourcesChanged() - { - var style = new Style(x => x.OfType()); - 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() { return new FuncControlTemplate((x, scope) => diff --git a/tests/Avalonia.Styling.UnitTests/StylesTests.cs b/tests/Avalonia.Styling.UnitTests/StylesTests.cs index a61b178d7b..a4afcd34f9 100644 --- a/tests/Avalonia.Styling.UnitTests/StylesTests.cs +++ b/tests/Avalonia.Styling.UnitTests/StylesTests.cs @@ -8,103 +8,59 @@ namespace Avalonia.Styling.UnitTests public class StylesTests { [Fact] - public void Adding_Style_With_Resources_Should_Raise_ResourceChanged() + public void Adding_Style_Should_Set_Owner() { - var style = new Style - { - Resources = { { "foo", "bar" } }, - }; + var host = new Mock(); + var target = new Styles(host.Object); + var style = new Mock(); + var rp = style.As(); - var target = new Styles(); - var raised = false; + host.ResetCalls(); + target.Add(style.Object); - target.ResourcesChanged += (_, __) => raised = true; - target.Add(style); - - Assert.True(raised); + rp.Verify(x => x.AddOwner(host.Object)); } [Fact] - public void Removing_Style_With_Resources_Should_Raise_ResourceChanged() + public void Removing_Style_Should_Clear_Owner() { - var target = new Styles - { - new Style - { - Resources = { { "foo", "bar" } }, - } - }; + var host = new Mock(); + var target = new Styles(host.Object); + var style = new Mock(); + var rp = style.As(); - var raised = false; + host.ResetCalls(); + target.Add(style.Object); + target.Remove(style.Object); - target.ResourcesChanged += (_, __) => raised = true; - target.Clear(); - - Assert.True(raised); + rp.Verify(x => x.RemoveOwner(host.Object)); } [Fact] - public void Adding_Style_Without_Resources_Should_Not_Raise_ResourceChanged() + public void Should_Set_Owner_On_Assigned_Resources() { - var style = new Style(); - var target = new Styles(); - var raised = false; + var host = new Mock(); + var target = new Style(); + ((IResourceProvider)target).AddOwner(host.Object); - target.ResourcesChanged += (_, __) => raised = true; - target.Add(style); + var resources = new Mock(); + target.Resources = resources.Object; - Assert.False(raised); + resources.Verify(x => x.AddOwner(host.Object), Times.Once); } [Fact] - public void Adding_Resource_Should_Raise_Child_ResourceChanged() + public void Should_Set_Owner_On_Assigned_Resources_2() { - 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_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(); - var setResourceParent = childStyle.As(); - var target = new Styles { childStyle.Object }; + var host = new Mock(); + var target = new Styles(); - setResourceParent.ResetCalls(); - ((ISetResourceParent)target).ParentResourcesChanged(new ResourcesChangedEventArgs()); + var resources = new Mock(); + target.Resources = resources.Object; - setResourceParent.Verify(x => x.ParentResourcesChanged( - It.IsAny()), - Times.Once); + host.ResetCalls(); + ((IResourceProvider)target).AddOwner(host.Object); + resources.Verify(x => x.AddOwner(host.Object), Times.Once); } [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); } }