I have a user interface that adapts to different device form factors using AdaptiveTrigger
. In particular, I have a SplitView
-based shell menu with two state triggers for when the minimum window width is 1000 epx and 600 epx respectively. The 600 epx state trigger can fire on certain mobile devices in landscape mode depending on their form factor and scale factor, and this is intended behavior.
However, I've noticed that on the desktop, the 600 epx state trigger fires in the background (changing ShellSplitView.DisplayMode
from Overlay
to CompactOverlay
) as soon as I change a theme setting such as toggling between dark and light mode, changing the accent color, or toggling high contrast mode; and on both the mobile emulator and a device, it fires when I go to the Settings app, change a theme setting, and return to my app. Most peculiarly, this only happens in the default state and only after I've caused any of the state triggers to fire, by resizing the window on the desktop or by rotating the mobile emulator or device. Resizing/rotating again after returning to the app makes the problem go away. I have not previously seen this in any other apps, including Microsoft's apps or any third-party UWP apps.
I've confirmed that this is not caused by some other code interfering with the state triggers, by successfully reproducing the problem in a blank UWP project with a single page containing nothing but a SplitView
with any number of visual states (and some text content):
<Page
x:Class="App1.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:App1"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState>
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="1000"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="ShellSplitView.DisplayMode" Value="Inline"/>
<Setter Target="ShellSplitView.IsPaneOpen" Value="True"/>
</VisualState.Setters>
</VisualState>
<VisualState>
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="600"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="ShellSplitView.DisplayMode" Value="CompactOverlay"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<SplitView
x:Name="ShellSplitView"
CompactPaneLength="48"
DisplayMode="Overlay"
IsPaneOpen="False"
OpenPaneLength="256">
<SplitView.Content>
<TextBlock Text="Content"/>
</SplitView.Content>
<SplitView.Pane>
<TextBlock Text="Pane"/>
</SplitView.Pane>
</SplitView>
</Grid>
</Page>
You can attempt to reproduce the problem yourself by creating a blank UWP project, replacing the contents of MainPage.xaml with the above XAML, and running the project.
Considering all I did was change the theme settings, I don't understand why any of the state triggers would fire. In fact, according to this guide on MSDN, the default values reflected in the SplitView
element's attributes should be reapplied automatically:
When you use state triggers, you don't need to define an empty
DefaultState
. The default settings are reapplied automatically when the conditions of the state trigger are no longer met.
And according to the Windows.UI.Xaml.AdaptiveTrigger
page itself:
By default, the
StackPanel
orientation isVertical
. When the window width is >= 720 effective pixels, theVisualState
change is triggered, and theStackPanel
orientation is changed toHorizontal
.
... which, together, reinforce my point — the window width does not change when I change the theme settings, so I don't see why the state trigger would kick in when it shouldn't, and why only after I've triggered a state change by resizing the window or rotating the device.
So why is this happening, and how do I fix it?
This issue appears to have been fixed in the Windows 10 Anniversary Update for all device families. The state triggers will no longer activate when they're not supposed to, and the default values as specified outside of the state triggers are now correctly reapplied.
You do not need this workaround if your app will only be running on the 1607 branch (build 14393) or newer. Note that this refers to the minimum version as specified in your project properties, and not the target version.
You need this workaround if your app will be running on the 1511 branch (build 10586) or 1507 branch (build 10240), even though it will behave correctly on devices with the newer builds.
Unfortunately, I haven't been able to work around this problem without just adding a default state to the VisualStateGroup
anyway, using a state trigger with a minimum dimension of 0 epx (you do not need any setters — you just need a state trigger):
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState>
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="1000"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="ShellSplitView.DisplayMode" Value="Inline"/>
<Setter Target="ShellSplitView.IsPaneOpen" Value="True"/>
</VisualState.Setters>
</VisualState>
<VisualState>
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="600"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="ShellSplitView.DisplayMode" Value="CompactOverlay"/>
</VisualState.Setters>
</VisualState>
<VisualState>
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="0"/>
</VisualState.StateTriggers>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
As to why this happens, my guess is that the VisualStateManager
is trying to go to a state when the system theme is updated, but as there is none defined for the initial (or "Default" or "Normal") state, it has to choose the next best alternative. What I still don't understand, is why the VisualStateManager
has to change visual states when the system theme is updated in the first place... don't ThemeResource
s refresh by themselves? Why would they depend on the VisualStateManager
?
It's interesting to note that most third-party tutorials on adaptive triggers in UWP already do this. It's not clear if the authors were aware of this problem, or if they added the default state simply because they were used to doing so in the Silverlight/WPF days. Based on the documentation above and the otherwise correct UWP behavior, though, this is probably an edge case.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With