65.9K
CodeProject 正在变化。 阅读更多。
Home

Windows 10 应用的高级视图状态

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.70/5 (8投票s)

2015年4月15日

CPOL

5分钟阅读

viewsIcon

49994

downloadIcon

342

具有多个属性和初始化的 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 中工作,我做了以下测试:

  1. 模拟默认的 AdaptativeTrigger。
  2. 调试以了解它是否被调用。

结论是,在 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 作为资源

© . All rights reserved.