Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(statetriggers): evaluate StateTriggers before the first layout cycle #19557

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
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;
using Microsoft.UI.Xaml.Media;
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;
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()
Expand Down Expand Up @@ -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<VisualStateGroup>
{
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
}
44 changes: 44 additions & 0 deletions src/Uno.UI/UI/Xaml/Controls/Control/Control.crossruntime.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Linq;
using Uno.Extensions;
using Uno.UI.Xaml;

namespace Microsoft.UI.Xaml.Controls;

Expand All @@ -25,4 +27,46 @@
{
return this.GetChildren()?.FirstOrDefault() as IFrameworkElement;
}

#if !__NETSTD_REFERENCE__
internal override void EnterImpl(EnterParams @params, int depth)
{
base.EnterImpl(@params, depth);

if (@params.IsLive)
{
// if (SupportsBuiltInStyles() && !m_fIsBuiltInStyleApplied)
// {

Check warning on line 39 in src/Uno.UI/UI/Xaml/Controls/Control/Control.crossruntime.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Uno.UI/UI/Xaml/Controls/Control/Control.crossruntime.cs#L39

Remove this commented out code.
// // 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 */);
}
}
#endif
}
16 changes: 9 additions & 7 deletions src/Uno.UI/UI/Xaml/FrameworkElement.Layout.crossruntime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -214,13 +214,15 @@
{
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)

Check notice on line 219 in src/Uno.UI/UI/Xaml/FrameworkElement.Layout.crossruntime.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Uno.UI/UI/Xaml/FrameworkElement.Layout.crossruntime.cs#L219

Remove the unused local variable '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;

Expand Down
6 changes: 5 additions & 1 deletion src/Uno.UI/UI/Xaml/VisualStateGroup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,15 @@ private void OnParentChanged(object instance, object key, DependencyObjectParent
if (this.GetParent() is IFrameworkElement fe)
{
OnOwnerElementChanged();
#if !UNO_HAS_ENHANCED_LIFECYCLE
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed now? Since it's defined only on wasm, won't it cause issues there?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UNO_HAS_ENHANCED_LIFECYCLE is defined on skia and wasm. I moved the StateTriggers initialization to EnterImpl to match WinUI, so we no longer need to subscribe to Loaded to initialize StateTriggers on platforms that fire EnterImpl

fe.Loaded += OnOwnerElementLoaded;
#endif
fe.Unloaded += OnOwnerElementUnloaded;
_parentLoadedDisposable.Disposable = Disposable.Create(() =>
{
#if !UNO_HAS_ENHANCED_LIFECYCLE
fe.Loaded -= OnOwnerElementLoaded;
#endif
fe.Unloaded -= OnOwnerElementUnloaded;
});
}
Expand All @@ -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)
{
Expand Down
69 changes: 69 additions & 0 deletions src/Uno.UI/UI/Xaml/VisualStateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -290,5 +290,74 @@

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();

Check warning on line 307 in src/Uno.UI/UI/Xaml/VisualStateManager.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Uno.UI/UI/Xaml/VisualStateManager.cs#L307

Remove this commented out code.
// 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<CVisualStateGroup*>(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<CFrameworkElement*>(pControl->GetFirstChildNoAddRef()), dataSource.get());
// IFC_RETURN(actuator.InitializeStateTriggers(pControl));

// Uno Specific
foreach (var visualStateGroup in GetVisualStateGroups(pControl))
{
visualStateGroup.OnOwnerElementLoaded(pControl, null);
visualStateGroup.RefreshStateTriggers(forceUpdate);
}
}
}
}
Loading