Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / UWP

Advanced View States for Windows 10 apps

4.70/5 (8 votes)
15 Apr 2015CPOL5 min read 48.9K   327  
StateTriggers with multiple properties and initialization, setters for attached properties, deep property path, set values to null,custom attached properties, visual state group as resource and trigger inside datatemplates workaround

Introduction

Image 1

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

C#
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)
            {
                //Initial Trigger
                NavigatedEventHandler framenavigated = null;
                framenavigated = (s, e) =>
                {
                    DeviceInformation.DisplayFrame.Navigated -= framenavigated;
                    SetTrigger();
                };
                DeviceInformation.DisplayFrame.Navigated += framenavigated;

                //Orientation Trigger
                DeviceInformation.DisplayInformation.OrientationChanged += (s, e) => SetTrigger();
            }
        }

And Finally the Method that refresh the StateTrigger

C#
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:

C#
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

XML
<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

XML
<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

XML
<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

XML
<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:

XML
<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

XML
<VisualState.Setters>
  <Setter Target="mainGrid.(Grid.Background).(ImageBrush.Stretch)" Value="Fill"/>
</VisualState.Setters>

Where mainGrid is the name of a Grid or

XML
<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:

  1. Emulate the default AdaptativeTrigger.
  2. 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://schemas.microsoft.com/winfx/2006/xaml/presentation">http://schemas.microsoft.com/winfx/2006/xaml/presentation</a>"
xmlns:x="<a href="http://schemas.microsoft.com/winfx/2006/xaml">http://schemas.microsoft.com/winfx/2006/xaml</a>"
xmlns:local="using:App8"
xmlns:d="<a href="http://schemas.microsoft.com/expression/blend/2008">http://schemas.microsoft.com/expression/blend/2008</a>"
xmlns:mc="<a href="http://schemas.openxmlformats.org/markup-compatibility/2006">http://schemas.openxmlformats.org/markup-compatibility/2006</a>">

<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)
        {
            //Initial Trigger
            NavigatedEventHandler framenavigated = null;
            framenavigated = (s, e) =>
            {
                DisplayFrame.Navigated -= framenavigated;
                SetTrigger(Window.Current.Bounds.Width);
            };
            DisplayFrame.Navigated += framenavigated;

            //Orientation Trigger
            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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)