Windows 10 应用的高级视图状态






4.70/5 (8投票s)
具有多个属性和初始化的 StateTrigger,附加属性的设置器,深层属性路径,将值设置为 null,自定义附加属性,将 Visual State Group 作为资源并在 DataTemplate 中设置触发器进行规避
引言
在 Windows 10 应用(以前称为 Windows UAP 应用、通用应用)中,有一个名为 StateTriggerBase 的新类,它允许根据设备功能、应用事件或属性触发 Visual States。
要创建一个管理多个属性以简化 XAML 和逻辑的 StateTrigger,首先需要访问所需的属性,创建一个服务来获取这些属性,然后创建 StateTrigger。
源代码可在此处获取:下载 StateTriggers.zip
设备信息
这是我们将要创建的服务,用于从设备读取信息,例如方向和设备系列。
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 }
方向信息有许多枚举值,我们将其简化为两个。而对于设备系列,我们只需要知道它是移动设备还是其他设备。
设备触发器
创建一个继承自 StateTriggerBase 的类
public class DeviceTrigger : StateTriggerBase
然后为 Orientation 和 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
现在我们需要在构造函数中通过以下方法进行初始化
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(); } }
最后是刷新 StateTrigger 的方法
private void SetTrigger()
{
SetActive(Orientation == DeviceInformation.Orientation && Family == DeviceInformation.Family);
}
注意:之前的方法是 SetTriggerValue,现在是 SetActive,请注意这一点。
至此,我们已经定义了实现 StateTriggers 所需的所有内容,这些 StateTriggers 可以根据我们在 XAML 中设置的值匹配设备系列和设备方向,现在我们将创建一个使用此 StateTrigger 的视图。
视图
视图将有一个属性,该属性将更改 ItemsControl 中某些控件的大小,并根据不同状态更改 RelativePanel 的背景。
依赖属性存储库
要拥有一个可在状态中使用的属性,我们需要在视图的代码中将其定义为依赖属性。
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
现在我们必须添加一个 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>
对于每个 VisualState,我们定义将从基状态更改的属性。
<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>
其中 PageInstance 是我们在 <Page> 标签中设置的 x:Name。
Visual States
最后,我们为每个状态实例化触发器
<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>
其中 't:' 是定义 DeviceTrigger 的命名空间
Content
现在是 Page 的内容,它将根据状态而变化
<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>
我们将 Border 的 Height 和 Width 绑定到名为 PageInstance 的 Page 实例的 Tilesize。
设置附加属性的设置器
如果您想更改附加属性,例如 RelativePanel.RightOf、Grid.RowSpan,在状态触发器更改时,您需要将附加属性放在括号内。
<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>
深层属性路径
如果您想更改元素的属性,而它不是直接属性,例如 ImageBrush.Stretch 属性,您可以这样做:
<VisualState.Setters>
<Setter Target="mainGrid.(Grid.Background).(ImageBrush.Stretch)" Value="Fill"/>
</VisualState.Setters>
其中 mainGrid 是 Grid 的名称,或者
<VisualState.Setters>
<Setter Target="mainGrid.(UIElement.Background).(ImageBrush.Stretch)" Value="Fill"/>
</VisualState.Setters>
将值设置为 null(对于 RelativePanels 非常有用)
在某些情况下,您可能需要将 UIElement 属性重置为 null,您可以这样做:
<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>
正如您所读到的,在一个状态下,Text 控件的 RelativePanel.RightOf 为 Null,而在另一个状态下,它具有 Image 的值。这对于 RelativePanel 在重新分配控件时很有趣。
自定义附加属性
假设您的设置器中有自定义附加属性,您会发现更改在状态更改时不会被调用。
让我们为 RelativePanel 创建 RelativeSizes 附加属性。
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; } } }
现在让我们在 XAML 设置器中添加这些状态。
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>
这些设置器不会被默认管理器触发,您必须自己管理。
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 }); } } }
如果您有其他自定义附加属性,请添加它们并按此方式调用。
Visual State Group 作为资源
在您的应用程序中,您可能有一个 VisualStateGroup,其中包含具有设置器的状态,而这些设置器又包含通用部分。在每个控件或页面中重复这些带有设置器的状态是很糟糕的。为了解决这个问题,我找到了一个技巧:我将 VisualStateGroup 存储在 DataTemplate 中,然后加载它并将其应用于控件。
在资源中创建 VisualStateGroup
<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>
正如您所见,通过这种方式,我创建了一个 DataTemplate 作为资源,其中包含 visualstategroup,现在我们必须提取它。
附加属性
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); } }
此属性读取 DataTemplate,提取 VisualStateGroup,并将其设置到附加属性被附加到的控件上。
使用附加属性
在您创建的控件的 XAML 中,只需添加以下内容即可添加和应用 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>
这就是我让它工作的方式。我曾尝试克隆和反序列化,但它在控件部分会失败,但通过这种方式,它效果非常好。
在 DataTemplates 中触发
为了测试 StateTriggers 是否能在 DataTemplate 中工作,我做了以下测试:
- 模拟默认的 AdaptativeTrigger。
- 调试以了解它是否被调用。
结论是,在 DataTemplate 的第一个控件中,或者在 DataTemplate 中,VisualStateGroup 永远不会被实例化。我希望行为会在新版本中有所改变。
解决方法
为了让触发器工作,您需要创建一个 UserControl,并将触发器放置在 UserControl 的第一个控件内部。我知道它应该在 DataTemplate 中工作,但在当前版本中,这是唯一可行的解决方案。
首先定义用户控件
<UserControl x:Class="App8.ItemTemplate" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:App8" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"> <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>
现在像往常一样将其添加到页面中
<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>
注意:我发布了自定义 Adaptative 代码,以便在新版本中进行测试,以检查它是否有效。
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); } } }
关注点
定义一个从控件或 Page 刷新 UI 的属性的唯一方法是使用依赖属性,您需要为 Page 命名才能在 Setter 中设置其属性。
您可以创建多个属性来设置触发器,就像创建自定义触发器一样。
首次触发触发器的最佳位置是 Frame 的 Navigated 事件。
历史
v 1.0 使用 Visual Studio 2015 CTP6(DeviceInformation 类在未来版本中可能会发生变化)
v 1.0.1 添加了关于附加属性的信息
v 1.0.2 添加了深层设置器目标属性
v 1.03 DataTemplate 中的触发器规避和触发器激活方法已更改
v 1.04 为 Visual States 添加了 null
v 1.1 添加了自定义附加属性
v 1.2 添加了 VisualStateGroup 作为资源