1

So I have this style that targets my ToggleButton, and I am trying to change the HorizontalAlignment of the border named ThumbCircle from Left to Right, I did read that changing that property is not as easy as just changing the value, I'm going to have to do some sort of LayoutTransform, however that didn't seem to work, when I click my button nothing happens, it doesnt move.

So my question is, how do I get the ThumbCircle to move to the right side of the Border that it's currently placed a.

<Style x:Key="MyToggleButton"
       TargetType="{x:Type ToggleButton}">

        <Style.Resources>
            <Color x:Key="Color.MyBtn.PrimaryColor">#2ecc71</Color>
            <Color x:Key="Color.MyBtn.SecondaryColor">#27ae60</Color>
        </Style.Resources>


        <Setter Property="Template">

            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ToggleButton}">
                    <Grid>
                        <Border Width="50" Height="12" Background="White" CornerRadius="6">

                        </Border>

                        <Border Width="25"
                                Background="#2ecc71"
                                CornerRadius="{Binding RelativeSource={RelativeSource Self}, Path=ActualHeight}"
                                HorizontalAlignment="Left"
                                x:Name="ThumbCircle">
                            <Border.Triggers>
                                <EventTrigger RoutedEvent="PreviewMouseDown">
                                    <BeginStoryboard>
                                        <Storyboard>
                                            <!-- Dont forget easing -->
                                            <DoubleAnimation Storyboard.TargetProperty="(LayoutTransform).(ScaleTransform.ScaleX)" Storyboard.TargetName="ThumbCircle" To="10" Duration="0:0:0.5" />
                                        </Storyboard>
                                    </BeginStoryboard>

                                </EventTrigger>
                            </Border.Triggers>
                        </Border>
                    </Grid>

                </ControlTemplate>
            </Setter.Value>

        </Setter>
    </Style>
Riley Varga
  • 670
  • 1
  • 5
  • 15

1 Answers1

1

Why nothing is showing up

The reason why you don't see anything moving is because you're targeting the (LayoutTransform).(ScaleTransform.ScaleX) property of your ThumbCircle, but it doesn't have any value set to its LayoutTransform property.

If you add this to your ThumbCircle Border:

    <Border.LayoutTransform>
        <ScaleTransform/>
    </Border.LayoutTransform>

Then you will see something happening. But you'll see a scaling, not a translation! What you want is to translate from one side to the other.

The intuitive fix doesn't work...

The easiest way would have been to first replace the LayoutTransform with a RenderTransform and the ScaleTransform with a TranslateTransform like this:

    <Border.RenderTransform>
        <TranslateTransform x:Name="MyTranslate"/>
    </Border.LayoutTransform>

Then give a name to your Grid like this:

<Grid x:Name="MyGrid">
    ...
</Grid>

And then animating the X property of the TranslateTransform from 0 to your Grid.ActualWidth like this:

<!-- This won't run -->
<DoubleAnimation Storyboard.TargetProperty="X" Storyboard.TargetName="MyTranslate" To="{Binding ElementName=MyGrid, Path=ActualWidth}" Duration="0:0:0.5" />

But it is not possible to achieve this as it is not possible to set a Binding on any property of an Animation when used like this, because WPF makes some optimizations that prevent this as explained here.

A XAML-intensive way to do it

So a way to do it is to define proxy elements whose property we animate from 0 to 1 ,and we make the multiplication with MyGrid.ActualWidth at another location.

So your whole XAML style becomes:

<Style x:Key="MyToggleButton" TargetType="{x:Type ToggleButton}">

    <Style.Resources>
        <Color x:Key="Color.MyBtn.PrimaryColor">#2ecc71</Color>
        <Color x:Key="Color.MyBtn.SecondaryColor">#27ae60</Color>
        <!-- Aded some converters here -->
        <views:MultiMultiplierConverter x:Key="MultiMultiplierConverter"></views:MultiMultiplierConverter>
        <views:OppositeConverter x:Key="OppositeConverter"></views:OppositeConverter>
    </Style.Resources>

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ToggleButton}">
                <Grid x:Name="ContainerGrid">
                    <Border Width="50" Height="12" Background="Red" CornerRadius="6">

                    </Border>
                    <Border Width="25"
                        Background="#2ecc71"
                        CornerRadius="{Binding RelativeSource={RelativeSource Self}, Path=ActualHeight}"
                        HorizontalAlignment="Left"
                        x:Name="ThumbCircle">
                        <Border.Resources>
                            <!-- Proxy object whose X property gets animated from 0 to 1. -->
                            <!-- Could be any DependencyObject with a property of type double. -->
                            <TranslateTransform x:Key="unusedKey" x:Name="Proxy"></TranslateTransform>
                        </Border.Resources>
                        <Border.RenderTransform>
                            <TransformGroup>
                                <!-- Main translation to move from one side of the grid to the other -->
                                <TranslateTransform>
                                    <TranslateTransform.X>
                                        <MultiBinding Converter="{StaticResource MultiMultiplierConverter}" ConverterParameter="2">
                                            <Binding ElementName="Proxy" Path="X"></Binding>
                                            <Binding ElementName="ContainerGrid" Path="ActualWidth"></Binding>
                                        </MultiBinding>
                                    </TranslateTransform.X>
                                </TranslateTransform>

                                <!-- Secondary translation to adjust to the actual width of the object to translate -->
                                <TranslateTransform>
                                    <TranslateTransform.X>
                                        <MultiBinding Converter="{StaticResource MultiMultiplierConverter}" ConverterParameter="2">
                                            <Binding ElementName="Proxy" Path="X"></Binding>
                                            <Binding ElementName="ThumbCircle" Path="ActualWidth" Converter="{StaticResource OppositeConverter}"></Binding>
                                        </MultiBinding>
                                    </TranslateTransform.X>
                                </TranslateTransform>
                            </TransformGroup>
                        </Border.RenderTransform>
                        <Border.Triggers>
                            <EventTrigger RoutedEvent="MouseDown">
                                <BeginStoryboard>
                                    <Storyboard>
                                        <!-- Dont forget easing -->
                                        <DoubleAnimation Storyboard.TargetProperty="X" Storyboard.TargetName="Proxy" To="1" Duration="0:0:0.5" />
                                    </Storyboard>
                                </BeginStoryboard>
                            </EventTrigger>
                        </Border.Triggers>
                    </Border>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

And you would need to define two IValueConverter to perform some basic arithmetic operations on your Bindings:

One for multiplying all supplied values in a MultiBinding:

/// <summary>
/// Defines a converter which multiplies all provided values.
/// The given parameter indicates number of arguments to multiply.
/// </summary>
public class MultiMultiplierConverter : IMultiValueConverter {
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
        double result = 1;
        int count = int.Parse((string)parameter);
        for (int i = 0; i < count; i++) {
            result *= (double)values[i];
        }
        return result;
    }
    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) {
        throw new NotSupportedException("Cannot convert back");
    }
}

And one to multiply the input by -1:

/// <summary>
/// Defines a converter which multiplies the provided value by -1
/// </summary>
public class OppositeConverter : IValueConverter {
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
        return (dynamic)value * -1;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
        throw new NotSupportedException("Cannot convert back");
    }
}

This is not an elegant way but it works!

How to implement back and forth animations?

So far we managed to animate the thumb to the right, on click. But that's not the whole point, is it?

What we are templating is a ToggleButton: at every click, an animation to the opposite side should be triggered. More exactly, whenever the IsChecked property gets True, we should trigger an animation to the right, and whenever the IsChecked property gets False, we should trigger an animation to the left.

This is possible by adding some Trigger objects in the ControlTemplate.Triggers collection. The Trigger shall be hooked to the IsChecked property (which we have no control over) and listen to its changes. We can specify an EnterAction which is our animation to the right, and an ExitAction which is our animation to the left.

The full Style becomes:

<Style x:Key="MyToggleButton" TargetType="{x:Type ToggleButton}">

    <Style.Resources>
        <Color x:Key="Color.MyBtn.PrimaryColor">#2ecc71</Color>
        <Color x:Key="Color.MyBtn.SecondaryColor">#27ae60</Color>
        <!-- Aded some converters here -->
        <views:MultiMultiplierConverter x:Key="MultiMultiplierConverter"></views:MultiMultiplierConverter>
        <views:OppositeConverter x:Key="OppositeConverter"></views:OppositeConverter>
    </Style.Resources>

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ToggleButton}">
                <ControlTemplate.Triggers>
                    <Trigger Property="IsChecked" Value="True">
                        <!-- Animation to the right -->
                        <Trigger.EnterActions>
                            <BeginStoryboard>
                                <Storyboard>
                                    <!-- Dont forget easing -->
                                    <DoubleAnimation Storyboard.TargetProperty="X" Storyboard.TargetName="Proxy" To="1" Duration="0:0:0.5" />
                                </Storyboard>
                            </BeginStoryboard>
                        </Trigger.EnterActions>

                        <!-- Animation to the left -->
                        <Trigger.ExitActions>
                            <BeginStoryboard>
                                <Storyboard>
                                    <!-- Dont forget easing -->
                                    <DoubleAnimation Storyboard.TargetProperty="X" Storyboard.TargetName="Proxy" To="0" Duration="0:0:0.5" />
                                </Storyboard>
                            </BeginStoryboard>
                        </Trigger.ExitActions>
                    </Trigger>
                </ControlTemplate.Triggers>
                <Grid x:Name="ContainerGrid">
                    <Border Width="50" Height="12" Background="Red" CornerRadius="6">

                    </Border>
                    <Border Width="25"
                Background="#2ecc71"
                CornerRadius="{Binding RelativeSource={RelativeSource Self}, Path=ActualHeight}"
                HorizontalAlignment="Left"
                x:Name="ThumbCircle">
                        <Border.Resources>
                            <!-- Proxy object whose X property gets animated from 0 to 1. -->
                            <!-- Could be any DependencyObject with a property of type double. -->
                            <TranslateTransform x:Key="unusedKey" x:Name="Proxy"></TranslateTransform>
                        </Border.Resources>
                        <Border.RenderTransform>
                            <TransformGroup>
                                <!-- Main translation to move from one side of the grid to the other -->
                                <TranslateTransform>
                                    <TranslateTransform.X>
                                        <MultiBinding Converter="{StaticResource MultiMultiplierConverter}" ConverterParameter="2">
                                            <Binding ElementName="Proxy" Path="X"></Binding>
                                            <Binding ElementName="ContainerGrid" Path="ActualWidth"></Binding>
                                        </MultiBinding>
                                    </TranslateTransform.X>
                                </TranslateTransform>

                                <!-- Secondary translation to adjust to the actual width of the object to translate -->
                                <TranslateTransform>
                                    <TranslateTransform.X>
                                        <MultiBinding Converter="{StaticResource MultiMultiplierConverter}" ConverterParameter="2">
                                            <Binding ElementName="Proxy" Path="X"></Binding>
                                            <Binding ElementName="ThumbCircle" Path="ActualWidth" Converter="{StaticResource OppositeConverter}"></Binding>
                                        </MultiBinding>
                                    </TranslateTransform.X>
                                </TranslateTransform>
                            </TransformGroup>
                        </Border.RenderTransform>
                    </Border>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Note that this Trigger is only accepted inside a ControlTemplate.Triggers collection, it is not possible to put such a Trigger in the original Border.Triggers collection, you can read more about it here.

Corentin Pane
  • 4,794
  • 1
  • 12
  • 29
  • That's a great answer! Is it possible to make it reverse the animation when IsChecked is false? – Riley Varga Oct 18 '19 at 12:38
  • I edited:) honestly this is one of the most frustrating issue with WPF, having to find a way to make things work and that makes sense... – Corentin Pane Oct 18 '19 at 14:12
  • That's really impressive! I really like your approach! I do however think Microsoft should allow binding in Storyboards without having the freeze issue. Thank you so much! I'm learning so much from this! – Riley Varga Oct 18 '19 at 19:32
  • Is it possible to change the background of the "Red" border when IsChecked is true? – Riley Varga Oct 18 '19 at 22:56
  • Yes. If you can't find by yourself, you should definitely ask another question on SO :) but I'm sure there are already plenty of answers to that one. – Corentin Pane Oct 19 '19 at 11:15