VisualStateManager and alternative to Triggers

The VisualStateManager came into being with Silverlight. I’m not a Silverlight developer, but I believe I’m right it saying that Silverlight didn’t support Triggers, hence the VSM was created to emulate aspects of the triggering system (or at least a state change management).

Where the VSM comes into it’s own is the ability to create a control with multiple “states”. Whereby when an event or other state change occurs we can inform the ControlTemplate via a simple string (used to define a state change key). This is similar to various MessageBus implementations for MVVM code. The VSM is literally a simple state machine, so not only can we handle the state changes but also the transitions between changes if we want. Offering the ability to not only change the UI on certain state changes but even do different things depending upon the transitions from state to state.

Note: Bot the Triggering system and the VSM can used toegether if desired.

Let’s see this in action as it’ll make things much clearer. I’ve implemented a simple WatermarkTextBox, initially using Triggers. When the textbox has focus the watermark text is hidden. When the textbox doesn’t have focus AND no text exists within the textbox, the watermark is displayed, otherwise it remains hidden. Here’s the XAML for the control

<Style TargetType="Controls:WatermarkTextBox" BasedOn="{StaticResource {x:Type TextBox}}">
   <Setter Property="Template">
      <Setter.Value>
         <ControlTemplate TargetType="Controls:WatermarkTextBox">
            <Grid>
               <Border BorderThickness="{Binding Path=BorderThickness, 
                   RelativeSource={RelativeSource TemplatedParent}, 
                   Mode=OneWay}" BorderBrush="{Binding Path=BorderBrush, 
                   RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}">
                  <Grid>                               
                     <ScrollViewer x:Name="PART_ContentHost" Margin="3"/>
                     <TextBlock x:Name="PART_Watermark" 
                         Text="{TemplateBinding Watermark}" FontStyle="Italic" 
                         VerticalAlignment="Center" Margin="5,0,0,0" 
                         FontWeight="Bold" Foreground="Gray"/>
                  </Grid>
               </Border>
            </Grid>

            <ControlTemplate.Triggers>
               <MultiTrigger>
                  <MultiTrigger.Conditions>
                     <Condition Property="IsFocused" Value="True"/>
                  </MultiTrigger.Conditions>
                  <Setter Property="Visibility" 
                            Value="Collapsed" TargetName="PART_Watermark" />
               </MultiTrigger>

               <MultiTrigger>
                  <MultiTrigger.Conditions>
                     <Condition Property="RemoveWatermark" Value="True"/>
                     <Condition Property="IsFocused" Value="False"/>
                  </MultiTrigger.Conditions>
                  <Setter Property="Visibility" 
                            Value="Collapsed" TargetName="PART_Watermark" />
               </MultiTrigger>
            </ControlTemplate.Triggers>

         </ControlTemplate>
      </Setter.Value>
   </Setter>
</Style>

and now the bare bones source code for this implementation looks like this

public class WatermarkTextBox : TextBox
{
   static WatermarkTextBox()
   {
      DefaultStyleKeyProperty.OverrideMetadata(typeof(WatermarkTextBox), 
              new FrameworkPropertyMetadata(typeof(WatermarkTextBox)));

      TextProperty.OverrideMetadata(typeof(WatermarkTextBox),
   	      new FrameworkPropertyMetadata(
                  new PropertyChangedCallback(TextPropertyChanged)));
   }	

   public static readonly DependencyProperty WatermarkProperty =
	DependencyProperty.Register("Watermark", typeof(string), 
           typeof(WatermarkTextBox),
	   new FrameworkPropertyMetadata(String.Empty));

   public string Watermark
   {
      get { return (string)GetValue(WatermarkProperty); }
      set { SetValue(WatermarkProperty, value); }
   }

   private static readonly DependencyPropertyKey RemoveWatermarkPropertyKey
            = DependencyProperty.RegisterReadOnly("RemoveWatermark", 
                    typeof(bool), typeof(WatermarkTextBox),
                    new FrameworkPropertyMetadata((bool)false));

   public static readonly DependencyProperty RemoveWatermarkProperty
            = RemoveWatermarkPropertyKey.DependencyProperty;

   public bool RemoveWatermark
   {
      get { return (bool)GetValue(RemoveWatermarkProperty); }
   }

   static void TextPropertyChanged(DependencyObject sender, 
                     DependencyPropertyChangedEventArgs args)
   {
      WatermarkTextBox watermarkTextBox = (WatermarkTextBox)sender;
      bool textExists = watermarkTextBox.Text.Length > 0;
      if (textExists != watermarkTextBox.RemoveWatermark)
      {
         watermarkTextBox.SetValue(RemoveWatermarkPropertyKey, textExists);
      }
   }
}

Now let’s look at an equivalent ControlTemplate using the VSM

<Style TargetType="Controls:WatermarkTextBox" BasedOn="{StaticResource {x:Type TextBox}}">
   <Setter Property="Template">
      <Setter.Value>
         <ControlTemplate TargetType="Controls:WatermarkTextBox">
            <Grid>
              <VisualStateManager.VisualStateGroups>
                 <VisualStateGroup x:Name="WatermarkGroup">
                    <VisualState x:Name="ShowWatermarkState">
                        <Storyboard>
                           <ObjectAnimationUsingKeyFrames 
                                       Duration="0:0:0" 
                                       Storyboard.TargetName="PART_Watermark" 
                                       Storyboard.TargetProperty="(UIElement.Visibility)">
                              <DiscreteObjectKeyFrame KeyTime="0:0:0" 
                                       Value="{x:Static Visibility.Visible}"/>
                           </ObjectAnimationUsingKeyFrames>
                        </Storyboard>
                        </VisualState>
                        <VisualState x:Name="HideWatermarkState">
                           <Storyboard>
                              <ObjectAnimationUsingKeyFrames Duration="0:0:0" 
                                        Storyboard.TargetName="PART_Watermark" 
                                        Storyboard.TargetProperty="(UIElement.Visibility)">
                                 <DiscreteObjectKeyFrame KeyTime="0:0:0" 
                                          Value="{x:Static Visibility.Collapsed}"/>
                              </ObjectAnimationUsingKeyFrames>
                           </Storyboard>
                        </VisualState>
                 </VisualStateGroup>
              </VisualStateManager.VisualStateGroups>

               <Border BorderThickness="{Binding Path=BorderThickness, 
                   RelativeSource={RelativeSource TemplatedParent}, 
                   Mode=OneWay}" BorderBrush="{Binding Path=BorderBrush, 
                   RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}">
                  <Grid>                               
                     <ScrollViewer x:Name="PART_ContentHost" Margin="3"/>
                     <TextBlock x:Name="PART_Watermark" 
                         Text="{TemplateBinding Watermark}" FontStyle="Italic" 
                         VerticalAlignment="Center" Margin="5,0,0,0" 
                         FontWeight="Bold" Foreground="Gray"/>
                  </Grid>
               </Border>
            </Grid>
         </ControlTemplate>
      </Setter.Value>
   </Setter>
</Style>

Now the source code is basically the same as previously listed but with the following change to TextPropertyChanged

static void TextPropertyChanged(DependencyObject sender, 
                  DependencyPropertyChangedEventArgs args)
{
   WatermarkTextBox watermarkTextBox = (WatermarkTextBox)sender;
   bool textExists = watermarkTextBox.Text.Length > 0;
   if (textExists != watermarkTextBox.RemoveWatermark)
   {
      watermarkTextBox.SetValue(RemoveWatermarkPropertyKey, textExists);
   }

   // added to update the VSM
   watermarkTextBox.UpdateState();
}

and the following new methods

private void UpdateState()
{
   bool textExists = Text.Length > 0;
   var watermark = GetTemplateChild("PART_Watermark") as FrameworkElement;
   var state = textExists || IsFocused ? "HideWatermarkState" : "ShowWatermarkState";

   VisualStateManager.GoToState(this, state, true);
}

protected override void OnGotFocus(RoutedEventArgs e)
{
   base.OnGotFocus(e);
   UpdateState();
}

protected override void OnLostFocus(RoutedEventArgs e)
{
   base.OnLostFocus(e);
   UpdateState();
}

So we’ve had to put a little extra work into the VSM version, but the key bit is in UpdateStatus where we tell the VSM to GoToState. In this we’re in essence sending a string message to the VSM to tell it to “trigger” it’s storyboard for the given state.

What this means is that we could define many changes in state that could then be handled via the VSM.

We could (as seems to occur in some controls) define states which our ControlTemplate does nothing with. This allows us to define states within the control’s code which somebody might wish to hook into. These might be declared in the default ControlTemplate as empty elements as per the code below

<VisualState x:Name="DoSomethingSomeDay" />

Then anyone defining their own ControlTemplate can override these states if they wish.

When defining our states a control should publish the states that it implements.uses using the TemplateVisualState attribute. This is not a requirement for the code to work bbut obviously tells anything/anyone wishing to retemplate the control, what states it codes to, so for our example we would mark the WatermarkTextBox thus

[TemplateVisualState(Name = "ShowWatermarkState", GroupName = "WatermarkGroup")]
[TemplateVisualState(Name = "HideWatermarkState", GroupName = "WatermarkGroup")]
public class WatermarkTextBox : TextBox
{
   // .. implementation
}

Along with the ability to set different visual states we can also create transitions (VisualTransitions) between two states, for example if a state changes from one state to another maybe we want to change the colour of the background of a control to show the transition. In some ways we can achieve most of what we want in the various VisualStates, but the VisualTransition can show difference based on the different workflows of a state transition.

Below is a simple example for the WatermarkTextBox, which could have been achieved solely with states, but just gives an idea of what you can do. Here we transition between ShowWatermarkState and HidewatermarkState and back. Using a DoubleAnimation we alter the Opacity of the watermark text.

<VisualStateManager.VisualStateGroups>
   <VisualStateGroup x:Name="WatermarkGroup">
       <VisualStateGroup.Transitions>
           <VisualTransition From="ShowWatermarkState" To="HideWatermarkState">
              <Storyboard>
                 <DoubleAnimation Storyboard.TargetName="PART_Watermark"
                                  Storyboard.TargetProperty="Opacity" From="1"
                                  To="0" Duration="0:0:2" />
               </Storyboard>                                        
            </VisualTransition>
            <VisualTransition From="HideWatermarkState" To="ShowWatermarkState">
               <Storyboard>
                  <DoubleAnimation Storyboard.TargetName="PART_Watermark"
                                   Storyboard.TargetProperty="Opacity" From="0"
                                   To="1" Duration="0:0:2" />
               </Storyboard>
            </VisualTransition>
         </VisualStateGroup.Transitions>
         <VisualState x:Name="ShowWatermarkState">
            <Storyboard>
               <ObjectAnimationUsingKeyFrames Duration="0:0:0" 
                             Storyboard.TargetName="PART_Watermark" 
                             Storyboard.TargetProperty="(UIElement.Visibility)">
                  <DiscreteObjectKeyFrame KeyTime="0:0:0" 
                             Value="{x:Static Visibility.Visible}"/>
               </ObjectAnimationUsingKeyFrames>
            </Storyboard>
         </VisualState>
         <VisualState x:Name="HideWatermarkState">
            <Storyboard>
               <ObjectAnimationUsingKeyFrames Duration="0:0:0" 
                             Storyboard.TargetName="PART_Watermark" 
                             Storyboard.TargetProperty="(UIElement.Visibility)">
                   <DiscreteObjectKeyFrame KeyTime="0:0:0" 
                             Value="{x:Static Visibility.Collapsed}"/>
               </ObjectAnimationUsingKeyFrames>
            </Storyboard>
        </VisualState>                                
    </VisualStateGroup>
</VisualStateManager.VisualStateGroups>

Now the above will not work quite as expected on the first time the texbox gets focus as no transition takes place so in the code if we add

public override void OnApplyTemplate()
{
   base.OnApplyTemplate();
   VisualStateManager.GoToState(this, "ShowWatermarkState", true);
}

Just to seed the initial state, then as the textbox gets focus the transition from ShowWatermarkState to HideWatermarkState takes place.