Search code examples
c#wpfxamlvisualstatemanager

Setting the initial value of a VisualState


I have a problem with a custom control using the VisualStateManager.

Transitions between states work as I expect, but I don't understand how to set the initial state.

I have made complete example to illustrate the problem. This example uses a custom control based on ButtonBase.

The control has a VisualState group with two states "Checked" and "Unchecked". This is the C# code of the control.

using System.Windows;
using System.Windows.Controls.Primitives;

namespace VisualStateTest
{
  [TemplateVisualStateAttribute(Name = "Checked",           GroupName = "CheckStates")]
  [TemplateVisualStateAttribute(Name = "Unchecked",         GroupName = "CheckStates")]
  public class CustomButton : ButtonBase
  {
    public static readonly DependencyProperty IsCheckedProperty =
        DependencyProperty.Register ( "IsChecked",
                                      typeof(bool),
                                      typeof(CustomButton),
                                      new FrameworkPropertyMetadata ( false,
                                                                      FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                                                                      OnCheckedChanged ) ) ;

    static CustomButton()
    {
      DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomButton), new FrameworkPropertyMetadata(typeof(CustomButton)));
    }

    public bool IsChecked
    {
      get { return (bool)GetValue(IsCheckedProperty); }
      set { SetValue(IsCheckedProperty, value); }
    }

    public static void OnCheckedChanged ( DependencyObject d, DependencyPropertyChangedEventArgs e )
    {
      var button = d as CustomButton ;

      if ((bool)e.NewValue)
      {
        VisualStateManager.GoToState(button, "Checked", true);
      }
      else
      {
        VisualStateManager.GoToState(button, "Unchecked", true);
      }
    }

  }
}

The control template shows a shadow on the left and top sides when the IsChecked property is set.

(I know that the design is poor, but this is not a question about graphical design.)

This is the control template:

<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:VisualStateTest">

  <Style TargetType="{x:Type local:CustomButton}">
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="{x:Type local:CustomButton}">

          <Border x:Name="outerborder"
                  Background="{TemplateBinding Background}"
                  BorderBrush="{TemplateBinding BorderBrush}"
                  BorderThickness="{TemplateBinding BorderThickness}">

            <VisualStateManager.VisualStateGroups>

              <VisualStateGroup x:Name="CheckStates">

                <VisualState x:Name="Checked">
                  <Storyboard>

                    <DoubleAnimationUsingKeyFrames BeginTime="0:0:0"
                                              Storyboard.TargetName="topshadow"
                                              Storyboard.TargetProperty="(UIElement.Opacity)">
                      <SplineDoubleKeyFrame KeyTime="0:0:0.5" Value="1.0"/>
                    </DoubleAnimationUsingKeyFrames>

                    <DoubleAnimationUsingKeyFrames BeginTime="0:0:0"
                                              Storyboard.TargetName="leftshadow"
                                              Storyboard.TargetProperty="(UIElement.Opacity)">
                      <SplineDoubleKeyFrame KeyTime="0:0:0.5" Value="1.0"/>
                    </DoubleAnimationUsingKeyFrames>

                  </Storyboard>
                </VisualState>

                <VisualState x:Name="Unchecked">
                  <Storyboard>

                    <DoubleAnimationUsingKeyFrames BeginTime="0:0:0"
                                              Storyboard.TargetName="topshadow"
                                              Storyboard.TargetProperty="(UIElement.Opacity)">
                      <SplineDoubleKeyFrame KeyTime="0:0:0.5" Value="0"/>
                    </DoubleAnimationUsingKeyFrames>

                    <DoubleAnimationUsingKeyFrames BeginTime="0:0:0"
                                              Storyboard.TargetName="leftshadow"
                                              Storyboard.TargetProperty="(UIElement.Opacity)">
                      <SplineDoubleKeyFrame KeyTime="0:0:0.5" Value="0"/>
                    </DoubleAnimationUsingKeyFrames>

                  </Storyboard>
                </VisualState>

              </VisualStateGroup>

            </VisualStateManager.VisualStateGroups>

            <Grid Cursor="Hand" ClipToBounds="True">

              <Grid.RowDefinitions>
                <RowDefinition Height="10"/>
                <RowDefinition Height="*"/>
              </Grid.RowDefinitions>
              <Grid.ColumnDefinitions>
                <ColumnDefinition Width="10"/>
                <ColumnDefinition Width="*"/>
              </Grid.ColumnDefinitions>

              <Rectangle x:Name="lineargradient"
                         Grid.RowSpan="2" Grid.ColumnSpan="2"
                         Stroke="#7F000000"
                         StrokeThickness="0">
                <Rectangle.Fill>
                  <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="#20808080"/>
                    <GradientStop Color="#008A8A8A" Offset="0.5"/>
                    <GradientStop Color="#20000000" Offset="1"/>
                  </LinearGradientBrush>
                </Rectangle.Fill>
              </Rectangle>

              <ContentPresenter HorizontalAlignment="Center"
                                x:Name="contentPresenter"
                                Grid.RowSpan="2" Grid.ColumnSpan="2"
                                VerticalAlignment="Center" />

              <Rectangle x:Name="topshadow"  Fill="#40000000" Grid.Row="0" Grid.ColumnSpan="2" Opacity="0">
                <Rectangle.Effect>
                  <BlurEffect Radius="3"/>
                </Rectangle.Effect>
              </Rectangle>
              <Rectangle x:Name="leftshadow" Fill="#40000000" Grid.Row="1" Grid.Column="0" Opacity="0">
                <Rectangle.Effect>
                  <BlurEffect Radius="3"/>
                </Rectangle.Effect>
              </Rectangle>

            </Grid>

          </Border>

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

For this test I have defined a ViewModel with two boolean properties (Option1 and Option2). One of the properties has the initial value false, the other one is true.

The main window with two CustomButton controls, wired up to the two option properties, and also two checkboxes connected to the same properties.

This is the complete code of the view model ...

using System;
using System.ComponentModel;
using System.Windows.Input;

namespace VisualStateTest
{
  public class ViewModel : INotifyPropertyChanged
  {
    // Events for INotifyPropertyChanged
    public event PropertyChangedEventHandler PropertyChanged;

    private bool        _option1 = false ;
    private bool        _option2 = true ;

    public  ICommand    Notify1Command    { get; private set; }
    public  ICommand    Notify2Command    { get; private set; }

    public ViewModel()
    {
      Notify1Command = new RelayCommand (new Action<object>(Execute_Notify1Command));
      Notify2Command = new RelayCommand (new Action<object>(Execute_Notify2Command));
    }

    public bool Option1
    {
      get { return _option1 ; }
      set
      {
        _option1 = value ;
        NotifyPropertyChanged ( "Option1" ) ;
      }
    }

    public bool Option2
    {
      get { return _option2 ; }
      set
      {
        _option2 = value ;
        NotifyPropertyChanged ( "Option2" ) ;
      }
    }

    public void Execute_Notify1Command ( object value )
    {
      Option1 = !Option1 ;
    }

    public void Execute_Notify2Command ( object value )
    {
      Option2 = !Option2 ;
    }

    private void NotifyPropertyChanged ( String propertyName )
    {
      if ( this.PropertyChanged != null )
      {
        this.PropertyChanged ( this, new PropertyChangedEventArgs(propertyName) ) ;
      }
    }
  }
}

and the main window ...

<Window x:Class="VisualStateTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:VisualStateTest"
        mc:Ignorable="d"
        WindowStartupLocation="CenterScreen"
        Title="MainWindow" Height="350" Width="525">

  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="*"/>
      <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*"/>
      <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>

    <local:CustomButton Grid.Row="0" Grid.Column="0"
                        Command="{Binding Notify1Command}"
                        IsChecked="{Binding Option1, Mode=OneWay}"
                        Content="Option 1"
                        Margin="20"/>

    <local:CustomButton Grid.Row="0" Grid.Column="1"
                        Command="{Binding Notify2Command}"
                        IsChecked="{Binding Option2, Mode=OneWay}"
                        Content="Option 2"
                        Margin="20"/>

    <CheckBox Grid.Row="1" Grid.Column="0"
              IsChecked="{Binding Option1}"
              Content="Option 1"
              Margin="20 5"/>

    <CheckBox Grid.Row="1" Grid.Column="1"
              IsChecked="{Binding Option2}"
              Content="Option 2"
              Margin="20 5"/>

  </Grid>

</Window>

After the program is started, clicking either the custom buttons or the checkboxes toggles the option and shows or hides the shadow effect.

This is what it looks like in a 'normal' state:

enter image description here

The problem is when the program starts. Although Option2 is initialized to true, and the function VisualStateManager.GoToState has been called, the shadow effect is not shown.

This is what it looks like on startup.

enter image description here

The checkbox on the right indicates that option 2 is true, but the shadow effect is not present.

I am sure that I am missing one small piece of the puzzle. If it helps, I can upload the sample program.

I'm sorry if this is too much detail.


Solution

  • I think that I have found the answer.

    I need to override the function OnApplyTemplate() in my custom control. I have extended the code of the CustomButton class with the following function:

    public override void OnApplyTemplate()
    {
      base.OnApplyTemplate();
    
      if ( IsChecked )
      {
        VisualStateManager.GoToState(this, "Checked", true);
      }
      else
      {
        VisualStateManager.GoToState(this, "Unchecked", true);
      }
    }
    

    I found this information in by reading the Microsoft documentation. With reference to the method OnApplyTemplate it states

    This is the earliest that the FrameworkElement in the ControlTemplate is available to the control.