diff --git a/src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlFileGenerator.cs b/src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlFileGenerator.cs index c37156540f4b..b122c41cee85 100644 --- a/src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlFileGenerator.cs +++ b/src/SourceGenerators/Uno.UI.SourceGenerators/XamlGenerator/XamlFileGenerator.cs @@ -4048,6 +4048,22 @@ void WriteBinding(bool isTemplateBindingAttachedProperty, string? prefix = null) } } + foreach (var option in bindingOptions) + { + var themeResourceCandidate = option.Objects.FirstOrDefault(); + if (themeResourceCandidate?.Type is { PreferredXamlNamespace: XamlConstants.PresentationXamlXmlNamespace, Name: "ThemeResource" }) + { + if (option.Member.Name == "TargetNullValue" && themeResourceCandidate.Members.FirstOrDefault().Value is string targetNullValueKey) + { + writer.AppendLineIndented($".ApplyTargetNullValueThemeResource(@\"{targetNullValueKey}\", {ParseContextPropertyAccess})"); + } + else if (option.Member.Name == "FallbackValue" && themeResourceCandidate.Members.FirstOrDefault().Value is string fallbackValueKey) + { + writer.AppendLineIndented($".ApplyFallbackValueThemeResource(@\"{fallbackValueKey}\", {ParseContextPropertyAccess})"); + } + } + } + // xbind initialization if (bindNode != null && !isBindingType) { diff --git a/src/Uno.UI/Helpers/BindingExtensions.cs b/src/Uno.UI/Helpers/BindingExtensions.cs new file mode 100644 index 000000000000..59834e2261ec --- /dev/null +++ b/src/Uno.UI/Helpers/BindingExtensions.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using Microsoft.UI.Xaml.Data; + +namespace Uno.UI.Helpers.Xaml; + +public static class BindingExtensions +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public static Binding ApplyTargetNullValueThemeResource(this Binding binding, string themeResourceName, object parseContext) + { + binding.TargetNullValueThemeResource = themeResourceName; + binding.ParseContext = parseContext; + return binding; + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public static Binding ApplyFallbackValueThemeResource(this Binding binding, string themeResourceName, object parseContext) + { + binding.FallbackValueThemeResource = themeResourceName; + binding.ParseContext = parseContext; + return binding; + } +} diff --git a/src/Uno.UI/UI/Xaml/Data/Binding.cs b/src/Uno.UI/UI/Xaml/Data/Binding.cs index 829e67750fd8..4e35e8ecc3bc 100644 --- a/src/Uno.UI/UI/Xaml/Data/Binding.cs +++ b/src/Uno.UI/UI/Xaml/Data/Binding.cs @@ -123,6 +123,8 @@ public object FallbackValue } } + internal string FallbackValueThemeResource { get; set; } + /// /// Gets or sets a value that indicates the direction of the data flow in the binding. /// @@ -180,6 +182,10 @@ public object Source /// The target null value. public object TargetNullValue { get; set; } + internal string TargetNullValueThemeResource { get; set; } + + internal object ParseContext { get; set; } + /// /// Gets or sets a value that determines the timing of binding source updates for two-way bindings. /// diff --git a/src/Uno.UI/UI/Xaml/DependencyObjectStore.cs b/src/Uno.UI/UI/Xaml/DependencyObjectStore.cs index 3315baa2d4a4..2f82a455333c 100644 --- a/src/Uno.UI/UI/Xaml/DependencyObjectStore.cs +++ b/src/Uno.UI/UI/Xaml/DependencyObjectStore.cs @@ -1289,13 +1289,31 @@ internal void UpdateResourceBindings(ResourceUpdateReason updateReason, Resource throw new ArgumentException(); } + ResourceDictionary[]? dictionariesInScope = null; + + if (updateReason == ResourceUpdateReason.ThemeResource) + { + dictionariesInScope = GetResourceDictionaries(includeAppResources: false, containingDictionary).ToArray(); + for (var i = dictionariesInScope.Length - 1; i >= 0; i--) + { + ResourceResolver.PushSourceToScope(dictionariesInScope[i]); + } + + _properties.UpdateBindingExpressions(); + + foreach (var dict in dictionariesInScope) + { + ResourceResolver.PopSourceFromScope(); + } + } + if (_resourceBindings == null || !_resourceBindings.HasBindings) { UpdateChildResourceBindings(updateReason); return; } - var dictionariesInScope = GetResourceDictionaries(includeAppResources: false, containingDictionary).ToArray(); + dictionariesInScope ??= GetResourceDictionaries(includeAppResources: false, containingDictionary).ToArray(); var bindings = _resourceBindings.GetAllBindings(); @@ -1428,6 +1446,8 @@ private void UpdateChildResourceBindings(ResourceUpdateReason updateReason) // Call OnThemeChanged after bindings of descendants have been updated themeChangeAware.OnThemeChanged(); } + + _properties.OnThemeChanged(); } } diff --git a/src/Uno.UI/UI/Xaml/DependencyPropertyDetailsCollection.Bindings.cs b/src/Uno.UI/UI/Xaml/DependencyPropertyDetailsCollection.Bindings.cs index 69db7e087528..5ebb8d4dda78 100644 --- a/src/Uno.UI/UI/Xaml/DependencyPropertyDetailsCollection.Bindings.cs +++ b/src/Uno.UI/UI/Xaml/DependencyPropertyDetailsCollection.Bindings.cs @@ -1,11 +1,12 @@ using System; using System.Collections.Generic; using System.Text; +using Microsoft.UI.Xaml.Data; using Uno.Collections; using Uno.Extensions; using Uno.Foundation.Logging; +using Uno.UI; using Uno.UI.DataBinding; -using Microsoft.UI.Xaml.Data; namespace Microsoft.UI.Xaml { @@ -219,5 +220,58 @@ internal BindingExpression GetBindingExpression(DependencyProperty dependencyPro return null; } + + internal void UpdateBindingExpressions() + { + foreach (var binding in _bindings) + { + UpdateBindingPropertiesFromThemeResources(binding.ParentBinding); + } + + foreach (var binding in _templateBindings) + { + UpdateBindingPropertiesFromThemeResources(binding.ParentBinding); + } + } + + private static void UpdateBindingPropertiesFromThemeResources(Binding binding) + { + if (binding.TargetNullValueThemeResource is { } targetNullValueThemeResourceKey) + { + binding.TargetNullValue = (object)ResourceResolverSingleton.Instance.ResolveResourceStatic(targetNullValueThemeResourceKey, typeof(object), context: binding.ParseContext); + } + + if (binding.FallbackValueThemeResource is { } fallbackValueThemeResourceKey) + { + binding.FallbackValue = (object)ResourceResolverSingleton.Instance.ResolveResourceStatic(fallbackValueThemeResourceKey, typeof(object), context: binding.ParseContext); + } + } + + internal void OnThemeChanged() + { + foreach (var binding in _bindings) + { + RefreshBindingValueIfNecessary(binding); + } + + foreach (var binding in _templateBindings) + { + RefreshBindingValueIfNecessary(binding); + } + } + + private void RefreshBindingValueIfNecessary(BindingExpression binding) + { + if (binding.ParentBinding.TargetNullValueThemeResource is not null || + binding.ParentBinding.FallbackValueThemeResource is not null) + { + // Note: This may refresh the binding more than really necessary. + // For example, if TargetNullValue is set to a theme resource, but the binding is not null + // In this case, a change to TargetNullValue should probably not refresh the binding. + // Another case is when the ThemeResource evaluates the same between light/dark themes. + // For now, it's not necessary. + binding.RefreshTarget(); + } + } } }