Search code examples
c#wpfxamlexpression-blendvisualstatemanager

Inheritance of VisualStateGroup / VisualState in a XAML Style / base class


  • 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!


Solution

  • 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