How to add an animation to a button when its command has executed in winui 3?

338 Views Asked by At

In a WinUi 3 application I created a custom Command that implements both an Executed event and a HasExecuted property with change notification. I want to design a CustomControl, preferably inheriting from Button that will show an animation once the Executed event is fired or the HasExecuted property becomes true.

Up to now I have come up with this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using Microsoft.UI;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;

namespace BridgeSystems.Framework.WinUI.Controls;

public class ConfirmButton : Button
{

protected Thickness _cachedMargin;
protected Brush _cachedForeground;
protected Brush _cachedBackground;
protected double _cachedFontSize;

public ConfirmButton()
{
    DefaultStyleKey = typeof(ConfirmButton);
}

protected override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    _cachedMargin = Margin;
    _cachedForeground = Foreground;
    _cachedFontSize = FontSize;
    _cachedBackground = Background;
}

public bool HasBeenExecuted
{
    get => (bool)GetValue(HasBeenExecutedProperty);
    set => SetValue(HasBeenExecutedProperty, value);
}

public static readonly DependencyProperty HasBeenExecutedProperty =
    DependencyProperty.Register(nameof(HasBeenExecuted), typeof(bool), typeof(ConfirmButton), new PropertyMetadata(false, OnExecuted));

private static void OnExecuted(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var button = d as ConfirmButton;
    if (button == null) return;

    if ((bool)e.NewValue)
    {
        button.Foreground = new SolidColorBrush(Colors.White);
        button.Background = new SolidColorBrush(Colors.Green);
        button.FontSize = 20;
    }
    else
    {
        button.Foreground = button._cachedForeground;
        button.Background = button._cachedBackground;
        button.FontSize = button._cachedFontSize;
    }
}

}

This works, but the green background is there to stay. I need to implement an animation to make the green background temporary. Delving deeper into this I found that the implementation above is not the way to go. It should be done by implementing a custom Visual state in the generic.xamlfile. and using GotoVisualStylein the OnExecuted method. But once I start using a style in `generic.xaml the button loses its default behaviour. Is there a way to do this without designing the Button from scratch?

2

There are 2 best solutions below

1
Nick On

You may want to look into Storyboarded animations. You can create your animation in code or in Xaml, and the preferred approach is to start it in code, in OnExecuted.

0
Andrew KeepCoding On

This is a tricky way but you can add a VisualState like this:

generic.xaml

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:controls="using:Microsoft.UI.Xaml.Controls"
    xmlns:local="using:VisualStatesExample">
    <Style TargetType="local:ConfirmButton">
        <Setter Property="Background" Value="{ThemeResource ButtonBackground}" />
        <Setter Property="BackgroundSizing" Value="InnerBorderEdge" />
        <Setter Property="Foreground" Value="{ThemeResource ButtonForeground}" />
        <Setter Property="BorderBrush" Value="{ThemeResource ButtonBorderBrush}" />
        <Setter Property="BorderThickness" Value="{ThemeResource ButtonBorderThemeThickness}" />
        <Setter Property="Padding" Value="{StaticResource ButtonPadding}" />
        <Setter Property="HorizontalAlignment" Value="Left" />
        <Setter Property="VerticalAlignment" Value="Center" />
        <Setter Property="FontFamily" Value="{ThemeResource ContentControlThemeFontFamily}" />
        <Setter Property="FontWeight" Value="Normal" />
        <Setter Property="FontSize" Value="{ThemeResource ControlContentThemeFontSize}" />
        <Setter Property="UseSystemFocusVisuals" Value="{StaticResource UseSystemFocusVisuals}" />
        <Setter Property="FocusVisualMargin" Value="-3" />
        <Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}" />
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <ContentPresenter
                        xmlns:local="using:Microsoft.UI.Xaml.Controls"
                        x:Name="ContentPresenter"
                        Padding="{TemplateBinding Padding}"
                        HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                        VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
                        local:AnimatedIcon.State="Normal"
                        AutomationProperties.AccessibilityView="Raw"
                        Background="{TemplateBinding Background}"
                        BackgroundSizing="{TemplateBinding BackgroundSizing}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        Content="{TemplateBinding Content}"
                        ContentTemplate="{TemplateBinding ContentTemplate}"
                        ContentTransitions="{TemplateBinding ContentTransitions}"
                        CornerRadius="{TemplateBinding CornerRadius}"
                        Foreground="{TemplateBinding Foreground}">
                        <ContentPresenter.BackgroundTransition>
                            <BrushTransition Duration="0:0:0.083" />
                        </ContentPresenter.BackgroundTransition>
                        <VisualStateManager.VisualStateGroups>
                            <VisualStateGroup x:Name="CommonStates">
                                <VisualState x:Name="Normal" />
                                <VisualState x:Name="PointerOver">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName="ContentPresenter"
                                            Storyboard.TargetProperty="Background">
                                            <DiscreteObjectKeyFrame
                                                KeyTime="0"
                                                Value="{ThemeResource ButtonBackgroundPointerOver}" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName="ContentPresenter"
                                            Storyboard.TargetProperty="BorderBrush">
                                            <DiscreteObjectKeyFrame
                                                KeyTime="0"
                                                Value="{ThemeResource ButtonBorderBrushPointerOver}" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName="ContentPresenter"
                                            Storyboard.TargetProperty="Foreground">
                                            <DiscreteObjectKeyFrame
                                                KeyTime="0"
                                                Value="{ThemeResource ButtonForegroundPointerOver}" />
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                    <VisualState.Setters>
                                        <Setter Target="ContentPresenter.(controls:AnimatedIcon.State)" Value="PointerOver" />
                                    </VisualState.Setters>
                                </VisualState>
                                <VisualState x:Name="Pressed">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName="ContentPresenter"
                                            Storyboard.TargetProperty="Background">
                                            <DiscreteObjectKeyFrame
                                                KeyTime="0"
                                                Value="{ThemeResource ButtonBackgroundPressed}" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName="ContentPresenter"
                                            Storyboard.TargetProperty="BorderBrush">
                                            <DiscreteObjectKeyFrame
                                                KeyTime="0"
                                                Value="{ThemeResource ButtonBorderBrushPressed}" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName="ContentPresenter"
                                            Storyboard.TargetProperty="Foreground">
                                            <DiscreteObjectKeyFrame
                                                KeyTime="0"
                                                Value="{ThemeResource ButtonForegroundPressed}" />
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                    <VisualState.Setters>
                                        <Setter Target="ContentPresenter.(controls:AnimatedIcon.State)" Value="Pressed" />
                                    </VisualState.Setters>
                                </VisualState>
                                <VisualState x:Name="Disabled">
                                    <Storyboard>
                                        <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName="ContentPresenter"
                                            Storyboard.TargetProperty="Background">
                                            <DiscreteObjectKeyFrame
                                                KeyTime="0"
                                                Value="{ThemeResource ButtonBackgroundDisabled}" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName="ContentPresenter"
                                            Storyboard.TargetProperty="BorderBrush">
                                            <DiscreteObjectKeyFrame
                                                KeyTime="0"
                                                Value="{ThemeResource ButtonBorderBrushDisabled}" />
                                        </ObjectAnimationUsingKeyFrames>
                                        <ObjectAnimationUsingKeyFrames
                                            Storyboard.TargetName="ContentPresenter"
                                            Storyboard.TargetProperty="Foreground">
                                            <DiscreteObjectKeyFrame
                                                KeyTime="0"
                                                Value="{ThemeResource ButtonForegroundDisabled}" />
                                        </ObjectAnimationUsingKeyFrames>
                                    </Storyboard>
                                    <VisualState.Setters>
                                        <!--  DisabledVisual Should be handled by the control, not the animated icon.  -->
                                        <Setter Target="ContentPresenter.(controls:AnimatedIcon.State)" Value="Normal" />
                                    </VisualState.Setters>
                                </VisualState>
                             
                                <!--  Confirmed VisualState  -->
                                <VisualState x:Name="Confirmed">
                                    <VisualState.Setters>
                                        <Setter Target="ContentPresenter.Background" Value="Green" />
                                        <Setter Target="ContentPresenter.FontSize" Value="20" />
                                    </VisualState.Setters>
                                </VisualState>
                            </VisualStateGroup>
                        </VisualStateManager.VisualStateGroups>
                    </ContentPresenter>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

ConfirmButton.cs

using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml;
using System;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;

namespace VisualStatesExample;

public class ConfirmButton : Button
{
    public static readonly DependencyProperty HasBeenExecutedProperty = DependencyProperty.Register(
        nameof(HasBeenExecuted),
        typeof(bool),
        typeof(ConfirmButton),
        new PropertyMetadata(false, OnExecuted));

    public ConfirmButton()
    {
        DefaultStyleKey = typeof(ConfirmButton);
    }

    public bool HasBeenExecuted
    {
        get => (bool)GetValue(HasBeenExecutedProperty);
        set => SetValue(HasBeenExecutedProperty, value);
    }
    private ContentPresenter? ContentPresenter { get; set; }

    private VisualStateGroup? CommonStatesVisualStateGroup { get; set; }

    private List<VisualState>? AllVisualStatesExceptConfirmed { get; set; }

    protected override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        ContentPresenter = GetTemplateChild(nameof(ContentPresenter)) as ContentPresenter;

        CommonStatesVisualStateGroup = VisualStateManager.GetVisualStateGroups(ContentPresenter)
            .Where(x => x.Name is "CommonStates")
            .FirstOrDefault();

        AllVisualStatesExceptConfirmed = CommonStatesVisualStateGroup?.States
            .Where(x => x.Name is not "Confirmed")
            .ToList();
    }

    private static void OnExecuted(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        (d as ConfirmButton)?.ApplyConfirmedVisualState();
    }

    private async Task ApplyConfirmedVisualState()
    {
        if (HasBeenExecuted is false ||
            ContentPresenter is null ||
            CommonStatesVisualStateGroup is null ||
            CommonStatesVisualStateGroup.CurrentState.Name is "Confirmed" ||
            AllVisualStatesExceptConfirmed is null)
        {
            return;
        }

        // Temporarily remove VisualStates except "Confirmed".
        foreach (VisualState state in AllVisualStatesExceptConfirmed)
        {
            CommonStatesVisualStateGroup.States.Remove(state);
        }

        _ = VisualStateManager.GoToState(this, "Confirmed", false);

        await Task.Delay(TimeSpan.FromMilliseconds(2000));

        foreach (VisualState state in AllVisualStatesExceptConfirmed)
        {
            CommonStatesVisualStateGroup.States.Add(state);
        }

        _ = VisualStateManager.GoToState(this, "Normal", false);
    }
}