Learn how the code-behind file works, how to hook up event handlers in XAML and in C#, what sender and RoutedEventArgs are, and how routed events bubble up the tree.

Bishal Biswas
Self Learner
What code-behind is
Every window in WPF has two files. The XAML file describes the look, and the code-behind file (the .xaml.cs file next to it) holds the C# that runs when something happens. You met both in Your First WPF App.
Things that happen in an app are called events: a click, a key press, the text in a box changing, the mouse moving over a button. A method that runs in response to an event is called an event handler.
This lesson builds a small click counter app and uses it to show how events work in WPF.
+-------------------------------+
| Clicks: 3 |
| [ +1 ] [ -1 ] [ Reset ] |
| Step: [ 1 ] |
| Last action: +1 |
+-------------------------------+
Hooking up an event in XAML
The easiest way to handle an event is to name the handler method right in XAML:
<Button Content="+1" Click="Increase_Click" />
Then write a method with that exact name in the code-behind:
private int _count;
private void Increase_Click(object sender, RoutedEventArgs e)
{
_count++;
CountText.Text = $"Clicks: {_count}";
}
When you type Click=" in Visual Studio, it offers to create the handler for you. Press Tab and the empty method appears in the code-behind.
The field _count keeps its value between clicks because it lives on the window object, which stays alive as long as the window is open. If fields are new to you, see fields and properties.
The handler signature
Almost every WPF event handler looks like this:
private void SomeName(object sender, SomeEventArgs e)
senderis the object whose handler is running. For a button click it is the button.eholds extra details about the event. Its type depends on the event:RoutedEventArgsforClick,KeyEventArgsforKeyDown,TextChangedEventArgsforTextChanged, and so on.- The return type is
void. The method is usuallyprivate, because only this window needs it.
The handler name is up to you. The common style is ControlName_EventName, like SaveButton_Click. Whatever you pick, the XAML and C# must match exactly.
Using sender
sender lets one handler serve several controls. Here three buttons share one method and the handler reads the button’s Tag, a spare property you can use for any small value:
<Button Content="+1" Tag="1" Click="Change_Click" />
<Button Content="+5" Tag="5" Click="Change_Click" />
<Button Content="-1" Tag="-1" Click="Change_Click" />
private void Change_Click(object sender, RoutedEventArgs e)
{
var button = (Button)sender;
int amount = int.Parse((string)button.Tag);
_count += amount;
CountText.Text = $"Clicks: {_count}";
}
sender has the type object, so you cast it (convert it) to Button to use button properties.
Hooking up an event in C#
You can also attach a handler in code with +=. This is the normal C# way to subscribe to an event, as covered in the events lesson:
public MainWindow()
{
InitializeComponent();
ResetButton.Click += ResetButton_Click;
}
private void ResetButton_Click(object sender, RoutedEventArgs e)
{
_count = 0;
CountText.Text = "Clicks: 0";
}
For tiny handlers, a lambda expression is shorter:
ResetButton.Click += (s, e) => CountText.Text = "Clicks: 0";
Attach handlers after InitializeComponent(). Before that line, ResetButton is still null.
To stop listening, use -= with the same method:
ResetButton.Click -= ResetButton_Click;
Which way is better? Use XAML for most cases, because the link is visible right where the control is. Use C# when you decide at run time whether to listen, or when the control is created in code.
Events you will use a lot
Button.Click The button was clicked (or Enter / Space pressed on it)
TextBox.TextChanged The text changed, on every key stroke
ComboBox.SelectionChanged A different item was picked
CheckBox.Checked The box was ticked (Unchecked for the opposite)
UIElement.KeyDown A key was pressed while the control had focus
UIElement.MouseEnter The mouse moved over the control (MouseLeave when it leaves)
Window.Loaded The window is on screen and ready
Here is TextChanged in action. It checks the step box as the user types:
<TextBox x:Name="StepBox" Text="1" Width="60" TextChanged="StepBox_TextChanged" />
private void StepBox_TextChanged(object sender, TextChangedEventArgs e)
{
bool ok = int.TryParse(StepBox.Text, out _);
StepBox.Background = ok ? Brushes.White : Brushes.MistyRose;
}
And KeyDown lets the user press Up and Down arrow keys instead of clicking:
private void Window_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Up) { _count++; }
if (e.Key == Key.Down) { _count--; }
CountText.Text = $"Clicks: {_count}";
}
Routed events: events that travel
Here is something that makes WPF different from many other frameworks. Most WPF events are routed events. A routed event does not just fire on one control. It travels through the tree of elements.
Picture a Button inside a StackPanel inside a Window. When you click the button, the Click event is raised on the button first, then on the StackPanel, then on the Window. It bubbles up, like a bubble rising through water.
Window 3. then here
StackPanel 2. then here
Button 1. the click starts here
That means a parent can handle an event for all its children at once. This XAML puts one handler on the StackPanel and none on the buttons:
<StackPanel ButtonBase.Click="AnyButton_Click" Orientation="Horizontal">
<Button Content="+1" Tag="1" />
<Button Content="-1" Tag="-1" />
<Button Content="Reset" Tag="0" />
</StackPanel>
ButtonBase.Click says “listen for the Click event that comes from any button inside me”. ButtonBase is the base class all buttons share.
sender versus e.Source
In a bubbling handler, sender and e.Source are different:
senderis the element whose handler is running (here, theStackPanel).e.Sourceis the element where the event started (here, the button that was clicked).
private void AnyButton_Click(object sender, RoutedEventArgs e)
{
if (e.Source is Button button)
{
LastActionText.Text = $"Last action: {button.Content}";
}
}
Stopping the bubble with e.Handled
Set e.Handled = true to say “I dealt with this, stop here”. Handlers further up the tree will not run (for normal handlers):
private void Increase_Click(object sender, RoutedEventArgs e)
{
_count++;
e.Handled = true; // the StackPanel and Window will not see this click
}
Preview events
Many events have a twin that starts with Preview, like PreviewKeyDown and PreviewMouseDown. These travel the other way, from the window down to the control. This is called tunnelling. A preview handler runs first, so it can block input before the control sees it. For example, a PreviewTextInput handler can reject letters in a number box. You will rarely need these as a beginner, but it is good to know the name.
You will learn more about the element tree in Logical and Visual Trees.
How much code should live in code-behind?
Code-behind is perfect for learning and for code that is truly about the screen, like moving focus or starting an animation. But if you put all your app’s logic there, the file grows huge and is hard to test.
Later in the course you will move most logic into view models using the MVVM pattern, and buttons will use commands instead of Click handlers. See Introduction to MVVM and Commands and ICommand. For now, code-behind is exactly the right tool.
Common mistakes
- Name mismatch.
Click="Increase_click"with a method calledIncrease_Clickfails to build. Capital letters matter. - Wrong parameter types. A
TextChangedhandler needsTextChangedEventArgs, notRoutedEventArgs. The build error tells you the expected type. - Touching controls before
InitializeComponent(). They are stillnull, so you get aNullReferenceException. - Forgetting that
TextChangedfires on every key. Do not do slow work in it, like saving a file. - Deleting a handler from C# but not from XAML. The build fails because XAML still points at the old method.
- TextChanged firing during start up. If you set
Text="1"in XAML,TextChangedcan run while the window is still loading, before other named controls exist. Check fornullor useIsLoadedwhen that matters.
Quick recap
- Code-behind is the
.xaml.csfile, and event handlers are methods that run when something happens. - Hook up handlers in XAML (
Click="Name") or in C# (button.Click += Name;). - Handlers take
object senderand an event args objectewhose type depends on the event. - One handler can serve many controls by checking
senderorTag. - Most WPF events are routed: they bubble up the tree, so a parent can handle its children’s events.
e.Sourceis where the event started, ande.Handled = truestops it from bubbling further.Previewevents tunnel down first and are useful for blocking input.
<!-- MainWindow.xaml -->
<Window x:Class="ClickCounter.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Click Counter" Width="360" SizeToContent="Height"
WindowStartupLocation="CenterScreen"
KeyDown="Window_KeyDown">
<StackPanel Margin="20">
<TextBlock x:Name="CountText" Text="Clicks: 0" FontSize="28" FontWeight="Bold" />
<!-- One handler on the panel catches Click from every button inside (bubbling) -->
<StackPanel Orientation="Horizontal" Margin="0,12,0,0"
ButtonBase.Click="AnyButton_Click">
<Button Content="+1" Tag="1" Width="60" Click="Change_Click" />
<Button Content="-1" Tag="-1" Width="60" Margin="8,0,0,0" Click="Change_Click" />
<!-- Reset is wired up in C# instead of XAML -->
<Button x:Name="ResetButton" Content="Reset" Width="70" Margin="8,0,0,0" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,12,0,0">
<TextBlock Text="Step:" VerticalAlignment="Center" Margin="0,0,8,0" />
<TextBox x:Name="StepBox" Text="1" Width="60" TextChanged="StepBox_TextChanged" />
</StackPanel>
<TextBlock x:Name="LastActionText" Text="Last action: none" Margin="0,12,0,0"
Foreground="Gray" />
<TextBlock Text="Tip: the Up and Down arrow keys work too." Margin="0,4,0,0"
Foreground="Gray" />
</StackPanel>
</Window>
// MainWindow.xaml.cs
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace ClickCounter;
public partial class MainWindow : Window
{
private int _count;
public MainWindow()
{
InitializeComponent();
// Attach a handler in C#, after InitializeComponent
ResetButton.Click += (s, e) =>
{
_count = 0;
ShowCount();
};
}
// Shared by +1 and -1: Tag holds the direction, StepBox holds the size
private void Change_Click(object sender, RoutedEventArgs e)
{
var button = (Button)sender;
int direction = int.Parse((string)button.Tag);
_count += direction * GetStep();
ShowCount();
}
// Bubbling handler on the StackPanel. e.Source is the button that was clicked.
private void AnyButton_Click(object sender, RoutedEventArgs e)
{
if (e.Source is Button button)
{
LastActionText.Text = $"Last action: {button.Content}";
}
}
// Colour the box pink when the step is not a whole number
private void StepBox_TextChanged(object sender, TextChangedEventArgs e)
{
bool ok = int.TryParse(StepBox.Text, out _);
StepBox.Background = ok ? Brushes.White : Brushes.MistyRose;
}
// Arrow keys change the count. Skip when typing in the step box.
private void Window_KeyDown(object sender, KeyEventArgs e)
{
if (e.OriginalSource is TextBox) return;
if (e.Key == Key.Up) { _count += GetStep(); ShowCount(); }
if (e.Key == Key.Down) { _count -= GetStep(); ShowCount(); }
}
private int GetStep() => int.TryParse(StepBox.Text, out int step) ? step : 1;
private void ShowCount() => CountText.Text = $"Clicks: {_count}";
}
Was this explanation helpful?
Your feedback helps us improve the content for everyone.
Thank you for your feedback!
We appreciate you helping us improve.
What could we improve? (Optional)
Read aloud
Ready