XAML的Control支持VisualStateManager进行视觉呈现状态管理。

VisualStateManager是直接从相依对象派生出来的,它声明有一个添附的相依部属,就是VisualStateManager.VisualStateGroups,可以用在其他元素之上,用来添加视觉呈现状态,下面是一个例子:

<ControlTemplate TargetType="Button">
  <Grid >
    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup x:Name="CommonStates">

        <VisualStateGroup.Transitions>

          <!--Take one half second to transition to the PointerOver state.-->
          <VisualTransition To="PointerOver" 
                              GeneratedDuration="0:0:0.5"/>
        </VisualStateGroup.Transitions>
        
        <VisualState x:Name="Normal" />

        <!--Change the SolidColorBrush, ButtonBrush, to red when the
            Pointer is over the button.-->
        <VisualState x:Name="PointerOver">
          <Storyboard>
            <ColorAnimation Storyboard.TargetName="ButtonBrush" 
                            Storyboard.TargetProperty="Color" To="Red" />
          </Storyboard>
        </VisualState>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    <Grid.Background>
      <SolidColorBrush x:Name="ButtonBrush" Color="Green"/>
    </Grid.Background>
  </Grid>
</ControlTemplate>

视觉呈现状态是按照VisualStateGroup来组织的,同一个组里面的状态彼此是互斥的,只有不同组之内的状态可以同时应用在一个元素之上。但是不同组内的状态不可同名。

XAML背后代码可以调用VisualStateManager.GoToState来切换状态。这个方法的第一个参数为被添附VisualStateManager.VisualStateGroups的元素对象(必须是Control派生的),第二个参数是状态的名字,第三个参数用来控制是否需要进行动画效果。动画效果可以参考Storyboarded animations for visual states。VisualStateManager也提供一定的自定义支持。

VisualStateGroup必须有一个用x:Name定义的名字。VisualStateGroup中的某个VisualState可以采用连画板(Storyboard),当跳转到自身的时候触发这个连画板效果。但是同一个VisualStateGroup下一定要定义一个没有连画板效果的VisualState,这样才能取消同组内其他VisualState触发的连画板效果。

除了默认的VisualState列表之外,还可以通过VisualStateGroup的Transitions部属添加VisualTransition元素。VisualTransition的子内容是一个Storyboard对象,VisualTransition的部属可以用来定义这个Storyboard的起始状态和终止状态。如果有多个VisualTransition可以用的话,那么优先级顺序如下:

  1. 起始和终止都定义,并且匹配
  2. 只定义终止,并且匹配
  3. 只定义起始,并且匹配

调用VisualStateManager.GoToState的时候会执行以下动作:

  • 之前的VisualState如果应用了Storyboard,那么该Storyboard效果会被撤销
  • 两个状态之间存在的的VisualTransition会被运行
  • 如果终止状态自带有Storyboard,那么那个Storyboard会被运行

对于只定义GeneratedDuration的VisualTransition,会变为隐式过渡。隐式过度只能应用在Double、Color或者Point之上。参考Implicit transitions

另一相关概念叫做过渡动画,跟VisualStateGroup没有太大关系,可以参考Storyboarded animations for visual states

VisualStateGroup里面包含的是VisualState,其默认内容也是一个Storyboard(但是可以不用指定)。但是VisualState具有Setters和StateTriggers部属,前者用来设置目标元素的一些部属,后者用来定义状态触发契机。要让StateTriggers生效,其VisualStateGroup必须是在XAML根元素的第一个子元素。

前面提到,使用StateTriggers可以用到定义状态触发的契机。StateTriggers所触发的Storyboard状态会在StateTriggers的条件失效时自动清除。所以并不需要使用GoToState 来跳到一个不包含动画效果的状态。另外使用StateTriggers来触发的状态,可以没有x:Name所定义的名字。

替换一个Control的模板的时候,记得要重新定义原先模板内VisualStateManager.VisualStateGroups 定义的所以命名的状态。因为Control的代码有可能会调用VisualStateManager.GoToState来跳转到之前定义的状态。如果新模板中没有定义跳转的目标状态,虽然不会抛出异常,但是这是一种设计错误。所以定义新模板时,最好从老模板的代码上开始修改。

对于自定义的控件,可以通过TemplateVisualStateAttribute从属来暴露涉及的VisualState的名字列表。

StateTrigger是从StateTriggerBase派生出来的。StateTriggerBase只有一个方法,就是SetActive。StateTrigger添加了IsActive部属。另一个从StateTriggerBase派生出来的时AdaptiveTrigger,其定义了MinWindowHeight和MinWindowWidth这两个部属,可以用来根据窗口大小变化而决定是否触发状态。

(本篇完)

2020-09-05更新

VisualStateManager.GoToState的第一个参数必须是一个XAML的Control。

直接在Page底下定义<VisualStateManager.VisualStateGroups>貌似VisualStateManager.GoToState会失败。如果在Page下加一层Panel,然后在Panel上定义<VisualStateManager.VisualStateGroups>VisualStateManager.GoToState就会成功:

<Page>
<Grid> <-- 或者是其他 panel -->
<VisualStateManager.VisualStateGroups> ... </VisualStateManager.VisualStateGroups>>
</Grid>
</Page>

参考

VisualStateManager.GoToState allways returns false

(更新完)

2020-11-20更新

Visual states for elements that aren’t controls Visual states are sometimes useful for scenarios where you want to change the state of some area of UI that’s not immediately a Control subclass. You can’t do this directly because the control parameter of the GoToState method requires a Control subclass, which refers to the object that the VisualStateManager acts upon. Page is a Control subclass, and it’s fairly rare that you’d be showing UI in a context where you don’t have a Page, or your Window.Content root isn’t a Control subclass. We recommend you define a custom UserControl to either be the Window.Content root or be a container for other content you want to apply states to (such as a Panel ). Then you can call GoToState on your UserControl and apply states regardless of whether the rest of the content is a Control. For example you could apply visual states to UI that otherwise consists of just a SwapChainPanel so long as you placed that within your UserControl and declared named states that apply to the properties of the parent UserControl or of the named SwapChainPanel part of the template.

(更新完)

2020-11-27更新

Try wrapping your DataTemplate inside a UserControl like this -

<DataTemplate>
    <UserControl>
        <Grid>
            <VisualStateManager.VisualStateGroups>
            ...
        </Grid>
    </UserControl>
</DataTemplate>

(更新完)