Binding not working on storyboard on custom control: values becomes null

212 views Asked by At

I have created a custom control and I want to change the visualstate of this control immediately after the control is loaded.

I want to use binding mode to get colors from dependency properties so i can set them dynamically.

I've found a way to binding the properties and avoid the limitation of freezable properties: separate storyboards and make them resources, as you can see in xaml code.

Changing the visualstate (to "Activated" in my case) works correctly and it uses the values from the bound properties, but problems starts when I want to change the state immediately after the control is loaded: binding becomes "broken" and the colors obtained from properties seems to be "null" (or transparent).

If i use a static color instead to bind a property (e.g. Green or #ffffffff) the color is loaded correctly even immediately after loading, but this way is not usable in my case because this means that the color will be "unchangeable".

xaml

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

  <Style TargetType="{x:Type local:Switcher}">
    <Setter Property="Background" Value="#FF3F3F46"/>
    <Setter Property="BackgroundOnActivated" Value="#FF2D2D30"/>
    <Setter Property="IsActivated" Value="False"/>

    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="{x:Type local:Switcher}">
          <Grid>
            <Grid.Resources>
              <Storyboard x:Key="SwitcherOnActivated">
                <ObjectAnimationUsingKeyFrames Storyboard.TargetName="PART_Border"
                                      Storyboard.TargetProperty="Background">
                  <!-- with Value="Green" animation works correctly -->
                  <DiscreteObjectKeyFrame KeyTime="0" 
                                          Value="{Binding BackgroundOnActivated,
                                          RelativeSource={RelativeSource TemplatedParent}}"/>
                  </ObjectAnimationUsingKeyFrames>
                </Storyboard>
              </Grid.Resources>

              <VisualStateManager.VisualStateGroups>
                <VisualStateGroup x:Name="CommonStates">
                  <VisualState x:Name="Normal"/>
                  <!-- it goes to "Activated" when IsActivated becomes true-->
                  <VisualState x:Name="Activated" 
                               Storyboard="{StaticResource SwitcherOnActivated}"/>
                </VisualStateGroup>
              </VisualStateManager.VisualStateGroups>

            <Border x:Name="PART_Border"
                    Background="{TemplateBinding Background}"/>
          </Grid>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
  </Style>

</ResourceDictionary>

code behind

public class Switcher : Control
{
    public Brush BackgroundOnActivated
    {
        get => (Brush)GetValue(BackgroundOnActivatedProperty);
        set => SetValue(BackgroundOnActivatedProperty, value);
    }

    public static readonly DependencyProperty BackgroundOnActivatedProperty =
        DependencyProperty.Register(nameof(BackgroundOnActivated), typeof(Brush), typeof(Switcher));

    public bool IsActivated
    {
        get => (bool)GetValue(IsActivatedProperty);
        set => SetValue(IsActivatedProperty, value);
    }

    public static readonly DependencyProperty IsActivatedProperty =
        DependencyProperty.Register(nameof(IsActivated), typeof(bool), typeof(Switcher),
            new PropertyMetadata(false, new PropertyChangedCallback(OnIsActivatedChanged)));

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

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();
        /*OnApplyTemplate() is used to apply the correct initial
          visualstate after loading the control (but it seems to be broken)*/
        _ = VisualStateManager.GoToState(this, IsActivated ? "Activated" : "Normal", true);
    }

    protected virtual void OnActivationChanged()
    {
        /*If the boolean value of IsActivated is changed, the visualstate
          is switched between "Normal" and "Activated"*/
        _ = VisualStateManager.GoToState(this, IsActivated ? "Activated" : "Normal", true);
    }

    private static void OnIsActivatedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((Switcher)d).OnActivationChanged();
    }

    protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
    {
        //When I click on the control the boolean value of the property 
          IsActivated is inverted and this will call OnIsActivatedChanged()*/
        base.OnMouseLeftButtonDown(e);
        IsActivated = !IsActivated;
    }
}

Try to add to MainWindow.xaml my control and set the property IsActivated to true, so after loaded the state has to become Activated, but this breaks the control and it becomes invisible.

<local:Switcher x:Name="switcher" HorizontalAlignment="Left" VerticalAlignment="Top"
       Margin="50,50,0,0" Height="100" Width="100" IsActivated="True"/>

The control on its normal state:

enter image description here

What I want to get:

enter image description here

What I actually get:

enter image description here

Any solutions / workaround to get the correct colors even if the visualstate is changed immediately after loading?

1

There are 1 answers

1
Keithernet On BEST ANSWER

Have you thought about using ControlTemplate.Triggers instead? It's much simpler to achieve what you want:

<Style TargetType="{x:Type local:Switcher}">
    <Setter Property="Background" Value="#FF000000" />
    <Setter Property="BackgroundOnActivated" Value="#FFFF0000" />
    <Setter Property="IsActivated" Value="False" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:Switcher}">
                <Grid>
                    <Border x:Name="PART_Border"
                        Background="{TemplateBinding Background}" />
                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsActivated" Value="True">
                        <Setter TargetName="PART_Border" Property="Background" Value="{Binding BackgroundOnActivated, RelativeSource={RelativeSource TemplatedParent}}" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

If you absolutely need to use the VisualStateManager, then you'll need to use separate Border controls and control their Visibility. The reason why your original idea doesn't work is due to the Storyboards being frozen at runtime.

<Style TargetType="{x:Type local:Switcher}">
    <Setter Property="Background" Value="#FF000000" />
    <Setter Property="BackgroundOnActivated" Value="#FFFF0000" />
    <Setter Property="IsActivated" Value="False" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:Switcher}">
                <Grid>
                    <Border x:Name="PART_Border"
                        Background="{TemplateBinding Background}" />
                    <Border x:Name="ActivatedBorder"
                        Background="{TemplateBinding BackgroundOnActivated}"
                        Visibility="Collapsed" />
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Normal" />
                            <VisualState x:Name="Activated">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames
                                        Storyboard.TargetName="PART_Border"
                                        Storyboard.TargetProperty="Visibility">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Collapsed</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                    <ObjectAnimationUsingKeyFrames
                                        Storyboard.TargetName="ActivatedBorder"
                                        Storyboard.TargetProperty="Visibility">
                                        <DiscreteObjectKeyFrame KeyTime="0">
                                            <DiscreteObjectKeyFrame.Value>
                                                <Visibility>Visible</Visibility>
                                            </DiscreteObjectKeyFrame.Value>
                                        </DiscreteObjectKeyFrame>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>