diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI/Given_WeakEventHelper.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI/Given_WeakEventHelper.cs index 2d33b095c8f7..cbba8d594fec 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI/Given_WeakEventHelper.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI/Given_WeakEventHelper.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; +using Microsoft.UI.Xaml.Controls; using Uno.Buffers; using Windows.Graphics.Capture; using Windows.UI.Core; @@ -13,7 +14,7 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI; [TestClass] -public class Given_WeakEventHelper +public partial class Given_WeakEventHelper { [TestMethod] public void When_Explicit_Dispose() @@ -96,6 +97,61 @@ void Do() SUT.Invoke(this, null); Assert.AreEqual(2, invoked); + + disposable.Dispose(); + disposable = null; + + GC.Collect(2); + GC.WaitForPendingFinalizers(); + + SUT.Invoke(this, null); + + Assert.AreEqual(2, invoked); + } + + [TestMethod] + [RunsOnUIThread] + public void When_UIElement_Target_Collected() + { + WeakEventHelper.WeakEventCollection SUT = new(); + + var invoked = 0; + IDisposable disposable = null; + + void Do() + { + Action action = () => invoked++; + + // Wrapping the action and registering the one on the target + // allows for the WeakEventHelper to check for collection native + // objects on android. + MyCollectibleObject target = new(action); + + disposable = WeakEventHelper.RegisterEvent(SUT, target.MyAction, (s, e, a) => (s as Action).Invoke()); + + SUT.Invoke(this, null); + + Assert.AreEqual(1, invoked); + } + + Do(); + + GC.Collect(2); + GC.WaitForPendingFinalizers(); + + SUT.Invoke(this, null); + + Assert.AreEqual(2, invoked); + + disposable.Dispose(); + disposable = null; + + GC.Collect(2); + GC.WaitForPendingFinalizers(); + + SUT.Invoke(this, null); + + Assert.AreEqual(2, invoked); } [TestMethod] @@ -220,6 +276,18 @@ public void When_Empty_Trim_Stops() Assert.AreEqual(1, invoked); } + private partial class MyCollectibleObject : Grid + { + private Action _action; + + public MyCollectibleObject(Action action) + { + _action = action; + } + + public void MyAction() => _action.Invoke(); + } + private class TestPlatformProvider : WeakEventHelper.ITrimProvider { private object _target; diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_ResourceDictionary.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_ResourceDictionary.cs index 8bc400d9dfeb..5a38cbd3ca6f 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_ResourceDictionary.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_ResourceDictionary.cs @@ -246,6 +246,65 @@ public void When_LinkedResDict_ThemeUpdated() ResourceDictionary.SetActiveTheme(theme); } } + + [TestMethod] + public void When_Key_Added_Then_NotFound_Cleared() + { + var resourceDictionary = new ResourceDictionary(); + + Assert.IsFalse(resourceDictionary.TryGetValue("Key1", out var res1, shouldCheckSystem: false)); + resourceDictionary["Key1"] = "Value1"; + Assert.IsTrue(resourceDictionary.TryGetValue("Key1", out var res2, shouldCheckSystem: false)); + } + + [TestMethod] + public void When_Merged_Dictionary_Added_Then_NotFound_Cleared() + { + var resourceDictionary = new ResourceDictionary(); + + Assert.IsFalse(resourceDictionary.TryGetValue("Key1", out var res1, shouldCheckSystem: false)); + + var m1 = new ResourceDictionary(); + m1["Key1"] = "Value1"; + + resourceDictionary.MergedDictionaries.Add(m1); + + Assert.IsTrue(resourceDictionary.TryGetValue("Key1", out var res2, shouldCheckSystem: false)); + } + + [TestMethod] + public void When_Merged_Dictionary_Key_Added_Then_NotFound_Cleared() + { + var resourceDictionary = new ResourceDictionary(); + + Assert.IsFalse(resourceDictionary.TryGetValue("Key1", out var res1, shouldCheckSystem: false)); + + var m1 = new ResourceDictionary(); + resourceDictionary.MergedDictionaries.Add(m1); + + Assert.IsFalse(resourceDictionary.TryGetValue("Key1", out var res2, shouldCheckSystem: false)); + + m1["Key1"] = "Value1"; + + Assert.IsTrue(resourceDictionary.TryGetValue("Key1", out var res3, shouldCheckSystem: false)); + } + + [TestMethod] + public void When_Theme_Dictionary_Key_Added_Then_NotFound_Cleared() + { + var resourceDictionary = new ResourceDictionary(); + + Assert.IsFalse(resourceDictionary.TryGetValue("Key1", out var res1, shouldCheckSystem: false)); + + var m1 = new ResourceDictionary(); + resourceDictionary.ThemeDictionaries["Light"] = m1; + + Assert.IsFalse(resourceDictionary.TryGetValue("Key1", out var res2, shouldCheckSystem: false)); + + m1["Key1"] = "Value1"; + + Assert.IsTrue(resourceDictionary.TryGetValue("Key1", out var res3, shouldCheckSystem: false)); + } #endif } } diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_Pivot.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_Pivot.cs index 32fc7b0a5aed..414e3ce20cc8 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_Pivot.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml_Controls/Given_Pivot.cs @@ -77,9 +77,14 @@ public async Task Check_Binding() tbs2.Should().NotBeNull(); - // For some reason, the count is 0 in Windows. So this doesn't currently match Windows. +#if !__IOS__ && !__ANDROID__ + // Pivot items are materialized on demand, there should not be any text block in the second item. + tbs2.Should().HaveCount(0); +#else + // iOS/Android still materializes the content of the second item, even if it's not visible. tbs2.Should().HaveCount(1); items[1].Content.Should().Be(tbs2.ElementAt(0).Text); +#endif } #if !WINAPPSDK // GetTemplateChild is protected in UWP while public in Uno. diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/Repeater/ItemsRepeater.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/Repeater/ItemsRepeater.cs index f5bbb6c91400..42c3c2002907 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/Repeater/ItemsRepeater.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/Repeater/ItemsRepeater.cs @@ -624,13 +624,12 @@ void OnLoaded(object sender, RoutedEventArgs args) // Uno specific: If the control was unloaded but is loaded again, reattach Layout and DataSource events if (_layoutSubscriptionsRevoker.Disposable is null && Layout is { } layout) { - layout.MeasureInvalidated += InvalidateMeasureForLayout; - layout.ArrangeInvalidated += InvalidateArrangeForLayout; - _layoutSubscriptionsRevoker.Disposable = Disposable.Create(() => - { - layout.MeasureInvalidated -= InvalidateMeasureForLayout; - layout.ArrangeInvalidated -= InvalidateArrangeForLayout; - }); + InvalidateMeasure(); + + var disposables = new CompositeDisposable(); + layout.RegisterMeasureInvalidated(InvalidateMeasureForLayout).DisposeWith(disposables); + layout.RegisterArrangeInvalidated(InvalidateArrangeForLayout).DisposeWith(disposables); + _layoutSubscriptionsRevoker.Disposable = disposables; } if (_dataSourceSubscriptionsRevoker.Disposable is null && m_itemsSourceView is not null) @@ -853,14 +852,14 @@ void OnLayoutChanged(Layout oldValue, Layout newValue) if (newValue != null) { + _layoutSubscriptionsRevoker.Disposable = null; + newValue.InitializeForContext(GetLayoutContext()); - newValue.MeasureInvalidated += InvalidateMeasureForLayout; - newValue.ArrangeInvalidated += InvalidateArrangeForLayout; - _layoutSubscriptionsRevoker.Disposable = Disposable.Create(() => - { - newValue.MeasureInvalidated -= InvalidateMeasureForLayout; - newValue.ArrangeInvalidated -= InvalidateArrangeForLayout; - }); + + var disposables = new CompositeDisposable(); + newValue.RegisterMeasureInvalidated(InvalidateMeasureForLayout).DisposeWith(disposables); + newValue.RegisterArrangeInvalidated(InvalidateArrangeForLayout).DisposeWith(disposables); + _layoutSubscriptionsRevoker.Disposable = disposables; } bool isVirtualizingLayout = newValue is VirtualizingLayout; diff --git a/src/Uno.UI/Microsoft/UI/Xaml/Controls/Repeater/Layout.cs b/src/Uno.UI/Microsoft/UI/Xaml/Controls/Repeater/Layout.cs index 3921d5ba2259..ec5469694621 100644 --- a/src/Uno.UI/Microsoft/UI/Xaml/Controls/Repeater/Layout.cs +++ b/src/Uno.UI/Microsoft/UI/Xaml/Controls/Repeater/Layout.cs @@ -4,11 +4,40 @@ using System; using Windows.Foundation; using Microsoft.UI.Xaml; +using Windows.UI.Core; namespace Microsoft/* UWP don't rename */.UI.Xaml.Controls { public partial class Layout : DependencyObject { + // Begin Uno specific: + // + // We rely on the GC to manage registrations + // but in the case of layouts, for ItemView for instance, actual instances + // may be placed directly in dictionaries, such as: + // https://github.com/unoplatform/uno/blob/c992ed058d1479cce8e6bca58acbf82cc54ce938/src/Uno.UI/Microsoft/UI/Xaml/Controls/ItemsView/ItemsView.xaml#L12-L16 + // To avoid memory leaks, it's best to use the two register methods. + + private WeakEventHelper.WeakEventCollection _measureInvalidatedHandlers; + private WeakEventHelper.WeakEventCollection _arrangeInvalidatedHandlers; + + internal IDisposable RegisterMeasureInvalidated(TypedEventHandler handler) + => WeakEventHelper.RegisterEvent( + _measureInvalidatedHandlers ??= new(), + handler, + (h, s, e) => + (h as TypedEventHandler)?.Invoke((Layout)s, e) + ); + internal IDisposable RegisterArrangeInvalidated(TypedEventHandler handler) + => WeakEventHelper.RegisterEvent( + _arrangeInvalidatedHandlers ??= new(), + handler, + (h, s, e) => + (h as TypedEventHandler)?.Invoke((Layout)s, e) + ); + + // End Uno specific: + public event TypedEventHandler MeasureInvalidated; public event TypedEventHandler ArrangeInvalidated; @@ -103,9 +132,15 @@ public Size Arrange(LayoutContext context, Size finalSize) } protected void InvalidateMeasure() - => MeasureInvalidated?.Invoke(this, null); + { + _measureInvalidatedHandlers?.Invoke(this, null); + MeasureInvalidated?.Invoke(this, null); + } protected void InvalidateArrange() - => ArrangeInvalidated?.Invoke(this, null); + { + _arrangeInvalidatedHandlers?.Invoke(this, null); + ArrangeInvalidated?.Invoke(this, null); + } } } diff --git a/src/Uno.UI/UI/Xaml/Application.cs b/src/Uno.UI/UI/Xaml/Application.cs index 4e23b7144cb4..87c5a3bd6b31 100644 --- a/src/Uno.UI/UI/Xaml/Application.cs +++ b/src/Uno.UI/UI/Xaml/Application.cs @@ -61,6 +61,7 @@ public partial class Application private ApplicationTheme _requestedTheme = ApplicationTheme.Dark; private SpecializedResourceDictionary.ResourceKey _requestedThemeForResources; private bool _isInBackground; + private ResourceDictionary _resources = new ResourceDictionary(); static Application() { @@ -208,7 +209,15 @@ internal void SetExplicitRequestedTheme(ApplicationTheme? explicitTheme) SetRequestedTheme(theme); } - public ResourceDictionary Resources { get; set; } = new ResourceDictionary(); + public ResourceDictionary Resources + { + get => _resources; + set + { + _resources = value; + _resources.InvalidateNotFoundCache(true); + } + } #pragma warning disable CS0067 // The event is never used /// diff --git a/src/Uno.UI/UI/Xaml/Controls/CommandBar/CommandBar.Partial.cs b/src/Uno.UI/UI/Xaml/Controls/CommandBar/CommandBar.Partial.cs index 0a4de192653e..229b67ca0e61 100644 --- a/src/Uno.UI/UI/Xaml/Controls/CommandBar/CommandBar.Partial.cs +++ b/src/Uno.UI/UI/Xaml/Controls/CommandBar/CommandBar.Partial.cs @@ -1411,6 +1411,8 @@ private void OnPrimaryCommandsChanged(IObservableVector send var element = m_tpDynamicPrimaryCommands?[(int)changeIndex]; if (element is { }) { + element.SetParent(this); + element.IsCompact = shouldBeCompact; PropagateDefaultLabelPositionToElement(element); } @@ -1425,6 +1427,8 @@ private void OnPrimaryCommandsChanged(IObservableVector send var element = m_tpDynamicPrimaryCommands?[i]; if (element is { }) { + element.SetParent(null); + PropagateDefaultLabelPositionToElement(element); } } @@ -1453,6 +1457,8 @@ private void OnSecondaryCommandsChanged(IObservableVector se if (element is { }) { + element.SetParent(this); + PropagateDefaultLabelPositionToElement(element); SetOverflowStyleAndInputModeOnSecondaryCommand((int)changeIndex, true); PropagateDefaultLabelPositionToElement(element); @@ -1468,6 +1474,8 @@ private void OnSecondaryCommandsChanged(IObservableVector se if (element is { }) { + element.SetParent(null); + SetOverflowStyleAndInputModeOnSecondaryCommand(i, true); PropagateDefaultLabelPositionToElement(element); } diff --git a/src/Uno.UI/UI/Xaml/DependencyObjectStore.cs b/src/Uno.UI/UI/Xaml/DependencyObjectStore.cs index 07440d333c92..f03d4f15761e 100644 --- a/src/Uno.UI/UI/Xaml/DependencyObjectStore.cs +++ b/src/Uno.UI/UI/Xaml/DependencyObjectStore.cs @@ -1632,7 +1632,7 @@ internal IEnumerable GetResourceDictionaries( { if (candidate is FrameworkElement fe) { - if (fe.Resources is { IsEmpty: false }) // It's legal (if pointless) on UWP to set Resources to null from user code, so check + if (fe.TryGetResources() is { IsEmpty: false }) // It's legal (if pointless) on UWP to set Resources to null from user code, so check { yield return fe.Resources; } diff --git a/src/Uno.UI/UI/Xaml/FrameworkElement.Layout.crossruntime.cs b/src/Uno.UI/UI/Xaml/FrameworkElement.Layout.crossruntime.cs index e5c7d1b244a5..44ad78967586 100644 --- a/src/Uno.UI/UI/Xaml/FrameworkElement.Layout.crossruntime.cs +++ b/src/Uno.UI/UI/Xaml/FrameworkElement.Layout.crossruntime.cs @@ -25,6 +25,7 @@ public partial class FrameworkElement private readonly static IEventProvider _trace = Tracing.Get(FrameworkElement.TraceProvider.Id); private bool m_firedLoadingEvent; + private bool m_requiresResourcesUpdate = true; private const double SIZE_EPSILON = 0.05d; private readonly Size MaxSize = new Size(double.PositiveInfinity, double.PositiveInfinity); @@ -285,8 +286,9 @@ private void InnerMeasureCore(Size availableSize) InvokeApplyTemplate(out _); // TODO: BEGIN Uno specific - if (this is Control thisAsControl) + if (m_requiresResourcesUpdate && this is Control thisAsControl) { + m_requiresResourcesUpdate = false; // Update bindings to ensure resources defined // in visual parents get applied. this.UpdateResourceBindings(); @@ -991,6 +993,10 @@ internal override void EnterImpl(EnterParams @params, int depth) { var core = this.GetContext(); + // ---------- Uno-specific BEGIN ---------- + m_requiresResourcesUpdate = true; + // ---------- Uno-specific END ---------- + //if (@params.IsLive && @params.CheckForResourceOverrides == false) //{ // var resources = GetResourcesNoCreate(); @@ -1066,7 +1072,7 @@ internal override void LeaveImpl(LeaveParams @params) // of properties that are marked with MetaDataPropertyInfoFlags::IsSparse and MetaDataPropertyInfoFlags::IsVisualTreeProperty // are entered as well. // The property we currently know it has an effect is Resources - if (Resources is not null) + if (TryGetResources() is not null) { // Using ValuesInternal to avoid Enumerator boxing foreach (var resource in Resources.ValuesInternal) diff --git a/src/Uno.UI/UI/Xaml/FrameworkElement.cs b/src/Uno.UI/UI/Xaml/FrameworkElement.cs index 1f6ebb6a0918..cf3c367a3835 100644 --- a/src/Uno.UI/UI/Xaml/FrameworkElement.cs +++ b/src/Uno.UI/UI/Xaml/FrameworkElement.cs @@ -73,6 +73,8 @@ public static class TraceProvider private bool _defaultStyleApplied; + private ResourceDictionary _resources; + private static readonly Uri DefaultBaseUri = new Uri("ms-appx://local"); private string _baseUriFromParser; @@ -249,7 +251,6 @@ partial void Initialize() #if !UNO_REFERENCE_API _layouter = new FrameworkElementLayouter(this, MeasureOverride, ArrangeOverride); #endif - Resources = new Microsoft.UI.Xaml.ResourceDictionary(); IFrameworkElementHelper.Initialize(this); } @@ -260,9 +261,21 @@ partial void Initialize() #endif Microsoft.UI.Xaml.ResourceDictionary Resources { - get; set; + get => _resources ??= new ResourceDictionary(); + set + { + _resources = value; + _resources.InvalidateNotFoundCache(true); + } } + /// + /// Tries getting the ResourceDictionary without initializing it. + /// + /// A ResourceDictionary instance or null + internal Microsoft.UI.Xaml.ResourceDictionary TryGetResources() + => _resources; + /// /// Gets the parent of this FrameworkElement in the object tree. /// @@ -956,7 +969,7 @@ async void ApplyPhase() /// internal virtual void UpdateThemeBindings(ResourceUpdateReason updateReason) { - Resources?.UpdateThemeBindings(updateReason); + TryGetResources()?.UpdateThemeBindings(updateReason); (this as IDependencyObjectStoreProvider).Store.UpdateResourceBindings(updateReason); if (updateReason == ResourceUpdateReason.ThemeResource) diff --git a/src/Uno.UI/UI/Xaml/ResourceDictionary.cs b/src/Uno.UI/UI/Xaml/ResourceDictionary.cs index 7ad64a0e912a..18a76c35352e 100644 --- a/src/Uno.UI/UI/Xaml/ResourceDictionary.cs +++ b/src/Uno.UI/UI/Xaml/ResourceDictionary.cs @@ -14,15 +14,19 @@ using System.Runtime.CompilerServices; using Microsoft.UI.Xaml.Data; using Uno.UI.DataBinding; +using System.Collections.ObjectModel; +using System.Runtime.InteropServices; namespace Microsoft.UI.Xaml { public partial class ResourceDictionary : DependencyObject, IDependencyObjectParse, IDictionary { private readonly SpecializedResourceDictionary _values = new SpecializedResourceDictionary(); - private readonly List _mergedDictionaries = new List(); + private readonly ObservableCollection _mergedDictionaries = new(); private ResourceDictionary _themeDictionaries; + private ResourceDictionary _parent; private ManagedWeakReference _sourceDictionary; + private HashSet _keyNotFoundCache; /// /// This event is fired when a key that has value of type is added or changed in the current @@ -36,6 +40,25 @@ public partial class ResourceDictionary : DependencyObject, IDependencyObjectPar public ResourceDictionary() { + _mergedDictionaries.CollectionChanged += (s, e) => + { + if (e.OldItems != null) + { + foreach (ResourceDictionary oldDict in e.OldItems) + { + oldDict._parent = null; + } + } + if (e.NewItems != null) + { + foreach (ResourceDictionary newDict in e.NewItems) + { + newDict._parent = this; + } + + InvalidateNotFoundCache(true); + } + }; } private Uri _source; @@ -70,7 +93,7 @@ private ResourceDictionary GetOrCreateThemeDictionaries() { if (_themeDictionaries is null) { - _themeDictionaries = new ResourceDictionary(); + _themeDictionaries = new ResourceDictionary() { _parent = this }; _themeDictionaries.ResourceDictionaryValueChange += (sender, e) => { // Invalidate the cache whenever a theme dictionary is added/removed. @@ -89,6 +112,10 @@ private ResourceDictionary GetOrCreateThemeDictionaries() /// internal bool IsSystemDictionary { get; set; } + + private HashSet KeyNotFoundCache + => _keyNotFoundCache ??= new(SpecializedResourceDictionary.ResourceKeyComparer.Default); + internal object Lookup(object key) { if (!TryGetValue(key, out var value)) @@ -203,7 +230,21 @@ internal bool TryGetValue(Type resourceKey, out object value, bool shouldCheckSy [MethodImpl(MethodImplOptions.AggressiveInlining)] internal bool TryGetValue(in ResourceKey resourceKey, out object value, bool shouldCheckSystem) { - if (_values.TryGetValue(resourceKey, out value)) + bool useKeysNotFoundCache = resourceKey.ShouldFilter; + var modifiedKey = resourceKey; + + if (useKeysNotFoundCache) + { + if (!shouldCheckSystem && KeyNotFoundCache.Contains(resourceKey)) + { + value = null; + return false; + } + + modifiedKey = modifiedKey with { ShouldFilter = false }; + } + + if (_values.TryGetValue(modifiedKey, out value)) { if (value is SpecialValue) { @@ -217,19 +258,25 @@ internal bool TryGetValue(in ResourceKey resourceKey, out object value, bool sho return true; } - if (GetFromMerged(resourceKey, out value)) + if (GetFromMerged(modifiedKey, out value)) { return true; } - if (GetFromTheme(resourceKey, GetActiveThemeDictionary(Themes.Active), out value)) + if (GetActiveThemeDictionary(Themes.Active) is { } activeThemeDictionary + && activeThemeDictionary.TryGetValue(resourceKey, out value, shouldCheckSystem: false)) { return true; } if (shouldCheckSystem && !IsSystemDictionary) // We don't fall back on system resources from within a system-defined dictionary, to avoid an infinite recurse { - return ResourceResolver.TrySystemResourceRetrieval(resourceKey, out value); + return ResourceResolver.TrySystemResourceRetrieval(modifiedKey, out value); + } + + if (useKeysNotFoundCache && !shouldCheckSystem) + { + KeyNotFoundCache.Add(resourceKey); } return false; @@ -273,12 +320,21 @@ private void Set(in ResourceKey resourceKey, object value, bool throwIfPresent) } else { - _values[resourceKey] = value; - if (value is ResourceDictionary) + _values.AddOrUpdate(resourceKey, value, out var previousValue); + + if (previousValue is ResourceDictionary previousDictionary) { + previousDictionary._parent = null; + } + + if (value is ResourceDictionary newDictionary) + { + newDictionary._parent = this; ResourceDictionaryValueChange?.Invoke(this, EventArgs.Empty); } } + + InvalidateNotFoundCache(true, resourceKey); } /// @@ -407,6 +463,7 @@ private ResourceDictionary GetActiveThemeDictionary(in ResourceKey activeTheme) { if (!activeTheme.Equals(_activeTheme)) { + InvalidateNotFoundCache(false); _activeTheme = activeTheme; _activeThemeDictionary = GetThemeDictionary(activeTheme) ?? GetThemeDictionary(Themes.Default); } @@ -425,34 +482,6 @@ private ResourceDictionary GetThemeDictionary(in ResourceKey theme) return null; } - private bool GetFromTheme(in ResourceKey resourceKey, ResourceDictionary activeThemeDictionary, out object value) - { - if (activeThemeDictionary != null && activeThemeDictionary.TryGetValue(resourceKey, out value, shouldCheckSystem: false)) - { - return true; - } - - return GetFromThemeMerged(resourceKey, activeThemeDictionary, out value); - } - - private bool GetFromThemeMerged(in ResourceKey resourceKey, ResourceDictionary activeThemeDictionary, out object value) - { - var count = _mergedDictionaries.Count; - - for (int i = count - 1; i >= 0; i--) - { - if (_mergedDictionaries[i].GetFromTheme(resourceKey, activeThemeDictionary, out value)) - { - return true; - } - } - - value = null; - - return false; - } - - private bool ContainsKeyTheme(in ResourceKey resourceKey, in ResourceKey activeTheme) { return GetActiveThemeDictionary(activeTheme)?.ContainsKey(resourceKey, shouldCheckSystem: false) ?? ContainsKeyThemeMerged(resourceKey, activeTheme); @@ -689,6 +718,46 @@ public StaticResourceAliasRedirect(string resourceKey, XamlParseContext parseCon internal static void SetActiveTheme(SpecializedResourceDictionary.ResourceKey key) => Themes.Active = key; + internal void InvalidateNotFoundCache(bool propagate) + { + if (propagate) + { + // Traverse dictionary sub-tree iteratively as it has less overhead. + var current = this; + + while (current is not null) + { + current._keyNotFoundCache?.Clear(); + + current = current._parent; + } + } + else + { + _keyNotFoundCache?.Clear(); + } + } + + internal void InvalidateNotFoundCache(bool propagate, in ResourceKey key) + { + if (propagate) + { + // Traverse dictionary sub-tree iteratively as it has less overhead. + var current = this; + + while (current is not null) + { + current._keyNotFoundCache?.Remove(key); + current = current._parent; + } + } + else + { + _keyNotFoundCache?.Remove(key); + } + } + + private static class Themes { public static SpecializedResourceDictionary.ResourceKey Light { get; } = "Light"; diff --git a/src/Uno.UI/UI/Xaml/ResourceResolver.cs b/src/Uno.UI/UI/Xaml/ResourceResolver.cs index e656b9548fb2..1eb739d3a400 100644 --- a/src/Uno.UI/UI/Xaml/ResourceResolver.cs +++ b/src/Uno.UI/UI/Xaml/ResourceResolver.cs @@ -395,9 +395,11 @@ internal static bool TryStaticRetrieval(in SpecializedResourceDictionary.Resourc var source = sourcesEnumerator.Current; - var dictionary = (source.Target as FrameworkElement)?.Resources + var dictionary = (source.Target as FrameworkElement)?.TryGetResources() ?? source.Target as ResourceDictionary; - if (dictionary != null && dictionary.TryGetValue(resourceKey, out value, shouldCheckSystem: false)) + + if (dictionary != null + && dictionary.TryGetValue(resourceKey, out value, shouldCheckSystem: false)) { return true; } diff --git a/src/Uno.UI/UI/Xaml/SpecializedResourceDictionary.cs b/src/Uno.UI/UI/Xaml/SpecializedResourceDictionary.cs index a2358cf5d56f..9ff54140e6be 100644 --- a/src/Uno.UI/UI/Xaml/SpecializedResourceDictionary.cs +++ b/src/Uno.UI/UI/Xaml/SpecializedResourceDictionary.cs @@ -31,6 +31,8 @@ public readonly struct ResourceKey public readonly Type TypeKey; public readonly uint HashCode; + public bool ShouldFilter { get; init; } = true; + public static ResourceKey Empty { get; } = new ResourceKey(false); public bool IsEmpty => Key == null; @@ -110,6 +112,20 @@ public static implicit operator ResourceKey(Type key) => new ResourceKey(key); } + internal class ResourceKeyComparer : IEqualityComparer + { + public bool Equals(ResourceKey x, ResourceKey y) + { + return x.Equals(y); + } + public int GetHashCode(ResourceKey obj) + { + return (int)obj.HashCode; + } + + public static ResourceKeyComparer Default { get; } = new(); + } + private int[] _buckets; private Entry[] _entries; #if TARGET_64BIT @@ -174,17 +190,22 @@ public object this[in ResourceKey key] } set { - bool modified = TryInsert(key, value, InsertionBehavior.OverwriteExisting); + bool modified = TryInsert(key, value, InsertionBehavior.OverwriteExisting, out _); Debug.Assert(modified); } } public void Add(in ResourceKey key, object value) { - bool modified = TryInsert(key, value, InsertionBehavior.ThrowOnExisting); + bool modified = TryInsert(key, value, InsertionBehavior.ThrowOnExisting, out _); Debug.Assert(modified); // If there was an existing key and the Add failed, an exception will already have been thrown. } + public void AddOrUpdate(in ResourceKey key, object value, out object previousValue) + { + TryInsert(key, value, InsertionBehavior.OverwriteExisting, out previousValue); + } + public void Clear() { int count = _count; @@ -248,7 +269,7 @@ public bool ContainsValue(object value) public Enumerator GetEnumerator() => new Enumerator(this, Enumerator.KeyValuePair); - private ref object FindValue(in ResourceKey key) + internal ref object FindValue(in ResourceKey key) { ref Entry entry = ref Unsafe.NullRef(); @@ -320,7 +341,7 @@ private int Initialize(int capacity) return size; } - private bool TryInsert(in ResourceKey key, object value, InsertionBehavior behavior) + private bool TryInsert(in ResourceKey key, object value, InsertionBehavior behavior, out object previousValue) { if (_buckets == null) { @@ -351,6 +372,7 @@ private bool TryInsert(in ResourceKey key, object value, InsertionBehavior behav { if (behavior == InsertionBehavior.OverwriteExisting) { + previousValue = entries[i].value; entries[i].value = value; return true; } @@ -360,6 +382,7 @@ private bool TryInsert(in ResourceKey key, object value, InsertionBehavior behav throw new InvalidOperationException("AddingDuplicateWithKeyArgumentException(key)"); } + previousValue = null; return false; } @@ -403,6 +426,7 @@ private bool TryInsert(in ResourceKey key, object value, InsertionBehavior behav bucket = index + 1; // Value in _buckets is 1-based _version++; + previousValue = null; return true; } @@ -585,7 +609,7 @@ public bool TryGetValue(in ResourceKey key, out object value) } public bool TryAdd(in ResourceKey key, object value) => - TryInsert(key, value, InsertionBehavior.None); + TryInsert(key, value, InsertionBehavior.None, out _); /// /// Ensures that the dictionary can hold up to 'capacity' entries without any further expansion of its backing storage diff --git a/src/Uno.UI/UI/Xaml/UIElement.Layout.crossruntime.cs b/src/Uno.UI/UI/Xaml/UIElement.Layout.crossruntime.cs index 99d520a90841..9205dbdf4455 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.Layout.crossruntime.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.Layout.crossruntime.cs @@ -245,7 +245,6 @@ private void DoMeasure(Size availableSize) if (this.Visibility == Visibility.Collapsed) { m_desiredSize = default; - RecursivelyApplyTemplateWorkaround(); return; } @@ -330,29 +329,6 @@ internal bool ShouldApplyLayoutClipAsAncestorClip() //&& !GetIsScrollViewerHeader(); // Special-case: ScrollViewer Headers, which can zoom, must scale the LayoutClip too } - private void RecursivelyApplyTemplateWorkaround() - { - // Uno workaround. The template should NOT be applied here. - // But, without this workaround, VerifyVisibilityChangeUpdatesCommandBarVisualState test will fail. - // The real root cause for the test failure is that FindParentCommandBarForElement will - // return null, that is because Uno doesn't yet properly have a "logical parent" concept. - // We eagerly apply the template so that FindParentCommandBarForElement will - // find the command bar through TemplatedParent - if (this is Control thisAsControl) - { - thisAsControl.ApplyTemplate(); - - // Update bindings to ensure resources defined - // in visual parents get applied. - this.UpdateResourceBindings(); - } - - foreach (var child in _children) - { - child.RecursivelyApplyTemplateWorkaround(); - } - } - public void Arrange(Rect finalRect) { diff --git a/src/Uno.UI/UI/Xaml/UIElement.mux.cs b/src/Uno.UI/UI/Xaml/UIElement.mux.cs index cbd09aca7ac8..9ff87203ddb6 100644 --- a/src/Uno.UI/UI/Xaml/UIElement.mux.cs +++ b/src/Uno.UI/UI/Xaml/UIElement.mux.cs @@ -1072,7 +1072,7 @@ private void DependencyObject_EnterImpl(EnterParams @params) // are entered as well. // The property we currently know it has an effect is Resources // In WinUI, it happens in CDependencyObject::EnterImpl (the call to EnterSparseProperties) - if (this is FrameworkElement { Resources: { } resources }) + if (this is FrameworkElement fe && fe.TryGetResources() is { } resources) { // Using ValuesInternal to avoid Enumerator boxing foreach (var resource in resources.ValuesInternal) diff --git a/src/Uno.UWP/UI/Core/WeakEventHelper.cs b/src/Uno.UWP/UI/Core/WeakEventHelper.cs index 9803c412b2dc..1d3bce5313a0 100644 --- a/src/Uno.UWP/UI/Core/WeakEventHelper.cs +++ b/src/Uno.UWP/UI/Core/WeakEventHelper.cs @@ -203,7 +203,19 @@ internal static IDisposable RegisterEvent(WeakEventCollection list, Delegate han if (weakHandler != null) { - raise(weakHandler, s, e); +#if __ANDROID__ + // If the target is a IJavaPeerable that does not have a valid peer reference, there + // is no need to call the handler. If it's not a IJavaPeerable, call the target. + // This scenario may happen when the object is about to be collected and has already + // collected its native counterpart. We know that the target will likely try to use + // native methods, which will fail if the peer reference is not valid. + var javaPeerable = weakHandler.Target as Java.Interop.IJavaPeerable; + if (javaPeerable?.PeerReference.IsValid ?? true) +#endif + { + + raise(weakHandler, s, e); + } } };