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
- WPF davant Windows Forms: XAML declaratiu i separació de UI/lògica
- Sintaxi XAML bàsica: elements, atributs i arbre de controls
- El patró MVVM a nivell introductori
INotifyPropertyChanged: notificar canvis a la interfície- Data binding: connectar XAML amb el ViewModel
ICommandbàsic: comandaments en lloc d'esdevenimentsClickDataContext: qui proveeix les dades a la vista- Exemple complet: catàleg de BiblioTech amb
ObservableCollectioni ViewModel
- 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.
- 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.
- 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.
INotifyPropertyChanged: notificar canvis a la interfície
INotifyPropertyChanged: notificar canvis a la interfíciePerquè 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.
- 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):
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:
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—.
ICommand bàsic: comandaments en lloc d'esdeveniments Click
ICommand bàsic: comandaments en lloc d'esdeveniments ClickA 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:
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çó.
DataContext: qui proveeix les dades a la vista
DataContext: qui proveeix les dades a la vistaDataContext é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.
- Exemple complet: catàleg de BiblioTech amb
ObservableCollection i ViewModel
ObservableCollection i ViewModelUnint 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, aInitializeComponent()i a assignarDataContext. - Oblidar
PropertyChanged?.Invoke(...)alsetd'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; laListBoxquedaria 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 alDataContextactual —un error silenciós en temps de compilació, però visible en temps d'execució.
Exercicis
-
Afegeix al
CatalegViewModeluna propietatint TotalDisponibles(només de lectura, calculada a partir deCataleg.Count(m => m.Disponible)) i unTextBlockal XAML que la mostri amb{Binding TotalDisponibles}. Actualitza-la (amb el seu propiPropertyChanged) cada vegada que s'executiPrestarMaterialSeleccionat. -
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#
- Introducció al C#
- Configuració de l'Entorn de Desenvolupament
- Programa Hola Món
- Sintaxi i Estructura Bàsica
- Variables i Tipus de Dades
- Arrays i Cadenes de Text
Mòdul 2: Estructures de Control
Mòdul 3: Programació Orientada a Objectes
- Classes i Objectes
- Mètodes
- Constructors i Destructors
- Herència
- Polimorfisme
- Encapsulació
- Abstracció
- Structs i Records: Tipus per Valor i per Referència
Mòdul 4: Conceptes Avançats de C#
- Interfícies
- Delegats i Esdeveniments
- Pattern Matching i Característiques Modernes de C#
- Genèrics
- Col·leccions
- LINQ (Consulta Integrada en el Llenguatge)
- Programació Asíncrona
Mòdul 5: Treballant amb Dades
- Entrada/Sortida de Fitxers
- Serialització
- Connectivitat amb Bases de Dades
- Entity Framework
- Treball amb JSON i Consum d'APIs REST
Mòdul 6: Temes Avançats
- Reflexió
- Atributs
- Programació Dinàmica
- Gestió de Memòria i Recollida d'Escombraries
- Multifil i Programació Paral·lela
Mòdul 7: Construcció d'Aplicacions
Mòdul 8: Bones Pràctiques i Patrons de Disseny
- Estàndards de Codificació i Bones Pràctiques
- Patrons de Disseny
- Injecció de Dependències i Inversió de Control
- Proves Unitàries
- Revisió i Refactorització de Codi
