Skip to content

Instantly share code, notes, and snippets.

@bc3tech
Last active February 14, 2020 08:27
Show Gist options
  • Save bc3tech/c56b1de3d8370b0c6cee to your computer and use it in GitHub Desktop.
Save bc3tech/c56b1de3d8370b0c6cee to your computer and use it in GitHub Desktop.
A XAML Button control that incorporates a "Busy" state w/ progress ring while its Command executes
<UserControl x:Class="App.Controls.BusyButton"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:App.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
MinWidth="32"
MinHeight="32">
<UserControl.Resources>
<local:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<!-- Duplicate of the Default button template -->
<ControlTemplate x:Key="ButtonControlDefaultTemplate"
TargetType="Button">
<Grid x:Name="RootGrid"
Background="{TemplateBinding Background}"
CornerRadius="5">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<Storyboard>
<PointerUpThemeAnimation Storyboard.TargetName="RootGrid" />
</Storyboard>
</VisualState>
<VisualState x:Name="PointerOver">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush"
Storyboard.TargetName="ContentPresenter">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource SystemControlHighlightBaseMediumLowBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
Storyboard.TargetName="ContentPresenter">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource SystemControlHighlightBaseHighBrush}" />
</ObjectAnimationUsingKeyFrames>
<PointerUpThemeAnimation Storyboard.TargetName="RootGrid" />
</Storyboard>
</VisualState>
<VisualState x:Name="Pressed">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background"
Storyboard.TargetName="RootGrid">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource SystemControlBackgroundBaseMediumLowBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush"
Storyboard.TargetName="ContentPresenter">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource SystemControlHighlightTransparentBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
Storyboard.TargetName="ContentPresenter">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource SystemControlHighlightBaseHighBrush}" />
</ObjectAnimationUsingKeyFrames>
<PointerDownThemeAnimation Storyboard.TargetName="RootGrid" />
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Background"
Storyboard.TargetName="RootGrid">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource SystemControlBackgroundBaseLowBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
Storyboard.TargetName="ContentPresenter">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource SystemControlDisabledBaseMediumLowBrush}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="BorderBrush"
Storyboard.TargetName="ContentPresenter">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{ThemeResource SystemControlDisabledTransparentBrush}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ContentPresenter x:Name="ContentPresenter"
AutomationProperties.AccessibilityView="Raw"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTransitions="{TemplateBinding ContentTransitions}"
Content="{TemplateBinding Content}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
Padding="{TemplateBinding Padding}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
</Grid>
</ControlTemplate>
</UserControl.Resources>
<Grid>
<Button x:Name="MainButton"
Content="{x:Bind ButtonContent, Mode=OneWay}"
Style="{x:Bind ButtonStyle, Mode=OneWay}"
Template="{x:Bind ButtonTemplate, Mode=OneWay}"
HorizontalAlignment="{x:Bind HorizontalContentAlignment, Mode=OneWay}"
VerticalAlignment="{x:Bind VerticalContentAlignment, Mode=OneWay}"
Command="{x:Bind ButtonCommand, Mode=OneWay}"
CommandParameter="{x:Bind ButtonCommandParameter, Mode=OneWay}" />
<Grid x:Name="BusyGrid"
Visibility="{x:Bind IsBusy, Converter={StaticResource BooleanToVisibilityConverter}, Mode=OneWay}">
<Grid.Background>
<SolidColorBrush Color="{ThemeResource SystemAltHighColor}"
Opacity=".3" />
</Grid.Background>
<ProgressRing x:Name="BusyRing"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Margin="10"
IsActive="{x:Bind IsBusy, Mode=OneWay}"
Style="{x:Bind ProgressRingStyle, Mode=OneWay}" />
</Grid>
</Grid>
</UserControl>
using System;
using System.Windows.Input;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Data;
namespace App.Controls
{
public sealed partial class BusyButton : UserControl
{
public BusyButton()
{
object globalButtonStyleDefinedByApp;
Style initialButtonStyle = new Style(typeof(Button));
if (Application.Current.Resources.TryGetValue(typeof(Button), out globalButtonStyleDefinedByApp))
{
initialButtonStyle = globalButtonStyleDefinedByApp as Style ?? initialButtonStyle;
}
this.ButtonStyle = initialButtonStyle;
this.InitializeComponent();
}
public object ButtonContent
{
get { return (object)GetValue(ButtonContentProperty); }
set { SetValue(ButtonContentProperty, value); }
}
// Using a DependencyProperty as the backing store for ButtonContent. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ButtonContentProperty =
DependencyProperty.Register("ButtonContent", typeof(object), typeof(BusyButton), new PropertyMetadata(null));
public Style ButtonStyle
{
get { return (Style)GetValue(ButtonStyleProperty); }
set { SetValue(ButtonStyleProperty, value); }
}
// Using a DependencyProperty as the backing store for ButtonStyle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ButtonStyleProperty =
DependencyProperty.Register("ButtonStyle", typeof(Style), typeof(BusyButton), new PropertyMetadata(null));
public ControlTemplate ButtonTemplate
{
get { return (ControlTemplate)GetValue(ButtonTemplateProperty); }
set { SetValue(ButtonTemplateProperty, value); }
}
// Using a DependencyProperty as the backing store for ButtonTemplate. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ButtonTemplateProperty =
DependencyProperty.Register("ButtonTemplate", typeof(ControlTemplate), typeof(BusyButton), new PropertyMetadata(new BusyButton().Resources["ButtonControlDefaultTemplate"]));
public ILongCommand ButtonCommand
{
get { return (ILongCommand)GetValue(ButtonCommandProperty); }
set { SetValue(ButtonCommandProperty, value); }
}
// Using a DependencyProperty as the backing store for ButtonCommand. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ButtonCommandProperty =
DependencyProperty.Register("ButtonCommand", typeof(ILongCommand), typeof(BusyButton), new PropertyMetadata(null, (o, p) =>
{
var button = o as BusyButton;
if (button != null && p.NewValue != p.OldValue)
{
var cmd = p.NewValue as ILongCommand;
if (cmd != null)
{
cmd.CommandStart += (s, e) => button.IsBusy = true;
cmd.CommandEnd += (s, e) => button.IsBusy = false;
}
}
}));
public object ButtonCommandParameter
{
get { return (object)GetValue(ButtonCommandParameterProperty); }
set { SetValue(ButtonCommandParameterProperty, value); }
}
// Using a DependencyProperty as the backing store for ButtonCommandParameter. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ButtonCommandParameterProperty =
DependencyProperty.Register("ButtonCommandParameter", typeof(object), typeof(BusyButton), new PropertyMetadata(null));
public Style ProgressRingStyle
{
get { return (Style)GetValue(ProgressRingStyleProperty); }
set { SetValue(ProgressRingStyleProperty, value); }
}
// Using a DependencyProperty as the backing store for ProgressRingStyle. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ProgressRingStyleProperty =
DependencyProperty.Register("ProgressRingStyle", typeof(Style), typeof(BusyButton), new PropertyMetadata(null));
public bool IsBusy
{
get { return (bool)GetValue(IsBusyProperty); }
set { SetValue(IsBusyProperty, value); }
}
// Using a DependencyProperty as the backing store for IsBusy. This enables animation, styling, binding, etc...
public static readonly DependencyProperty IsBusyProperty =
DependencyProperty.Register("IsBusy", typeof(bool), typeof(BusyButton), new PropertyMetadata(false));
}
class BooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language) => ((value as bool?) == true) ? Visibility.Visible : Visibility.Collapsed;
public object ConvertBack(object value, Type targetType, object parameter, string language) => (value as Visibility?) == Visibility.Visible;
}
public interface ILongCommand : ICommand
{
event EventHandler CommandStart;
event EventHandler CommandEnd;
}
}
<controls:BusyButton ButtonCommand="{x:Bind BuyCommand}" Content="Buy">
<controls:BusyButton.ProgressRingStyle>
<Style TargetType="ProgressRing">
<Setter Property="Foreground"
Value="{ThemeResource SystemControlForegroundAccentBrush}" />
</Style>
</controls:BusyButton.ProgressRingStyle>
</controls:BusyButton>
class BuyCommandCmd : ILongCommand
{
public event EventHandler CanExecuteChanged;
public event EventHandler CommandEnd;
public event EventHandler CommandStart;
public bool CanExecute(object parameter) => true;
async public void Execute(object parameter)
{
this.CommandStart?.Invoke(this, EventArgs.Empty);
try
{
// do work
}
finally
{
this.CommandEnd?.Invoke(this, EventArgs.Empty);
}
}
}
@bc3tech
Copy link
Author

bc3tech commented Jan 7, 2016

Idea here is to have a Button that makes itself impervious to multi-click by the user while it's executing a command. As a bonus, let's have a ProgressRing show up on it while it's working!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment