WPF animation: binding to the "To" attribute of storyboard animation

39,124

Solution 1

I've had similar situations in ControlTemplates where I've wanted to bind the "To" attribute to a value (rather than hard-coding it), and I finally found a solution.

Quick side note: If you dig around on the web you'll find examples of people being able to use data binding for the "From" or "To" properties. However, in those examples the Storyboards are not in a Style or ControlTemplate. If your Storyboard is in a Style or ControlTemplate, you'll have to use a different approach, such as this solution.

This solution gets around the freezable issue because it simply animates a double value from 0 to 1. It works with a clever use of the Tag property and a Multiply converter. You use a multibinding to bind to both a desired property and your "scale" (the Tag), which get multiplied together. Basically the idea is that your Tag value is what you animate, and its value acts like a "scale" (from 0 to 1) bringing your desired attribute value to "full scale" once you've animated the Tag to 1.

You can see this in action here. The crux of it is this:

<local:MultiplyConverter x:Key="multiplyConverter" />
<ControlTemplate x:Key="RevealExpanderTemp" TargetType="{x:Type Expander}">
    <!-- (other stuff here...) -->
    <ScrollViewer x:Name="ExpanderContentScrollView">
        <!-- ** BEGIN IMPORTANT PART #1 ...  -->
        <ScrollViewer.Tag>
            <sys:Double>0.0</sys:Double>
        </ScrollViewer.Tag>
        <ScrollViewer.Height>
            <MultiBinding Converter="{StaticResource multiplyConverter}">
               <Binding Path="ActualHeight" ElementName="ExpanderContent"/>
               <Binding Path="Tag" RelativeSource="{RelativeSource Self}" />
            </MultiBinding>
        </ScrollViewer.Height>
        <!-- ...end important part #1.  -->
        <ContentPresenter x:Name="ExpanderContent" ContentSource="Content"/>

    </ScrollViewer>

  <ControlTemplate.Triggers>
    <Trigger Property="IsExpanded" Value="True">
        <Trigger.EnterActions>
            <BeginStoryboard>
                <Storyboard>
                   <!-- ** BEGIN IMPORTANT PART #2 (make TargetProperty 'Tag') ...  -->
                   <DoubleAnimation Storyboard.TargetName="ExpanderContentScrollView"
                         Storyboard.TargetProperty="Tag"
                         To="1"
                         Duration="0:0:0.4"/>
                    <!-- ...end important part #2 -->
               </Storyboard>
            </BeginStoryboard>
        </Trigger.EnterActions>
    </Trigger>
  </ControlTemplate.Triggers>
</ControlTemplate>

With this value converter:

public class MultiplyConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
       double result = 1.0;
       for (int i = 0; i < values.Length; i++)
       {
           if (values[i] is double)
               result *= (double)values[i];
       }

       return result;
    }

   public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
   {
       throw new Exception("Not implemented");
   }
}

Solution 2

As far as I know, you can't bind the animation to/from because the animation has to be freezable.

Solution 3

I like @Jason Frank's solution. However it is even easier and less error-prone if you don't use the Tag, but instead e.g. the Width property of an empty dummy Border element. It is a native double property, so no need for <sys:Double> syntax and you can name the Border just like you would do with a variable like so:

<!-- THIS IS JUST USED FOR SLIDING ANIMATION MATH -->
<!-- animated Border.Width    From 0 to 1 -->
<Border x:Name="Var_Animation_0to1" Width="0"/>
<!-- animated Border.Width    From 0 to (TotalWidth-SliderWidth) -->
<Border x:Name="Var_Slide_Length">
    <Border.Width>
        <MultiBinding Converter="{mvvm:MathConverter}" ConverterParameter="a * (b-c)">
            <Binding ElementName="Var_Animation_0to1" Path="Width"/>
            <Binding ElementName="BackBorder" Path="ActualWidth"/>
            <Binding ElementName="Slider" Path="ActualWidth"/>
        </MultiBinding>
    </Border.Width>
</Border>

That makes the bindings much more readable.

The Animation is always 0..1, as Jason pointed out:

<BeginStoryboard Name="checkedSB">
    <Storyboard Storyboard.TargetProperty="Width" Storyboard.TargetName="Var_Animation_0to1">
        <DoubleAnimation To="1" Duration="00:00:00.2"/>
    </Storyboard>
</BeginStoryboard>

Then bind whatever you want to animate to the Width of the dummy border. This way you can even chain converters to each other like so:

<Border x:Name="Slider" HorizontalAlignment="Left"
        Margin="{Binding ElementName=Var_Slide_Length, Path=Width, Converter={StaticResource DoubleToThickness}, ConverterParameter=x 0 0 0}"/>

Combined with the MathConverter you can do almost anything in styles: https://www.codeproject.com/Articles/239251/MathConverter-How-to-Do-Math-in-XAML

Solution 4

I implemented this exact thing.

<UserControl x:Class="YOURNAMESPACE.UserControls.SliderControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:YOURNAMESPACE.UserControls"
             xmlns:converter="clr-namespace:YOURNAMESPACE.ValueConverters"
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300"
             SizeChanged="UserControl_SizeChanged">
    <UserControl.Resources>

        <converter:MathConverter x:Key="mathConverter" />

        <LinearGradientBrush x:Key="CheckedBlue" StartPoint="0,0" EndPoint="0,1">
            <GradientStop Color="#e4f5fc" Offset="0" />
            <GradientStop Color="#e4f5fc" Offset="0.1" />
            <GradientStop Color="#e4f5fc" Offset="0.1" />
            <GradientStop Color="#9fd8ef" Offset="0.5" />
            <GradientStop Color="#9fd8ef" Offset="0.5" />
            <GradientStop Color="#bfe8f9" Offset="1" />
        </LinearGradientBrush>
        <LinearGradientBrush x:Key="CheckedOrange" StartPoint="0,0" EndPoint="0,1">
            <GradientStop Color="#FFCA6A13" Offset="0" />
            <GradientStop Color="#FFF67D0C" Offset="0.1" />
            <GradientStop Color="#FFFE7F0C" Offset="0.1" />
            <GradientStop Color="#FFFA8E12" Offset="0.5" />
            <GradientStop Color="#FFFF981D" Offset="0.5" />
            <GradientStop Color="#FFFCBC5A" Offset="1" />
        </LinearGradientBrush>

        <SolidColorBrush x:Key="CheckedOrangeBorder" Color="#FF8E4A1B" />
        <SolidColorBrush x:Key="CheckedBlueBorder" Color="#FF143874" />

        <Style x:Key="CheckBoxSlider" TargetType="{x:Type CheckBox}">
            <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.WindowTextBrushKey}}" />
            <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}" />

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

                        <DockPanel x:Name="dockPanel" 
                               Width="{TemplateBinding ActualWidth}" 
                               Height="{TemplateBinding Height}" >
                            <DockPanel.Resources>

                                <Storyboard x:Key="ShowRightStoryboard">
                                    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="slider" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)">
                                        <SplineDoubleKeyFrame KeyTime="00:00:00.1000000" Value="0" />
                                    </DoubleAnimationUsingKeyFrames>
                                </Storyboard>

                                <Storyboard x:Key="ShowLeftStoryboard" >
                                    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
                                                           Storyboard.TargetName="slider" 
                                                           Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)"
                                                           >
                                        <SplineDoubleKeyFrame x:Name="RightHalfKeyFrame" KeyTime="00:00:00.1000000" Value="300" />
                                    </DoubleAnimationUsingKeyFrames>
                                </Storyboard>
                            </DockPanel.Resources>

                            <ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" 
                                          Content="{TemplateBinding Content}" 
                                          ContentStringFormat="{TemplateBinding ContentStringFormat}" 
                                          ContentTemplate="{TemplateBinding ContentTemplate}" 
                                          RecognizesAccessKey="True" 
                                          VerticalAlignment="Center" />
                            <Grid>

                                <Border x:Name="BackgroundBorder" BorderBrush="#FF939393" BorderThickness="1" CornerRadius="3" 

                                Width="{TemplateBinding ActualWidth}" 
                                Height="{TemplateBinding Height}" >

                                    <Border.Background>
                                        <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                                            <GradientStop Color="#FFB5B5B5" Offset="0" />
                                            <GradientStop Color="#FFDEDEDE" Offset="0.1" />
                                            <GradientStop Color="#FFEEEEEE" Offset="0.5" />
                                            <GradientStop Color="#FFFAFAFA" Offset="0.5" />
                                            <GradientStop Color="#FFFEFEFE" Offset="1" />
                                        </LinearGradientBrush>
                                    </Border.Background>
                                    <Grid>
                                        <Grid.ColumnDefinitions>
                                            <ColumnDefinition />
                                            <ColumnDefinition />
                                        </Grid.ColumnDefinitions>
                                        <TextBlock x:Name="LeftTextBlock"  Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:SliderControl}}, Path=LeftText, Mode=TwoWay}" Grid.Column="0" HorizontalAlignment="Center" VerticalAlignment="Center"  />
                                        <TextBlock x:Name="RightTextBlock"  Text="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:SliderControl}}, Path=RightText, Mode=TwoWay}" Grid.Column="1" HorizontalAlignment="Center" VerticalAlignment="Center"  />
                                    </Grid>
                                </Border>

                                <Border x:Name="slider" 
                                    BorderBrush="#FF939393" 
                                    HorizontalAlignment="Left" 
                                    Width="{TemplateBinding ActualWidth, Converter={StaticResource mathConverter}, ConverterParameter=/2}" 
                                    Height="{TemplateBinding Height}"
                                    BorderThickness="1" 
                                    CornerRadius="3" 
                                    RenderTransformOrigin="0.5,0.5" Margin="0"
                                    >
                                    <Border.RenderTransform>
                                        <TransformGroup>
                                            <ScaleTransform ScaleX="1" ScaleY="1" />
                                            <SkewTransform AngleX="0" AngleY="0" />
                                            <RotateTransform Angle="0" />
                                            <TranslateTransform X="{TemplateBinding ActualWidth, Converter={StaticResource mathConverter}, ConverterParameter=/2}" Y="0" />
                                        </TransformGroup>
                                    </Border.RenderTransform>
                                    <Border.Background>
                                        <LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
                                            <GradientStop Color="#FFF0F0F0" Offset="0" />
                                            <GradientStop Color="#FFCDCDCD" Offset="0.1" />
                                            <GradientStop Color="#FFFBFBFB" Offset="1" />
                                        </LinearGradientBrush>
                                    </Border.Background>
                                    <DockPanel Background="Transparent" LastChildFill="False">
                                        <Viewbox x:Name="SlideRight" Stretch="Uniform" Width="28" Height="28" DockPanel.Dock="Right" Margin="0,0,50,0" >
                                            <Path  Stretch="Fill"  Fill="{DynamicResource TextBrush}">
                                                <Path.Data>
                                                    <PathGeometry Figures="m 27.773437 48.874779 -8.818359 9.902343 -4.833984 0 8.847656 -9.902343 -8.847656 -10.019532 4.833984 0 z m -11.396484 0 -8.7597655 9.902343 -4.9804687 0 9.0234372 -9.902343 -9.0234372 -10.019532 4.9804687 0 z" FillRule="NonZero"/>
                                                </Path.Data>
                                            </Path>
                                        </Viewbox>
                                        <Viewbox x:Name="SlideLeft" Stretch="Uniform" Width="28" Height="28" DockPanel.Dock="Left" Margin="50,0,0,0" >
                                            <Path  Stretch="Fill"  Fill="{DynamicResource TextBrush}">
                                                <Path.LayoutTransform>
                                                    <TransformGroup>
                                                        <ScaleTransform ScaleX="-1"/>
                                                    </TransformGroup>
                                                </Path.LayoutTransform>
                                                <Path.Data>
                                                    <PathGeometry Figures="m 27.773437 48.874779 -8.818359 9.902343 -4.833984 0 8.847656 -9.902343 -8.847656 -10.019532 4.833984 0 z m -11.396484 0 -8.7597655 9.902343 -4.9804687 0 9.0234372 -9.902343 -9.0234372 -10.019532 4.9804687 0 z" FillRule="NonZero"/>
                                                </Path.Data>
                                            </Path>
                                        </Viewbox>
                                    </DockPanel>
                                </Border>
                            </Grid>
                        </DockPanel>

                        <ControlTemplate.Triggers>
                            <Trigger Property="IsChecked" Value="True">
                                <Setter TargetName="BackgroundBorder" Property="Background" Value="{StaticResource CheckedOrange}" />
                                <Setter TargetName="BackgroundBorder" Property="BorderBrush" Value="{StaticResource CheckedOrangeBorder}" />
                                <Setter TargetName="SlideRight" Property="Visibility" Value="Collapsed" />
                                <Setter TargetName="SlideLeft" Property="Visibility" Value="Visible" />
                            </Trigger>
                            <Trigger Property="IsChecked" Value="False">
                                <Setter TargetName="BackgroundBorder" Property="Background" Value="{StaticResource CheckedBlue}" />
                                <Setter TargetName="BackgroundBorder" Property="BorderBrush" Value="{StaticResource CheckedBlueBorder}" />
                                <Setter TargetName="SlideRight" Property="Visibility" Value="Visible" />
                                <Setter TargetName="SlideLeft" Property="Visibility" Value="Collapsed" />
                            </Trigger>
                        </ControlTemplate.Triggers>

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


    </UserControl.Resources>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition  Width="*"/>
        </Grid.ColumnDefinitions>

        <CheckBox   x:Name="checkBox" 
                    Style="{StaticResource CheckBoxSlider}" 
                    HorizontalAlignment="Stretch"
                    DockPanel.Dock="Top" 
                    Height="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:SliderControl}}, Path=Height, Mode=TwoWay}"
                    IsChecked="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type local:SliderControl}}, Path=IsLeftVisible, Mode=TwoWay}"
                    Checked="CheckBox_Checked" 
                    Unchecked="CheckBox_Unchecked"
                    />
    </Grid>
</UserControl>

code behind.

namespace YOURNAMESPACE.UserControls
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Animation;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;

    /// <summary>
    /// Interaction logic for SliderControl.xaml
    /// </summary>
    public partial class SliderControl : UserControl
    {
        public static readonly DependencyProperty IsLeftVisibleProperty =
         DependencyProperty.RegisterAttached(
             "IsLeftVisible",
             typeof(bool),
             typeof(SliderControl),
             new UIPropertyMetadata(true, IsLeftVisibleChanged));

        public static readonly DependencyProperty LeftTextProperty =
            DependencyProperty.RegisterAttached(
                "LeftText",
                typeof(string),
                typeof(SliderControl),
                new UIPropertyMetadata(null, LeftTextChanged));

        public static readonly DependencyProperty RightTextProperty =
        DependencyProperty.RegisterAttached(
            "RightText",
            typeof(string),
            typeof(SliderControl),
            new UIPropertyMetadata(null, RightTextChanged));

         /// <summary>
        /// Initializes a new instance of the <see cref="SliderControl"/> class.
        /// </summary>
        public SliderControl()
        {
            this.InitializeComponent();
        }

        public string LeftText { get; set; }

        public string RightText { get; set; }

        [AttachedPropertyBrowsableForType(typeof(SliderControl))]
        public static bool GetIsLeftVisible(SliderControl sliderControl)
        {
            return (bool)sliderControl.GetValue(IsLeftVisibleProperty);
        }

        [AttachedPropertyBrowsableForType(typeof(SliderControl))]
        public static string GetLeftText(SliderControl sliderControl)
        {
            return (string)sliderControl.GetValue(LeftTextProperty);
        }

        public static void SetIsLeftVisible(SliderControl sliderControl, bool value)
        {
            sliderControl.SetValue(IsLeftVisibleProperty, value);
        }

        public static void IsLeftVisibleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            SliderControl slider = d as SliderControl;

            if ((bool)e.NewValue == true)
            {
                slider.RunAnimation("ShowLeftStoryboard");
            }
            else
            {
                slider.RunAnimation("ShowRightStoryboard");
            }
        }

        [AttachedPropertyBrowsableForType(typeof(SliderControl))]
        public static string GetRightText(SliderControl sliderControl)
        {
            return (string)sliderControl.GetValue(RightTextProperty);
        }

        public static void SetLeftText(SliderControl sliderControl, string value)
        {
            sliderControl.SetValue(LeftTextProperty, value);
        }

        public static void SetRightText(SliderControl sliderControl, string value)
        {
            sliderControl.SetValue(RightTextProperty, value);
        }

        private static void LeftTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            SliderControl slider = d as SliderControl;
            slider.LeftText = e.NewValue as string;
        }

        private static void RightTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            SliderControl slider = d as SliderControl;
            slider.RightText = e.NewValue as string;
        }

        private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            this.checkBox.Width = e.NewSize.Width;

            CheckBox cb = this.checkBox;
            var controlTemplate = cb.Template;

            DockPanel dockPanel = controlTemplate.FindName("dockPanel", cb) as DockPanel;
            Storyboard story = dockPanel.Resources["ShowLeftStoryboard"] as Storyboard;

            DoubleAnimationUsingKeyFrames dk = story.Children[0] as DoubleAnimationUsingKeyFrames;
            SplineDoubleKeyFrame sk = dk.KeyFrames[0] as SplineDoubleKeyFrame;

            // must manipulate this in code behind, binding does not work, 
            // also it cannot be inside of a control template 
            // because storyboards in control templates become frozen and cannot be modified
            sk.Value = cb.Width / 2;

            if (cb.IsChecked == true)
            {
                story.Begin(cb, cb.Template);
            }
        }

        private void CheckBox_Checked(object sender, RoutedEventArgs e)
        {
            this.RunAnimation("ShowLeftStoryboard");
        }

        private void CheckBox_Unchecked(object sender, RoutedEventArgs e)
        {
            this.RunAnimation("ShowRightStoryboard");
        }

        private void RunAnimation(string storyboard)
        {
            CheckBox cb = this.checkBox;
            var controlTemplate = cb.Template;

            DockPanel dockPanel = controlTemplate.FindName("dockPanel", cb) as DockPanel;

            if (dockPanel != null)
            {
                Storyboard story = dockPanel.Resources[storyboard] as Storyboard;

                DoubleAnimationUsingKeyFrames dk = story.Children[0] as DoubleAnimationUsingKeyFrames;
                SplineDoubleKeyFrame sk = dk.KeyFrames[0] as SplineDoubleKeyFrame;
                story.Begin(cb, cb.Template);
            }
        }
    }
}

IValueConverter

namespace YOURNAMESPACE.ValueConverters
{
    using System;
    using System.Collections.Generic;
    using System.Data;
    using System.Globalization;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows.Data;
      /// <summary>
        /// Does basic math operations, eg. value = 60 parameter = "*2 + 1", result = 121
        /// </summary>
        /// <seealso cref="System.Windows.Data.IValueConverter" />
        [ValueConversion(typeof(double), typeof(double))]
        public class MathConverter : IValueConverter
        {
            public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
            {
                double d = (double)value;
                string mathExpression = d.ToString() + parameter;

                if (mathExpression.Contains("^"))
                {
                    throw new Exception("Doesn't handle powers or square roots");
                }

                DataTable table = new DataTable();
                table.Columns.Add("expression", typeof(string), mathExpression);
                DataRow row = table.NewRow();
                table.Rows.Add(row);
                double ret = double.Parse((string)row["expression"]);

                if (ret <= 0)
                {
                    return 1d;
                }

                return ret;
            }

            public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
            {
                // not implemented
                return null;
            }
        }
}

example usage:

<chart:SliderControl x:Name="sliderControl" 
                                                         LeftText="{Binding Path=LeftTextProperty}"
                                                         RightText="{Binding Path=RightTextProperty}"
                                                         Grid.Row="0" 
                                                         IsLeftVisible="{Binding Path=IsLeftVisible, Mode=TwoWay}" 
                                                         Height="35"  />
Share:
39,124
Admin
Author by

Admin

Updated on August 12, 2020

Comments

  • Admin
    Admin almost 4 years

    I'm trying to create a button that behaves similarly to the "slide" button on the iPhone. I have an animation that adjusts the position and width of the button, but I want these values to be based on the text used in the control. Currently, they're hardcoded.

    Here's my working XAML, so far:

    <CheckBox x:Class="Smt.Controls.SlideCheckBox"
              xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
              xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
              xmlns:local="clr-namespace:Smt.Controls"
              xmlns:System.Windows="clr-namespace:System.Windows;assembly=PresentationCore"
              Name="SliderCheckBox"
              mc:Ignorable="d">
        <CheckBox.Resources>
            <System.Windows:Duration x:Key="AnimationTime">0:0:0.2</System.Windows:Duration>
            <Storyboard x:Key="OnChecking">
                <DoubleAnimation Storyboard.TargetName="CheckButton"
                                 Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(TranslateTransform.X)"
                                 Duration="{StaticResource AnimationTime}"
                                 To="40" />
                <DoubleAnimation Storyboard.TargetName="CheckButton"
                                 Storyboard.TargetProperty="(Button.Width)"
                                 Duration="{StaticResource AnimationTime}"
                                 To="41" />
            </Storyboard>
            <Storyboard x:Key="OnUnchecking">
                <DoubleAnimation Storyboard.TargetName="CheckButton"
                                 Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(TranslateTransform.X)"
                                 Duration="{StaticResource AnimationTime}"
                                 To="0" />
                <DoubleAnimation Storyboard.TargetName="CheckButton"
                                 Storyboard.TargetProperty="(Button.Width)"
                                 Duration="{StaticResource AnimationTime}"
                                 To="40" />
            </Storyboard>
            <Style x:Key="SlideCheckBoxStyle"
                   TargetType="{x:Type local:SlideCheckBox}">
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="{x:Type local:SlideCheckBox}">
                            <Canvas>
                                <ContentPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
                                                  Content="{TemplateBinding Content}"
                                                  ContentTemplate="{TemplateBinding ContentTemplate}"
                                                  RecognizesAccessKey="True"
                                                  VerticalAlignment="Center"
                                                  HorizontalAlignment="Center" />
                                <Canvas>
                                    <!--Background-->
                                    <Rectangle Width="{Binding ElementName=ButtonText, Path=ActualWidth}"
                                               Height="{Binding ElementName=ButtonText, Path=ActualHeight}"
                                               Fill="LightBlue" />
                                </Canvas>
                                <Canvas>
                                    <!--Button-->
                                    <Button Width="{Binding ElementName=CheckedText, Path=ActualWidth}"
                                            Height="{Binding ElementName=ButtonText, Path=ActualHeight}"
                                            Name="CheckButton"
                                            Command="{x:Static local:SlideCheckBox.SlideCheckBoxClicked}">
                                        <Button.RenderTransform>
                                            <TransformGroup>
                                                <TranslateTransform />
                                            </TransformGroup>
                                        </Button.RenderTransform>
                                    </Button>
                                </Canvas>
                                <Canvas>
                                    <!--Text-->
                                    <StackPanel Name="ButtonText"
                                                Orientation="Horizontal"
                                                IsHitTestVisible="False">
                                        <Grid Name="CheckedText">
                                            <Label Margin="7 0"
                                                   Content="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:SlideCheckBox}}, Path=CheckedText}" />
                                        </Grid>
                                        <Grid Name="UncheckedText"
                                              HorizontalAlignment="Right">
                                            <Label Margin="7 0"
                                                   Content="{Binding RelativeSource={RelativeSource AncestorType={x:Type local:SlideCheckBox}}, Path=UncheckedText}" />
                                        </Grid>
                                    </StackPanel>
                                </Canvas>
                            </Canvas>
                            <ControlTemplate.Triggers>
                                <Trigger Property="IsChecked"
                                         Value="True">
                                    <Trigger.EnterActions>
                                        <BeginStoryboard Storyboard="{StaticResource OnChecking}" />
                                    </Trigger.EnterActions>
                                    <Trigger.ExitActions>
                                        <BeginStoryboard Storyboard="{StaticResource OnUnchecking}" />
                                    </Trigger.ExitActions>
                                </Trigger>
                            </ControlTemplate.Triggers>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </CheckBox.Resources>
        <CheckBox.CommandBindings>
            <CommandBinding Command="{x:Static local:SlideCheckBox.SlideCheckBoxClicked}"
                            Executed="OnSlideCheckBoxClicked" />
        </CheckBox.CommandBindings>
    </CheckBox>
    

    And the code behind:

    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    
    namespace Smt.Controls
    {
        public partial class SlideCheckBox : CheckBox
        {
            public SlideCheckBox()
            {
                InitializeComponent();
                Loaded += OnLoaded;
            }
    
            public static readonly DependencyProperty CheckedTextProperty = DependencyProperty.Register("CheckedText", typeof(string), typeof(SlideCheckBox), new PropertyMetadata("Checked Text"));
            public string CheckedText
            {
                get { return (string)GetValue(CheckedTextProperty); }
                set { SetValue(CheckedTextProperty, value); }
            }
    
            public static readonly DependencyProperty UncheckedTextProperty = DependencyProperty.Register("UncheckedText", typeof(string), typeof(SlideCheckBox), new PropertyMetadata("Unchecked Text"));
            public string UncheckedText
            {
                get { return (string)GetValue(UncheckedTextProperty); }
                set { SetValue(UncheckedTextProperty, value); }
            }
    
            public static readonly RoutedCommand SlideCheckBoxClicked = new RoutedCommand();
    
            void OnLoaded(object sender, RoutedEventArgs e)
            {
                Style style = TryFindResource("SlideCheckBoxStyle") as Style;
                if (!ReferenceEquals(style, null))
                {
                    Style = style;
                }
            }
    
            void OnSlideCheckBoxClicked(object sender, ExecutedRoutedEventArgs e)
            {
                IsChecked = !IsChecked;
            }
        }
    }
    

    The problem comes when I try to bind the "To" attribute in the DoubleAnimations to the actual width of the text, the same as I'm doing in the ControlTemplate. If I bind the values to an ActualWidth of an element in the ControlTemplate, the control comes up as a blank checkbox (my base class). However, I'm binding to the same ActualWidths in the ControlTemplate itself without any problems. Just seems to be the CheckBox.Resources that have a problem with it.

    For instance, the following will break it:

            <DoubleAnimation Storyboard.TargetName="CheckButton"
                             Storyboard.TargetProperty="(Button.Width)"
                             Duration="{StaticResource AnimationTime}"
                             To="{Binding ElementName=CheckedText, Path=ActualWidth}" />
    

    I don't know whether this is because it's trying to bind to a value that doesn't exist until a render pass is done, or if it's something else. Anyone have any experience with this sort of animation binding?