La lliçó anterior va connectar BiblioTech a Windows Forms, col·locant controls amb un dissenyador visual que genera codi C# imperatiu per darrere. WPF (Windows Presentation Foundation), també exclusiu de Windows, resol el mateix problema —una interfície d'escriptori— amb una filosofia diferent: la interfície es descriu de forma declarativa en un llenguatge de marcatge anomenat XAML, separada amb disciplina de la lògica de l'aplicació gràcies al patró MVVM (Model-View-ViewModel) i al data binding. Aquesta lliçó presenta la sintaxi bàsica de XAML, introdueix MVVM a nivell introductori, i reconstrueix el mateix escenari de préstec de la lliçó anterior —ara amb una llista enllaçada per data binding al catàleg de Biblioteca— perquè puguis comparar directament tots dos enfocaments.

Contingut

  1. WPF davant Windows Forms: XAML declaratiu i separació de UI/lògica
  2. Sintaxi XAML bàsica: elements, atributs i arbre de controls
  3. El patró MVVM a nivell introductori
  4. INotifyPropertyChanged: notificar canvis a la interfície
  5. Data binding: connectar XAML amb el ViewModel
  6. ICommand bàsic: comandaments en lloc d'esdeveniments Click
  7. DataContext: qui proveeix les dades a la vista
  8. Exemple complet: catàleg de BiblioTech amb ObservableCollection i ViewModel

  1. WPF davant Windows Forms: XAML declaratiu i separació de UI/lògica

Windows Forms descriu la interfície imperativament: codi C# que crea objectes Button, ListBox, en fixa les propietats una a una i els afegeix a Controls. WPF descriu la interfície declarativament: un fitxer XAML (XML d'interfície) enumera quins controls existeixen i com es relacionen, sense necessitat de codi C# per construir l'arbre visual.

Windows Forms WPF
Com es descriu la interfície Codi C# imperatiu (generat pel dissenyador) XAML declaratiu
Connexió amb les dades Manual: llegir/escriure propietats de controls a mà Data binding: la interfície s'actualitza sola quan canvien les dades
Separació UI/lògica Feble; el codi d'esdeveniments sol barrejar-se amb la lògica Forta amb MVVM: la vista (XAML) no coneix la lògica, només el binding
Estils i plantilles visuals Limitats Molt flexibles (estils, plantilles, animacions)
Plataforma Només Windows Només Windows

La diferència més profunda no és visual, sinó d'arquitectura: a Windows Forms, és habitual que el codi del formulari llegeixi llistaCataleg.SelectedItem directament quan ho necessita; a WPF amb MVVM, la vista (XAML) mai no llegeix ni escriu dades per si mateixa —es limita a declarar bindings—, i és el binding qui manté la vista sincronitzada amb el ViewModel de forma automàtica, en totes dues direccions si cal.

  1. Sintaxi XAML bàsica: elements, atributs i arbre de controls

XAML és un dialecte d'XML: cada control és un element, i les seves propietats són atributs (o elements fills, per a valors complexos). Un exemple mínim:

<Window x:Class="BiblioTech.Escriptori.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="Cataleg de BiblioTech" FontSize="18" FontWeight="Bold" />
        <ListBox Name="llistaCataleg" Height="250" />
        <Button Content="Prestar" Width="100" HorizontalAlignment="Left" Margin="0,10,0,0" />
    </StackPanel>

</Window>

Cada peça compleix un paper:

Element/atribut Paper
<Window> La finestra arrel, equivalent al Form de Windows Forms
xmlns="..." Espai de noms que defineix el vocabulari de controls WPF disponibles
xmlns:x="..." Espai de noms del propi llenguatge XAML (x:Class, x:Name...)
<StackPanel> Un contenidor de layout: apila els seus fills verticalment (o horitzontalment amb Orientation="Horizontal")
<TextBlock> Equivalent a Label a Windows Forms: text no editable
Name="llistaCataleg" Dona un nom al control per referenciar-lo des de C#, si calgués

WPF ofereix diversos contenidors de layout (StackPanel, Grid, DockPanel...) que organitzen automàticament els seus fills, a diferència de Windows Forms, on cada control porta una posició absoluta (Location, Size). Cada fitxer .xaml té associat un fitxer .xaml.cs (code-behind) amb el mateix nom de classe (x:Class="BiblioTech.Escriptori.MainWindow"), on hi pot viure codi C#, tot i que l'objectiu de MVVM (apartat 3) és que aquest code-behind quedi pràcticament buit.

  1. El patró MVVM a nivell introductori

MVVM (Model-View-ViewModel) organitza una aplicació WPF en tres capes amb responsabilitats separades:

flowchart LR
    M["Model<br/>(Biblioteca, Llibre, Soci...)"] <--> VM["ViewModel<br/>(propietats + comandaments per a la vista)"]
    VM <-->|Data binding| V["View<br/>(XAML: MainWindow.xaml)"]
Capa Què és a BiblioTech Responsabilitat
Model Biblioteca, MaterialBibliotecari, Llibre, Soci (Mòdul 2) El domini i la lògica de negoci, sense saber res d'interfícies
View El .xaml (MainWindow.xaml) Només declara controls i bindings; no conté lògica de negoci ni accedeix al Model directament
ViewModel Una classe nova (CatalegViewModel, apartat 8) Exposa el Model d'una forma que la View pot consumir per binding, i tradueix les accions de l'usuari (comandaments) en crides al Model

La idea central: la View mai no parla directament amb el Model. Tot passa pel ViewModel, que actua d'intermediari. Això comporta un avantatge concret i comprovable: el ViewModel es pot provar amb proves automatitzades sense necessitat d'obrir cap finestra (una cosa que es retomarà amb més detall a la lliçó de Proves Unitàries del Mòdul 8), perquè no depèn de cap control visual concret.

  1. INotifyPropertyChanged: notificar canvis a la interfície

Perquè la vista s'actualitzi automàticament quan canvia una propietat del ViewModel, aquesta propietat ha d'avisar del canvi mitjançant la interfície INotifyPropertyChanged:

using System.ComponentModel;

class CatalegViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    private string _missatgeEstat = string.Empty;

    public string MissatgeEstat
    {
        get => _missatgeEstat;
        set
        {
            _missatgeEstat = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MissatgeEstat)));
        }
    }
}

PropertyChanged és un esdeveniment (el mateix mecanisme del Mòdul 4, i el mateix que Biblioteca.PrestecRegistrat); quan s'invoca amb el nom de la propietat (nameof(MissatgeEstat)), WPF —que s'ha subscrit internament a aquest esdeveniment tan bon punt detecta un binding sobre MissatgeEstat— torna a llegir el valor i actualitza la vista, sense que el programador hagi de tocar cap control manualment. nameof(...) (ja fet servir en mòduls anteriors) evita escriure el nom de la propietat com a cadena literal, amb el risc de desincronitzar-se si es reanomena la propietat.

  1. Data binding: connectar XAML amb el ViewModel

El data binding és la sintaxi XAML {Binding NomDePropietat}, que connecta una propietat d'un control amb una propietat de l'objecte assignat a DataContext (apartat 7):

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

Aquesta línia substitueix completament escriure manualment etiqueta.Text = "..."; en C# (com es feia a Windows Forms): tan bon punt CatalegViewModel.MissatgeEstat canvia i dispara PropertyChanged, WPF actualitza el TextBlock automàticament, sense cap línia de codi addicional. Per a llistes completes, ItemsControl (i les seves variants ListBox, ListView, DataGrid) s'enllacen amb ItemsSource:

<ListBox ItemsSource="{Binding Cataleg}"
         DisplayMemberPath="Titol" />

DisplayMemberPath="Titol" indica quina propietat de cada element de Cataleg mostrar com a text a la llista —equivalent a compondre manualment el text al bucle foreach que es feia servir a Windows Forms per emplenar la ListBox—.

  1. ICommand bàsic: comandaments en lloc d'esdeveniments Click

A Windows Forms, un botó es connecta amb botoPrestar.Click += BotoPrestar_Click; (codi al code-behind). A WPF amb MVVM, un botó s'enllaça a un comandament del ViewModel, sense cap codi al code-behind:

<Button Content="Prestar" Command="{Binding ComandamentPrestar}" />

ICommand és la interfície que representa una acció invocable des de la vista:

using System.Windows.Input;

class ComandamentRelay : ICommand
{
    private readonly Action _accio;

    public ComandamentRelay(Action accio)
    {
        _accio = accio;
    }

    public event EventHandler? CanExecuteChanged;

    public bool CanExecute(object? parametre) => true; // simplificacio: sempre executable

    public void Execute(object? parametre) => _accio();
}

ComandamentRelay és una implementació mínima i reutilitzable d'ICommand (en projectes reals sol venir ja feta en llibreries de tercers, com RelayCommand de CommunityToolkit.Mvvm, però aquí s'escriu a mà per veure exactament què fa): embolcalla qualsevol Action en un objecte que WPF sap invocar quan l'usuari fa clic al botó enllaçat. CanExecute permet, si es volgués, desactivar el botó automàticament quan l'acció no és vàlida (per exemple, sense cap llibre seleccionat) —aquí simplificat a true sempre, per abast de la lliçó.

  1. DataContext: qui proveeix les dades a la vista

DataContext és la propietat que indica a quin objecte es refereixen tots els {Binding ...} d'una vista (i dels seus controls fills, per herència). S'assigna típicament al constructor del code-behind, en l'única línia que sol quedar-hi amb MVVM:

// MainWindow.xaml.cs (code-behind, gairebe buit amb MVVM)
public partial class MainWindow : Window
{
    public MainWindow(Biblioteca biblioteca)
    {
        InitializeComponent();
        DataContext = new CatalegViewModel(biblioteca); // tots els {Binding ...} del XAML apunten aqui
    }
}

Tan bon punt DataContext val una instància de CatalegViewModel, {Binding MissatgeEstat} es resol com ((CatalegViewModel)DataContext).MissatgeEstat, i {Binding Cataleg} com ((CatalegViewModel)DataContext).Cataleg. És la peça que tanca el circuit complet entre XAML i ViewModel: sense assignar DataContext, cap binding de la vista tindria d'on llegir.

  1. Exemple complet: catàleg de BiblioTech amb ObservableCollection i ViewModel

Unint totes les peces anteriors, es reconstrueix el mateix escenari de la lliçó anterior —mostrar el catàleg i prestar un llibre amb un clic— amb l'enfocament MVVM de WPF:

// CatalegViewModel.cs
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Input;

class CatalegViewModel : INotifyPropertyChanged
{
    private readonly Biblioteca _biblioteca;

    public ObservableCollection<MaterialBibliotecari> Cataleg { get; }

    public MaterialBibliotecari? MaterialSeleccionat { get; set; }

    private string _missatgeEstat = string.Empty;
    public string MissatgeEstat
    {
        get => _missatgeEstat;
        set
        {
            _missatgeEstat = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MissatgeEstat)));
        }
    }

    public ICommand ComandamentPrestar { get; }

    public event PropertyChangedEventHandler? PropertyChanged;

    public CatalegViewModel(Biblioteca biblioteca)
    {
        _biblioteca = biblioteca;
        // ObservableCollection embolcalla el cataleg: notifica sola a la vista si s'afegeixen/treuen elements
        Cataleg = new ObservableCollection<MaterialBibliotecari>(_biblioteca.Cataleg);

        ComandamentPrestar = new ComandamentRelay(PrestarMaterialSeleccionat);
    }

    private void PrestarMaterialSeleccionat()
    {
        if (MaterialSeleccionat is null)
        {
            MissatgeEstat = "Selecciona un llibre de la llista abans de prestar.";
            return;
        }

        if (!MaterialSeleccionat.Disponible)
        {
            MissatgeEstat = $"'{MaterialSeleccionat.Titol}' ja esta prestat.";
            return;
        }

        MaterialSeleccionat.Prestar(); // logica de domini ja existent, Modul 2
        MissatgeEstat = $"Prestec registrat: {MaterialSeleccionat.Titol}";
    }
}
<!-- MainWindow.xaml -->
<Window x:Class="BiblioTech.Escriptori.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="Cataleg de BiblioTech" FontSize="18" FontWeight="Bold" />

        <ListBox ItemsSource="{Binding Cataleg}"
                 DisplayMemberPath="Titol"
                 SelectedItem="{Binding MaterialSeleccionat}"
                 Height="250" />

        <Button Content="Prestar"
                Command="{Binding ComandamentPrestar}"
                Width="100" HorizontalAlignment="Left" Margin="0,10,0,0" />

        <TextBlock Text="{Binding MissatgeEstat}" Margin="0,10,0,0" />
    </StackPanel>

</Window>

SelectedItem="{Binding MaterialSeleccionat}" és un binding en totes dues direccions: quan l'usuari selecciona un element a la ListBox, WPF escriu automàticament MaterialSeleccionat al ViewModel; no cal cap esdeveniment SelectionChanged gestionat a mà, com sí que hauria calgut a Windows Forms. Cal notar també que ObservableCollection<T> (de System.Collections.ObjectModel) és com List<T> però notifica automàticament a la vista si s'afegeixen o es treuen elements de la col·lecció —aquí no cal aquesta notificació addicional perquè el catàleg no canvia de mida, només canvia l'estat intern dels seus elements, reflectit mitjançant MissatgeEstat—.

Errors Comuns i Consells

  • Escriure lògica de negoci al code-behind (.xaml.cs): trenca la separació MVVM; la regla pràctica és que el code-behind s'hauria de limitar, gairebé sempre, a InitializeComponent() i a assignar DataContext.
  • Oblidar PropertyChanged?.Invoke(...) al set d'una propietat enllaçada: sense aquesta notificació, la vista no s'assabenta del canvi i continua mostrant el valor antic, encara que el ViewModel ja tingui el valor nou internament.
  • Fer servir List<T> en lloc d'ObservableCollection<T> quan la col·lecció pot créixer o encongir-se dinàmicament: List<T> no notifica la vista si s'afegeixen o s'eliminen elements després d'assignar el binding inicial; la ListBox quedaria desactualitzada.
  • Consell: XAML permet depurar bindings trencats revisant la finestra de sortida/depuració, que sol mostrar un avís quan un {Binding NomPropietat} no troba aquesta propietat al DataContext actual —un error silenciós en temps de compilació, però visible en temps d'execució.

Exercicis

  1. Afegeix al CatalegViewModel una propietat int TotalDisponibles (només de lectura, calculada a partir de Cataleg.Count(m => m.Disponible)) i un TextBlock al XAML que la mostri amb {Binding TotalDisponibles}. Actualitza-la (amb el seu propi PropertyChanged) cada vegada que s'executi PrestarMaterialSeleccionat.

  2. Explica, en un paràgraf breu, per què a MVVM la View no hauria d'accedir mai directament al Model (Biblioteca), i quin avantatge concret aporta passar sempre pel ViewModel.

Solucions

private int _totalDisponibles;
public int TotalDisponibles
{
    get => _totalDisponibles;
    private set
    {
        _totalDisponibles = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TotalDisponibles)));
    }
}

public CatalegViewModel(Biblioteca biblioteca)
{
    _biblioteca = biblioteca;
    Cataleg = new ObservableCollection<MaterialBibliotecari>(_biblioteca.Cataleg);
    ComandamentPrestar = new ComandamentRelay(PrestarMaterialSeleccionat);

    ActualitzarTotalDisponibles();
}

private void ActualitzarTotalDisponibles()
{
    TotalDisponibles = Cataleg.Count(m => m.Disponible);
}

private void PrestarMaterialSeleccionat()
{
    // ... codi existent ...
    MaterialSeleccionat.Prestar();
    MissatgeEstat = $"Prestec registrat: {MaterialSeleccionat.Titol}";
    ActualitzarTotalDisponibles();
}
<TextBlock Text="{Binding TotalDisponibles}" Margin="0,10,0,0" />

Si la View accedís directament a Biblioteca, quedaria acoblada als detalls concrets del domini (com es diu cada mètode, quines comprovacions cal fer abans de prestar), i aquest coneixement s'hauria de repetir a cada vista que necessités la mateixa acció. Passar sempre pel ViewModel centralitza aquesta lògica en un sol lloc, permet provar-la amb proves automatitzades sense obrir cap finestra (no depèn de cap control visual), i permet canviar la vista (per exemple, substituir la ListBox per un DataGrid) sense tocar ni una línia del ViewModel ni del Model.

Conclusió

En aquesta lliçó has vist com WPF substitueix l'enfocament imperatiu de Windows Forms per XAML declaratiu, i com el patró MVVM separa amb disciplina la vista (XAML) de la lògica (ViewModel), comunicades mitjançant data binding i INotifyPropertyChanged. El mateix escenari de préstec de la lliçó anterior —mostrar el catàleg, prestar un llibre— s'ha reconstruït aquí sense cap esdeveniment Click gestionat a mà al code-behind, amb ICommand i ObservableCollection fent la feina de sincronització automàticament.

Les dues lliçons següents canvien de plataforma: ASP.NET Core porta BiblioTech a la web, exposant la seva lògica com una API que qualsevol client HTTP pot consumir —inclosa, més endavant, una versió web d'aquest mateix catàleg amb Blazor, que reutilitza bona part de les idees de data binding i components ja vistes aquí, però executant-se en un navegador en lloc d'una finestra d'escriptori.

Curs de Programació en C#

Mòdul 1: Introducció al C#

Mòdul 2: Estructures de Control

Mòdul 3: Programació Orientada a Objectes

Mòdul 4: Conceptes Avançats de C#

Mòdul 5: Treballant amb Dades

Mòdul 6: Temes Avançats

Mòdul 7: Construcció d'Aplicacions

Mòdul 8: Bones Pràctiques i Patrons de Disseny

Mòdul 9: Projecte Final

© Copyright 2026. Tots els drets reservats