XAML 样式/基础 class 中的 VisualStateGroup/VisualState 的继承

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

问题是,在派生的 class 中,有 none 个 VisualState 可用。

var states = VisualStateManager.GetVisualStateGroups(this);

Returns 一个空列表。

如果我复制我的 XAML VisualState 定义并将其粘贴到我的 DerivadedFooSecurePage 中,我可以轻松地进入状态:

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

与此处描述的问题相同:VisualState in abstract control


更多细节

安全页面

[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>

信息页

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>

实时调试

将 VisualState 复制到派生的 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 >

我只是想在SecurePageXAML Style定义中定义状态,然后去任何派生的状态class!

我厌倦了重新生成你提到的场景,我能够从基础 class 中获得 VisualStateGroup 的列表。这是我实现的示例。

首先,您需要获取当前基础 class 的 VisualStateGroup,然后将自定义创建的组添加到其中。

基础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);
    }
}

派生Class

派生的根 class 是基 class 的类型。请记住一定要改变它。 XAML 文件的第一行。检查下面。

<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);
    }
} 

进一步可以在任何 window 或其他用户控件上实现 DerivadedFooSecurePage

解决方案

我定义了一个 enum,每个可用的 VisualState 都有一个值。

public enum ESecurePageVisualState
{
    Normal,
    Blink
}

SecurePage class 中添加了一个 DependencyProperty

/// <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));

并更改了SecurePageStyle

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

现在我可以通过 属性 更改 VisualState

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

如果您找到更好的解决方案,请告诉我!

诊断

经过一番摸索,我终于找到了罪魁祸首 - 它是 UserControl 本身。更准确地说 - overridden StateGroupsRoot property,它被 VisualStateManager.GoToState 方法使用。通常,它 return 是控件模板的根元素,但在 UserControl 的情况下,它 return 是 UserControl.Content 属性 的值。因此,当您调用 GoToState 时,模板中定义的状态不会被考虑在内。

解决方案

这个问题至少有两个解决方案。

第一个解决方案 是从ContentControl 而不是UserControl 派生基数class (SecurePage)。后者并没有太大不同——它将 FocusableIsTabStop 属性默认为 false,而 HorizontanContentAlignmentVerticalContentAlignment 属性默认为 Stretch。此外,除了覆盖提到的 StateGroupsRoot 属性 之外,它还提供了自己的 AutomationPeerUserControlAutomationPeer),但我认为您不必为此担心。

第二种解决方案 是在模板根目录上使用VisualStateManager.GoToElementState。例如:

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);
    }
}

其他注意事项

在你的控件上调用 VisualStateManager.GetVisualStateGroups 会产生一个空列表,因为它只是一个普通的附加依赖 属性 访问器,而你没有设置 1 属性 由您控制。要掌握您在模板中定义的组,您应该将模板根作为参数传递给它。根据同样的原则,您不希望 Grid.GetColumn 调用您的控件来 return 您在模板中某处设置的值。

关于在您的控件的构造函数中调用 GoToState - 它很可能不会起作用,因为您的控件只是 被实例化 ,并且很可能是它的 Templtate 为 null(请记住,您在模板中定义了视觉状态)。最好将该逻辑移动到 OnApplyTemplate 覆盖。


1 由于 VisualStateManager.VisualStateGroupsProperty 是只读的,通过 set 我的意思是将项目添加到列表 return由VisualStateManager.GetVisualStateGroups编辑