From eb684d505554e3401ac4d35a3ee1f096c1a90568 Mon Sep 17 00:00:00 2001 From: Ramez Ragaa Date: Tue, 18 Feb 2025 13:58:41 +0200 Subject: [PATCH 1/5] fix(statetriggers): evaluate StateTriggers before the first layout cycle --- .../Controls/Control/Control.crossruntime.cs | 42 +++++++++++ .../FrameworkElement.Layout.crossruntime.cs | 16 +++-- src/Uno.UI/UI/Xaml/VisualStateGroup.cs | 6 +- src/Uno.UI/UI/Xaml/VisualStateManager.cs | 69 +++++++++++++++++++ 4 files changed, 125 insertions(+), 8 deletions(-) diff --git a/src/Uno.UI/UI/Xaml/Controls/Control/Control.crossruntime.cs b/src/Uno.UI/UI/Xaml/Controls/Control/Control.crossruntime.cs index cc31110ce284..e6f18c64f855 100644 --- a/src/Uno.UI/UI/Xaml/Controls/Control/Control.crossruntime.cs +++ b/src/Uno.UI/UI/Xaml/Controls/Control/Control.crossruntime.cs @@ -1,4 +1,6 @@ using System.Linq; +using Uno.Extensions; +using Uno.UI.Xaml; namespace Microsoft.UI.Xaml.Controls; @@ -25,4 +27,44 @@ internal IFrameworkElement GetTemplateRoot() { return this.GetChildren()?.FirstOrDefault() as IFrameworkElement; } + + internal override void EnterImpl(EnterParams @params, int depth) + { + base.EnterImpl(@params, depth); + + if (@params.IsLive) + { + // if (SupportsBuiltInStyles() && !m_fIsBuiltInStyleApplied) + // { + // // When we apply the built-in style, we may resolve theme resources in doing so + // // that haven't yet been resolved - for example, if a property value references + // // a resource that then references another resource. + // // We need to make sure we're operating under the correct theme during that resolution. + // bool removeRequestedTheme = false; + // const auto theme = GetTheme(); + // const auto oldRequestedThemeForSubTree = GetRequestedThemeForSubTreeFromCore(); + // + // if (theme != Theming::Theme::None && Theming::GetBaseValue(theme) != oldRequestedThemeForSubTree) + // { + // SetRequestedThemeForSubTreeOnCore(theme); + // removeRequestedTheme = true; + // } + // + // auto themeGuard = wil::scope_exit([&] { + // if (removeRequestedTheme) + // { + // SetRequestedThemeForSubTreeOnCore(oldRequestedThemeForSubTree); + // } + // }); + // + // IFC_RETURN(ApplyBuiltInStyle()); + // } + + // Initialize StateTriggers at this time. We need to wait for this to enter a visual tree + // since we need for it to be part of the main visual tree to know which visual tree's + // qualifier context to use. We also need to force an update because our visual tree root + // may have changed since the last enter. + VisualStateManager.InitializeStateTriggers(this, true /* forceUpdate */); + } + } } diff --git a/src/Uno.UI/UI/Xaml/FrameworkElement.Layout.crossruntime.cs b/src/Uno.UI/UI/Xaml/FrameworkElement.Layout.crossruntime.cs index 44ad78967586..3d7f49c1f445 100644 --- a/src/Uno.UI/UI/Xaml/FrameworkElement.Layout.crossruntime.cs +++ b/src/Uno.UI/UI/Xaml/FrameworkElement.Layout.crossruntime.cs @@ -214,13 +214,15 @@ internal void InvokeApplyTemplate(out bool addedVisuals) { ApplyTemplate(out addedVisuals); - //if (auto visualTree = VisualTree::GetForElementNoRef(pControl)) - // { - // // Create VisualState StateTriggers and perform evaulation to determine initial state, - // // if we're in the visual tree (since we need it to get our qualifier context). - // // If we're not in the visual tree, we'll do this when we enter it. - // IFC(CVisualStateManager2::InitializeStateTriggers(this)); - //} + var pControl = this as Control; + + if (VisualTree.GetForElement(pControl) is { } visualTree) + { + // Create VisualState StateTriggers and perform evaulation to determine initial state, + // if we're in the visual tree (since we need it to get our qualifier context). + // If we're not in the visual tree, we'll do this when we enter it. + VisualStateManager.InitializeStateTriggers(pControl); + } //var control = this as Control; diff --git a/src/Uno.UI/UI/Xaml/VisualStateGroup.cs b/src/Uno.UI/UI/Xaml/VisualStateGroup.cs index 10e3a0a6d974..782c8bfde5c5 100644 --- a/src/Uno.UI/UI/Xaml/VisualStateGroup.cs +++ b/src/Uno.UI/UI/Xaml/VisualStateGroup.cs @@ -150,11 +150,15 @@ private void OnParentChanged(object instance, object key, DependencyObjectParent if (this.GetParent() is IFrameworkElement fe) { OnOwnerElementChanged(); +#if !__CROSSRUNTIME__ fe.Loaded += OnOwnerElementLoaded; +#endif fe.Unloaded += OnOwnerElementUnloaded; _parentLoadedDisposable.Disposable = Disposable.Create(() => { +#if !__CROSSRUNTIME__ fe.Loaded -= OnOwnerElementLoaded; +#endif fe.Unloaded -= OnOwnerElementUnloaded; }); } @@ -171,7 +175,7 @@ private void OnOwnerElementChanged() ExecuteOnTriggers(t => t.OnOwnerElementChanged()); } - private void OnOwnerElementLoaded(object sender, RoutedEventArgs args) + internal void OnOwnerElementLoaded(object sender, RoutedEventArgs args) { if (_pendingOnOwnerElementChanged) { diff --git a/src/Uno.UI/UI/Xaml/VisualStateManager.cs b/src/Uno.UI/UI/Xaml/VisualStateManager.cs index 2344b756ec8a..b9e2637254af 100644 --- a/src/Uno.UI/UI/Xaml/VisualStateManager.cs +++ b/src/Uno.UI/UI/Xaml/VisualStateManager.cs @@ -290,5 +290,74 @@ private static (VisualStateGroup, VisualState) GetValidGroupAndState(string stat return (null, null); } + + internal static void InitializeStateTriggers(DependencyObject pDO, bool forceUpdate = false) + { + if (pDO is not Control pControl) + { + return; + } + + var groupCollection = VisualStateManager.GetVisualStateGroups(pControl); + if (groupCollection is null) + { + return; + } + + // auto& groupCollectionMap = groupCollection->GetStateTriggerVariantMaps(); + // if (forceUpdate) + // { + // // Forcing an update, clear the map + // groupCollectionMap.clear(); + // + // // If we're forcing an update, we should also clear the state trigger variant maps in each group. + // for (auto group : *groupCollection) + // { + // static_cast(group)->m_pStateTriggerVariantMap = nullptr; + // } + // + // for (auto& groupContext : groupCollection->GetGroupContext()) + // { + // groupContext.GetStateTriggerVariantMap() = nullptr; + // } + // } + // + // if (!groupCollectionMap.empty()) + // { + // // If the map isn't clear, then we are already initialized or we weren't forced by diagnostics to update. + // // Adding triggers isn't something usually supported at runtime. + // return S_OK; + // } + // + // auto dataSource = CreateVisualStateManagerDataSource(groupCollection); + // auto dataSourceMap = dataSource->GetStateTriggerVariantMaps(); + // + // if (!dataSourceMap.empty()) + // { + // if (forceUpdate) + // { + // // Forcing an update, clear the map + // dataSourceMap.clear(); + // } + // + // if (!dataSourceMap.empty()) + // { + // // If the map isn't clear, then we are already initialized or we weren't forced by diagnostics to update. + // // Adding triggers isn't something usually supported at runtime. + // return S_OK; + // } + // } + // + // VisualStateManagerActuator actuator( + // static_cast(pControl->GetFirstChildNoAddRef()), dataSource.get()); + // IFC_RETURN(actuator.InitializeStateTriggers(pControl)); + + // Uno Specific + foreach (var visualStateGroup in GetVisualStateGroups(pControl)) + { + visualStateGroup.OnOwnerElementLoaded(pControl, null); + visualStateGroup.RefreshStateTriggers(forceUpdate); + } + } } } From 7ab05d384bc7eca0574ba952ccd59f8c4ef272db Mon Sep 17 00:00:00 2001 From: Ramez Ragaa Date: Wed, 19 Feb 2025 04:02:28 +0200 Subject: [PATCH 2/5] test: add When_StateTriggers_Evaluated_Before_First_Layout --- .../Given_VisualStateManager.cs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_VisualStateManager.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_VisualStateManager.cs index 4c6567b05172..cde6ad0eaa23 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_VisualStateManager.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_VisualStateManager.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Windows.Foundation; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls.Primitives; @@ -8,6 +10,7 @@ using Microsoft.UI.Xaml.Shapes; using Microsoft.VisualStudio.TestTools.UnitTesting; using MUXControlsTestApp.Utilities; +using SamplesApp.UITests; using Uno.Extensions; using Uno.UI.Extensions; using Uno.UI.RuntimeTests.Helpers; @@ -79,4 +82,65 @@ public async Task SelectorItem_SelectedState() var states = VisualStateHelper.GetCurrentVisualStateName(container2).ToArray(); Assert.IsTrue(states.Contains("MultiSelectEnabled"), $"container2 is not in 'MultiSelectEnabled' state: states={states.JoinBy(",")}"); } + +#if HAS_UNO + [TestMethod] + [UnoWorkItem("https://github.com/unoplatform/uno/issues/19364")] + public async Task When_StateTriggers_Evaluated_Before_First_Layout() + { + MyUserControl uc = new MyUserControl(); + VisualStateManager.SetVisualStateGroups(uc, new List + { + new VisualStateGroup() + { + States = + { + new VisualState + { + Name = "MyVisualState1", + }, + new VisualState + { + Name = "MyVisualState2", + StateTriggers = + { + new AdaptiveTrigger() + { + MinWindowWidth = 1 + } + } + } + } + } + }); + + var contentControl = new ContentControl + { + Content = "0", + ContentTemplate = new DataTemplate(() => + { + return uc; + }) + }; + + await UITestHelper.Load(contentControl, control => control.IsLoaded); + Assert.AreEqual("MyVisualState2", uc.VisualStateOnFirstMeasure?.Name); + } + + private partial class MyUserControl : UserControl + { + private bool _firstMeasure = true; + public VisualState VisualStateOnFirstMeasure { get; set; } + + protected override Size MeasureOverride(Size availableSize) + { + if (_firstMeasure) + { + _firstMeasure = false; + VisualStateOnFirstMeasure = VisualStateManager.GetVisualStateGroups(this)[0].CurrentState; + } + return base.MeasureOverride(availableSize); + } + } +#endif } From 73a4e0b308a72c9fb5a20103e8d3db28b7220ddc Mon Sep 17 00:00:00 2001 From: Ramez Ragaa Date: Wed, 19 Feb 2025 15:11:46 +0200 Subject: [PATCH 3/5] chore: use UNO_HAS_ENHANCED_LIFECYCLE --- src/Uno.UI/UI/Xaml/VisualStateGroup.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Uno.UI/UI/Xaml/VisualStateGroup.cs b/src/Uno.UI/UI/Xaml/VisualStateGroup.cs index 782c8bfde5c5..524d468472bd 100644 --- a/src/Uno.UI/UI/Xaml/VisualStateGroup.cs +++ b/src/Uno.UI/UI/Xaml/VisualStateGroup.cs @@ -150,13 +150,13 @@ private void OnParentChanged(object instance, object key, DependencyObjectParent if (this.GetParent() is IFrameworkElement fe) { OnOwnerElementChanged(); -#if !__CROSSRUNTIME__ +#if !UNO_HAS_ENHANCED_LIFECYCLE fe.Loaded += OnOwnerElementLoaded; #endif fe.Unloaded += OnOwnerElementUnloaded; _parentLoadedDisposable.Disposable = Disposable.Create(() => { -#if !__CROSSRUNTIME__ +#if !UNO_HAS_ENHANCED_LIFECYCLE fe.Loaded -= OnOwnerElementLoaded; #endif fe.Unloaded -= OnOwnerElementUnloaded; From 7586c48feff32fcc5664f6f068091e1c84a38e12 Mon Sep 17 00:00:00 2001 From: Ramez Ragaa Date: Wed, 19 Feb 2025 15:13:28 +0200 Subject: [PATCH 4/5] chore: reference build error --- src/Uno.UI/UI/Xaml/Controls/Control/Control.crossruntime.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Uno.UI/UI/Xaml/Controls/Control/Control.crossruntime.cs b/src/Uno.UI/UI/Xaml/Controls/Control/Control.crossruntime.cs index e6f18c64f855..eb385373291e 100644 --- a/src/Uno.UI/UI/Xaml/Controls/Control/Control.crossruntime.cs +++ b/src/Uno.UI/UI/Xaml/Controls/Control/Control.crossruntime.cs @@ -28,6 +28,7 @@ internal IFrameworkElement GetTemplateRoot() return this.GetChildren()?.FirstOrDefault() as IFrameworkElement; } +#if !__NETSTD_REFERENCE__ internal override void EnterImpl(EnterParams @params, int depth) { base.EnterImpl(@params, depth); @@ -67,4 +68,5 @@ internal override void EnterImpl(EnterParams @params, int depth) VisualStateManager.InitializeStateTriggers(this, true /* forceUpdate */); } } +#endif } From 730583b581307851baa7459299f0b4e789684731 Mon Sep 17 00:00:00 2001 From: Ramez Ragaa Date: Wed, 19 Feb 2025 15:42:48 +0200 Subject: [PATCH 5/5] chore: build errors --- .../Tests/Windows_UI_Xaml/Given_VisualStateManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_VisualStateManager.cs b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_VisualStateManager.cs index cde6ad0eaa23..9461d9dd7ba9 100644 --- a/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_VisualStateManager.cs +++ b/src/Uno.UI.RuntimeTests/Tests/Windows_UI_Xaml/Given_VisualStateManager.cs @@ -18,7 +18,7 @@ namespace Uno.UI.RuntimeTests.Tests.Windows_UI_Xaml; [TestClass] [RunsOnUIThread] -public class Given_VisualStateManager +public partial class Given_VisualStateManager { [TestMethod] public async Task When_Transition_Modifies_SubProperty_Of_Property_Set_By_Previous_State()