This is Part 1 of a five-part series covering WPF, MVVM, Razor, Blazor, and Bootstrap, each genuinely new territory, explained from the ground up. The example running through this post is a small Task Tracker desktop app, add a task, mark it complete, delete it, filter by status (All, Active, Completed). It's built here using plain, direct code-behind, deliberately without MVVM, the next post in the series takes this exact same app and refactors it into proper MVVM, once the specific problems with this version are visible firsthand rather than described abstractly.
What WPF Actually Is
WPF (Windows Presentation Foundation) is a framework for building desktop applications with rich, flexible user interfaces, using C# for logic and a separate markup language, XAML, to describe the visual layout.
Think of building a house. XAML is the architectural blueprint, it describes where the rooms are, how big the windows are, what the layout looks like. C# code-behind is the electrician and plumber, it makes things actually do something, a light switch that turns on a light, a button that actually adds a task, once the structure is in place.
XAML: The Markup Language, Explained
XAML (eXtensible Application Markup Language) is a declarative way to describe a user interface, what controls exist, how they're arranged, and their visual properties, separately from the C# code that gives them behavior.
<Window x:Class="TaskTracker.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Task Tracker" Height="450" Width="400">
<Grid>
<TextBox x:Name="TaskInput" />
<Button Content="Add Task" Click="AddTask_Click" />
</Grid>
</Window>
<!-- x:Name gives an element an identifier that C#
code-behind can reference directly (TaskInput.Text,
for example)
Click="AddTask_Click" wires this button to a specific
C# METHOD, written in the paired code-behind file -->
How this compares to something more familiar: XAML plays a similar role to HTML, describing structure and content declaratively, but it's specifically for a desktop window, not a browser page, and its tags map to actual .NET classes, a Button in XAML corresponds directly to the System.Windows.Controls.Button class, rather than browser-rendered HTML elements.
Windows and Controls
A Window is the actual application window itself, the outermost container. Everything else, buttons, text boxes, lists, are controls placed inside it.
- TextBox is a single-line text input, where a new task's description gets typed.
- Button is a clickable element, triggering a specific action when clicked, adding a task, deleting one.
- ListBox displays a scrollable list of items, shows every task currently in the tracker.
- CheckBox is a toggle, marks a specific task as complete or not.
Layout Panels: Three Different Philosophies
A layout panel controls how its child elements are actually arranged on screen. WPF offers several, each with a genuinely different approach.
StackPanel stacks children one after another, vertically by default or horizontally, the simplest layout, good for a straightforward list of elements in a row or column.
<StackPanel Orientation="Horizontal">
<TextBox x:Name="TaskInput" Width="200" />
<Button Content="Add" Click="AddTask_Click" />
</StackPanel>
Grid arranges children in rows and columns, like a table, giving precise control over exactly where each element sits.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">...</StackPanel>
<ListBox Grid.Row="1" x:Name="TaskListBox" />
</Grid>
DockPanel has children that "dock" to a specific edge, top, bottom, left, right, with the last child typically filling whatever space remains.
<DockPanel>
<StackPanel DockPanel.Dock="Top">...</StackPanel>
<ListBox DockPanel.Dock="Bottom" />
</DockPanel>
Think of StackPanel like stacking books on a shelf, one after another. Grid is like a spreadsheet, with precise rows and columns. DockPanel is like sticky notes pressed to the edges of a whiteboard, with whatever's left in the middle.
Event Handling in Code-Behind
"Code-behind" refers to the C# file paired directly with a XAML file, MainWindow.xaml.cs, paired with MainWindow.xaml, this is where the actual logic lives, referencing controls from the XAML by their x:Name.
// MainWindow.xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void AddTask_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(TaskInput.Text))
{
TaskListBox.Items.Add(TaskInput.Text);
TaskInput.Clear();
}
}
}
// AddTask_Click is wired directly to the Button's
// Click event in the XAML (Click="AddTask_Click").
// This method reaches DIRECTLY into TaskListBox and
// TaskInput - the controls defined in the XAML file -
// by their x:Name
Building the Complete Task Tracker
Here's the full XAML:
<Window x:Class="TaskTracker.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Task Tracker" Height="450" Width="400">
<DockPanel Margin="10">
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,10">
<TextBox x:Name="TaskInput" Width="250" Margin="0,0,10,0" />
<Button Content="Add Task" Click="AddTask_Click" />
</StackPanel>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="All" Click="ShowAll_Click" Margin="5" />
<Button Content="Active" Click="ShowActive_Click" Margin="5" />
<Button Content="Completed" Click="ShowCompleted_Click" Margin="5" />
</StackPanel>
<ListBox x:Name="TaskListBox">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding IsCompleted}"
Checked="TaskCheckBox_Changed"
Unchecked="TaskCheckBox_Changed"
Tag="{Binding}" />
<TextBlock Text="{Binding Description}" Margin="10,0,0,0" />
<Button Content="Delete" Click="DeleteTask_Click" Tag="{Binding}" Margin="20,0,0,0" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Window>
And the full code-behind:
public partial class MainWindow : Window
{
private List<TaskItem> _allTasks = new List<TaskItem>();
public MainWindow()
{
InitializeComponent();
}
private void AddTask_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(TaskInput.Text))
{
var task = new TaskItem { Description = TaskInput.Text, IsCompleted = false };
_allTasks.Add(task);
TaskInput.Clear();
RefreshList(_allTasks);
}
}
private void DeleteTask_Click(object sender, RoutedEventArgs e)
{
var button = (Button)sender;
var task = (TaskItem)button.Tag;
_allTasks.Remove(task);
RefreshList(_allTasks);
}
private void TaskCheckBox_Changed(object sender, RoutedEventArgs e)
{
var checkBox = (CheckBox)sender;
var task = (TaskItem)checkBox.Tag;
task.IsCompleted = checkBox.IsChecked ?? false;
}
private void ShowAll_Click(object sender, RoutedEventArgs e)
=> RefreshList(_allTasks);
private void ShowActive_Click(object sender, RoutedEventArgs e)
=> RefreshList(_allTasks.Where(t => !t.IsCompleted).ToList());
private void ShowCompleted_Click(object sender, RoutedEventArgs e)
=> RefreshList(_allTasks.Where(t => t.IsCompleted).ToList());
private void RefreshList(List<TaskItem> tasks)
{
TaskListBox.ItemsSource = null;
TaskListBox.ItemsSource = tasks;
}
}
public class TaskItem
{
public string Description { get; set; }
public bool IsCompleted { get; set; }
}
The Pain Points Worth Noticing (Setting Up the Next Post)
This app genuinely works, but a few things are worth sitting with honestly, since they're exactly what the next post's MVVM refactor addresses directly.
Every single interaction is handled by code directly reaching into UI controls, TaskListBox.ItemsSource, TaskInput.Text, TaskInput.Clear(), the "business logic," what a task is, how filtering works, is tangled together with UI manipulation code in the exact same methods.
RefreshList() has to be called manually, by hand, after every single change, add a task, refresh manually; delete a task, refresh manually; check a box, forgot to refresh at all, the checkbox handler above doesn't call it, so the Completed filter won't reflect a just-checked box until you click a filter button again, a genuinely easy bug to introduce accidentally.
There is no way to unit test any of this logic in isolation, testing whether filtering to Active works correctly requires actually running the full WPF application and clicking buttons, since the filtering logic lives directly inside a Click event handler, tightly coupled to the UI itself.
Key Lessons
XAML is a declarative markup language describing WPF UI structure, playing a similar role to HTML but mapping directly to .NET classes rather than browser elements.
StackPanel, Grid, and DockPanel represent three genuinely different layout philosophies, stacking, table-like precision, and edge-docking, each suited to different UI shapes.
Code-behind directly wires XAML events to C# methods via x:Name references, straightforward, and genuinely workable for a small app like this one.
The specific pain in this version isn't that it's wrong, it's that business logic and UI manipulation code live in the exact same place, making the app harder to test and easier to introduce subtle bugs into, like the missed refresh on checkbox change, as it grows.
What's Next
Part 2 of this series takes this exact Task Tracker and refactors it into proper MVVM, separating what a task is and how filtering works, the ViewModel, from how it's actually displayed, the View, fixing the manual-refresh problem and making the filtering logic genuinely unit-testable for the first time.
Summary
WPF describes a desktop UI declaratively through XAML, with C# code-behind providing the actual behavior, referencing controls directly by name. Layout panels, StackPanel, Grid, DockPanel, offer different ways to arrange those controls on screen. The Task Tracker built here genuinely works, but every interaction reaches directly into UI controls from within event handlers, tangling business logic with UI manipulation, which is precisely the shape of problem MVVM, covered in the next post, exists to solve.
More from TechStack Blog: C# / .NET: https://www.techstackblog.com/category.html?cat=csharp
CS Fundamentals: https://www.techstackblog.com/category.html?cat=cs-fundamentals
Top comments (0)