diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 8c5f9abb8d..725163bba8 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -149,7 +149,7 @@ namespace Avalonia IStyleHost IStyleHost.StylingParent => null; /// - bool IResourceNode.HasResources => _resources?.Count > 0; + bool IResourceProvider.HasResources => _resources?.Count > 0; /// IResourceNode IResourceNode.ResourceParent => null; @@ -182,7 +182,7 @@ namespace Avalonia } /// - bool IResourceNode.TryGetResource(string key, out object value) + bool IResourceProvider.TryGetResource(string key, out object value) { value = null; return (_resources?.TryGetResource(key, out value) ?? false) || diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index bde9bb6760..b6283ab7ee 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -401,7 +401,7 @@ namespace Avalonia.Controls IAvaloniaReadOnlyList ILogical.LogicalChildren => LogicalChildren; /// - bool IResourceNode.HasResources => _resources?.Count > 0 || Styles.HasResources; + bool IResourceProvider.HasResources => _resources?.Count > 0 || Styles.HasResources; /// IResourceNode IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode; @@ -491,7 +491,7 @@ namespace Avalonia.Controls } /// - bool IResourceNode.TryGetResource(string key, out object value) + bool IResourceProvider.TryGetResource(string key, out object value) { value = null; return (_resources?.TryGetResource(key, out value) ?? false) || diff --git a/src/Avalonia.Styling/Controls/IResourceDictionary.cs b/src/Avalonia.Styling/Controls/IResourceDictionary.cs index 0ddeda0f3d..e6da375544 100644 --- a/src/Avalonia.Styling/Controls/IResourceDictionary.cs +++ b/src/Avalonia.Styling/Controls/IResourceDictionary.cs @@ -9,28 +9,11 @@ namespace Avalonia.Controls /// /// An indexed dictionary of resources. /// - public interface IResourceDictionary : IDictionary + public interface IResourceDictionary : IResourceProvider, IDictionary { - /// - /// Raised when resources in the dictionary are changed. - /// - event EventHandler ResourcesChanged; - /// /// Gets a collection of child resource dictionaries. /// - IList MergedDictionaries { get; } - - /// - /// Tries to find a resource within the dictionary. - /// - /// 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(string key, out object value); + IList MergedDictionaries { get; } } } diff --git a/src/Avalonia.Styling/Controls/IResourceNode.cs b/src/Avalonia.Styling/Controls/IResourceNode.cs index 6ff5fb8d2f..b6a6cdc3e3 100644 --- a/src/Avalonia.Styling/Controls/IResourceNode.cs +++ b/src/Avalonia.Styling/Controls/IResourceNode.cs @@ -3,35 +3,13 @@ namespace Avalonia.Controls { /// - /// Defines an element that can be queried for resources. + /// Represents resource provider in a tree. /// - public interface IResourceNode + public interface IResourceNode : IResourceProvider { - /// - /// Raised when resources in the element are changed. - /// - event EventHandler ResourcesChanged; - - /// - /// Gets a value indicating whether the node has resources. - /// - bool HasResources { get; } - /// /// Gets the parent resource node, if any. /// IResourceNode ResourceParent { get; } - - /// - /// Tries to find a resource within the element. - /// - /// 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(string key, out object value); } } diff --git a/src/Avalonia.Styling/Controls/IResourceProvider.cs b/src/Avalonia.Styling/Controls/IResourceProvider.cs new file mode 100644 index 0000000000..2ca83ea2d2 --- /dev/null +++ b/src/Avalonia.Styling/Controls/IResourceProvider.cs @@ -0,0 +1,32 @@ +using System; + +namespace Avalonia.Controls +{ + /// + /// Represents an object that can be queried for resources. + /// + public interface IResourceProvider + { + /// + /// Raised when resources in the provider are changed. + /// + event EventHandler ResourcesChanged; + + /// + /// Gets a value indicating whether the element has resources. + /// + bool HasResources { get; } + + /// + /// Tries to find a resource within the provider. + /// + /// 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(string key, out object value); + } +} diff --git a/src/Avalonia.Styling/Controls/ResourceDictionary.cs b/src/Avalonia.Styling/Controls/ResourceDictionary.cs index 30fd8056f1..ec0a59dad9 100644 --- a/src/Avalonia.Styling/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Styling/Controls/ResourceDictionary.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using Avalonia.Collections; namespace Avalonia.Controls @@ -13,7 +14,7 @@ namespace Avalonia.Controls /// public class ResourceDictionary : AvaloniaDictionary, IResourceDictionary { - private AvaloniaList _mergedDictionaries; + private AvaloniaList _mergedDictionaries; /// /// Initializes a new instance of the class. @@ -27,18 +28,18 @@ namespace Avalonia.Controls public event EventHandler ResourcesChanged; /// - public IList MergedDictionaries + public IList MergedDictionaries { get { if (_mergedDictionaries == null) { - _mergedDictionaries = new AvaloniaList(); + _mergedDictionaries = new AvaloniaList(); _mergedDictionaries.ResetBehavior = ResetBehavior.Remove; _mergedDictionaries.ForEachItem( x => { - if (x.Count > 0) + if (x.HasResources) { OnResourcesChanged(); } @@ -47,7 +48,7 @@ namespace Avalonia.Controls }, x => { - if (x.Count > 0) + if (x.HasResources) { OnResourcesChanged(); } @@ -61,6 +62,12 @@ namespace Avalonia.Controls } } + /// + bool IResourceProvider.HasResources + { + get => Count > 0 || (_mergedDictionaries?.Any(x => x.HasResources) ?? false); + } + /// public bool TryGetResource(string key, out object value) { diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 6e79ee038e..4182ffada3 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -79,10 +79,10 @@ namespace Avalonia.Styling public IList Setters { get; set; } = new List(); /// - bool IResourceNode.HasResources => _resources?.Count > 0; + IResourceNode IResourceNode.ResourceParent => _parent; /// - IResourceNode IResourceNode.ResourceParent => _parent; + bool IResourceProvider.HasResources => _resources?.Count > 0; /// /// Attaches the style to a control if the style's selector matches. diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 5c2d228e62..f04f148c5b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -34,6 +34,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Data/ResourceInclude.cs new file mode 100644 index 0000000000..035765fae0 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Data/ResourceInclude.cs @@ -0,0 +1,63 @@ +using System; +using System.ComponentModel; +using Avalonia.Controls; +using Portable.Xaml.ComponentModel; +using Portable.Xaml.Markup; + +namespace Avalonia.Markup.Xaml.Data +{ + /// + /// Loads a resource dictionary from a specified URL. + /// + public class ResourceInclude : MarkupExtension, IResourceProvider + { + private Uri _baseUri; + private IResourceDictionary _loaded; + + public event EventHandler ResourcesChanged; + + /// + /// Gets the loaded resource dictionary. + /// + public IResourceDictionary Loaded + { + get + { + if (_loaded == null) + { + var loader = new AvaloniaXamlLoader(); + _loaded = (IResourceDictionary)loader.Load(Source, _baseUri); + + if (_loaded.HasResources) + { + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + } + } + + return _loaded; + } + } + + /// + /// Gets or sets the source URL. + /// + public Uri Source { get; set; } + + /// + bool IResourceProvider.HasResources => Loaded.HasResources; + + /// + bool IResourceProvider.TryGetResource(string key, out object value) + { + return Loaded.TryGetResource(key, out value); + } + + /// + public override object ProvideValue(IServiceProvider serviceProvider) + { + var tdc = (ITypeDescriptorContext)serviceProvider; + _baseUri = tdc?.GetBaseUri(); + return this; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index c1867da9c0..fb308e62b8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -52,7 +52,7 @@ namespace Avalonia.Markup.Xaml.Styling } /// - bool IResourceNode.HasResources => Loaded.HasResources; + bool IResourceProvider.HasResources => Loaded.HasResources; /// IResourceNode IResourceNode.ResourceParent => _parent; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/ResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/ResourceIncludeTests.cs new file mode 100644 index 0000000000..6dc56e425c --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/ResourceIncludeTests.cs @@ -0,0 +1,55 @@ +using System; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Data +{ + public class ResourceIncludeTests + { + public class StaticResourceExtensionTests + { + [Fact] + public void ResourceInclude_Loads_ResourceDictionary() + { + var includeXaml = @" + + #ff506070 + +"; + using (StartWithResources(("test:include.xaml", includeXaml))) + { + var xaml = @" + + + + + + + + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + var brush = (SolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + } + + private IDisposable StartWithResources(params (string, string)[] assets) + { + var assetLoader = new MockAssetLoader(assets); + var services = new TestServices(assetLoader: assetLoader); + return UnitTestApplication.Start(services); + } + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs index c751f9e056..d55e34cbe6 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -485,7 +485,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions Assert.Null(border.Background); - userControl.Resources.MergedDictionaries[0].Add("brush", new SolidColorBrush(0xff506070)); + ((IResourceDictionary)userControl.Resources.MergedDictionaries[0]).Add("brush", new SolidColorBrush(0xff506070)); var brush = (SolidColorBrush)border.Background; Assert.NotNull(brush); diff --git a/tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs b/tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs index f31cbbac0a..1eb3cd9750 100644 --- a/tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs +++ b/tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs @@ -167,7 +167,7 @@ namespace Avalonia.Styling.UnitTests var raised = false; target.ResourcesChanged += (_, __) => raised = true; - target.MergedDictionaries[0].Add("foo", "bar"); + ((IResourceDictionary)target.MergedDictionaries[0]).Add("foo", "bar"); Assert.True(raised); }