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

  1. WPF versus Windows Forms: declarative XAML and UI/logic separation
  2. Basic XAML syntax: elements, attributes, and the control tree
  3. The MVVM pattern at an introductory level
  4. INotifyPropertyChanged: notifying the interface of changes
  5. Data binding: connecting XAML to the ViewModel
  6. Basic ICommand: commands instead of Click events
  7. DataContext: who provides the data to the view
  8. Complete example: BiblioTech's catalog with ObservableCollection and a ViewModel

  1. 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.

  1. 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.

  1. 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.

  1. INotifyPropertyChanged: notifying the interface of changes

For 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.

  1. 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):

<TextBlock Text="{Binding StatusMessage}" />

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:

<ListBox ItemsSource="{Binding Catalog}"
         DisplayMemberPath="Title" />

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.

  1. Basic ICommand: commands instead of Click events

In 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:

<Button Content="Lend" Command="{Binding LendCommand}" />

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.

  1. DataContext: who provides the data to the view

DataContext 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.

  1. Complete example: BiblioTech's catalog with ObservableCollection and a ViewModel

Putting 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 to InitializeComponent() and assigning DataContext.
  • Forgetting PropertyChanged?.Invoke(...) in a bound property's set: 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 of ObservableCollection<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; the ListBox would 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 current DataContext — a silent error at compile time, but visible at run time.

Exercises

  1. Add a read-only property int TotalAvailable to CatalogViewModel (computed from Catalog.Count(item => item.Available)) and a TextBlock in the XAML that shows it with {Binding TotalAvailable}. Update it (with its own PropertyChanged) every time LendSelectedItem runs.

  2. 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.

© Copyright 2026. All rights reserved