The previous lesson connected BiblioTech to Windows Forms, placing controls with a visual
designer that generates imperative C# code behind the scenes. WPF (Windows Presentation
Foundation), also exclusive to Windows, solves the same problem — a desktop interface — with a
different philosophy: the interface is described declaratively in a markup language called
XAML, disciplined and separated from the application logic thanks to the MVVM pattern
(Model-View-ViewModel) and data binding. This lesson presents basic XAML syntax, introduces
MVVM at an introductory level, and rebuilds the same lending scenario from the previous lesson —
now with a list bound via data binding to Library's catalog — so you can compare both
approaches directly.
Contents
- WPF versus Windows Forms: declarative XAML and UI/logic separation
- Basic XAML syntax: elements, attributes, and the control tree
- The MVVM pattern at an introductory level
INotifyPropertyChanged: notifying the interface of changes- Data binding: connecting XAML to the ViewModel
- Basic
ICommand: commands instead ofClickevents DataContext: who provides the data to the view- Complete example: BiblioTech's catalog with
ObservableCollectionand a ViewModel
- WPF versus Windows Forms: declarative XAML and UI/logic separation
Windows Forms describes the interface imperatively: C# code that creates Button, ListBox
objects, sets their properties one by one, and adds them to Controls. WPF describes the
interface declaratively: a XAML file (interface XML) lists which controls exist and how they
relate to each other, with no need for C# code to build the visual tree.
| Windows Forms | WPF | |
|---|---|---|
| How the interface is described | Imperative C# code (generated by the designer) | Declarative XAML |
| Connection with data | Manual: reading/writing control properties by hand | Data binding: the interface updates itself when the data changes |
| UI/logic separation | Weak; event-handling code tends to get mixed in with logic | Strong with MVVM: the view (XAML) doesn't know the logic, only the binding |
| Visual styles and templates | Limited | Very flexible (styles, templates, animations) |
| Platform | Windows only | Windows only |
The deepest difference isn't visual, but architectural: in Windows Forms, it's common for the
form's code to read catalogListBox.SelectedItem directly whenever it needs it; in WPF with
MVVM, the view (XAML) never reads or writes data on its own — it only declares bindings — and
it's the binding that keeps the view synchronized with the ViewModel automatically, in both
directions if needed.
- Basic XAML syntax: elements, attributes, and the control tree
XAML is an XML dialect: each control is an element, and its properties are attributes (or child elements, for complex values). A minimal example:
<Window x:Class="BiblioTech.Desktop.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="BiblioTech" Height="450" Width="600">
<StackPanel Margin="10">
<TextBlock Text="BiblioTech Catalog" FontSize="18" FontWeight="Bold" />
<ListBox Name="catalogListBox" Height="250" />
<Button Content="Lend" Width="100" HorizontalAlignment="Left" Margin="0,10,0,0" />
</StackPanel>
</Window>Each piece plays a role:
| Element/attribute | Role |
|---|---|
<Window> |
The root window, equivalent to Windows Forms' Form |
xmlns="..." |
Namespace that defines the vocabulary of available WPF controls |
xmlns:x="..." |
Namespace of the XAML language itself (x:Class, x:Name...) |
<StackPanel> |
A layout container: stacks its children vertically (or horizontally with Orientation="Horizontal") |
<TextBlock> |
Equivalent to Label in Windows Forms: non-editable text |
Name="catalogListBox" |
Gives the control a name so it can be referenced from C#, if needed |
WPF offers several layout containers (StackPanel, Grid, DockPanel...) that automatically
organize their children, unlike Windows Forms, where each control carries an absolute position
(Location, Size). Every .xaml file has an associated .xaml.cs file (code-behind) with
the same class name (x:Class="BiblioTech.Desktop.MainWindow"), where C# code can live, although
the goal of MVVM (section 3) is to keep that code-behind practically empty.
- The MVVM pattern at an introductory level
MVVM (Model-View-ViewModel) organizes a WPF application into three layers with separated responsibilities:
flowchart LR
M["Model<br/>(Library, Book, Member...)"] <--> VM["ViewModel<br/>(properties + commands for the view)"]
VM <-->|Data binding| V["View<br/>(XAML: MainWindow.xaml)"]
| Layer | What it is in BiblioTech | Responsibility |
|---|---|---|
| Model | Library, LibraryItem, Book, Member (Module 2) |
The domain and business logic, with no knowledge of interfaces |
| View | The .xaml (MainWindow.xaml) |
Only declares controls and bindings; contains no business logic and never accesses the Model directly |
| ViewModel | A new class (CatalogViewModel, section 8) |
Exposes the Model in a form the View can consume via binding, and translates the user's actions (commands) into calls to the Model |
The central idea: the View never talks directly to the Model. Everything goes through the ViewModel, which acts as an intermediary. This brings one concrete, verifiable benefit: the ViewModel can be tested with automated tests with no need to open a single window (something picked up in more detail in the Unit Testing lesson of Module 8), because it doesn't depend on any specific visual control.
INotifyPropertyChanged: notifying the interface of changes
INotifyPropertyChanged: notifying the interface of changesFor the view to update automatically when a ViewModel property changes, that property must
announce the change through the INotifyPropertyChanged interface:
using System.ComponentModel;
class CatalogViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private string _statusMessage = string.Empty;
public string StatusMessage
{
get => _statusMessage;
set
{
_statusMessage = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StatusMessage)));
}
}
}PropertyChanged is an event (the same mechanism from Module 4, and the same one behind
Library.LoanRegistered); when it's invoked with the property's name (nameof(StatusMessage)),
WPF — which has internally subscribed to this event as soon as it detects a binding on
StatusMessage — reads the value again and updates the view, with no need for the programmer to
touch any control by hand. nameof(...) (already used in earlier modules) avoids writing the
property name as a literal string, with the risk of it getting out of sync if the property is
renamed.
- Data binding: connecting XAML to the ViewModel
Data binding is the XAML syntax {Binding PropertyName}, which connects a control's property
to a property of the object assigned to DataContext (section 7):
This line completely replaces manually writing label.Text = "..."; in C# (as was done in
Windows Forms): as soon as CatalogViewModel.StatusMessage changes and raises PropertyChanged,
WPF updates the TextBlock automatically, with no additional line of code. For full lists,
ItemsControl (and its variants ListBox, ListView, DataGrid) are bound with ItemsSource:
DisplayMemberPath="Title" indicates which property of each element in Catalog to show as text
in the list — equivalent to manually composing the text inside the foreach loop that was used
in Windows Forms to fill the ListBox.
- Basic
ICommand: commands instead of Click events
ICommand: commands instead of Click eventsIn Windows Forms, a button is wired with lendButton.Click += LendButton_Click; (code in the
code-behind). In WPF with MVVM, a button is bound to a command on the ViewModel, with no
code at all in the code-behind:
ICommand is the interface representing an action invokable from the view:
using System.Windows.Input;
class RelayCommand : ICommand
{
private readonly Action _action;
public RelayCommand(Action action)
{
_action = action;
}
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parameter) => true; // simplification: always executable
public void Execute(object? parameter) => _action();
}RelayCommand is a minimal, reusable implementation of ICommand (in real projects it usually
comes ready-made in third-party libraries, such as RelayCommand from
CommunityToolkit.Mvvm, but it's written by hand here to see exactly what it does): it wraps any
Action in an object that WPF knows how to invoke when the user clicks the bound button.
CanExecute allows, if desired, disabling the button automatically when the action isn't valid
(for example, with no book selected) — here simplified to always true, given the lesson's scope.
DataContext: who provides the data to the view
DataContext: who provides the data to the viewDataContext is the property that indicates which object all the {Binding ...} in a view
(and its child controls, by inheritance) refer to. It's typically assigned in the code-behind
constructor, in the one line that usually remains there with MVVM:
// MainWindow.xaml.cs (code-behind, nearly empty with MVVM)
public partial class MainWindow : Window
{
public MainWindow(Library library)
{
InitializeComponent();
DataContext = new CatalogViewModel(library); // every {Binding ...} in the XAML points here
}
}Once DataContext holds a CatalogViewModel instance, {Binding StatusMessage} resolves to
((CatalogViewModel)DataContext).StatusMessage, and {Binding Catalog} to
((CatalogViewModel)DataContext).Catalog. It's the piece that closes the full circuit between
XAML and ViewModel: without assigning DataContext, none of the view's bindings would have
anything to read from.
- Complete example: BiblioTech's catalog with
ObservableCollection and a ViewModel
ObservableCollection and a ViewModelPutting all the previous pieces together, the same scenario from the previous lesson — showing the catalog and lending a book with one click — is rebuilt with WPF's MVVM approach:
// CatalogViewModel.cs
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Input;
class CatalogViewModel : INotifyPropertyChanged
{
private readonly Library _library;
public ObservableCollection<LibraryItem> Catalog { get; }
public LibraryItem? SelectedItem { get; set; }
private string _statusMessage = string.Empty;
public string StatusMessage
{
get => _statusMessage;
set
{
_statusMessage = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StatusMessage)));
}
}
public ICommand LendCommand { get; }
public event PropertyChangedEventHandler? PropertyChanged;
public CatalogViewModel(Library library)
{
_library = library;
// ObservableCollection wraps the catalog: it notifies the view on its own if items are added/removed
Catalog = new ObservableCollection<LibraryItem>(_library.Catalog);
LendCommand = new RelayCommand(LendSelectedItem);
}
private void LendSelectedItem()
{
if (SelectedItem is null)
{
StatusMessage = "Select a book from the list before lending it.";
return;
}
if (!SelectedItem.Available)
{
StatusMessage = $"'{SelectedItem.Title}' is already on loan.";
return;
}
SelectedItem.Lend(); // domain logic that already exists, Module 2
StatusMessage = $"Loan registered: {SelectedItem.Title}";
}
}<!-- MainWindow.xaml -->
<Window x:Class="BiblioTech.Desktop.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="BiblioTech" Height="450" Width="600">
<StackPanel Margin="10">
<TextBlock Text="BiblioTech Catalog" FontSize="18" FontWeight="Bold" />
<ListBox ItemsSource="{Binding Catalog}"
DisplayMemberPath="Title"
SelectedItem="{Binding SelectedItem}"
Height="250" />
<Button Content="Lend"
Command="{Binding LendCommand}"
Width="100" HorizontalAlignment="Left" Margin="0,10,0,0" />
<TextBlock Text="{Binding StatusMessage}" Margin="0,10,0,0" />
</StackPanel>
</Window>SelectedItem="{Binding SelectedItem}" is a two-way binding: when the user selects an item
in the ListBox, WPF automatically writes SelectedItem on the ViewModel; no SelectionChanged
event needs to be handled by hand, as it would have in Windows Forms. Note also that
ObservableCollection<T> (from System.Collections.ObjectModel) is like List<T> but
automatically notifies the view if items are added to or removed from the collection — no such
additional notification is needed here because the catalog doesn't change size, only the internal
state of its elements changes, reflected through StatusMessage.
Common Mistakes and Tips
- Writing business logic in the code-behind (
.xaml.cs): this breaks MVVM separation; the practical rule is that the code-behind should almost always be limited toInitializeComponent()and assigningDataContext. - Forgetting
PropertyChanged?.Invoke(...)in a bound property'sset: without that notification, the view never learns of the change and keeps showing the old value, even though the ViewModel already holds the new value internally. - Using
List<T>instead ofObservableCollection<T>when the collection can grow or shrink dynamically:List<T>doesn't notify the view if items are added or removed after the initial binding is assigned; theListBoxwould end up out of date. - Tip: XAML lets you debug broken bindings by checking the output/debug window, which
usually shows a warning when a
{Binding PropertyName}can't find that property on the currentDataContext— a silent error at compile time, but visible at run time.
Exercises
-
Add a read-only property
int TotalAvailabletoCatalogViewModel(computed fromCatalog.Count(item => item.Available)) and aTextBlockin the XAML that shows it with{Binding TotalAvailable}. Update it (with its ownPropertyChanged) every timeLendSelectedItemruns. -
Explain, in a short paragraph, why in MVVM the View should never access the Model (
Library) directly, and what concrete benefit always going through the ViewModel brings.
Solutions
private int _totalAvailable;
public int TotalAvailable
{
get => _totalAvailable;
private set
{
_totalAvailable = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TotalAvailable)));
}
}
public CatalogViewModel(Library library)
{
_library = library;
Catalog = new ObservableCollection<LibraryItem>(_library.Catalog);
LendCommand = new RelayCommand(LendSelectedItem);
UpdateTotalAvailable();
}
private void UpdateTotalAvailable()
{
TotalAvailable = Catalog.Count(item => item.Available);
}
private void LendSelectedItem()
{
// ... existing code ...
SelectedItem.Lend();
StatusMessage = $"Loan registered: {SelectedItem.Title}";
UpdateTotalAvailable();
}
<TextBlock Text="{Binding TotalAvailable}" Margin="0,10,0,0" />
If the View accessed Library directly, it would become coupled to the domain's concrete
details (what each method is called, which checks need to happen before lending), and that
knowledge would have to be repeated in every view needing the same action. Always going through
the ViewModel centralizes that logic in one place, lets it be tested with automated tests with
no window ever opened (it doesn't depend on any visual control), and lets the view change (for
example, replacing the ListBox with a DataGrid) without touching a single line of the
ViewModel or the Model.
Conclusion
In this lesson you've seen how WPF replaces Windows Forms's imperative approach with declarative
XAML, and how the MVVM pattern disciplines the separation between the view (XAML) and the logic
(ViewModel), communicating through data binding and INotifyPropertyChanged. The same lending
scenario from the previous lesson — showing the catalog, lending a book — has been rebuilt here
with not a single Click event handled by hand in the code-behind, with ICommand and
ObservableCollection doing the synchronization work automatically.
The next two lessons change platform: ASP.NET Core takes BiblioTech to the web, exposing its logic as an API that any HTTP client can consume — including, later on, a web version of this same catalog with Blazor, which reuses much of the data binding and component ideas already covered here, but running in a browser instead of a desktop window.
C# Programming Course
Module 1: Introduction to C#
- Introduction to C#
- Setting Up the Development Environment
- Hello World Program
- Basic Syntax and Structure
- Variables and Data Types
- Arrays and Strings
Module 2: Control Structures
Module 3: Object-Oriented Programming
- Classes and Objects
- Methods
- Constructors and Destructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- Structs and Records: Value Types and Reference Types
Module 4: Advanced C# Concepts
- Interfaces
- Delegates and Events
- Pattern Matching and Modern C# Features
- Generics
- Collections
- LINQ (Language Integrated Query)
- Asynchronous Programming
Module 5: Working with Data
- File I/O
- Serialization
- Database Connectivity
- Entity Framework
- Working with JSON and Consuming REST APIs
Module 6: Advanced Topics
- Reflection
- Attributes
- Dynamic Programming
- Memory Management and Garbage Collection
- Multithreading and Parallel Programming
Module 7: Building Applications
Module 8: Best Practices and Design Patterns
- Coding Standards and Best Practices
- Design Patterns
- Dependency Injection and Inversion of Control
- Unit Testing
- Code Review and Refactoring
