VisualStateManager или как удобно определить внешний вид приложений на WPF и Silverlight

Все мы знаем, что технологии WPF и XAML позволяют достаточно существенно изменять внешний вид приложения. Однако, как выясняется, далеко не все знают как это можно делать удобно.

Для определения внешнего вида элементов управления мы обычно используем ControlTemplate, а динамику задаем используя триггеры. Несомненно, триггеры в XAML – мощнейший инструмент благодаря которому у нас появляется столько дополнительных возможностей. Тем не менее при построении сложных интерфейсов логика, построенная на триггерах бывает нетривиальной и непрозрачной, а порой и вовсе задает определенные ограничения.

Альтернативным способом построения задания внешнего вида элементов управления является использование объекта VisualStateManager. Я не буду вдаваться в глубокие технические детали реализации VisualStateManager, упомяну лишь самое необходимое.

VisualStateManager – это DependencyObject, со всеми вытекающими последствиями. Мы можем задать поведение этого объекта для каждого элемента управления, которым хотим управлять. Идея VSM основана на реализации состояния элемента управления. Мы можем определить набор состояний (VisualState), в которых может находится элемент управлений и задать для каждого состояния свой вид. Состояния упаковываются в группы (VisualStateGroup). Таким образом, можно определить логические группы состояний и определить в них переходы.

Каждое состояние (VisualState) по сути представляет собой Storyboard. Реализация состояния заключается в том, что при переходе в это состояние срабатывает заданный Storyboard, т.е. запускается анимация.

Можно графически представить представление описанной структуры.

Давайте создадим небольшой пример на основе состояний. Создадим новый UserControl в WPF приложении с самой простой структурой.

<UserControl x:Class="WpfApplication15.UserControl1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Grid>
    <Border Name="State1Panel" Background="Red" Opacity="1"/>

    <TextBlock Text="Text of control" Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center"/>
  </Grid>
</UserControl>

Как видно, это обычный элемент управления с текстом и красным фоном. Давайте определим два состояния для этого элемента управления – State1 и State2. Каждое из состояний визуально будет отличаться фоном. Для этого опишем два объекта VisualState и зададим анимации перехода. В нашем случае просто положим друг на друга два элемента Border с разным фоном. Анимация будет заключаться в скрытии и отображении нужного элемента в зависимости от состояния.

<UserControl x:Class="WpfApplication15.UserControl1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <Grid>
    <VisualStateManager.VisualStateGroups>
      <VisualStateGroup x:Name="Common">
        <VisualState x:Name="State1">
          <Storyboard>
              <DoubleAnimation To="1" Duration="0:00:00.4" Storyboard.TargetName="State1Panel" Storyboard.TargetProperty="(UIElement.Opacity)" />
              <DoubleAnimation To="0" Duration="0:00:00.7" Storyboard.TargetName="State2Panel" Storyboard.TargetProperty="(UIElement.Opacity)" />
          </Storyboard>
        </VisualState>
        <VisualState x:Name="State2">
          <Storyboard>
              <DoubleAnimation To="0" Duration="0:00:00.7" Storyboard.TargetName="State1Panel" Storyboard.TargetProperty="(UIElement.Opacity)" />
              <DoubleAnimation To="1" Duration="0:00:00.4" Storyboard.TargetName="State2Panel" Storyboard.TargetProperty="(UIElement.Opacity)" />
          </Storyboard>
        </VisualState>
      </VisualStateGroup>
    </VisualStateManager.VisualStateGroups>
    <Border Name="State2Panel" Background="Green" Opacity="0"/>
    <Border Name="State1Panel" Background="Red" Opacity="1"/>

    <TextBlock Text="Text of control" Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center"/>
  </Grid>
</UserControl>

Теперь, используя VisualStateManager, можно переключать состояния элемента управления и таким образом влиять на внешний вид. Для этого разместим элемент управления на форме.

<Window x:Class="WpfApplication15.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:WpfApplication15="clr-namespace:WpfApplication15"
  Title="Window1" Height="300" Width="300">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition/>
      <RowDefinition Height="40"/>
    </Grid.RowDefinitions>
    <WpfApplication15:UserControl1 x:Name="Control1" Height="118" VerticalAlignment="Top" Margin="50,12,101,0" />
    
    <StackPanel Orientation="Horizontal" Grid.Row="1">            
      <Button Name="State1Button" Width="75" Click="State1Button_Click">State1</Button>

      <Button Name="State2Button" Width="75" Click="State2Button_Click">State2</Button>
    </StackPanel>
  </Grid>
</Window>

И создадим обработчики нажатия кнопок.

private void State1Button_Click(object sender, RoutedEventArgs e)
{
  VisualStateManager.GoToState(Control1, "State1", true);
}

private void State2Button_Click(object sender, RoutedEventArgs e)
{
  VisualStateManager.GoToState(Control1, "State2", true);
}

В итоге имеем следующее приложение.

Таким образом мы можем создать несколько состояний нашего элемента управления и на основе него строить пользовательский интерфейс.

Такой подход становится особенно полезным, когда мы видим, что для стандартных элементов управления существует набор предопределенных состояний. Например, для кнопки – Normal, MouseOver, Pressed, Disabled и т.д. Такие состояния есть и у других элементов. Благодаря такой возможности существенно упрощается создание шаблонов для элементов управления. Например, кнопку мы можем описать следующим образом.

<Style TargetType="{x:Type Button}">
  <Setter Property="Margin" Value="5"/>
  <Setter Property="Width" Value="100"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type Button}">
        <Grid>
          <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="Common">
              <VisualState x:Name="Normal"/>
              <VisualState x:Name="MouseOver">
                <Storyboard>
                    <DoubleAnimation To="0" Duration="0:00:00.5" Storyboard.TargetName="NormalBorder" Storyboard.TargetProperty="(UIElement.Opacity)" />
                    <DoubleAnimation To="1" Duration="0:00:00.3" Storyboard.TargetName="OverBorder" Storyboard.TargetProperty="(UIElement.Opacity)" />
                    <DoubleAnimation From="0" To="10" AutoReverse="True" Duration="0:00:00.3" Storyboard.TargetName="OverBorderRotateTransform" Storyboard.TargetProperty="Angle" />
                </Storyboard>
              </VisualState>
              <VisualState x:Name="Pressed">
                <Storyboard>
                    <DoubleAnimation To="0" Duration="0:00:00.5" Storyboard.TargetName="NormalBorder" Storyboard.TargetProperty="(UIElement.Opacity)" />
                    <DoubleAnimation To="1" Duration="0:00:00.3" Storyboard.TargetName="OverBorder" Storyboard.TargetProperty="(UIElement.Opacity)" />
                    <DoubleAnimation From="0" To="360" AutoReverse="True" Duration="0:00:00.4" Storyboard.TargetName="OverBorderRotateTransform" Storyboard.TargetProperty="Angle" />
                </Storyboard>
              </VisualState>
            </VisualStateGroup>
          </VisualStateManager.VisualStateGroups>
          
          <Border Name="NormalBorder" CornerRadius="5" Background="Gray" Opacity="1"/>
          <Border Name="OverBorder" CornerRadius="5" Background="Green" Opacity="0">
            <Border.RenderTransform>
                <RotateTransform x:Name="OverBorderRotateTransform" CenterX="50" CenterY="20" />
            </Border.RenderTransform>
          </Border>
          
          <ContentPresenter Content="{TemplateBinding Content}" Margin="5" HorizontalAlignment="Center" VerticalAlignment="Center"/>
        </Grid>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>

Теперь наши кнопки будут выглядеть следующим образом.

Аналогичным образом можно настраивать внешний вид в приложениях на Silverlight.