Search code examples
c#wpftriggersstoryboard

Why is my story board actions skipped when my trigger triggers


I have a Tabcontrol that I'm trying to add actions based on triggers.

Using Routed Events I can get my storyboard to trigger and the my actions respond as intended. Now I'm trying to prevent my storyboard actions from triggering for a specific tab header if that header is selected.

This has forced me to move away from routed events since I can't use a property condition and a routed event to determine if my storyboard will execute.

Fast forward and now I have finally been able to get IsMouseOver to respond as I wish to the mouse. (My background properties changes and changes back as the mouse enters and leaves the tabitem header. I'm almost there I think but as soon as I add the exact same storyboard as before my code decides to be lazy and skip it for some reason. The background setter is still being triggered but the storyboard remains silent.

I tried removing the setter but the storyboard still does not trigger.

End of the Day: I looking to set styles that transition nicely between each other for all possible combinations of Is Mouse Over and Is Selected.

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:Tenant_Tool_Analytics_Module.Resources.Components"
                    xmlns:controls="clr-namespace:Tenant_Tool_Analytics_Module.Resources.Components">
    <Style TargetType="{x:Type controls:NavigationElement}">
        <Setter Property="Template">


            <Setter.Value>
                <ControlTemplate TargetType="{x:Type controls:NavigationElement}">
                    <Border BorderBrush="Black" BorderThickness=".7"  x:Name="Bd">
                        <Grid Width="150" Height="50" Opacity="0.75" x:Name="NavigationElementGrid">


                            <Grid.Background >
                                <RadialGradientBrush GradientOrigin="0.5,0.5" Center="0.5,0.5" RadiusX="0.5" RadiusY="2.0">
                                    <GradientStop Color="#006A4D" Offset="1.0"/>
                                    <GradientStop Color="#56be88" Offset=".2"/>
                                </RadialGradientBrush >
                            </Grid.Background>
                            <ContentControl Grid.Column="0" Content="{TemplateBinding Icon}"/>
                            <TextBlock x:Name="NavigationElementText" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left" Text="{TemplateBinding LabelText}" FontFamily="Futura XBlkIt BT" FontSize="12"/>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="40"/>
                                <ColumnDefinition Width="*"/>
                            </Grid.ColumnDefinitions>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="50" />
                            </Grid.RowDefinitions>
                        </Grid>
                    </Border>
                    <!--
                    Trigger related to when the mouse is over the header
                        I would like it to Execute the doubleanimations 
                    -->
                    <ControlTemplate.Triggers>
                        <MultiTrigger>
                            <MultiTrigger.Conditions>
                                <Condition Property="IsMouseOver" Value="True"/>
                                <!-- Missing condition: If selected = false -->
                            </MultiTrigger.Conditions>
                            <!-- Start BUG the bellow code does not execute -->
                            <MultiTrigger.EnterActions>
                                <BeginStoryboard>
                                    <Storyboard>

                                        <DoubleAnimation Storyboard.TargetName="NavigationElementGrid" Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.3"/>
                                        <DoubleAnimation Storyboard.TargetName="NavigationElementText" Storyboard.TargetProperty="FontSize" To="14" Duration="0:0:0.3"/>
                                    </Storyboard>
                                </BeginStoryboard>
                            </MultiTrigger.EnterActions>
                            <!-- End BUG -->
                            <!-- The triggers are fireing becuase this is being set. -->
                            <Setter Property="Background" TargetName="Bd" Value="Blue"/>
                        </MultiTrigger>

                        <!-- 
                        When the mouse leaves I want it to return to it's original 
                        state. 


                        -->
                        <MultiTrigger>
                            <MultiTrigger.Conditions>
                                <Condition Property="IsMouseOver" Value="False"/>
                                <!-- Missing condition: If selected = false -->
                            </MultiTrigger.Conditions>
                            <!-- Start BUG the bellow code does not execute -->
                            <MultiTrigger.EnterActions>
                                <BeginStoryboard>
                                    <Storyboard>
                                        <DoubleAnimation Storyboard.TargetName="NavigationElementGrid" Storyboard.TargetProperty="Opacity" To=".75" Duration="0:0:0.3"/>
                                        <DoubleAnimation Storyboard.TargetName="NavigationElementText" Storyboard.TargetProperty="FontSize" To="12" Duration="0:0:0.3"/>
                                    </Storyboard>
                                </BeginStoryboard>
                            </MultiTrigger.EnterActions>
                            <!-- End BUG -->
                            <!-- The triggers are fireing becuase this is being set. -->
                            <Setter Property="Background" TargetName="Bd" Value="Red"/>
                        </MultiTrigger>

                        <!-- Missing two more MultiTriggers (very similar to above) for the cases of if the tab is selected.-->
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Code behind

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace Tenant_Tool_Analytics_Module.Resources.Components
{
    public class NavigationElement : Control
    {
        public string LabelText
        {
            get
            {
                return (string)GetValue(LabelTextProperty);
            }
            set
            {
                SetValue(LabelTextProperty, value);
            }
        }

        public static readonly DependencyProperty LabelTextProperty =
            DependencyProperty.Register("LabelText", typeof(string), typeof(NavigationElement), new PropertyMetadata(string.Empty));

        public object Icon
        {
            get
            {
                return (object)GetValue(IconProperty);
            }
            set
            {
                SetValue(IconProperty, value);
            }
        }

        public static readonly DependencyProperty IconProperty =
            DependencyProperty.Register("Icon", typeof(object), typeof(NavigationElement), new PropertyMetadata(string.Empty));


        public System.Windows.Media.Brush BackgroundColour
        {
            get
            {
                return (System.Windows.Media.Brush)GetValue(BackgroundColourProperty);
            }
            set
            {
                SetValue(BackgroundColourProperty, value);
            }
        }

        public static readonly DependencyProperty BackgroundColourProperty =
            DependencyProperty.Register("BackgroundColour", typeof(System.Windows.Media.Brush), typeof(NavigationElement), new PropertyMetadata(Brushes.Black));

    }
}

Implementation code

<Window x:Class="Tenant_Tool_Analytics_Module.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Tenant_Tool_Analytics_Module"
        xmlns:views="clr-namespace:Tenant_Tool_Analytics_Module.Views"
        xmlns:controls="clr-namespace:Tenant_Tool_Analytics_Module.Resources.Components"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <DockPanel LastChildFill="True">
        <views:HeaderView x:Name="HeaderView" DockPanel.Dock="Top"/>
        <TabControl DockPanel.Dock="Left" TabStripPlacement="Left" Margin="0, 0, 0, 10">
            <TabItem Padding="0">
                <TabItem.Header>
                    <controls:NavigationElement LabelText="Stacking Plan" Icon="{StaticResource StackPlanIcon}"/>
                </TabItem.Header>
                <Button Content="HI"/>
            </TabItem>
            <TabItem Padding="0">
                <TabItem.Header>
                    <controls:NavigationElement LabelText="Tenant Profile" Icon="{StaticResource TenantProfileIcon}"/>
                </TabItem.Header>
                <Button Content="HI"/>
            </TabItem>
            <TabItem Padding="0">
                <TabItem.Header>
                    <controls:NavigationElement LabelText="Submarket" Icon="{StaticResource SubmarketIcon}"/>
                </TabItem.Header>
                <Button Content="HI"/>
            </TabItem>
            <TabItem Padding="0">
                <TabItem.Header>
                    <controls:NavigationElement LabelText="Industry" Icon="{StaticResource IndustryIcon}"/>
                </TabItem.Header>
                <Button Content="HI"/>
            </TabItem>
        </TabControl>
    </DockPanel>
</Window>

Solution

  • I think you weren't aware of the fact that MultiDataTrigger objects have both EnterActions and ExitActions. Instead of having one MultiDataTrigger triggering on true with only EnterActions and another MultiDataTrigger triggering on false with only EnterActions, you can use only one MultiDataTrigger triggering on true with both EnterActions (to change the object to an abnormal state) and ExitActions to transition it back to its normal state.

    The Triggers collection now works as expected and becomes, as a bonus, easier to read:

    <ControlTemplate.Triggers>
        <MultiTrigger>
            <MultiTrigger.Conditions>
                <Condition Property="IsMouseOver" Value="True"/>
                <!-- Missing condition: If selected = false -->
            </MultiTrigger.Conditions>
            <MultiTrigger.EnterActions>
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetName="NavigationElementGrid"
                                         Storyboard.TargetProperty="Opacity"
                                         To="1"
                                         Duration="0:0:0.3"/>
                        <DoubleAnimation Storyboard.TargetName="NavigationElementText"
                                         Storyboard.TargetProperty="FontSize"
                                         To="14" Duration="0:0:0.3"/>
                        <ColorAnimationUsingKeyFrames Storyboard.TargetName="BorderBackgroundBrush"
                                                      Storyboard.TargetProperty="Color">
                            <DiscreteColorKeyFrame KeyTime="0" Value="Blue"/>
                        </ColorAnimationUsingKeyFrames>
                    </Storyboard>
                </BeginStoryboard>
            </MultiTrigger.EnterActions>
            <MultiTrigger.ExitActions>
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetName="NavigationElementGrid"
                                         Storyboard.TargetProperty="Opacity"
                                         To=".75"
                                         Duration="0:0:0.3"/>
                        <DoubleAnimation Storyboard.TargetName="NavigationElementText"
                                         Storyboard.TargetProperty="FontSize"
                                         To="12"
                                         Duration="0:0:0.3"/>
                        <ColorAnimationUsingKeyFrames Storyboard.TargetName="BorderBackgroundBrush"
                                                      Storyboard.TargetProperty="Color">
                            <DiscreteColorKeyFrame KeyTime="0" Value="Red"/>
                        </ColorAnimationUsingKeyFrames>
                    </Storyboard>
                </BeginStoryboard>
            </MultiTrigger.ExitActions>
        </MultiTrigger>
        <!-- Missing two more MultiTriggers (very similar to above) for the cases of if the tab is selected.-->
    </ControlTemplate.Triggers>
    

    Also notice how I've used a ColorAnimationUsingKeyFrames to change the Border.Background property without needing a Setter in another Trigger. This way, all changes are performed in the same Storyboard. For this to work, you just need to assign a named SolidColorBrush to your "Bd" Border:

    <Border.Background>
        <SolidColorBrush x:Name="BorderBackgroundBrush" Color="Red"></SolidColorBrush>
    </Border.Background>
    

    To prevent the Storyboard to play if the ancestor TabItem is selected, I suggest you add a boolean IsSelected DependencyProperty to your NavigationElement, so that you can bind this property to its TabItem ancestor by adding a Setter in your Style like this:

    <Setter Property="IsSelected" Value="{Binding IsSelected, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=TabItem}}"/>
    

    And you just have to add the condition in your MultiDataTrigger (but you already figured that out):

        <Condition Property="IsMouseOver" Value="True"/>
        <Condition Property="IsSelected" Value="False"/>
    

    Sidenote: I recommend you wrap and indent your XAML attributes to avoid long XAML lines that force you to scroll. Besides the increased readability, having each XAML attribute on a new line is more version control-friendly because one attribute change only impacts one line.