Search code examples
c#wpfvisualstatemanagervisualstates

Manage a new visual state in buttons


I want to add a new "Activated" state to a WPF Button and I want to avoid re-creating a control from scratch.

This new state is linked to the IsActivated dependency property and must change the background color of the Button. Here is the truth table of the interaction between the IsEnabled and IsActivated dependency properties:

Truth table of relations between IsActivated and IsEnabled

I wrote a class extending from Button, created dependency properties, and in the callback of IsActivated, I computed the visual state of the button.

The issue is that the ButtonBase type already manages the visual state through the ChangeVisualState function, which cannot be overridden.

After managing callbacks of both dependency properties, the interaction between IsActivated and IsEnabled works as intended, but clicking on the button, or putting the mouse over the button overrides the Activated visual state.

Is this possible to accomplish this with visual states, or should I use simple triggers?

The code of the Control so far:

using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace OrganizationName.BasicControls.Primitive
{
    public class MultiStateButton : Button
    {
        static MultiStateButton()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(MultiStateButton), new FrameworkPropertyMetadata(typeof(MultiStateButton)));
            IsEnabledProperty.OverrideMetadata(typeof(MultiStateButton), new FrameworkPropertyMetadata(propertyChangedCallback: IsEnabledCallback));
        }

        internal void ChangeVisualState(bool useTransitions)
        {
            if (!IsEnabled)
            {
                VisualStateManager.GoToState(this, "Disabled", useTransitions);
            }
            else if (IsActivated)
            {
                VisualStateManager.GoToState(this, "Activated", useTransitions);
            }
            else if (IsPressed)
            {
                VisualStateManager.GoToState(this, "Pressed", useTransitions);
            }
            else
            {
                VisualStateManager.GoToState(this, "Normal", useTransitions);
            }
        }

        #region IsEnabled override
        private static void IsEnabledCallback(DependencyObject o, DependencyPropertyChangedEventArgs args)
        {
            MultiStateButton multiStateButton = o as MultiStateButton;
            if (multiStateButton == null) return;

            multiStateButton.ChangeVisualState(true);
        }
        #endregion IsEnabled override

        #region DP IsActivated 
        public bool IsActivated
        {
            get { return (bool)GetValue(IsActivatedProperty); }
            set { SetValue(IsActivatedProperty, value); }
        }

        private static void IsActivatedCallback(DependencyObject o, DependencyPropertyChangedEventArgs args)
        {
            MultiStateButton multiStateButton = o as MultiStateButton;
            if (multiStateButton == null) return;

            multiStateButton.ChangeVisualState(true);
        }

        private readonly static FrameworkPropertyMetadata IsActivatedMetadata = new FrameworkPropertyMetadata
        {
            PropertyChangedCallback = IsActivatedCallback,
            DefaultUpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
        };

        public static readonly DependencyProperty IsActivatedProperty =
            DependencyProperty.Register("IsActivated", typeof(bool), typeof(MultiStateButton), IsActivatedMetadata);
        #endregion DP IsActivated
    }
}


The default style of the button:

<sys:Double x:Key="ButtonCornerRadiusValue">5</sys:Double>

<CornerRadius x:Key="ButtonCornerRadius"
              TopLeft="{StaticResource ButtonCornerRadiusValue}"
              BottomLeft="{StaticResource ButtonCornerRadiusValue}"
              TopRight="{StaticResource ButtonCornerRadiusValue}"
              BottomRight="{StaticResource ButtonCornerRadiusValue}"/>
<CornerRadius x:Key="ButtonCornerRadiusLeft"
              TopLeft="{StaticResource ButtonCornerRadiusValue}"
              BottomLeft="{StaticResource ButtonCornerRadiusValue}"/>
<CornerRadius x:Key="ButtonCornerRadiusRight"
              TopRight="{StaticResource ButtonCornerRadiusValue}"
              BottomRight="{StaticResource ButtonCornerRadiusValue}"/>
<CornerRadius x:Key="ButtonCornerRadiusTop"
              TopRight="{StaticResource ButtonCornerRadiusValue}"
              TopLeft="{StaticResource ButtonCornerRadiusValue}"/>
<CornerRadius x:Key="ButtonCornerRadiusBottom"
              BottomLeft="{StaticResource ButtonCornerRadiusValue}"
              BottomRight="{StaticResource ButtonCornerRadiusValue}"/>

<Style x:Key="{x:Type primitives:MultiStateButton}"
       TargetType="{x:Type primitives:MultiStateButton}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type primitives:MultiStateButton}">
                <Border TextBlock.Foreground="{TemplateBinding Foreground}"
                        x:Name="Border"
                        CornerRadius="{StaticResource ButtonCornerRadius}"
                        Background="White"
                        BorderThickness="1">
                    <Border.Effect>
                        <DropShadowEffect Color="#CDD5E3"
                                          ShadowDepth="0"
                                          BlurRadius="13"
                                          Direction="0"/>
                    </Border.Effect>
                    <Border.BorderBrush>
                        <RadialGradientBrush Center="0.5,0.5"
                                             RadiusY="1"
                                             RadiusX="5"
                                             GradientOrigin="0.5,0.5">
                            <RadialGradientBrush.GradientStops>
                                <GradientStopCollection>
                                    <GradientStop Color="Transparent"
                                                  Offset="0" />
                                    <GradientStop Color="Transparent"
                                                  Offset="0.5" />
                                    <GradientStop Color="{StaticResource BorderPushedColor}"
                                                  Offset="0.8" />
                                    <GradientStop Color="{StaticResource BorderPushedColor}"
                                                  Offset="1" />
                                </GradientStopCollection>
                            </RadialGradientBrush.GradientStops>
                        </RadialGradientBrush>
                    </Border.BorderBrush>
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Normal"/>
                            <VisualState x:Name="MouseOver"/>
                            <VisualState x:Name="Pressed">
                                <Storyboard>
                                    <DoubleAnimation Storyboard.TargetProperty="(Border.Width)"
                                                     Storyboard.TargetName="LeftBorder"
                                                     Duration="00:00:00"
                                                     To="10"/>
                                    <DoubleAnimation Storyboard.TargetProperty="(Border.Width)"
                                                     Storyboard.TargetName="RightBorder"
                                                     Duration="00:00:00"
                                                     To="10"/>
                                    <DoubleAnimation Storyboard.TargetProperty="(Border.Height)"
                                                     Storyboard.TargetName="TopBorder"
                                                     Duration="00:00:00"
                                                     To="10"/>
                                    <DoubleAnimation Storyboard.TargetProperty="(Border.Height)"
                                                     Storyboard.TargetName="BottomBorder"
                                                     Duration="00:00:00"
                                                     To="10"/>
                                    <DoubleAnimation Storyboard.TargetProperty="(Border.Effect).(DropShadowEffect.BlurRadius)"
                                                     Storyboard.TargetName="Border"
                                                     Duration="00:00:00"
                                                     To="10"/>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Disabled">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.Background)"
                                                                  Storyboard.TargetName="Border">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource SkyblueLight}"/>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Activated">
                                <Storyboard>
                                    <ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(Border.Background)"
                                                                   Storyboard.TargetName="Border">
                                        <DiscreteObjectKeyFrame KeyTime="0" Value="{StaticResource ReflexBlue}"/>
                                    </ObjectAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                    <Grid Background="Transparent">
                        <ContentPresenter Margin="2"
                                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                                          RecognizesAccessKey="True"/>
                        <Border BorderThickness="0"
                                Width="0"
                                HorizontalAlignment="Left"
                                CornerRadius="{StaticResource ButtonCornerRadiusLeft}"
                                x:Name="LeftBorder">
                            <Border.Background>
                                <LinearGradientBrush StartPoint="1,0.5" EndPoint="0,0.5">
                                    <LinearGradientBrush.GradientStops>
                                        <GradientStop Offset="0" Color="Transparent"/>
                                        <GradientStop Offset="1" Color="#D9DDE4"/>
                                    </LinearGradientBrush.GradientStops>
                                </LinearGradientBrush>
                            </Border.Background>
                        </Border>
                        <Border BorderThickness="0"
                                Width="0"
                                HorizontalAlignment="Right"
                                CornerRadius="{StaticResource ButtonCornerRadiusRight}"
                                x:Name="RightBorder">
                            <Border.Background>
                                <LinearGradientBrush StartPoint="0,0.5" EndPoint="1,0.5">
                                    <LinearGradientBrush.GradientStops>
                                        <GradientStop Offset="0" Color="Transparent"/>
                                        <GradientStop Offset="1" Color="#D9DDE4"/>
                                    </LinearGradientBrush.GradientStops>
                                </LinearGradientBrush>
                            </Border.Background>
                        </Border>

                        <Border BorderThickness="0"
                                Height="0"
                                VerticalAlignment="Top"
                                CornerRadius="{StaticResource ButtonCornerRadiusTop}"
                                x:Name="TopBorder">
                            <Border.Background>
                                <LinearGradientBrush StartPoint="0.5,1" EndPoint="0.5,0">
                                    <LinearGradientBrush.GradientStops>
                                        <GradientStop Offset="0" Color="Transparent"/>
                                        <GradientStop Offset="1" Color="#D9DDE4"/>
                                    </LinearGradientBrush.GradientStops>
                                </LinearGradientBrush>
                            </Border.Background>
                        </Border>

                        <Border BorderThickness="0"
                                Height="0"
                                VerticalAlignment="Bottom"
                                CornerRadius="{StaticResource ButtonCornerRadiusBottom}"
                                x:Name="BottomBorder">
                            <Border.Background>
                                <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
                                    <LinearGradientBrush.GradientStops>
                                        <GradientStop Offset="0" Color="Transparent"/>
                                        <GradientStop Offset="1" Color="#D9DDE4"/>
                                    </LinearGradientBrush.GradientStops>
                                </LinearGradientBrush>
                            </Border.Background>
                        </Border>
                    </Grid>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Solution

  • When it comes to adding a simple visual state to an existing control, I typically avoid creating a subclass and instead use attached properties.

    When it comes to implementing a new visual state for an existing control, you don't have to use the VisualStateManager at all. Especially if you aren't using animations.

    I recommend using Triggers instead.

    If you want to continue to use your MultiStateButton control, you can simply do something like this:

    <ControlTemplate ...>
        ...
        <ControlTemplate.Triggers>
            <MultiTrigger>
                <MultiTrigger.Conditions>
                    <Condition Property="IsEnabled" Value="True" />
                    <Condition Property="IsActivated" Value="True" />
                </MultiTrigger.Conditions>
                <Setter TargetName="Border" Property="Background" Value="{StaticResource ReflexBlue}" />
            </MultiTrigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
    

    However, if you don't need to keep using MultiStateButton, I would keep your custom button style, and control template, and use an attached property class to add the new property.

    public static class MultiStateButtonProperties
    {
        public static readonly DependencyProperty IsActivatedProperty = DependencyProperty.RegisterAttached("IsActivated", typeof(bool), typeof(MultiStateButtonProperties), new FrameworkPropertyMetadata(false));
    
        public static bool GetIsActivated(DependencyObject obj)
        {
            return (bool)obj.GetValue(IsActivatedProperty);
        }
    
        public static void SetIsActivated(DependencyObject obj, bool value)
        {
            obj.SetValue(IsActivatedProperty, value);
        }
    }
    

    Then, in your style's control template, you can use a MultiTrigger like the one above and do something like this:

    <ControlTemplate ...>
        ...
        <ControlTemplate.Triggers>
            <MultiTrigger>
                <MultiTrigger.Conditions>
                    <Condition Property="IsEnabled" Value="True" />
                    <Condition Property="ap:MultiStateButtonProperties.IsActivated" Value="True" />
                </MultiTrigger.Conditions>
                <Setter TargetName="Border" Property="Background" Value="{StaticResource ReflexBlue}" />
            </MultiTrigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
    

    I hope this helps.