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