WPF Standard Control Demo App › Button

Button Inputs

Button runs an action when it is clicked, through its Click event or a command bound to Command. ClickMode, IsPressed, and the command properties come from its base class ButtonBase.

Esc and Enter: IsCancel and IsDefault

IsCancel makes Esc click the button. The demo app places the button in a UserControl, and it works there: with the focus in a TextBox inside the UserControl, Esc clicked it. Whether the window closes depends on how it was opened. In a window opened with ShowDialog, Esc closed the window and ShowDialog returned false. In a window opened with Show, the button was clicked but the window stayed open, so close such a window yourself.

Keep one IsCancel button per window. With two, Esc clicked neither: it moved the focus to the first one, and pressing it again moved the focus to the second.

IsDefault makes Enter click the button. With the focus in a TextBox, Enter clicked it, and its IsDefaulted was True. With the focus in a TextBox that has AcceptsReturn="True", Enter added a line break and did not click it. With the focus on another button, Enter clicked that button instead, and IsDefaulted was False.

case measured
in a UserControl, focus in a TextBox: Esc -> clicked / window still open IsCancel / True
Enter -> clicked (IsDefaulted before the key) IsDefault (True)
focus in a TextBox with AcceptsReturn: Enter -> clicked / line breaks in the text (none) / 1
focus on another Button: Enter -> clicked (IsDefaulted of the IsDefault button) Other (False)
two IsCancel buttons: Esc -> clicked / keyboard focus (none) / Cancel 1
Esc again -> clicked / keyboard focus (none) / Cancel 2
IsCancel in a window shown with ShowDialog: Esc -> window closed / ShowDialog returned True / False

IsCancel and IsDefault by where the focus is and how the window was opened. Measured on .NET 10 / Windows 11.

When a click happens: ClickMode and IsPressed

ClickMode comes from ButtonBase. With a real mouse, Release (the default) clicked when the mouse button was released. If the pointer was moved off the button before the mouse button was released, IsPressed became False and there was no click. Press clicked when the mouse button went down. Hover clicked as soon as the pointer entered the button, without any mouse button.

The keyboard follows different rules. Space clicked on key up with Release and on key down with Press, and Enter clicked on key down with both. A Hover button cannot be clicked from the keyboard at all: Space and Enter did nothing. Do not use ClickMode="Hover" for buttons that must work from the keyboard.

IsPressed is read-only. It was True while the mouse button was held down on the button and while Space was held down, and False after release. A Hover button was pressed while the pointer was over it, but the keyboard does not press it: IsPressed stayed False while Space was held down.

case measured
default ClickMode / IsPressed is read-only Release / True
Hover (mouse): pointer enters / pointer leaves Hover, IsPressed True / (none), IsPressed False
Press (mouse): button down / button up Press, IsPressed True / (none), IsPressed False
Release (mouse): button down / button up (none), IsPressed True / Release, IsPressed False
Release (mouse): down on the button, moved off, released outside IsPressed False after moving off / (none)
Release (keyboard): Space down / Space up / Enter down (none), IsPressed True / Release, IsPressed False / Release
Press (keyboard): Space down / Space up / Enter down Press, IsPressed True / (none), IsPressed False / Press
Hover (keyboard): Space down / Space up / Enter down (none), IsPressed False / (none), IsPressed False / (none)

ClickMode and IsPressed with a real mouse and with the keyboard. Measured on .NET 10 / Windows 11.

Commands: what enables the button, and when it is checked again

While a bound command’s CanExecute returned false, the button’s IsEnabled was False, even with IsEnabled="True" set on the button. Let CanExecute decide whether the button is enabled.

The demo app’s RelayCommand passes CanExecuteChanged on to CommandManager.RequerySuggested. With that, the button did not follow a change in CanExecute’s result until CommandManager.InvalidateRequerySuggested was called, or until something such as a focus change made WPF check again. That check asks every button again: with 20 buttons bound to the same command, one focus change called CanExecute 20 times, so keep CanExecute cheap.

CommandParameter is the value passed to CanExecute and Execute. The demo app binds it to a TextBox’s Text, after Command. With that XAML, both CanExecute calls during loading received ShowMessageText, never null. A change of the parameter is checked at once: when the text was cleared from code, CanExecute ran once and the button became disabled, with no InvalidateRequerySuggested. Execute received the text at the time of the click. In a list, a button in an item template can pass its item: with CommandParameter="{Binding}", a real click on the button of the second row passed Row 2 to Execute.

case measured
CanExecute false: IsEnabled / with IsEnabled="True" set False / False
CanExecute turns true: IsEnabled / after InvalidateRequerySuggested False / True
20 buttons, same command: CanExecute calls per focus change 20
demo XAML: parameters passed to CanExecute while loading ShowMessageText, ShowMessageText
TextBox.Text set to empty from code: CanExecute calls / IsEnabled 1 / False
TextBox.Text set to Hello, then clicked: parameter of Execute Hello
item template, CommandParameter="{Binding}": real click on the second row: Execute received Row 2

Command and CommandParameter. Measured on .NET 10 / Windows 11.

Content, access keys, and the template

Button derives from ButtonBase, which derives from ContentControl, so its Content can be text, an image, or a panel. The default template shows the content with a ContentPresenter whose RecognizesAccessKey is True: with Content="_Save", S became the access key and the UI Automation name was Save. A button that shows only an image had an empty UI Automation name; give it an AutomationProperties.Name.

The default template has no visual state groups. It changes its look with triggers on IsDefaulted, IsMouseOver, IsPressed, and IsChecked (all true) and on IsEnabled = false.

case measured
base classes ButtonBase -> ContentControl
default template: VisualStateGroups / triggers 0 / IsDefaulted=True, IsMouseOver=True, IsPressed=True, IsChecked=True, IsEnabled=False
ContentPresenter.RecognizesAccessKey True
UI Automation name: Content "Save" Save
Content "_Save" Save (access key S)
Content is an Image (empty)
an Image with AutomationProperties.Name="Save" Save

Base classes, the default template, and UI Automation names. Measured on .NET 10 / Windows 11.

Trying it in the demo app

The Button page of the demo app, with the control list on the left and the first section, IsCancel / IsDefault

The Button page of the demo app has sections for IsCancel and IsDefault, ClickMode, IsPressed, Command, and CommandParameter. The “Show Code” link under each section displays its XAML. The following XAML is the CommandParameter section (ButtonUsageControl.xaml), with the styles and the surrounding GroupBoxes left out and the namespace declarations added:

<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <TextBox x:Name="CommandParameterText" Text="ShowMessageText" />
  <Button x:Name="ClickWithParameterCommandButton"
          Command="{Binding ClickWithParameterCommand}"
          CommandParameter="{Binding Text, ElementName=CommandParameterText}"
          Content="ShowMessageBox" />
</StackPanel>

The command is defined in the view model (ButtonUsageViewModel.cs):

public ICommand ClickWithParameterCommand { get; } =
    new RelayCommand(param => MessageBox.Show(param?.ToString()), null);

Source code and how it was measured

Every behavior on this page was measured by running it on .NET 10 / Windows 11, using ButtonDemoScene in this site’s screenshot tool. Keys were sent through WPF’s input manager, the same path as keys typed on a keyboard, so that IsCancel and IsDefault, which are handled there, respond to them. ClickMode was measured by moving and clicking the real mouse over the measuring window. The click with a CommandParameter from a TextBox was made through UI Automation’s Invoke, and the click on a row’s button in an item template with the real mouse.

View Button source code on GitHub →