diff --git a/src/CommunityToolkit.Maui/Essentials/AppTheme/AppThemeObject.shared.cs b/src/CommunityToolkit.Maui/Essentials/AppTheme/AppThemeObject.shared.cs index 3f365594b4..72b48b3e78 100644 --- a/src/CommunityToolkit.Maui/Essentials/AppTheme/AppThemeObject.shared.cs +++ b/src/CommunityToolkit.Maui/Essentials/AppTheme/AppThemeObject.shared.cs @@ -29,7 +29,7 @@ public abstract class AppThemeObject public T? Default { get; set; } /// - /// Gets a bindable object which holds the diffent values for each operating system theme. + /// Gets a bindable object which holds the different values for each operating system theme. /// /// A instance with the respective theme values. public virtual BindingBase GetBinding() diff --git a/src/CommunityToolkit.Maui/Extensions/AppThemeResourceExtension.shared.cs b/src/CommunityToolkit.Maui/Extensions/AppThemeResourceExtension.shared.cs index f96cbe622f..c63cdb4b59 100644 --- a/src/CommunityToolkit.Maui/Extensions/AppThemeResourceExtension.shared.cs +++ b/src/CommunityToolkit.Maui/Extensions/AppThemeResourceExtension.shared.cs @@ -1,9 +1,11 @@ -namespace CommunityToolkit.Maui.Extensions; +using System.Diagnostics.CodeAnalysis; + +namespace CommunityToolkit.Maui.Extensions; /// /// A XAML markup extension that enables using and from XAML. /// -[ContentProperty(nameof(Key)), RequireService([typeof(IServiceProvider), typeof(IProvideParentValues)])] +[ContentProperty(nameof(Key)), RequireService([typeof(IServiceProvider), typeof(IProvideValueTarget), typeof(IRootObjectProvider)])] public sealed class AppThemeResourceExtension : IMarkupExtension { /// @@ -21,69 +23,107 @@ public BindingBase ProvideValue(IServiceProvider serviceProvider) if (Key is null) { - throw new XamlParseException($"{nameof(AppThemeResourceExtension)}.{nameof(Key)} Cannot be null. You must set a {nameof(Key)} that specifies the AppTheme resource to use", serviceProvider); + throw new XamlParseException($"{nameof(AppThemeResourceExtension)}.{nameof(Key)} cannot be null.", serviceProvider); } - if (serviceProvider.GetService(typeof(IProvideValueTarget)) is not IProvideParentValues valueProvider) + var valueTarget = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget; + var targetObject = valueTarget?.TargetObject; + if (targetObject is null) { - throw new ArgumentException(null, nameof(serviceProvider)); + var info = (serviceProvider.GetService(typeof(IXmlLineInfoProvider)) as IXmlLineInfoProvider)?.XmlLineInfo; + throw new XamlParseException($"Cannot determine target for {nameof(AppThemeResourceExtension)}.", info); } - if (!TryGetResource(Key, valueProvider.ParentObjects, out var resource, out var resourceDictionary) - && !TryGetApplicationLevelResource(Key, out resource, out resourceDictionary)) + if (TryFindResourceInVisualElement(targetObject, Key, out var resource)) { - var xmlLineInfo = serviceProvider.GetService(typeof(IXmlLineInfoProvider)) is IXmlLineInfoProvider xmlLineInfoProvider ? xmlLineInfoProvider.XmlLineInfo : null; - throw new XamlParseException($"Resource not found for key {Key}", xmlLineInfo); + switch (resource) + { + case AppThemeColor color: + return color.GetBinding(); + case AppThemeObject theme: + return theme.GetBinding(); + default: + var info = (serviceProvider.GetService(typeof(IXmlLineInfoProvider)) as IXmlLineInfoProvider)?.XmlLineInfo; + throw new XamlParseException($"Resource found for key {Key} is not a valid AppTheme resource.", info); + } } - switch (resource) + // Fallback to root object ResourceDictionary (e.g. page-level resources) + var rootProvider = serviceProvider.GetService(typeof(IRootObjectProvider)) as IRootObjectProvider; + var root = rootProvider?.RootObject; + if (root is IResourcesProvider { IsResourcesCreated: true } rootResources + && rootResources.Resources.TryGetValue(Key, out resource)) { - case AppThemeColor color: - return color.GetBinding(); - case AppThemeObject themeResource: - return themeResource.GetBinding(); - default: - { - var xmlLineInfo = serviceProvider.GetService(typeof(IXmlLineInfoProvider)) is IXmlLineInfoProvider xmlLineInfoProvider ? xmlLineInfoProvider.XmlLineInfo : null; - throw new XamlParseException($"Resource found for key {Key} is not of type {nameof(AppThemeColor)} or {nameof(AppThemeObject)}", xmlLineInfo); - } + switch (resource) + { + case AppThemeColor rootColor: + return rootColor.GetBinding(); + case AppThemeObject rootTheme: + return rootTheme.GetBinding(); + default: + var info = (serviceProvider.GetService(typeof(IXmlLineInfoProvider)) as IXmlLineInfoProvider)?.XmlLineInfo; + throw new XamlParseException($"Resource found for key {Key} is not a valid AppTheme resource.", info); + } } + if (Application.Current?.Resources.TryGetValueAndSource(Key, out resource, out _) is true) + { + switch (resource) + { + case AppThemeColor color: + return color.GetBinding(); + case AppThemeObject theme: + return theme.GetBinding(); + default: + var info = (serviceProvider.GetService(typeof(IXmlLineInfoProvider)) as IXmlLineInfoProvider)?.XmlLineInfo; + throw new XamlParseException($"Resource found for key {Key} is not a valid AppTheme resource.", info); + } + } + + var xmlInfo = (serviceProvider.GetService(typeof(IXmlLineInfoProvider)) as IXmlLineInfoProvider)?.XmlLineInfo; + throw new XamlParseException($"Resource not found for key {Key}.", xmlInfo); } - static bool TryGetResource(string key, IEnumerable parentObjects, out object? resource, out ResourceDictionary? resourceDictionary) + /// + /// Attempts to locate a resource by walking up the visual tree from a target object. + /// + static bool TryFindResourceInVisualElement(object element, string key, [NotNullWhen(true)] out object? resource) { resource = null; - resourceDictionary = null; - foreach (var parentObject in parentObjects) + // If the element has a Resources property via IResourcesProvider + if (element is IResourcesProvider { IsResourcesCreated: true } provider + && provider.Resources.TryGetValue(key, out resource)) { - var resDict = parentObject is IResourcesProvider { IsResourcesCreated: true } resourcesProvider - ? resourcesProvider.Resources - : parentObject as ResourceDictionary; - if (resDict is null) - { - continue; - } + return true; + } - if (resDict.TryGetValueAndSource(key, out resource, out resourceDictionary)) + switch (element) + { + // Walk up the element tree to try to find the resource + case Element elementObj: { - return true; + var parent = elementObj.Parent; + while (parent is not null) + { + if (parent is IResourcesProvider { IsResourcesCreated: true } parentProvider + && parentProvider.Resources.TryGetValue(key, out resource)) + { + return true; + } + + parent = parent.Parent; + } + + break; } + // If it's a ResourceDictionary, check it directly + case ResourceDictionary dict when dict.TryGetValue(key, out resource): + return true; } return false; } - static bool TryGetApplicationLevelResource(string key, out object? resource, out ResourceDictionary? resourceDictionary) - { - resource = null; - resourceDictionary = null; - - return Application.Current is not null - && ((IResourcesProvider)Application.Current).IsResourcesCreated - && Application.Current.Resources.TryGetValueAndSource(key, out resource, out resourceDictionary); - } - object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider) => ProvideValue(serviceProvider); } \ No newline at end of file