Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public abstract class AppThemeObject<T>
public T? Default { get; set; }

/// <summary>
/// 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.
/// </summary>
/// <returns>A <see cref="AppThemeBinding"/> instance with the respective theme values.</returns>
public virtual BindingBase GetBinding()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
namespace CommunityToolkit.Maui.Extensions;
using System.Diagnostics.CodeAnalysis;

namespace CommunityToolkit.Maui.Extensions;

/// <summary>
/// A XAML markup extension that enables using <see cref="AppThemeColor"/> and <see cref="AppThemeObject"/> from XAML.
/// </summary>
[ContentProperty(nameof(Key)), RequireService([typeof(IServiceProvider), typeof(IProvideParentValues)])]
[ContentProperty(nameof(Key)), RequireService([typeof(IServiceProvider), typeof(IProvideValueTarget), typeof(IRootObjectProvider)])]
public sealed class AppThemeResourceExtension : IMarkupExtension<BindingBase>
{
/// <summary>
Expand All @@ -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<object> parentObjects, out object? resource, out ResourceDictionary? resourceDictionary)
/// <summary>
/// Attempts to locate a resource by walking up the visual tree from a target object.
/// </summary>
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);
}
Loading