Introduction
In Windows 10 Apps (formerly Windows UAP Apps, Universal Apps) there is a new class called StateTriggerBase that allows to trigger Visual States depending on device features, app events or properties
To create one StateTrigger that manages several properties to simplify XAML and logic, it is neccesary first have access to the properties we need, create a Service to get these properties and then create the StateTrigger.
The source code is available here: Download StateTriggers.zip
Device Information
This is the service we are going to create to read the information from the device, like the orientation and the family.
using static Windows.ApplicationModel.Resources.Core.ResourceContext;
using static Windows.ApplicationModel.DesignMode;
using Windows.Graphics.Display;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml;
namespace StateTriggers.Services
{
#region Enums
public enum Families { Mobile, Desktop }
public enum Orientations { Portrait, Landscape }
public class DeviceInformation
{
public static Orientations Orientation =>
DisplayInformation.GetForCurrentView().CurrentOrientation.ToString().Contains("Landscape")
? Orientations.Landscape : Orientations.Portrait;
public static Families Family =>
GetForCurrentView().QualifierValues["DeviceFamily"] == "Mobile"
? Families.Mobile : Families.Desktop;
public static DisplayInformation DisplayInformation =>
DisplayInformation.GetForCurrentView();
public static Frame DisplayFrame =>
Window.Current.Content == null ? null : Window.Current.Content as Frame;
}
#endregion
}
The information from Orientation has many enum values, we simplified it in two. And Family we only need to know if it is Mobile or any other.
Device Trigger
Create a class that inherits from StateTriggerBase
public class DeviceTrigger : StateTriggerBase
Then define the dependency properties for the Orientation and the Family
#region Familiy
public Families Family
{
get { return (Families)GetValue(FamilyProperty); }
set { SetValue(FamilyProperty, value); }
}
public static readonly DependencyProperty FamilyProperty =
DependencyProperty.Register(nameof(Family), typeof(Families), typeof(DeviceTrigger),
new PropertyMetadata(Families.Desktop));
#endregion
#region Orientation
public Orientations Orientation
{
get { return (Orientations)GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
public static readonly DependencyProperty OrientationProperty =
DependencyProperty.Register(nameof(Orientation), typeof(Orientations), typeof(DeviceTrigger),
new PropertyMetadata(Orientations.Portrait));
#endregion
Now we need to initialize it in the Constructor with the following method
private void Initialize()
{
if (!DesignModeEnabled)
{
NavigatedEventHandler framenavigated = null;
framenavigated = (s, e) =>
{
DeviceInformation.DisplayFrame.Navigated -= framenavigated;
SetTrigger();
};
DeviceInformation.DisplayFrame.Navigated += framenavigated;
DeviceInformation.DisplayInformation.OrientationChanged += (s, e) => SetTrigger();
}
}
And Finally the Method that refresh the StateTrigger
private void SetTrigger()
{
SetActive(Orientation == DeviceInformation.Orientation && Family == DeviceInformation.Family);
}
NOTE: The previous method was SetTriggerValue and now is SetActive be care about this.
At this point we have defined all the necessary to implement StateTriggers that matches the Device family and the Device Orientation with the values we set in XAML, now we are goint to create a View that uses this StateTrigger.
View
The view will have a property that will change the size of some controls inside a ItemsControl and will change the Background of the RelativePanel depending on different states
Repository of Dependency Properties
To have a property that can be used in the States we need to define it as a dependency property in the code of the view:
public sealed partial class MainView : Page
{
public double TileSize
{
get { return (double)GetValue(TileSizeProperty); }
set { SetValue(TileSizeProperty, value); }
}
public static readonly DependencyProperty TileSizeProperty =
DependencyProperty.Register(nameof(TileSize), typeof(double), typeof(MainView), new PropertyMetadata(96));
public MainView()
{
this.InitializeComponent();
}
}
Visual State Groups
Now we have to add a VisualStateGroup
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="VisualStateGroup">
<VisualState x:Name="VisualStatePhoneLandscape">
</VisualState>
<VisualState x:Name="VisualStatePhonePortrait">
</VisualState>
<VisualState x:Name="VisualStateTabletLandscape" >
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
Per each VisualState we define what properties will change from the base state
<VisualState x:Name="VisualStatePhoneLandscape">
<VisualState.Setters>
<Setter Target="PageInstance.TileSize" Value="120" />
<Setter Target="MainPanel.Background" Value="Yellow" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="VisualStatePhonePortrait">
<VisualState.Setters>
<Setter Target="PageInstance.TileSize" Value="96" />
<Setter Target="MainPanel.Background" Value="Blue" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="VisualStateTabletLandscape" >
<VisualState.Setters>
<Setter Target="PageInstance.TileSize" Value="144" />
<Setter Target="MainPanel.Background" Value="White" />
</VisualState.Setters>
</VisualState>
where PageInstance is the x:Name we set in the <Page> tag.
Visual States
And finally we instatiate the triggers for each state
<VisualState x:Name="VisualStatePhoneLandscape">
<VisualState.StateTriggers>
<t:DeviceTrigger Family="Mobile" Orientation="Landscape"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="PageInstance.TileSize" Value="120" />
<Setter Target="MainPanel.Background" Value="Yellow" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="VisualStatePhonePortrait">
<VisualState.StateTriggers>
<t:DeviceTrigger Family="Mobile" Orientation="Portrait"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="PageInstance.TileSize" Value="96" />
<Setter Target="MainPanel.Background" Value="Blue" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="VisualStateTabletLandscape" >
<VisualState.StateTriggers>
<t:DeviceTrigger Family="Desktop" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="PageInstance.TileSize" Value="144" />
<Setter Target="MainPanel.Background" Value="White" />
</VisualState.Setters>
</VisualState>
where 't:' is the namespace where the DeviceTrigger was defined
Content
And now the content of the Page that will change depending on the state
<ItemsControl x:Name="ItemsPanel" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapGrid Margin="0,40,0,0" Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate >
<Border Background="Orange" Margin="6,0,0,6" Padding="6" Height="{Binding TileSize, ElementName=PageInstance}" Width="{Binding TileSize, ElementName=PageInstance}">
<TextBlock Foreground="White" FontSize="20" Text="Subject"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.Items>
<x:Int32>1</x:Int32>
<x:Int32>1</x:Int32>
<x:Int32>1</x:Int32>
<x:Int32>1</x:Int32>
<x:Int32>1</x:Int32>
<x:Int32>1</x:Int32>
</ItemsControl.Items>
</ItemsControl>
where we bind the Height and Width of the Border to the Tilesize of the Page instance called PageInstance
Setters for Attached Properties
In case you want to change an attached property like the RelativePanel.RightOf, Grid.RowSpan when the state triggers change, you need to place the attached property inside parenthesis:
<VisualState.Setters>
<Setter Target="DayDetailsView.Margin" Value="0,0,0,160" />
<Setter Target="TimetablesView.(RelativePanel.AlignRightWithPanel)" Value="False"/>
<Setter Target="TimetablesView.(RelativePanel.AlignLeftWithPanel)" Value="True"/>
</VisualState.Setters>
Deep property path
In case you want to change the property of an element and it is not a direct property like the ImageBrush.Stretch property you can do the following
<VisualState.Setters>
<Setter Target="mainGrid.(Grid.Background).(ImageBrush.Stretch)" Value="Fill"/>
</VisualState.Setters>
Where mainGrid is the name of a Grid or
<VisualState.Setters>
<Setter Target="mainGrid.(UIElement.Background).(ImageBrush.Stretch)" Value="Fill"/>
</VisualState.Setters>
Set values to null (useful for RelativePanels)
Might be in some cases you have to reset a UIElement Property to null, you can do in the following way:
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="VisualStateGroup">
<VisualState x:Name="NarrowView">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="Text.(RelativePanel.RightOf)" Value="{x:Null}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="WideView">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="860" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="Text.(RelativePanel.RightOf)" Value="Image" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
As you can read, in one state the Text Control has Null for RelativePanel.RightOf and in the other state it has Value of Image. Interesting for RelativePanel when reallocating controls.
Custom Attached Properties
Suppose you have custom attached properties in the setters, you will realize that the changes are not called when the states change.
Let's create the RelativeSizes Attached Properties for the RelativePanel:
public class RelativeSize : DependencyObject
{
private static List<FrameworkElement> elements = new List<FrameworkElement>();
private static FrameworkElement Container = null;
private static bool containerready = false;
public static void SetContainer(UIElement element, FrameworkElement value)
{
element.SetValue(ContainerProperty, value);
}
public static FrameworkElement GetContainer(UIElement element)
{
return (FrameworkElement)element.GetValue(ContainerProperty);
}
public static readonly DependencyProperty ContainerProperty =
DependencyProperty.RegisterAttached("Container", typeof(FrameworkElement), typeof(RelativeSize), new PropertyMetadata(null,ContainerChanged));
private static void ContainerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Container = (e.NewValue as FrameworkElement);
Container.SizeChanged += (sc, ec) =>
{
foreach (var element in elements)
{
var rWidth = element.GetValue(RelativeSize.WidthProperty);
if (rWidth != null)
{
element.Width = (double)rWidth * Container.ActualWidth;
}
}
};
containerready = true;
}
public static void SetWidth(UIElement element, double value)
{
element.SetValue(WidthProperty, value);
}
public static double GetWidth(UIElement element)
{
return (double)element.GetValue(WidthProperty);
}
public static readonly DependencyProperty WidthProperty =
DependencyProperty.RegisterAttached("Width", typeof(double), typeof(RelativeSize), new PropertyMetadata(0.0, WidthChanged));
private static async void WidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
while (!containerready)
await Task.Delay(60);
var fe = d as FrameworkElement;
if(fe!=null)
{
if (!elements.Contains(fe))
elements.Add(fe);
fe.Width = (double)e.NewValue * Container.ActualWidth;
}
}
}
Now let's add this states in XAML setters:
xmlns:p="using:Controls.Views.Properties"
...
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="VisualStateGroup" CurrentStateChanged="VisualStateGroup_CurrentStateChanged">
<VisualState x:Name="NarrowView" >
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="Text.(RelativePanel.Below)" Value="Image" />
<Setter Target="Content.(RelativePanel.Below)" Value="Text" />
<Setter Target="Text.(RelativePanel.RightOf)" Value="{x:Null}" />
<Setter Target="Text.(p:RelativeSize.Width)" Value="1"/>
<Setter Target="Image.(p:RelativeSize.Width)" Value="1" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="WideView">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="860" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="Text.(RelativePanel.Below)" Value="{x:Null}" />
<Setter Target="Text.(RelativePanel.RightOf)" Value="Image" />
<Setter Target="Content.(RelativePanel.Below)" Value="Image" />
<Setter Target="Text.(p:RelativeSize.Width)" Value="0.6" />
<Setter Target="Image.(p:RelativeSize.Width)" Value="0.4" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
This setters won't be fired by the default manager, you have to manage by yourself:
private void VisualStateGroup_CurrentStateChanged(object sender, VisualStateChangedEventArgs e)
{
foreach (var sbase in e.NewState.Setters)
{
var setter = sbase as Setter;
var spath = setter.Target.Path.Path;
var element = setter.Target.Target as FrameworkElement;
if (spath.Contains(nameof(RelativeSize)))
{
string property = spath.Split('.').Last().TrimEnd(')');
var prop = typeof(RelativeSize).GetMethod($"Set{property}");
prop.Invoke(null, new object[] { element, setter.Value });
}
}
}
In case you have other Custom Attached Properties add them and invoke in this way.
Visual State Group as Resource
Might be in your application you have a VisualStateGroup that contains states with setters that contains common parts, it is awful to repeat those states with setters inside every control or page, to solve this I found using a trick, I store the VisualStateGroup in a DataTemplate I load it and I set to controls:
Creating a VisualStateGroup in resources
<Application.Resources>
<DataTemplate x:Key="VisualStateTemplate">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup >
<VisualState x:Name="NarrowView" >
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="Text.(RelativePanel.Below)" Value="Image" />
<Setter Target="Content.(RelativePanel.Below)" Value="Text" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="WideView">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="860" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="Text.(RelativePanel.RightOf)" Value="Image" />
<Setter Target="Content.(RelativePanel.Below)" Value="Image" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</DataTemplate>
</Application.Resources>
As you see with this I create a DataTemplate as resource and inside we have the visualstategroup, now we have to extract it:
Attached Property
public class VisualStateExtensions : DependencyObject
{
public static void SetVisualStatefromTemplate(UIElement element, DataTemplate value)
{
element.SetValue(VisualStatefromTemplateProperty, value);
}
public static DataTemplate GetVisualStatefromTemplate(UIElement element)
{
return (DataTemplate)element.GetValue(VisualStatefromTemplateProperty);
}
public static readonly DependencyProperty VisualStatefromTemplateProperty =
DependencyProperty.RegisterAttached("VisualStatefromTemplate",
typeof(DataTemplate), typeof(VisualStateExtensions), new PropertyMetadata(null, VisualStatefromTemplateChanged));
private static void VisualStatefromTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var visualstategroups = VisualStateManager.GetVisualStateGroups(d as FrameworkElement);
var template = (DataTemplate)e.NewValue;
var content = (FrameworkElement)template.LoadContent();
var source = VisualStateManager.GetVisualStateGroups(content);
var original = source.First();
source.RemoveAt(0);
visualstategroups.Add(original);
}
}
This property reads the datatemplate, extracts the visualstategroup and sets it to the control where the property was attached.
Using the attached property
In the XAML of the controls you are creating simply add the following to add and apply the visualstategroup
<UserControl x:Class="Example.MyUserControl1"...>
<RelativePanel x:Name="Root" local:VisualStateExtensions.VisualStatefromTemplate="{StaticResource VisualStateTemplate}" >
</UserControl>
<UserControl x:Class="Example.MyUserControl2"...>
<RelativePanel x:Name="Root" local:VisualStateExtensions.VisualStatefromTemplate="{StaticResource VisualStateTemplate}" >
</UserControl>
This is the way I made it to work, I have tried to clone and deserialize but it does fail with the controls part, but with this way it works like a charm.
Trigger DataTemplates
In order to try if the StateTriggers work inside a DataTemplate I made the following test:
- Emulate the default AdaptativeTrigger.
- Debug to know if it is called.
The conclusion is that inside the first control of the datatemplate, or inside the datatemplate the VisualStateGroup is never instantiated. I hope the behavior changes in new releases.
Workaround
To make the trigger work you need to create a UserControl and place inside the trigger inside the first control of the UserControl. I know it should work the datatemplate but at this release that is the only working solution.
First define the user control:
<UserControl
x:Class="App8.ItemTemplate"
xmlns="<a href="http:
xmlns:x="<a href="http:
xmlns:local="using:App8"
xmlns:d="<a href="http:
xmlns:mc="<a href="http:
<StackPanel Orientation="Horizontal" x:Name="Panel" x:FieldModifier="Public" >
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="WindowSizeStates" >
<VisualState x:Name="WideScreen" >
<VisualState.StateTriggers>
<local:DeviceTrigger MinWindowWidth="720" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="Panel.Background" Value="Green"/>
<Setter Target="textBlock1.FontSize" Value="20"/>
<Setter Target="textBlock2.FontSize" Value="20"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="SmallScreen">
<VisualState.StateTriggers>
<local:DeviceTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="Panel.Background" Value="Purple"/>
<Setter Target="textBlock1.FontSize" Value="5"/>
<Setter Target="textBlock2.FontSize" Value="5"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<TextBlock x:Name="textBlock1" Text="Example " FontSize="20"/>
<TextBlock x:Name="textBlock2" Text="{Binding}" FontSize="20"/>
</StackPanel>
</UserControl>
And now add inside the page as usual:
<ItemsControl>
<ItemsControl.ItemTemplate>
<DataTemplate >
<local:ItemTemplate />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.Items>
<x:Int32>1</x:Int32>
<x:Int32>2</x:Int32>
<x:Int32>3</x:Int32>
<x:Int32>4</x:Int32>
<x:Int32>5</x:Int32>
</ItemsControl.Items>
</ItemsControl>
NOTE: I post the Custom Adaptative code to test with new releases to check if it works:
using Windows.UI.Xaml;
using Windows.UI.Xaml.Navigation;
using static Windows.ApplicationModel.DesignMode;
using Windows.UI.Xaml.Controls;
namespace App8
{
public class DeviceTrigger : StateTriggerBase
{
public static Frame DisplayFrame =>
Window.Current.Content == null ? null : Window.Current.Content as Frame;
public double MinWindowWidth
{
get { return (double)GetValue(MinWindowWidthProperty); }
set { SetValue(MinWindowWidthProperty, value); }
}
public static readonly DependencyProperty MinWindowWidthProperty =
DependencyProperty.Register("MinWindowWidth", typeof(double), typeof(DeviceTrigger), new PropertyMetadata(0.0));
public DeviceTrigger()
{
Initialize();
}
private void Initialize()
{
if (!DesignModeEnabled)
{
NavigatedEventHandler framenavigated = null;
framenavigated = (s, e) =>
{
DisplayFrame.Navigated -= framenavigated;
SetTrigger(Window.Current.Bounds.Width);
};
DisplayFrame.Navigated += framenavigated;
Window.Current.SizeChanged += (s, e) =>
{
SetTrigger(e.Size.Width);
};
}
}
private void SetTrigger(double width)
{
SetActive(width >= MinWindowWidth);
}
}
}
Points of Interest
The only way to define a property that refreshes the UI coming from the control or a Page is a Dependency Property and you need to name the Page to set its properties from a Setter.
You can have several properties to set the trigger as you create your custom trigger.
To fire the trigger for the first time the best place is in the Navigated Event of the Frame.
History
v 1.0 With Visual Studio 2015 CTP6 (DeviceInformation class could change in new versions)
v 1.0.1 Added information about attached properties
v 1.0.2 Added deep setter target property
v 1.03 Trigger inside datatemplate workaround and Trigger Activation method changed
v 1.04 Added nulls for visual states
v 1.1 Added custom attached properties
v 1.2 Added VisualStateGroup as Resource