Browse Source

Merge pull request #3957 from AvaloniaUI/refactor/resources

Refactor resources to produce less `ResourcesChanged` events.
pull/3972/head
Dariusz Komosiński 6 years ago
committed by GitHub
parent
commit
8b26f4ef25
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 60
      src/Avalonia.Controls/Application.cs
  2. 15
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  3. 12
      src/Avalonia.Controls/Primitives/VisualLayerManager.cs
  4. 4
      src/Avalonia.Controls/TopLevel.cs
  5. 4
      src/Avalonia.Styling/Controls/IResourceDictionary.cs
  6. 32
      src/Avalonia.Styling/Controls/IResourceHost.cs
  7. 28
      src/Avalonia.Styling/Controls/IResourceNode.cs
  8. 41
      src/Avalonia.Styling/Controls/IResourceProvider.cs
  9. 27
      src/Avalonia.Styling/Controls/ISetResourceParent.cs
  10. 164
      src/Avalonia.Styling/Controls/ResourceDictionary.cs
  11. 103
      src/Avalonia.Styling/Controls/ResourceNodeExtensions.cs
  12. 1
      src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs
  13. 8
      src/Avalonia.Styling/IStyledElement.cs
  14. 123
      src/Avalonia.Styling/StyledElement.cs
  15. 2
      src/Avalonia.Styling/Styling/IStyle.cs
  16. 2
      src/Avalonia.Styling/Styling/IStyleHost.cs
  17. 71
      src/Avalonia.Styling/Styling/Style.cs
  18. 181
      src/Avalonia.Styling/Styling/Styles.cs
  19. 32
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs
  20. 52
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs
  21. 18
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs
  22. 66
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
  23. 2
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  24. 65
      tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs
  25. 74
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs
  26. 19
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs
  27. 152
      tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs
  28. 28
      tests/Avalonia.Styling.UnitTests/StyleTests.cs
  29. 77
      tests/Avalonia.Styling.UnitTests/StyledElementTests.cs
  30. 36
      tests/Avalonia.Styling.UnitTests/StyledElementTests_Resources.cs
  31. 115
      tests/Avalonia.Styling.UnitTests/StylesTests.cs

60
src/Avalonia.Controls/Application.cs

@ -30,7 +30,7 @@ namespace Avalonia
/// method.
/// - Tracks the lifetime of the application.
/// </remarks>
public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceNode
public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IResourceHost
{
/// <summary>
/// The application-global data templates.
@ -129,26 +129,13 @@ namespace Avalonia
/// </summary>
public IResourceDictionary Resources
{
get => _resources ?? (Resources = new ResourceDictionary());
get => _resources ??= new ResourceDictionary(this);
set
{
Contract.Requires<ArgumentNullException>(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
/// <remarks>
/// Global styles apply to all windows in the application.
/// </remarks>
public Styles Styles
{
get
{
if (_styles == null)
{
_styles = new Styles(this);
_styles.ResourcesChanged += ThisResourcesChanged;
}
return _styles;
}
}
public Styles Styles => _styles ??= new Styles(this);
/// <inheritdoc/>
bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
/// <inheritdoc/>
bool IResourceNode.HasResources => (_resources?.HasResources ?? false) ||
(((IResourceNode?)_styles)?.HasResources ?? false);
/// <summary>
/// Gets the styling parent of the application, which is null.
/// </summary>
@ -185,13 +164,7 @@ namespace Avalonia
/// <inheritdoc/>
bool IStyleHost.IsStylesInitialized => _styles != null;
/// <inheritdoc/>
bool IResourceProvider.HasResources => _resources?.Count > 0;
/// <inheritdoc/>
IResourceNode IResourceNode.ResourceParent => null;
/// <summary>
/// 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() { }
/// <inheritdoc/>
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<IStyle> styles)
{
_stylesAdded?.Invoke(styles);
@ -282,9 +260,7 @@ namespace Avalonia
try
{
_notifyingResourcesChanged = true;
(_resources as ISetResourceParent)?.ParentResourcesChanged(e);
(_styles as ISetResourceParent)?.ParentResourcesChanged(e);
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
ResourcesChanged?.Invoke(this, ResourcesChangedEventArgs.Empty);
}
finally
{

15
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -287,6 +287,21 @@ namespace Avalonia.Controls.Primitives
return this;
}
protected sealed override void NotifyChildResourcesChanged(ResourcesChangedEventArgs e)
{
var count = VisualChildren.Count;
for (var i = 0; i < count; ++i)
{
if (VisualChildren[i] is ILogical logical)
{
logical.NotifyResourcesChanged(e);
}
}
base.NotifyChildResourcesChanged(e);
}
/// <inheritdoc/>
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{

12
src/Avalonia.Controls/Primitives/VisualLayerManager.cs

@ -55,8 +55,15 @@ namespace Avalonia.Controls.Primitives
((ILogical)layer).NotifyAttachedToLogicalTree(new LogicalTreeAttachmentEventArgs(_logicalRoot, layer, this));
InvalidateArrange();
}
protected override void NotifyChildResourcesChanged(ResourcesChangedEventArgs e)
{
foreach (var l in _layers)
((ILogical)l).NotifyResourcesChanged(e);
base.NotifyChildResourcesChanged(e);
}
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
@ -74,7 +81,6 @@ namespace Avalonia.Controls.Primitives
((ILogical)l).NotifyDetachedFromLogicalTree(e);
}
protected override Size MeasureOverride(Size availableSize)
{
foreach (var l in _layers)

4
src/Avalonia.Controls/TopLevel.cs

@ -127,11 +127,11 @@ namespace Avalonia.Controls
x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty<Cursor>())
.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);
}
}

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

@ -1,11 +1,13 @@
using System.Collections.Generic;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
/// An indexed dictionary of resources.
/// </summary>
public interface IResourceDictionary : IResourceProvider, IDictionary<object, object>
public interface IResourceDictionary : IResourceProvider, IDictionary<object, object?>
{
/// <summary>
/// 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;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
/// Represents resource provider in a tree.
/// Represents an object that can be queried for resources.
/// </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>
/// 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>
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 Avalonia.Styling;
#nullable enable
namespace Avalonia.Controls
{
/// <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>
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>
/// 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>
event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
event EventHandler OwnerChanged;
/// <summary>
/// Gets a value indicating whether the element has resources.
/// Adds an owner to the resource provider.
/// </summary>
bool HasResources { get; }
/// <param name="owner">The owner.</param>
void AddOwner(IResourceHost owner);
/// <summary>
/// Tries to find a resource within the provider.
/// Removes a resource provider owner.
/// </summary>
/// <param name="key">The resource key.</param>
/// <param name="value">
/// When this method returns, contains the value associated with the specified key,
/// if the key is found; otherwise, null.
/// </param>
/// <returns>
/// True if the resource if found, otherwise false.
/// </returns>
bool TryGetResource(object key, out object value);
/// <param name="owner">The owner.</param>
void RemoveOwner(IResourceHost owner);
}
}

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.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Metadata;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
/// An indexed dictionary of resources.
/// </summary>
public class ResourceDictionary : AvaloniaDictionary<object, object>,
IResourceDictionary,
IResourceNode,
ISetResourceParent
public class ResourceDictionary : AvaloniaDictionary<object, object?>, IResourceDictionary
{
private IResourceNode _parent;
private AvaloniaList<IResourceProvider> _mergedDictionaries;
private IResourceHost? _owner;
private AvaloniaList<IResourceProvider>? _mergedDictionaries;
/// <summary>
/// Initializes a new instance of the <see cref="ResourceDictionary"/> class.
@ -25,10 +24,28 @@ namespace Avalonia.Controls
CollectionChanged += OnCollectionChanged;
}
/// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
/// <summary>
/// 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
{
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;
}
}
/// <inheritdoc/>
bool IResourceProvider.HasResources
bool IResourceNode.HasResources
{
get => Count > 0 || (_mergedDictionaries?.Any(x => x.HasResources) ?? false);
}
/// <inheritdoc/>
IResourceNode IResourceNode.ResourceParent => _parent;
get
{
if (Count > 0)
{
return true;
}
/// <inheritdoc/>
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;
}
}
}
/// <inheritdoc/>
void ISetResourceParent.SetParent(IResourceNode parent)
{
if (_parent != null && parent != null)
{
throw new InvalidOperationException("The ResourceDictionary already has a parent.");
return false;
}
_parent = parent;
}
/// <inheritdoc/>
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(ResourcesChangedEventArgs.Empty);
}
}
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(ResourcesChangedEventArgs.Empty);
}
}
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
var ev = new ResourcesChangedEventArgs();
NotifyMergedDictionariesResourcesChanged(ev);
OnResourcesChanged();
Owner?.NotifyHostedResourcesChanged(ResourcesChangedEventArgs.Empty);
}
private void MergedDictionaryResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnResourcesChanged();
}
}

103
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
/// <param name="control">The control.</param>
/// <param name="key">The resource key.</param>
/// <returns>The resource, or <see cref="AvaloniaProperty.UnsetValue"/> if not found.</returns>
public static object FindResource(this IResourceNode control, 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
/// <param name="key">The resource key.</param>
/// <param name="value">On return, contains the resource if found, otherwise null.</param>
/// <returns>True if the resource was found; otherwise false.</returns>
public static bool TryFindResource(this IResourceNode control, object key, out object value)
public static bool TryFindResource(this IResourceHost control, object key, out object? value)
{
Contract.Requires<ArgumentNullException>(control != null);
Contract.Requires<ArgumentNullException>(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<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;
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<object> observer, bool first)
protected override void Subscribed(IObserver<object?> observer, bool first)
{
observer.OnNext(_target.FindResource(_key));
}
@ -88,5 +105,65 @@ namespace Avalonia.Controls
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();
}
}
}
}

1
src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs

@ -4,5 +4,6 @@ namespace Avalonia.Controls
{
public class ResourcesChangedEventArgs : EventArgs
{
public static new readonly ResourcesChangedEventArgs Empty = new ResourcesChangedEventArgs();
}
}

8
src/Avalonia.Styling/IStyledElement.cs

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

123
src/Avalonia.Styling/StyledElement.cs

@ -205,52 +205,20 @@ namespace Avalonia
/// each styled element may in addition define its own styles which are applied to the styled element
/// itself and its children.
/// </remarks>
public Styles Styles
{
get
{
if (_styles is null)
{
_styles = new Styles(this);
_styles.ResourcesChanged += ThisResourcesChanged;
NotifyResourcesChanged(new ResourcesChangedEventArgs());
}
return _styles;
}
}
public Styles Styles => _styles ??= new Styles(this);
/// <summary>
/// Gets or sets the styled element's resource dictionary.
/// </summary>
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 +280,8 @@ namespace Avalonia
IAvaloniaReadOnlyList<ILogical> ILogical.LogicalChildren => LogicalChildren;
/// <inheritdoc/>
bool IResourceProvider.HasResources => _resources?.Count > 0 || Styles.HasResources;
/// <inheritdoc/>
IResourceNode? IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode;
bool IResourceNode.HasResources => (_resources?.HasResources ?? false) ||
(((IResourceNode?)_styles)?.HasResources ?? false);
/// <inheritdoc/>
IAvaloniaReadOnlyList<string> IStyleable.Classes => Classes;
@ -407,7 +373,10 @@ namespace Avalonia
void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e);
/// <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;
return (_resources?.TryGetResource(key, out value) ?? false) ||
@ -442,17 +411,6 @@ namespace Avalonia
OnDetachedFromLogicalTreeCore(e);
}
if (old != null)
{
old.ResourcesChanged -= ThisResourcesChanged;
}
if (Parent != null)
{
Parent.ResourcesChanged += ThisResourcesChanged;
}
NotifyResourcesChanged(new ResourcesChangedEventArgs());
var newRoot = FindLogicalRoot(this);
if (newRoot is object)
@ -460,6 +418,18 @@ namespace Avalonia
var e = new LogicalTreeAttachmentEventArgs(newRoot, this, parent);
OnAttachedToLogicalTreeCore(e);
}
else if (parent is null)
{
// If we were attached to the logical tree, we piggyback on the tree traversal
// there to raise resources changed notifications. If we're being removed from
// the logical tree, then traverse the tree raising notifications now.
//
// We don't raise resources changed notifications if we're being attached to a
// non-rooted control beacuse it's unlikely that dynamic resources need to be
// correct until the control is added to the tree, and it causes a *lot* of
// notifications.
NotifyResourcesChanged();
}
#nullable disable
RaisePropertyChanged(
@ -527,6 +497,28 @@ namespace Avalonia
}
}
/// <summary>
/// Notifies child controls that a change has been made to resources that apply to them.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void NotifyChildResourcesChanged(ResourcesChangedEventArgs e)
{
if (_logicalChildren is object)
{
var count = _logicalChildren.Count;
if (count > 0)
{
e ??= ResourcesChangedEventArgs.Empty;
for (var i = 0; i < count; ++i)
{
_logicalChildren[i].NotifyResourcesChanged(e);
}
}
}
}
/// <summary>
/// Called when the styled element is added to a rooted logical tree.
/// </summary>
@ -656,6 +648,7 @@ namespace Avalonia
_logicalRoot = e.Root;
ApplyStyling();
NotifyResourcesChanged(propagate: false);
OnAttachedToLogicalTree(e);
AttachedToLogicalTree?.Invoke(this, e);
@ -804,31 +797,23 @@ namespace Avalonia
}
}
private void NotifyResourcesChanged(ResourcesChangedEventArgs e)
private void NotifyResourcesChanged(
ResourcesChangedEventArgs? e = null,
bool propagate = true)
{
if (_notifyingResourcesChanged)
if (ResourcesChanged is object)
{
return;
e ??= ResourcesChangedEventArgs.Empty;
ResourcesChanged(this, e);
}
try
{
_notifyingResourcesChanged = true;
(_resources as ISetResourceParent)?.ParentResourcesChanged(e);
(_styles as ISetResourceParent)?.ParentResourcesChanged(e);
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
}
finally
if (propagate)
{
_notifyingResourcesChanged = false;
e ??= ResourcesChangedEventArgs.Empty;
NotifyChildResourcesChanged(e);
}
}
private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e)
{
NotifyResourcesChanged(e);
}
private static IReadOnlyList<IStyle> RecurseStyles(IReadOnlyList<IStyle> styles)
{
var count = styles.Count;

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

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

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

@ -27,7 +27,7 @@ namespace Avalonia.Styling
/// <summary>
/// Gets the parent style host element.
/// </summary>
IStyleHost StylingParent { get; }
IStyleHost? StylingParent { get; }
/// <summary>
/// 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>
/// Defines a style.
/// </summary>
public class Style : AvaloniaObject, IStyle, ISetResourceParent
public class Style : AvaloniaObject, IStyle, IResourceProvider
{
private IResourceNode? _parent;
private IResourceHost? _owner;
private IResourceDictionary? _resources;
private List<ISetter>? _setters;
private List<IAnimation>? _animations;
@ -34,8 +34,18 @@ namespace Avalonia.Styling
Selector = selector(null);
}
/// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged;
public IResourceHost? Owner
{
get => _owner;
private set
{
if (_owner != value)
{
_owner = value;
OwnerChanged?.Invoke(this, EventArgs.Empty);
}
}
}
/// <summary>
/// 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(ResourcesChangedEventArgs.Empty);
}
}
}
}
@ -81,15 +89,11 @@ namespace Avalonia.Styling
/// </summary>
public IList<IAnimation> Animations => _animations ??= new List<IAnimation>();
/// <inheritdoc/>
IResourceNode? IResourceNode.ResourceParent => _parent;
/// <inheritdoc/>
bool IResourceProvider.HasResources => _resources?.Count > 0;
bool IResourceNode.HasResources => _resources?.Count > 0;
IReadOnlyList<IStyle> IStyle.Children => Array.Empty<IStyle>();
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
public bool TryGetResource(object key, out object? result)
{
result = null;
@ -130,26 +133,28 @@ namespace Avalonia.Styling
}
}
/// <inheritdoc/>
void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
void IResourceProvider.AddOwner(IResourceHost owner)
{
ResourcesChanged?.Invoke(this, e);
}
owner = owner ?? throw new ArgumentNullException(nameof(owner));
/// <inheritdoc/>
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);
}
}
}
}

181
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
/// <summary>
/// A style that consists of a number of child styles.
/// </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 IResourceNode? _parent;
private IResourceHost? _owner;
private IResourceDictionary? _resources;
private Dictionary<Type, List<IStyle>?>? _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;
/// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs>? ResourcesChanged;
/// <inheritdoc/>
public int Count => _styles.Count;
/// <inheritdoc/>
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);
}
}
}
/// <summary>
/// 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);
}
}
}
/// <inheritdoc/>
IResourceNode? IResourceNode.ResourceParent => _parent;
/// <inheritdoc/>
bool ICollection<IStyle>.IsReadOnly => false;
/// <inheritdoc/>
bool IResourceNode.HasResources
{
get
{
if (_resources?.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];
IReadOnlyList<IStyle> IStyle.Children => this;
/// <inheritdoc/>
public IStyle this[int index]
{
get => _styles[index];
set => _styles[index] = value;
}
/// <inheritdoc/>
public SelectorMatchResult TryAttach(IStyleable target, IStyleHost? host)
{
_cache ??= new Dictionary<Type, List<IStyle>?>();
@ -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,45 @@ namespace Avalonia.Styling
IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator();
/// <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);
foreach (var child in this)
{
if (child is IResourceProvider r)
{
r.AddOwner(owner);
}
}
}
/// <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);
foreach (var child in this)
{
if (child is IResourceProvider r)
{
r.RemoveOwner(owner);
}
}
}
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
@ -241,22 +285,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<IStyle>(items));
(Owner as IStyleHost)?.StylesAdded(ToReadOnlyList<IStyle>(items));
}
void Remove(IList items)
@ -265,22 +302,15 @@ namespace Avalonia.Styling
{
var style = (IStyle)items[i];
if (style.ResourceParent == this && style is ISetResourceParent setParent)
{
setParent.SetParent(null);
setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
}
if (style.HasResources)
if (Owner is object && style is IResourceProvider resourceProvider)
{
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
resourceProvider.RemoveOwner(Owner);
}
style.ResourcesChanged -= NotifyResourcesChanged;
_cache = null;
}
GetHost()?.StylesRemoved(ToReadOnlyList<IStyle>(items));
(Owner as IStyleHost)?.StylesRemoved(ToReadOnlyList<IStyle>(items));
}
switch (e.Action)
@ -301,50 +331,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;
}
}
}
}

32
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<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;
}
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;
}

52
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
{
/// <summary>
/// Loads a resource dictionary from a specified URL.
/// </summary>
public class ResourceInclude : IResourceNode, ISetResourceParent
public class ResourceInclude : IResourceProvider
{
private IResourceNode _parent;
private Uri _baseUri;
private IResourceDictionary _loaded;
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
private Uri? _baseUri;
private IResourceDictionary? _loaded;
/// <summary>
/// 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;
/// <summary>
/// Gets or sets the source URL.
/// </summary>
public Uri Source { get; set; }
/// <inhertidoc/>
bool IResourceProvider.HasResources => Loaded.HasResources;
public Uri? Source { get; set; }
/// <inhertidoc/>
IResourceNode IResourceNode.ResourceParent => _parent;
bool IResourceNode.HasResources => Loaded.HasResources;
/// <inhertidoc/>
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;
}
/// <inhertidoc/>
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);
}
/// <inhertidoc/>
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)
{

18
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<IResourceNode>())
var stack = serviceProvider.GetService<IAvaloniaXamlIlParentStackProvider>();
// 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.

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

@ -10,32 +10,30 @@ namespace Avalonia.Markup.Xaml.Styling
/// <summary>
/// Includes a style from a URL.
/// </summary>
public class StyleInclude : IStyle, ISetResourceParent
public class StyleInclude : IStyle, IResourceProvider
{
private Uri _baseUri;
private readonly Uri _baseUri;
private IStyle[]? _loaded;
private IResourceNode? _parent;
/// <summary>
/// Initializes a new instance of the <see cref="StyleInclude"/> class.
/// </summary>
/// <param name="baseUri"></param>
/// <param name="baseUri">The base URL for the XAML context.</param>
public StyleInclude(Uri 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)
{
_baseUri = serviceProvider.GetContextBaseUri();
}
/// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged
{
add {}
remove {}
}
public IResourceHost? Owner => (Loaded as IResourceProvider)?.Owner;
/// <summary>
/// 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
}
}
/// <inheritdoc/>
bool IResourceProvider.HasResources => Loaded.HasResources;
/// <inheritdoc/>
IResourceNode? IResourceNode.ResourceParent => _parent;
bool IResourceNode.HasResources => (Loaded as IResourceProvider)?.HasResources ?? false;
IReadOnlyList<IStyle> IStyle.Children => _loaded ?? Array.Empty<IStyle>();
/// <inheritdoc/>
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)
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;
}
}
}
/// <inheritdoc/>
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);
}
}

2
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@ -18,7 +18,7 @@
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.11.5" />
<PackageReference Include="BenchmarkDotNet" Version="0.12.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />

65
tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs

@ -10,6 +10,7 @@ using Avalonia.Styling;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
using Avalonia.Media;
namespace Avalonia.Controls.UnitTests.Primitives
{
@ -512,6 +513,70 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
}
[Fact]
public void Templated_Child_Should_Find_Resource_In_TemplatedParent()
{
var target = new ContentControl
{
Resources =
{
{ "red", Brushes.Red },
},
Template = new FuncControlTemplate<ContentControl>((x, scope) =>
{
var result = new ContentPresenter
{
Name = "PART_ContentPresenter",
[!ContentPresenter.ContentProperty] = x[!ContentControl.ContentProperty],
}.RegisterInNameScope(scope);
result.Bind(ContentPresenter.BackgroundProperty, result.GetResourceObservable("red"));
return result;
}),
};
var root = new TestRoot(target);
target.ApplyTemplate();
var contentPresenter = Assert.IsType<ContentPresenter>(target.GetVisualChildren().Single());
Assert.Same(Brushes.Red, contentPresenter.Background);
}
[Fact]
public void Changing_Resource_In_Templated_Parent_Should_Affect_Templated_Child()
{
var target = new ContentControl
{
Resources =
{
{ "red", Brushes.Red },
},
Template = new FuncControlTemplate<ContentControl>((x, scope) =>
{
var result = new ContentPresenter
{
Name = "PART_ContentPresenter",
[!ContentPresenter.ContentProperty] = x[!ContentControl.ContentProperty],
}.RegisterInNameScope(scope);
result.Bind(ContentPresenter.BackgroundProperty, result.GetResourceObservable("red"));
return result;
}),
};
var root = new TestRoot(target);
target.ApplyTemplate();
var contentPresenter = Assert.IsType<ContentPresenter>(target.GetVisualChildren().Single());
Assert.Same(Brushes.Red, contentPresenter.Background);
target.Resources["red"] = Brushes.Green;
Assert.Same(Brushes.Green, contentPresenter.Background);
}
private static IControl ScrollingContentControlTemplate(ContentControl control, INameScope scope)
{
return new Border

74
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs

@ -592,36 +592,78 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
}
}
[Fact]
public void DynamicResource_Can_Be_Found_In_Nested_Style_File()
{
var style1Xaml = @"
<Styles xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<StyleInclude Source='test:style2.xaml'/>
</Styles>";
var style2Xaml = @"
<Style xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Style.Resources>
<Color x:Key='Red'>Red</Color>
<SolidColorBrush x:Key='RedBrush' Color='{DynamicResource Red}'/>
</Style.Resources>
</Style>";
using (StyledWindow(
("test:style1.xaml", style1Xaml),
("test:style2.xaml", style2Xaml)))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Styles>
<StyleInclude Source='test:style1.xaml'/>
</Window.Styles>
<Border Name='border' Background='{DynamicResource RedBrush}'/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var border = window.FindControl<Border>("border");
var borderBrush = (ISolidColorBrush)border.Background;
Assert.NotNull(borderBrush);
Assert.Equal(0xffff0000, borderBrush.Color.ToUint32());
}
}
[Fact]
public void Control_Property_Is_Updated_When_Parent_Is_Changed()
{
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
using (StyledWindow())
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources>
<Window.Resources>
<SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
</UserControl.Resources>
</Window.Resources>
<Border Name='border' Background='{DynamicResource brush}'/>
</UserControl>";
</Window>";
var loader = new AvaloniaXamlLoader();
var userControl = (UserControl)loader.Load(xaml);
var border = userControl.FindControl<Border>("border");
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var border = window.FindControl<Border>("border");
DelayedBinding.ApplyBindings(border);
DelayedBinding.ApplyBindings(border);
var brush = (SolidColorBrush)border.Background;
Assert.Equal(0xff506070, brush.Color.ToUint32());
var brush = (SolidColorBrush)border.Background;
Assert.Equal(0xff506070, brush.Color.ToUint32());
userControl.Content = null;
window.Content = null;
Assert.Null(border.Background);
Assert.Null(border.Background);
userControl.Content = border;
window.Content = border;
brush = (SolidColorBrush)border.Background;
Assert.Equal(0xff506070, brush.Color.ToUint32());
brush = (SolidColorBrush)border.Background;
Assert.Equal(0xff506070, brush.Color.ToUint32());
}
}
[Fact]

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]
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
{
[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]
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<IResourceHost>();
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");
Assert.True(raised);
host.Verify(x => x.NotifyHostedResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()));
}
[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<IResourceHost>();
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<ResourcesChangedEventArgs>()),
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<IResourceHost>();
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<ResourcesChangedEventArgs>()),
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<IResourceHost>();
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<ResourcesChangedEventArgs>()),
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<IResourceHost>();
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<ResourcesChangedEventArgs>()),
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<ISetResourceParent>();
var host = new Mock<IResourceHost>();
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<ResourcesChangedEventArgs>()),
Times.Once);
[Fact]
public void AddOwner_Sets_MergedDictionary_Owner()
{
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]
public void MergedDictionary_ParentResourcesChanged_Should_Be_Called_On_NotifyResourceChanged()
public void RemoveOwner_Clears_MergedDictionary_Owner()
{
var target = new ResourceDictionary();
var merged = new Mock<ISetResourceParent>();
var host = new Mock<IResourceHost>();
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<ResourcesChangedEventArgs>()),
Times.Once);
Assert.Null(target.Owner);
Assert.Null(((ResourceDictionary)target.MergedDictionaries[0]).Owner);
}
}
}

28
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<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
{
public static readonly StyledProperty<string> FooProperty =

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

@ -101,7 +101,7 @@ namespace Avalonia.Styling.UnitTests
}
[Fact]
public void AttachedToLogicalParent_Should_Be_Called_When_Added_To_Tree()
public void AttachedToLogicalTree_Should_Be_Called_When_Added_To_Tree()
{
var root = new TestRoot();
var parent = new Border();
@ -128,9 +128,9 @@ namespace Avalonia.Styling.UnitTests
Assert.True(childRaised);
Assert.True(grandchildRaised);
}
[Fact]
public void AttachedToLogicalParent_Should_Be_Called_Before_Parent_Change_Signalled()
public void AttachedToLogicalTree_Should_Be_Called_Before_Parent_Change_Signalled()
{
var root = new TestRoot();
var child = new Border();
@ -150,7 +150,7 @@ namespace Avalonia.Styling.UnitTests
}
[Fact]
public void AttachedToLogicalParent_Should_Not_Be_Called_With_GlobalStyles_As_Root()
public void AttachedToLogicalTree_Should_Not_Be_Called_With_GlobalStyles_As_Root()
{
var globalStyles = Mock.Of<IGlobalStyles>();
var root = new TestRoot { StylingParent = globalStyles };
@ -169,7 +169,7 @@ namespace Avalonia.Styling.UnitTests
}
[Fact]
public void AttachedToLogicalParent_Should_Have_Source_Set()
public void AttachedToLogicalTree_Should_Have_Source_Set()
{
var root = new TestRoot();
var canvas = new Canvas();
@ -191,7 +191,7 @@ namespace Avalonia.Styling.UnitTests
}
[Fact]
public void AttachedToLogicalParent_Should_Have_Parent_Set()
public void AttachedToLogicalTree_Should_Have_Parent_Set()
{
var root = new TestRoot();
var canvas = new Canvas();
@ -213,7 +213,7 @@ namespace Avalonia.Styling.UnitTests
}
[Fact]
public void DetachedFromLogicalParent_Should_Be_Called_When_Removed_From_Tree()
public void DetachedFromLogicalTree_Should_Be_Called_When_Removed_From_Tree()
{
var root = new TestRoot();
var parent = new Border();
@ -239,7 +239,7 @@ namespace Avalonia.Styling.UnitTests
}
[Fact]
public void DetachedFromLogicalParent_Should_Not_Be_Called_With_GlobalStyles_As_Root()
public void DetachedFromLogicalTree_Should_Not_Be_Called_With_GlobalStyles_As_Root()
{
var globalStyles = Mock.Of<IGlobalStyles>();
var root = new TestRoot { StylingParent = globalStyles };
@ -259,7 +259,7 @@ namespace Avalonia.Styling.UnitTests
}
[Fact]
public void Parent_Should_Be_Null_When_DetachedFromLogicalParent_Called()
public void Parent_Should_Be_Null_When_DetachedFromLogicalTree_Called()
{
var target = new TestControl();
var root = new TestRoot(target);
@ -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<IResourceDictionary>();
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<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()
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 Adding_To_Logical_Tree_Raises_ResourcesChanged()
{
var style = new Mock<IStyle>();
var setResourceParent = style.As<ISetResourceParent>();
var target = new TestControl { Styles = { style.Object } };
var target = new TestRoot();
var parent = new Decorator { Resources = { { "foo", "bar" } } };
var raised = 0;
setResourceParent.ResetCalls();
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 } };
target.ResourcesChanged += (s, e) => ++raised;
setResourceParent.ResetCalls();
target.Resources.Add("foo", "bar");
parent.Child = target;
setResourceParent.Verify(x =>
x.ParentResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
Times.Once);
Assert.Equal(1, raised);
}
[Fact]

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

@ -154,18 +154,15 @@ namespace Avalonia.Controls.UnitTests
};
var raisedOnTarget = false;
var raisedOnPresenter = false;
var raisedOnChild = false;
target.Measure(Size.Infinity);
target.ResourcesChanged += (_, __) => raisedOnTarget = true;
target.Presenter.ResourcesChanged += (_, __) => raisedOnPresenter = true;
child.ResourcesChanged += (_, __) => raisedOnChild = true;
target.Resources.Add("foo", "bar");
Assert.True(raisedOnTarget);
Assert.True(raisedOnPresenter);
Assert.True(raisedOnChild);
}
@ -201,39 +198,6 @@ namespace Avalonia.Controls.UnitTests
Assert.True(raised);
}
[Fact]
public void Setting_Logical_Parent_Raises_Child_ResourcesChanged()
{
var parent = new ContentControl();
var child = new StyledElement();
((ISetLogicalParent)child).SetParent(parent);
var raisedOnChild = false;
child.ResourcesChanged += (_, __) => raisedOnChild = true;
parent.Resources.Add("foo", "bar");
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()
{
return new FuncControlTemplate<ContentControl>((x, scope) =>

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

@ -8,105 +8,89 @@ 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 target = new Styles();
var raised = false;
var host = new Mock<IResourceHost>();
var target = new Styles(host.Object);
var style = new Mock<IStyle>();
var rp = style.As<IResourceProvider>();
target.ResourcesChanged += (_, __) => raised = true;
target.Add(style);
host.ResetCalls();
target.Add(style.Object);
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<IResourceHost>();
var target = new Styles(host.Object);
var style = new Mock<IStyle>();
var rp = style.As<IResourceProvider>();
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 host = new Mock<IResourceHost>();
var target = new Styles();
var raised = false;
((IResourceProvider)target).AddOwner(host.Object);
target.ResourcesChanged += (_, __) => raised = true;
target.Add(style);
var resources = new Mock<IResourceDictionary>();
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;
var host = new Mock<IResourceHost>();
var target = new Styles();
child.ResourcesChanged += (_, __) => raised = true;
target.Resources.Add("foo", "bar");
var resources = new Mock<IResourceDictionary>();
target.Resources = resources.Object;
Assert.True(raised);
host.ResetCalls();
((IResourceProvider)target).AddOwner(host.Object);
resources.Verify(x => x.AddOwner(host.Object), Times.Once);
}
[Fact]
public void Adding_Resource_To_Sibling_Style_Should_Raise_ResourceChanged()
public void Should_Set_Owner_On_Child_Style()
{
Style style1;
Style style2;
var target = new Styles
{
(style1 = new Style()),
(style2 = new Style()),
};
var raised = false;
var host = new Mock<IResourceHost>();
var target = new Styles();
((IResourceProvider)target).AddOwner(host.Object);
style2.ResourcesChanged += (_, __) => raised = true;
style1.Resources.Add("foo", "bar");
var style = new Mock<IStyle>();
var resourceProvider = style.As<IResourceProvider>();
target.Add(style.Object);
Assert.True(raised);
resourceProvider.Verify(x => x.AddOwner(host.Object), Times.Once);
}
[Fact]
public void ParentResourcesChanged_Should_Be_Propagated_To_Children()
public void Should_Set_Owner_On_Child_Style_2()
{
var childStyle = new Mock<IStyle>();
var setResourceParent = childStyle.As<ISetResourceParent>();
var target = new Styles { childStyle.Object };
var host = new Mock<IResourceHost>();
var target = new Styles();
setResourceParent.ResetCalls();
((ISetResourceParent)target).ParentResourcesChanged(new ResourcesChangedEventArgs());
var style = new Mock<IStyle>();
var resourceProvider = style.As<IResourceProvider>();
target.Add(style.Object);
setResourceParent.Verify(x => x.ParentResourcesChanged(
It.IsAny<ResourcesChangedEventArgs>()),
Times.Once);
host.ResetCalls();
((IResourceProvider)target).AddOwner(host.Object);
resourceProvider.Verify(x => x.AddOwner(host.Object), Times.Once);
}
[Fact]
public void Finds_Resource_In_Merged_Dictionary()
{
@ -124,8 +108,7 @@ namespace Avalonia.Styling.UnitTests
}
};
var result = target.FindResource("foo");
Assert.True(target.TryGetResource("foo", out var result));
Assert.Equal("bar", result);
}
}

Loading…
Cancel
Save