设置 VisualState 的初始值

Setting the initial value of a VisualState

我对使用 VisualStateManager 的自定义控件有疑问。

状态之间的转换如我所料,但我不明白如何设置初始状态。

我做了完整的例子来说明问题。 此示例使用基于 ButtonBase 的自定义控件。

控件有一个 VisualState 组,有两个状态 "Checked" 和 "Unchecked"。 这是控件的C#代码。

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);
      }
    }

  }
}

设置 IsChecked 属性 后,控件模板在左侧和顶部显示阴影。

(我知道设计不好,但这不是图形设计的问题。)

这是控件模板:

<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>

对于此测试,我定义了一个具有两个布尔属性(Option1 和 Option2)的 ViewModel。 其中一个属性的初始值为 false,另一个为 true。

主要 window 有两个 CustomButton 控件,连接到两个选项属性,还有两个复选框连接到相同的属性。

这是视图模型的完整代码...

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) ) ;
      }
    }
  }
}

和主要 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>

程序启动后,单击自定义按钮或复选框可切换选项并显示或隐藏阴影效果。

这是 'normal' 状态下的样子:

问题出在程序启动时。虽然Option2初始化为true,并且调用了函数VisualStateManager.GoToState,但是没有显示阴影效果

这是启动时的样子。

右侧的复选框表示选项 2 为真,但不存在阴影效果。

我确信我遗漏了一小块拼图。如果有帮助,我可以上传示例程序。

很抱歉,如果这太详细了。

我想我找到了答案。

我需要在自定义控件中覆盖函数 OnApplyTemplate()。我用以下函数扩展了 CustomButton class 的代码:

public override void OnApplyTemplate()
{
  base.OnApplyTemplate();

  if ( IsChecked )
  {
    VisualStateManager.GoToState(this, "Checked", true);
  }
  else
  {
    VisualStateManager.GoToState(this, "Unchecked", true);
  }
}

我通过阅读 Microsoft documentation 找到了此信息。关于 OnApplyTemplate 方法,它指出

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