diff --git a/build/cSpell.json b/build/cSpell.json index 4cf602c04999..2a0c9283a561 100644 --- a/build/cSpell.json +++ b/build/cSpell.json @@ -3,6 +3,7 @@ "language": "en", "words": [ "Avalonia", + "ambiently", "binlog", "Blazor", "blockquotes", diff --git a/doc/articles/migrating-from-previous-releases.md b/doc/articles/migrating-from-previous-releases.md index 049692fa8c64..82435fff1175 100644 --- a/doc/articles/migrating-from-previous-releases.md +++ b/doc/articles/migrating-from-previous-releases.md @@ -35,6 +35,14 @@ This change ensures that the XAML parser will only look for types in an explicit In order to resolve types properly in a conditional XAML namespace, make use to use the [new syntax introduced in Uno 4.8](https://platform.uno/docs/articles/platform-specific-xaml.html?q=condition#specifying-namespaces). +#### ResourceDictionary now require an explicit Uri reference + +Resources dictionaries are now required to be explicitly referenced by URI to be considered during resource resolution. Applications that are already running properly on WinAppSDK should not be impacted by this change. + +The reason for this change is the alignment of the inclusion behavior with WinUI, which does not automatically place dictionaries as ambiently available. + +This behavior can be disabled by using `FeatureConfiguration.ResourceDictionary.IncludeUnreferencedDictionaries`, by setting the value `true`. + #### `IsEnabled` property is moved from `FrameworkElement` to `Control` This property was incorrectly located on `FrameworkElement` but its behavior has not changed. diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyAlias.xaml b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyAlias.xaml new file mode 100644 index 000000000000..1f9e95f135a8 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyAlias.xaml @@ -0,0 +1,13 @@ + + + + + + + + + + + + diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyBrush.xaml b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyBrush.xaml new file mode 100644 index 000000000000..6e95977b53f7 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyBrush.xaml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyButton.xaml b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyButton.xaml new file mode 100644 index 000000000000..13df52197fe0 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyButton.xaml @@ -0,0 +1,12 @@ + + + + + + diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyColorGreen.xaml b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyColorGreen.xaml new file mode 100644 index 000000000000..a957ac87cfd4 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyColorGreen.xaml @@ -0,0 +1,13 @@ + + + + Green + + + DarkGreen + + + + Dummy + diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyColorRed.xaml b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyColorRed.xaml new file mode 100644 index 000000000000..f09230fdbc1b --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyColorRed.xaml @@ -0,0 +1,14 @@ + + + + Red + + + + DarkRed + + + + Dummy + diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_Theme_Changing_Override.xaml b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_Theme_Changing_Override.xaml new file mode 100644 index 000000000000..eaa4a4594940 --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_Theme_Changing_Override.xaml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_Theme_Changing_Override.xaml.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_Theme_Changing_Override.xaml.cs new file mode 100644 index 000000000000..245183c7bf7b --- /dev/null +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_Theme_Changing_Override.xaml.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238 + +namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml.Controls; + +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class ThemeResource_Theme_Changing_Override : Page +{ + public ThemeResource_Theme_Changing_Override() + { + this.InitializeComponent(); + } +} + +public class ThemeResource_Theme_Changing_Override_Custom : ResourceDictionary +{ + private const string GreenUri = "ms-appx:///Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyColorGreen.xaml"; + private const string RedUri = "ms-appx:///Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyColorRed.xaml"; + private const string BrushUri = "ms-appx:///Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyBrush.xaml"; + private const string AliasUri = "ms-appx:///Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyAlias.xaml"; + private const string ButtonUri = "ms-appx:///Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Controls/ThemeResource_TCO_MyButton.xaml"; + + private string _mode; + + public string Mode + { + get => _mode; + set + { + _mode = value; + var colorUri = _mode == "Green" ? GreenUri : RedUri; + + var myBrush = new ResourceDictionary { Source = new Uri(BrushUri) }; + var myAlias = new ResourceDictionary { Source = new Uri(AliasUri) }; + var myButton = new ResourceDictionary { Source = new Uri(ButtonUri) }; + + myBrush.MergedDictionaries.Add(new ResourceDictionary { Source = new Uri(colorUri) }); + myAlias.MergedDictionaries.Add(myBrush); + myButton.MergedDictionaries.Add(myAlias); + + MergedDictionaries.Add(myButton); + } + } +} diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_ThemeResource.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_ThemeResource.cs index be25605160b9..25d5d8641f07 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_ThemeResource.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_ThemeResource.cs @@ -159,6 +159,42 @@ public async Task When_ThemeResource_Style_Switch() } } + [TestMethod] + public async Task When_Theme_Changed() + { + using var _ = StyleHelper.UseFluentStyles(); + + var control = new ThemeResource_Theme_Changing_Override(); + WindowHelper.WindowContent = control; + + await WindowHelper.WaitForIdle(); + + Assert.AreEqual(Colors.Red, (control.button01.Background as SolidColorBrush)?.Color); + Assert.AreEqual(Colors.Red, (control.button02.Background as SolidColorBrush)?.Color); + Assert.AreEqual(Colors.Red, (control.button03.Background as SolidColorBrush)?.Color); + Assert.AreEqual(Colors.Red, (control.button04.Background as SolidColorBrush)?.Color); + + Assert.AreEqual(Colors.Green, (control.button01_override.Background as SolidColorBrush)?.Color); + Assert.AreEqual(Colors.Green, (control.button02_override.Background as SolidColorBrush)?.Color); + Assert.AreEqual(Colors.Green, (control.button03_override.Background as SolidColorBrush)?.Color); + Assert.AreEqual(Colors.Green, (control.button04_override.Background as SolidColorBrush)?.Color); + + using (ThemeHelper.UseDarkTheme()) + { + await WindowHelper.WaitForIdle(); + + Assert.AreEqual(Colors.DarkRed, (control.button01.Background as SolidColorBrush)?.Color); + Assert.AreEqual(Colors.DarkRed, (control.button02.Background as SolidColorBrush)?.Color); + Assert.AreEqual(Colors.DarkRed, (control.button03.Background as SolidColorBrush)?.Color); + Assert.AreEqual(Colors.DarkRed, (control.button04.Background as SolidColorBrush)?.Color); + + Assert.AreEqual(Colors.DarkGreen, (control.button01_override.Background as SolidColorBrush)?.Color); + Assert.AreEqual(Colors.DarkGreen, (control.button02_override.Background as SolidColorBrush)?.Color); + Assert.AreEqual(Colors.DarkGreen, (control.button03_override.Background as SolidColorBrush)?.Color); + Assert.AreEqual(Colors.DarkGreen, (control.button04_override.Background as SolidColorBrush)?.Color); + } + } + private async Task When_DefaultForeground(Color lightThemeColor, Color darkThemeColor) { var run = new Run() diff --git a/src/Uno.UI.Tests/Windows_UI_Xaml/Given_ThemeResource.cs b/src/Uno.UI.Tests/Windows_UI_Xaml/Given_ThemeResource.cs index 6f5481212379..0d8ff599d64b 100644 --- a/src/Uno.UI.Tests/Windows_UI_Xaml/Given_ThemeResource.cs +++ b/src/Uno.UI.Tests/Windows_UI_Xaml/Given_ThemeResource.cs @@ -206,7 +206,6 @@ async Task GoTo(string stateName) } [TestMethod] - [Ignore("DoubleAnimation not supported by Uno.IS_UNIT_TESTS")] public async Task When_Visual_States_DoubleAnimation_Theme_Changed_Reapplied() { var page = new ThemeResource_In_Visual_States_Page(); diff --git a/src/Uno.UI/FeatureConfiguration.cs b/src/Uno.UI/FeatureConfiguration.cs index 973f4de4ada2..df6acbc1f789 100644 --- a/src/Uno.UI/FeatureConfiguration.cs +++ b/src/Uno.UI/FeatureConfiguration.cs @@ -169,6 +169,15 @@ public static class DependencyObject = true; } + public static class ResourceDictionary + { + /// + /// Determines whether unreferenced ResourceDictionary present in the assembly + /// are accessible from app resources. + /// + public static bool IncludeUnreferencedDictionaries { get; set; } + } + public static class Font { private static string _symbolsFont = diff --git a/src/Uno.UI/UI/Xaml/Data/ResourceUpdateReason.cs b/src/Uno.UI/UI/Xaml/Data/ResourceUpdateReason.cs index 7a3305f5081c..4b9f3185a186 100644 --- a/src/Uno.UI/UI/Xaml/Data/ResourceUpdateReason.cs +++ b/src/Uno.UI/UI/Xaml/Data/ResourceUpdateReason.cs @@ -24,6 +24,11 @@ internal enum ResourceUpdateReason /// HotReload = 4, + /// + /// Update marked as XamlLoader + /// + XamlParser = 8, + /// /// Updates that should be propagated recursively through the visual tree /// diff --git a/src/Uno.UI/UI/Xaml/DependencyObjectStore.cs b/src/Uno.UI/UI/Xaml/DependencyObjectStore.cs index 32e83470fa22..490a628c78b7 100644 --- a/src/Uno.UI/UI/Xaml/DependencyObjectStore.cs +++ b/src/Uno.UI/UI/Xaml/DependencyObjectStore.cs @@ -1285,11 +1285,11 @@ internal void UpdateResourceBindings(ResourceUpdateReason updateReason, Resource var dictionariesInScope = GetResourceDictionaries(includeAppResources: false, containingDictionary).ToArray(); - var bindings = _resourceBindings.GetAllBindings().ToList(); //The original collection may be mutated during DP assignations + var bindings = _resourceBindings.GetAllBindings(); - foreach (var (property, binding) in bindings) + foreach (var binding in bindings) { - InnerUpdateResourceBindings(updateReason, dictionariesInScope, property, binding); + InnerUpdateResourceBindings(updateReason, dictionariesInScope, binding.Property, binding.Binding); } UpdateChildResourceBindings(updateReason); @@ -1330,6 +1330,17 @@ private void InnerUpdateResourceBindingsUnsafe(ResourceUpdateReason updateReason return; } + if ((updateReason & ResourceUpdateReason.ResolvedOnLoading) != 0) + { + // Add the current dictionaries to the resolver scope, + // this allows for StaticResource.ResourceKey to resolve properly + + for (var i = dictionariesInScope.Length - 1; i >= 0; i--) + { + ResourceResolver.PushSourceToScope(dictionariesInScope[i]); + } + } + var wasSet = false; foreach (var dict in dictionariesInScope) { @@ -1348,6 +1359,14 @@ private void InnerUpdateResourceBindingsUnsafe(ResourceUpdateReason updateReason SetResourceBindingValue(property, binding, value); } } + + if ((updateReason & ResourceUpdateReason.ResolvedOnLoading) != 0) + { + foreach (var dict in dictionariesInScope) + { + ResourceResolver.PopSourceFromScope(); + } + } } private void SetResourceBindingValue(DependencyProperty property, ResourceBinding binding, object? value) @@ -1469,7 +1488,7 @@ private IEnumerable GetChildrenDependencyObjects() /// internal IEnumerable GetResourceDictionaries(bool includeAppResources, ResourceDictionary? containingDictionary = null) { - if (containingDictionary != null) + if (containingDictionary is not null) { yield return containingDictionary; } @@ -1477,13 +1496,13 @@ internal IEnumerable GetResourceDictionaries(bool includeApp var candidate = ActualInstance; var candidateFE = candidate as FrameworkElement; - while (candidate != null) + while (candidate is not null) { var parent = candidate.GetParent() as DependencyObject; - if (candidateFE != null) + if (candidateFE is not null) { - if (candidateFE.Resources != null) // It's legal (if pointless) on UWP to set Resources to null from user code, so check + if (candidateFE.Resources is { IsEmpty: false }) // It's legal (if pointless) on UWP to set Resources to null from user code, so check { yield return candidateFE.Resources; } @@ -1505,6 +1524,7 @@ internal IEnumerable GetResourceDictionaries(bool includeApp candidate = parent; } } + if (includeAppResources && Application.Current != null) { // In the case of StaticResource resolution we skip Application.Resources because we assume these were already checked at initialize-time. diff --git a/src/Uno.UI/UI/Xaml/ResourceBindingCollection.cs b/src/Uno.UI/UI/Xaml/ResourceBindingCollection.cs index 95f66aca2872..054c1b80e144 100644 --- a/src/Uno.UI/UI/Xaml/ResourceBindingCollection.cs +++ b/src/Uno.UI/UI/Xaml/ResourceBindingCollection.cs @@ -9,47 +9,69 @@ using Uno.UI.DataBinding; using Windows.UI.Xaml.Data; -namespace Windows.UI.Xaml +namespace Windows.UI.Xaml; + +internal class ResourceBindingCollection { - internal class ResourceBindingCollection - { - private readonly Dictionary> _bindings = new Dictionary>(); + private readonly Dictionary> _bindings = new(); + private BindingEntry[]? _cachedAllBindings; + + public bool HasBindings => _bindings.Count > 0 && _bindings.Any(b => b.Value.Any()); - public bool HasBindings => _bindings.Count > 0 && _bindings.Any(b => b.Value.Any()); + public record struct BindingEntry(DependencyProperty Property, ResourceBinding Binding); - public IEnumerable<(DependencyProperty Property, ResourceBinding Binding)> GetAllBindings() + public BindingEntry[] GetAllBindings() + { + if (_cachedAllBindings is null) { + List allBindings = new(); + foreach (var kvp in _bindings) { foreach (var kvpInner in kvp.Value) { - yield return (kvp.Key, kvpInner.Value); + allBindings.Add(new(kvp.Key, kvpInner.Value)); } } + + // We return a fully materialized list every time + // as the callers may enumerate the list and new items + // can be added when resource bindings are evaluated. + _cachedAllBindings = allBindings.ToArray(); } - public IEnumerable? GetBindingsForProperty(DependencyProperty property) - { - if (_bindings.TryGetValue(property, out var bindingsForProperty)) - { - return bindingsForProperty.Values; - } + return _cachedAllBindings; + } - return null; + public IEnumerable? GetBindingsForProperty(DependencyProperty property) + { + if (_bindings.TryGetValue(property, out var bindingsForProperty)) + { + return bindingsForProperty.Values; } - public void Add(DependencyProperty property, ResourceBinding resourceBinding) + return null; + } + + public void Add(DependencyProperty property, ResourceBinding resourceBinding) + { + if (!_bindings.TryGetValue(property, out var dict)) { - var dict = _bindings.FindOrCreate(property, () => new Dictionary()); - dict[resourceBinding.Precedence] = resourceBinding; + _bindings[property] = dict = new(); } - public void ClearBinding(DependencyProperty property, DependencyPropertyValuePrecedences precedence) + dict[resourceBinding.Precedence] = resourceBinding; + + _cachedAllBindings = null; + } + + public void ClearBinding(DependencyProperty property, DependencyPropertyValuePrecedences precedence) + { + if (_bindings.TryGetValue(property, out var bindingsByPrecedence)) { - if (_bindings.TryGetValue(property, out var bindingsByPrecedence)) - { - bindingsByPrecedence.Remove(precedence); - } + bindingsByPrecedence.Remove(precedence); + + _cachedAllBindings = null; } } } diff --git a/src/Uno.UI/UI/Xaml/ResourceDictionary.cs b/src/Uno.UI/UI/Xaml/ResourceDictionary.cs index 420a448c0bcc..a1e05d82bd0c 100644 --- a/src/Uno.UI/UI/Xaml/ResourceDictionary.cs +++ b/src/Uno.UI/UI/Xaml/ResourceDictionary.cs @@ -58,6 +58,14 @@ public Uri Source public IList MergedDictionaries => _mergedDictionaries; public IDictionary ThemeDictionaries => GetOrCreateThemeDictionaries(); + /// + /// Determines if this instance is empty + /// + internal bool IsEmpty + => Count == 0 + && ThemeDictionaries.Count == 0 + && MergedDictionaries.Count == 0; + private ResourceDictionary GetOrCreateThemeDictionaries() { if (_themeDictionaries is null) @@ -195,14 +203,14 @@ private bool TryGetValue(in ResourceKey resourceKey, ResourceKey activeTheme, ou return true; } - if (activeTheme.IsEmpty) + if (GetFromMerged(resourceKey, out value)) { - activeTheme = Themes.Active; + return true; } - if (GetFromMerged(resourceKey, out value)) + if (activeTheme.IsEmpty) { - return true; + activeTheme = Themes.Active; } if (GetFromTheme(resourceKey, activeTheme, out value)) @@ -272,10 +280,24 @@ private void TryMaterializeLazy(in ResourceKey key, ref object value) if (value is LazyInitializer lazyInitializer) { object newValue = null; + bool hasEmptyCurrentScope = lazyInitializer.CurrentScope.Sources.IsEmpty; try { _values.Remove(key); // Temporarily remove the key to make this method safely reentrant, if it's a framework- or application-level theme dictionary - ResourceResolver.PushNewScope(lazyInitializer.CurrentScope); + + if (!hasEmptyCurrentScope) + { + ResourceResolver.PushNewScope(lazyInitializer.CurrentScope); + } + + // Lazy initialized resources must also resolve using the current dictionary + // In previous versions of Uno (4 and earlier), this used to not be needed because all ResourceDictionary + // files where implicitly available at the app level. + if (!FeatureConfiguration.ResourceDictionary.IncludeUnreferencedDictionaries) + { + ResourceResolver.PushSourceToScope(this); + } + newValue = lazyInitializer.Initializer(); } finally @@ -286,7 +308,16 @@ private void TryMaterializeLazy(in ResourceKey key, ref object value) { ResourceDictionaryValueChange?.Invoke(this, EventArgs.Empty); } - ResourceResolver.PopScope(); + + if (!FeatureConfiguration.ResourceDictionary.IncludeUnreferencedDictionaries) + { + ResourceResolver.PopSourceFromScope(); + } + + if (!hasEmptyCurrentScope) + { + ResourceResolver.PopScope(); + } } } } diff --git a/src/Uno.UI/UI/Xaml/ResourceResolver.cs b/src/Uno.UI/UI/Xaml/ResourceResolver.cs index d51c2adfe638..77324a2d3af4 100644 --- a/src/Uno.UI/UI/Xaml/ResourceResolver.cs +++ b/src/Uno.UI/UI/Xaml/ResourceResolver.cs @@ -30,8 +30,8 @@ public static class ResourceResolver Uno.UI.GlobalStaticResources.MasterDictionary; #endif - private static readonly Dictionary> _registeredDictionariesByUri = new Dictionary>(StringComparer.OrdinalIgnoreCase); - private static readonly Dictionary _registeredDictionariesByAssembly = new Dictionary(StringComparer.Ordinal); + private static readonly Dictionary> _registeredDictionariesByUri = new(StringComparer.OrdinalIgnoreCase); + private static readonly Dictionary _registeredDictionariesByAssembly = new(StringComparer.Ordinal); /// /// This is used by hot reload (since converting the file path to a Source is impractical at runtime). /// @@ -245,6 +245,27 @@ internal static object ResolveTopLevelResource(SpecializedResourceDictionary.Res /// Optional parameter that provides parse-time context [EditorBrowsable(EditorBrowsableState.Never)] public static void ApplyResource(DependencyObject owner, DependencyProperty property, object resourceKey, bool isThemeResourceExtension, bool isHotReloadSupported, object context = null) + => ApplyResource( + owner: owner, + property: property, + resourceKey: resourceKey, + isThemeResourceExtension: isThemeResourceExtension, + isHotReloadSupported, + fromXamlParser: false, + context: context); + + /// + /// Apply a StaticResource or ThemeResource assignment to a DependencyProperty of a DependencyObject. The assignment will be provisionally + /// made immediately using Application.Resources if possible, and retried at load-time using the visual-tree scope. + /// + /// Owner of the property + /// The property to assign + /// Key to the resource + /// True for {ThemeResource Foo}, false for {StaticResource Foo} + /// True when the invocation is performed from generated markup code + /// Optional parameter that provides parse-time context + [EditorBrowsable(EditorBrowsableState.Never)] + public static void ApplyResource(DependencyObject owner, DependencyProperty property, object resourceKey, bool isThemeResourceExtension, bool isHotReloadSupported, bool fromXamlParser, object context = null) { var updateReason = ResourceUpdateReason.None; if (isThemeResourceExtension) @@ -260,13 +281,24 @@ public static void ApplyResource(DependencyObject owner, DependencyProperty prop updateReason |= ResourceUpdateReason.HotReload; } + if (fromXamlParser && ResourceResolver.CurrentScope.Sources.IsEmpty) + { + updateReason |= ResourceUpdateReason.XamlParser; + } + ApplyResource(owner, property, new SpecializedResourceDictionary.ResourceKey(resourceKey), updateReason, context, null); } internal static void ApplyResource(DependencyObject owner, DependencyProperty property, SpecializedResourceDictionary.ResourceKey specializedKey, ResourceUpdateReason updateReason, object context, DependencyPropertyValuePrecedences? precedence) { + // If the invocation comes from XAML and from theme resources, resolution + // must happen lazily, done through walking the visual tree. + var immediateResolution = + (updateReason & ResourceUpdateReason.XamlParser) != 0 + && (updateReason & ResourceUpdateReason.ThemeResource) != 0; + // Set initial value based on statically-available top-level resources. - if (TryStaticRetrieval(specializedKey, context, out var value)) + if (!immediateResolution && TryStaticRetrieval(specializedKey, context, out var value)) { owner.SetValue(property, BindingPropertyHelper.Convert(() => property.Type, value), precedence); @@ -383,9 +415,9 @@ private static bool TryStaticRetrieval(in SpecializedResourceDictionary.Resource internal static bool TryTopLevelRetrieval(in SpecializedResourceDictionary.ResourceKey resourceKey, object context, out object value) { value = null; - return (Application.Current?.Resources.TryGetValue(resourceKey, out value, shouldCheckSystem: false) ?? false) || - TryAssemblyResourceRetrieval(resourceKey, context, out value) || - TrySystemResourceRetrieval(resourceKey, out value); + return (Application.Current?.Resources.TryGetValue(resourceKey, out value, shouldCheckSystem: false) ?? false) + || TryAssemblyResourceRetrieval(resourceKey, context, out value) + || TrySystemResourceRetrieval(resourceKey, out value); } /// @@ -503,11 +535,21 @@ public static void RegisterResourceDictionaryBySource(string uri, XamlParseConte if (context != null) { - // We store the dictionaries inside a ResourceDictionary to utilize the lazy-loading machinery - var assemblyDict = _registeredDictionariesByAssembly.FindOrCreate(context.AssemblyName, () => new ResourceDictionary()); - var initializer = new ResourceDictionary.ResourceInitializer(dictionary); - _assemblyRef++; // We don't actually use this key, we just need it to be unique - assemblyDict[_assemblyRef] = initializer; + var isGenericXaml = uri.EndsWith("themes/generic.xaml", StringComparison.OrdinalIgnoreCase); + + if (isGenericXaml || FeatureConfiguration.ResourceDictionary.IncludeUnreferencedDictionaries) + { + // We store the dictionaries inside a ResourceDictionary to utilize the lazy-loading machinery + // to convert ResourceDictionary.ResourceInitializer into actual instances + if (!_registeredDictionariesByAssembly.TryGetValue(context.AssemblyName, out var assemblyDict)) + { + _registeredDictionariesByAssembly[context.AssemblyName] = assemblyDict = new(); + } + + var initializer = new ResourceDictionary.ResourceInitializer(dictionary); + _assemblyRef++; // We don't actually use this key, we just need it to be unique + assemblyDict[_assemblyRef] = initializer; + } } if (filePath != null) diff --git a/src/Uno.UI/UI/Xaml/ResourceResolverSingleton.cs b/src/Uno.UI/UI/Xaml/ResourceResolverSingleton.cs index 3f8377a16c42..f527c4763ade 100644 --- a/src/Uno.UI/UI/Xaml/ResourceResolverSingleton.cs +++ b/src/Uno.UI/UI/Xaml/ResourceResolverSingleton.cs @@ -25,15 +25,19 @@ namespace Uno.UI public sealed class ResourceResolverSingleton { private static ResourceResolverSingleton _instance; - public static ResourceResolverSingleton Instance => _instance ??= new ResourceResolverSingleton(); + public static ResourceResolverSingleton Instance + => _instance ??= new ResourceResolverSingleton(); [EditorBrowsable(EditorBrowsableState.Never)] - public object ResolveResourceStatic(object key, Type type, object context) => ResourceResolver.ResolveResourceStatic(key, type, context); + public object ResolveResourceStatic(object key, Type type, object context) + => ResourceResolver.ResolveResourceStatic(key, type, context); [EditorBrowsable(EditorBrowsableState.Never)] - public void ApplyResource(DependencyObject owner, DependencyProperty property, object resourceKey, bool isThemeResourceExtension, bool isHotReloadSupported, object context) => ResourceResolver.ApplyResource(owner, property, resourceKey, isThemeResourceExtension, isHotReloadSupported, context); + public void ApplyResource(DependencyObject owner, DependencyProperty property, object resourceKey, bool isThemeResourceExtension, bool isHotReloadSupported, object context) + => ResourceResolver.ApplyResource(owner, property, resourceKey, isThemeResourceExtension, isHotReloadSupported, true, context); [EditorBrowsable(EditorBrowsableState.Never)] - public object ResolveStaticResourceAlias(string resourceKey, object parseContext) => ResourceResolver.ResolveStaticResourceAlias(resourceKey, parseContext); + public object ResolveStaticResourceAlias(string resourceKey, object parseContext) + => ResourceResolver.ResolveStaticResourceAlias(resourceKey, parseContext); } } diff --git a/src/Uno.UI/UI/Xaml/XamlScope.cs b/src/Uno.UI/UI/Xaml/XamlScope.cs index 258994ffd10b..57a7e3a4e3ec 100644 --- a/src/Uno.UI/UI/Xaml/XamlScope.cs +++ b/src/Uno.UI/UI/Xaml/XamlScope.cs @@ -11,21 +11,15 @@ namespace Windows.UI.Xaml /// opposed to the visual tree. In particular it allows correct resolution during template resolution, where the visual tree may be /// arbitrarily distant from the xaml tree. /// - internal class XamlScope + internal record XamlScope(ImmutableStack Sources) { - private readonly ImmutableStack _resourceSources; + public XamlScope Push(ManagedWeakReference source) + => new(Sources.Push(source)); - public IEnumerable Sources => _resourceSources; + public XamlScope Pop() + => new(Sources.Pop()); - private XamlScope(ImmutableStack sources) - { - _resourceSources = sources; - } - - public XamlScope Push(ManagedWeakReference source) => new XamlScope(_resourceSources.Push(source)); - - public XamlScope Pop() => new XamlScope(_resourceSources.Pop()); - - public static XamlScope Create() => new XamlScope(ImmutableStack.Create()); + public static XamlScope Create() + => new(ImmutableStack.Create()); } }