2
  • I have a base class SecurePage which inherit from UserControl.
  • Every 'page' inside of the app inherit from SecurePage.
  • I want to define in the default Style of SecurePage a VisualStateGroup with some VisualStates.

The problem is, that in the derived classes are none of these VisualStates available.

var states = VisualStateManager.GetVisualStateGroups(this);

Returns an empty list.

If I copy my XAML VisualState definition and paste it into my DerivadedFooSecurePage, I can easily go to the state:

VisualStateManager.GoToState(this, "Blink", false);

Same problem as described here: VisualState in abstract control


Some more Details

SecurePage

[TemplateVisualState(GroupName = "State", Name = "Normal")]
[TemplateVisualState(GroupName = "State", Name = "Blink")]
public class SecurePage: UserControl
{
    public SecurePage()
    {
        DefaultStyleKey = typeof(HtSecurePage);
    }
}

<Style TargetType="basic:SecurePage">
    <Setter Property="FontSize" Value="14"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="basic:SecurePage">
                <Grid>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="Signals">
                            <VisualState x:Name="Normal"/>
                            <VisualState x:Name="Blink">
                                <Storyboard>
                                    <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="border">
                                        <EasingColorKeyFrame KeyTime="0:0:0.4" Value="#FF3AFF00">
                                            <EasingColorKeyFrame.EasingFunction>
                                                <BounceEase EasingMode="EaseIn" Bounciness="3" Bounces="4"/>
                                            </EasingColorKeyFrame.EasingFunction>
                                        </EasingColorKeyFrame>
                                    </ColorAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <ContentPresenter Content="{TemplateBinding Content}"/>
                    <Border 
                        x:Name="border"
                        BorderThickness="5"
                        BorderBrush="Transparent"
                        IsHitTestVisible="False"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

InfoPage

Info.xaml.cs

namespace Views.General
{
    public partial class Info
    {
        public Info()
        {
            InitializeComponent();
        }
    }
}

Info.xaml

<basic:SecurePage
    x:Class="Views.General.Info"
    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:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:basic="clr-namespace:Foo.PlcFramework.Controls.Basic;assembly=Foo"
    FontSize="14">
    <Grid>
        <TextBlock Text="HelloWorld"/>
    </Grid>
</basic:SecurePage>

Live Debugging

enter image description here

  • states = 0
  • Nothing happens after calling VisualStateManager.GoToState(this, "Blink", false);

enter image description here

  • states = 0
  • Nothing happens after calling VisualStateManager.GoToState(this, "Blink", false);

Copy the VisualState into the derivaded class

namespace Views.General
{
    [TemplateVisualState(GroupName = "State", Name = "Normal")]
    [TemplateVisualState(GroupName = "State", Name = "Blink")]
    public partial class Info
    {
        public Info()
        {
            InitializeComponent();
            var states = VisualStateManager.GetVisualStateGroups(this);
            VisualStateManager.GoToState(this, "Blink", false);
        }
    }
}

<basic:SecurePage 
    x:Class="Views.General.Info"
    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:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:basic="clr-namespace:Foo.PlcFramework.Controls.Basic;assembly=Foo"
    FontSize="14">
    <Grid>
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup x:Name="Signals">
                <VisualState x:Name="Normal"/>
                <VisualState x:Name="Blink">
                    <Storyboard>
                        <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="border">
                            <EasingColorKeyFrame KeyTime="0:0:0.4" Value="#FF3AFF00">
                                <EasingColorKeyFrame.EasingFunction>
                                    <BounceEase EasingMode="EaseIn" Bounciness="3" Bounces="4"/>
                                </EasingColorKeyFrame.EasingFunction>
                            </EasingColorKeyFrame>
                        </ColorAnimationUsingKeyFrames>
                    </Storyboard>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
        <TextBlock Text="HelloWorld"/>
        <Border 
            x:Name="border"
            BorderThickness="5"
            BorderBrush="Transparent"
            IsHitTestVisible="False"/>
    </Grid>
</basic:SecurePage >

enter image description here

  • states = 0
  • After calling VisualStateManager.GoToState(this, "Blink", false); the state is changed!!

I just want to define the state in the XAML Style definition of SecurePage and go to the state in any derivaded class!

Dominic Jonas
  • 4,717
  • 1
  • 34
  • 77

3 Answers3

2

Diagnosis

After some poking around I've finally found the culprit - it's the UserControl itself. More precisely - overridden StateGroupsRoot property, which is used by the VisualStateManager.GoToState method. Normally, it returns the root element of the control's template, but in case of UserControl it returns the value of UserControl.Content property. So what happens is that when you call GoToState, states defined in your template are not taken into account.

Solution

There are at least two solutions to this problem.

First solution is to derive your base class (SecurePage) from ContentControl instead of UserControl. The latter isn't that much different - it defaults Focusable and IsTabStop properties to false, and HorizontanContentAlignment and VerticalContentAlignment to Stretch. Also, apart from overriding mentioned StateGroupsRoot property, it provides its own AutomationPeer (a UserControlAutomationPeer), but I don't think you need to worry about that.

The second solution is to use VisualStateManager.GoToElementState on the template root instead. For example:

public class SecurePage : UserControl
{
    //Your code here...

    private FrameworkElement TemplateRoot { get; set; }

    public override void OnApplyTemplate()
    {
        if (Template != null)
            TemplateRoot = GetVisualChild(0) as FrameworkElement;
        else
            TemplateRoot = null;
    }

    public bool GoToVisualState(string name, bool useTransitions)
    {
        if (TemplateRoot is null)
            return false;
        return VisualStateManager.GoToElementState(TemplateRoot, name, useTransitions);
    }
}

Other considerations

Calling VisualStateManager.GetVisualStateGroups on your control yields an empty list because it's just an ordinary attached dependency property accessor, and you didn't set1 that property on your control. To get hold of the groups you've defined in the template, you should call it passing the template root as argument. By the same principle, you don't expect Grid.GetColumn called on your control to return a value you set somewhere in your template.

Regarding calling GoToState in your control's constructor - it's most likely not going to work, since your control is only being instantiated, and most likely its Templtate is null (and remember you define visual states inside the template). It's better to move that logic to OnApplyTemplate override.


1 Since VisualStateManager.VisualStateGroupsProperty is read-only, by set I mean add items to the list returned by VisualStateManager.GetVisualStateGroups

Grx70
  • 10,041
  • 1
  • 40
  • 55
0

I tired to regenerate the scenario you mentioned in question and I am able to get the list of VisualStateGroupfrom base class. Here is sample which I implemented.

First, you need to get VisualStateGroup of current base class and then add custom created group to it.

Base Class

public partial class SecurePage : UserControl
{
    public VisualStateGroup vsGroup = new VisualStateGroup();

    public SecurePage()
    {
        System.Collections.IList groups = VisualStateManager.GetVisualStateGroups(this);


        VisualState state1 = new VisualState() { Name = "State1" };

        Storyboard sb = new Storyboard();
        DoubleAnimation anim = new DoubleAnimation(1, 0, TimeSpan.FromSeconds(1.0));
        Storyboard.SetTargetProperty(anim, new PropertyPath(FrameworkElement.OpacityProperty));
        sb.AutoReverse = true;
        sb.Children.Add(anim);

        state1.Storyboard = sb;

        vsGroup.States.Add(state1);

        groups.Add(vsGroup);
    }
}

Derived Class

Root of derived class is type of Base class. Keep in mind to change that without fail. First line of XAML file. Check below.

<UserControl x:Class="WPFTest.SecurePage"
      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:WPFTest"
      mc:Ignorable="d" 
      d:DesignHeight="300" d:DesignWidth="300">

    <Grid>
        <Rectangle x:Name="rect" Height="100" Width="100" Fill="Red" HorizontalAlignment="Center" VerticalAlignment="Center"/>
    </Grid>
</UserControl>

public partial class DerivadedFooSecurePage : SecurePage
{
    public DerivadedFooSecurePage()
    {
        InitializeComponent();

        var states = VisualStateManager.GetVisualStateGroups(this);

        //VisualStateManager.GoToElementState(this.rect, "State1", false);
    }
} 

Further can implement DerivadedFooSecurePage on any window or other usercontrol.

Gaurang Dave
  • 3,956
  • 2
  • 15
  • 34
  • Thanks for your reply! *I want to define VisualStates in a base class and use it in the derivaded classes. Define it in the Code behind is no solution!* I have added more details to understand my problem – Dominic Jonas Jul 12 '18 at 08:05
0

Workaround solution

I defined an enum with a value for every available VisualState.

public enum ESecurePageVisualState
{
    Normal,
    Blink
}

Added a DependencyProperty in the SecurePage class.

/// <summary>
/// Set the visual State of the page
/// </summary>
public ESecurePageVisualState VisualState
{
    get => (ESecurePageVisualState)GetValue(VisualStateProperty);
    set => SetValue(VisualStateProperty, value);
}

/// <summary>
/// The <see cref="VisualState"/> DependencyProperty.
/// </summary>
public static readonly DependencyProperty VisualStateProperty = DependencyProperty.Register("VisualState", typeof(ESecurePageVisualState), typeof(SecurePage), new PropertyMetadata(ESecurePageVisualState.Normal));

And changed the Style of the SecurePage:

<Style TargetType="basic:SecurePage">
    <Setter Property="FontSize" Value="14"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="basic:SecurePage">
                <Grid x:Name="Grid">
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="Signals">
                            <VisualState x:Name="Normal"/>
                            <VisualState x:Name="Blink">
                                <Storyboard>
                                    <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)" Storyboard.TargetName="border">
                                        <EasingColorKeyFrame KeyTime="0:0:0.4" Value="#FF3AFF00">
                                            <EasingColorKeyFrame.EasingFunction>
                                                <BounceEase EasingMode="EaseIn" Bounciness="3" Bounces="4"/>
                                            </EasingColorKeyFrame.EasingFunction>
                                        </EasingColorKeyFrame>
                                    </ColorAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <ContentPresenter Content="{TemplateBinding Content}"/>
                    <Border 
                        x:Name="border"
                        BorderThickness="5"
                        BorderBrush="Transparent"
                        IsHitTestVisible="False"/>
                    <i:Interaction.Triggers>                            
                        <ei:DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=basic:SecurePage}, Path=VisualState}" Value="{x:Static basic:ESecurePageVisualState.Normal}">
                            <ei:GoToStateAction StateName="Normal" TargetName="Grid" TargetObject="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Grid}}"/>
                        </ei:DataTrigger>
                        <ei:DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=basic:SecurePage}, Path=VisualState}" Value="{x:Static basic:ESecurePageVisualState.Blink}">
                            <ei:GoToStateAction StateName="Blink" TargetName="Grid" TargetObject="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Grid}}"/>
                        </ei:DataTrigger>
                    </i:Interaction.Triggers>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Now I'm able to Change the VisualState via a property.

public partial class Info
{
    public Info()
    {
        InitializeComponent();
        Loaded += (sender, args) =>
        {
            VisualState = ESecurePageVisualState.Blink;
        };
    }       
}

If you find a better solution, let me know!

Dominic Jonas
  • 4,717
  • 1
  • 34
  • 77