-1

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?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
devpelux
  • 2,492
  • 3
  • 18
  • 38
  • It's absolutely not clear what is going on in your code snippets. You forget to provide some context. Anyway, subscribe to the `Switcher.Loaded` event instead. – BionicCode Oct 15 '20 at 13:52
  • Just a hunch, but try moving your Storyboard from the Grid.Resources and make it a direct child of the VIsualState without the x:Key. It may be a resource resolution issue. – Keithernet Oct 15 '20 at 13:59
  • It seems that what you're trying to achieve already exists - it's a ToggleButton. Just re-template it. – Maciek Świszczowski Oct 15 '20 at 14:54
  • @Keithernet not works because to use that way the properties must be frozen, and binding cannot be used. – devpelux Oct 16 '20 at 18:59

1 Answers1

0

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>
Keithernet
  • 2,349
  • 1
  • 10
  • 16
  • I never considered that triggers could be used inside control templates and could reference directly a property of an element with TargetName. I always used them to change directly the style properties with "overwriting" problems like this: https://stackoverflow.com/q/61259338/3605220 So thanks for this delucidation. However visualstates are always great for animations (using the second solution). – devpelux Oct 16 '20 at 22:15