La lección anterior conectó BiblioTech a Windows Forms, colocando controles con un diseñador
visual que genera código C# imperativo por detrás. WPF (Windows Presentation Foundation),
también exclusivo de Windows, resuelve el mismo problema —una interfaz de escritorio— con una
filosofía distinta: la interfaz se describe de forma declarativa en un lenguaje de marcado
llamado XAML, separada con disciplina de la lógica de la aplicación gracias al patrón MVVM
(Model-View-ViewModel) y al data binding. Esta lección presenta la sintaxis básica de XAML,
introduce MVVM a nivel introductorio, y reconstruye el mismo escenario de préstamo de la lección
anterior —ahora con una lista enlazada por data binding al catálogo de Biblioteca— para que
puedas comparar directamente ambos enfoques.
Contenido
- WPF frente a Windows Forms: XAML declarativo y separación de UI/lógica
- Sintaxis XAML básica: elementos, atributos y árbol de controles
- El patrón MVVM a nivel introductorio
INotifyPropertyChanged: notificar cambios a la interfaz- Data binding: conectar XAML con el ViewModel
ICommandbásico: comandos en vez de eventosClickDataContext: quién provee los datos a la vista- Ejemplo completo: catálogo de BiblioTech con
ObservableCollectiony ViewModel
- WPF frente a Windows Forms: XAML declarativo y separación de UI/lógica
Windows Forms describe la interfaz imperativamente: código C# que crea objetos Button,
ListBox, fija sus propiedades una a una y los añade a Controls. WPF describe la interfaz
declarativamente: un fichero XAML (XML de interfaz) enumera qué controles existen y cómo se
relacionan, sin necesidad de código C# para construir el árbol visual.
| Windows Forms | WPF | |
|---|---|---|
| Cómo se describe la interfaz | Código C# imperativo (generado por el diseñador) | XAML declarativo |
| Conexión con los datos | Manual: leer/escribir propiedades de controles a mano | Data binding: la interfaz se actualiza sola cuando cambian los datos |
| Separación UI/lógica | Débil; el código de eventos suele mezclarse con la lógica | Fuerte con MVVM: la vista (XAML) no conoce la lógica, solo el binding |
| Estilos y plantillas visuales | Limitados | Muy flexibles (estilos, plantillas, animaciones) |
| Plataforma | Solo Windows | Solo Windows |
La diferencia más profunda no es visual, sino de arquitectura: en Windows Forms, es habitual
que el código del formulario lea listaCatalogo.SelectedItem directamente cuando lo necesita; en
WPF con MVVM, la vista (XAML) nunca lee ni escribe datos por sí misma —se limita a declarar
bindings—, y es el binding quien mantiene la vista sincronizada con el ViewModel de forma
automática, en ambas direcciones si hace falta.
- Sintaxis XAML básica: elementos, atributos y árbol de controles
XAML es un dialecto de XML: cada control es un elemento, y sus propiedades son atributos (o elementos hijos, para valores complejos). Un ejemplo mínimo:
<Window x:Class="BiblioTech.Escritorio.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="Catalogo de BiblioTech" FontSize="18" FontWeight="Bold" />
<ListBox Name="listaCatalogo" Height="250" />
<Button Content="Prestar" Width="100" HorizontalAlignment="Left" Margin="0,10,0,0" />
</StackPanel>
</Window>Cada pieza cumple un papel:
| Elemento/atributo | Papel |
|---|---|
<Window> |
La ventana raíz, equivalente al Form de Windows Forms |
xmlns="..." |
Espacio de nombres que define el vocabulario de controles WPF disponibles |
xmlns:x="..." |
Espacio de nombres del propio lenguaje XAML (x:Class, x:Name...) |
<StackPanel> |
Un contenedor de layout: apila sus hijos verticalmente (u horizontalmente con Orientation="Horizontal") |
<TextBlock> |
Equivalente a Label en Windows Forms: texto no editable |
Name="listaCatalogo" |
Da un nombre al control para referenciarlo desde C#, si hiciera falta |
WPF ofrece varios contenedores de layout (StackPanel, Grid, DockPanel...) que organizan
automáticamente a sus hijos, a diferencia de Windows Forms, donde cada control lleva una posición
absoluta (Location, Size). Cada fichero .xaml tiene asociado un fichero .xaml.cs
(code-behind) con el mismo nombre de clase (x:Class="BiblioTech.Escritorio.MainWindow"),
donde puede vivir código C#, aunque el objetivo de MVVM (apartado 3) es que ese code-behind
quede prácticamente vacío.
- El patrón MVVM a nivel introductorio
MVVM (Model-View-ViewModel) organiza una aplicación WPF en tres capas con responsabilidades separadas:
flowchart LR
M["Model<br/>(Biblioteca, Libro, Socio...)"] <--> VM["ViewModel<br/>(propiedades + comandos para la vista)"]
VM <-->|Data binding| V["View<br/>(XAML: MainWindow.xaml)"]
| Capa | Qué es en BiblioTech | Responsabilidad |
|---|---|---|
| Model | Biblioteca, MaterialBibliotecario, Libro, Socio (Módulo 2) |
El dominio y la lógica de negocio, sin saber nada de interfaces |
| View | El .xaml (MainWindow.xaml) |
Solo declara controles y bindings; no contiene lógica de negocio ni accede al Model directamente |
| ViewModel | Una clase nueva (CatalogoViewModel, apartado 8) |
Expone el Model en una forma que la View puede consumir por binding, y traduce las acciones del usuario (comandos) en llamadas al Model |
La idea central: la View nunca habla directamente con el Model. Todo pasa por el ViewModel, que actúa de intermediario. Esto trae una ventaja concreta y comprobable: el ViewModel se puede probar con pruebas automatizadas sin necesidad de abrir ninguna ventana (algo que se retomará con más detalle en la lección de Pruebas Unitarias del Módulo 8), porque no depende de ningún control visual concreto.
INotifyPropertyChanged: notificar cambios a la interfaz
INotifyPropertyChanged: notificar cambios a la interfazPara que la vista se actualice automáticamente cuando cambia una propiedad del ViewModel, esa
propiedad debe avisar del cambio mediante la interfaz INotifyPropertyChanged:
using System.ComponentModel;
class CatalogoViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private string _mensajeEstado = string.Empty;
public string MensajeEstado
{
get => _mensajeEstado;
set
{
_mensajeEstado = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MensajeEstado)));
}
}
}PropertyChanged es un evento (el mismo mecanismo del Módulo 4, y el mismo que
Biblioteca.PrestamoRegistrado); cuando se invoca con el nombre de la propiedad
(nameof(MensajeEstado)), WPF —que se ha suscrito internamente a este evento en cuanto detecta
un binding sobre MensajeEstado— vuelve a leer el valor y actualiza la vista, sin que el
programador tenga que tocar ningún control manualmente. nameof(...) (ya usado en módulos
anteriores) evita escribir el nombre de la propiedad como cadena literal, con el riesgo de
desincronizarse si se renombra la propiedad.
- Data binding: conectar XAML con el ViewModel
El data binding es la sintaxis XAML {Binding NombreDePropiedad}, que conecta una propiedad de
un control con una propiedad del objeto asignado a DataContext (apartado 7):
Esta línea sustituye por completo a escribir manualmente etiqueta.Text = "..."; en C# (como se
hacía en Windows Forms): en cuanto CatalogoViewModel.MensajeEstado cambia y dispara
PropertyChanged, WPF actualiza el TextBlock automáticamente, sin ninguna línea de código
adicional. Para listas completas, ItemsControl (y sus variantes ListBox, ListView,
DataGrid) se enlazan con ItemsSource:
DisplayMemberPath="Titulo" indica qué propiedad de cada elemento de Catalogo mostrar como
texto en la lista —equivalente a componer manualmente el texto en el bucle foreach que se
usaba en Windows Forms para rellenar la ListBox—.
ICommand básico: comandos en vez de eventos Click
ICommand básico: comandos en vez de eventos ClickEn Windows Forms, un botón se conecta con botonPrestar.Click += BotonPrestar_Click; (código en
el code-behind). En WPF con MVVM, un botón se enlaza a un comando del ViewModel, sin ningún
código en el code-behind:
ICommand es la interfaz que representa una acción invocable desde la vista:
using System.Windows.Input;
class ComandoRelay : ICommand
{
private readonly Action _accion;
public ComandoRelay(Action accion)
{
_accion = accion;
}
public event EventHandler? CanExecuteChanged;
public bool CanExecute(object? parametro) => true; // simplificacion: siempre ejecutable
public void Execute(object? parametro) => _accion();
}ComandoRelay es una implementación mínima y reutilizable de ICommand (en proyectos reales
suele venir ya hecha en librerías de terceros, como RelayCommand de CommunityToolkit.Mvvm,
pero aquí se escribe a mano para ver exactamente qué hace): envuelve cualquier Action en un
objeto que WPF sabe invocar cuando el usuario hace clic en el botón enlazado. CanExecute
permite, si se quisiera, deshabilitar el botón automáticamente cuando la acción no es válida
(por ejemplo, sin ningún libro seleccionado) —aquí simplificado a true siempre, por alcance de
la lección.
DataContext: quién provee los datos a la vista
DataContext: quién provee los datos a la vistaDataContext es la propiedad que indica a qué objeto se refieren todos los {Binding ...}
de una vista (y de sus controles hijos, por herencia). Se asigna típicamente en el constructor
del code-behind, en la única línea que suele quedar allí con MVVM:
// MainWindow.xaml.cs (code-behind, casi vacio con MVVM)
public partial class MainWindow : Window
{
public MainWindow(Biblioteca biblioteca)
{
InitializeComponent();
DataContext = new CatalogoViewModel(biblioteca); // todos los {Binding ...} del XAML apuntan aqui
}
}En cuanto DataContext vale una instancia de CatalogoViewModel, {Binding MensajeEstado} se
resuelve como ((CatalogoViewModel)DataContext).MensajeEstado, y {Binding Catalogo} como
((CatalogoViewModel)DataContext).Catalogo. Es la pieza que cierra el circuito completo entre
XAML y ViewModel: sin asignar DataContext, ningún binding de la vista tendría de dónde leer.
- Ejemplo completo: catálogo de BiblioTech con
ObservableCollection y ViewModel
ObservableCollection y ViewModelUniendo todas las piezas anteriores, se reconstruye el mismo escenario de la lección anterior —mostrar el catálogo y prestar un libro con un clic— con el enfoque MVVM de WPF:
// CatalogoViewModel.cs
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Input;
class CatalogoViewModel : INotifyPropertyChanged
{
private readonly Biblioteca _biblioteca;
public ObservableCollection<MaterialBibliotecario> Catalogo { get; }
public MaterialBibliotecario? MaterialSeleccionado { get; set; }
private string _mensajeEstado = string.Empty;
public string MensajeEstado
{
get => _mensajeEstado;
set
{
_mensajeEstado = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(MensajeEstado)));
}
}
public ICommand ComandoPrestar { get; }
public event PropertyChangedEventHandler? PropertyChanged;
public CatalogoViewModel(Biblioteca biblioteca)
{
_biblioteca = biblioteca;
// ObservableCollection envuelve el catalogo: notifica sola a la vista si se añaden/quitan elementos
Catalogo = new ObservableCollection<MaterialBibliotecario>(_biblioteca.Catalogo);
ComandoPrestar = new ComandoRelay(PrestarMaterialSeleccionado);
}
private void PrestarMaterialSeleccionado()
{
if (MaterialSeleccionado is null)
{
MensajeEstado = "Selecciona un libro de la lista antes de prestar.";
return;
}
if (!MaterialSeleccionado.Disponible)
{
MensajeEstado = $"'{MaterialSeleccionado.Titulo}' ya esta prestado.";
return;
}
MaterialSeleccionado.Prestar(); // logica de dominio ya existente, Modulo 2
MensajeEstado = $"Prestamo registrado: {MaterialSeleccionado.Titulo}";
}
}<!-- MainWindow.xaml -->
<Window x:Class="BiblioTech.Escritorio.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="Catalogo de BiblioTech" FontSize="18" FontWeight="Bold" />
<ListBox ItemsSource="{Binding Catalogo}"
DisplayMemberPath="Titulo"
SelectedItem="{Binding MaterialSeleccionado}"
Height="250" />
<Button Content="Prestar"
Command="{Binding ComandoPrestar}"
Width="100" HorizontalAlignment="Left" Margin="0,10,0,0" />
<TextBlock Text="{Binding MensajeEstado}" Margin="0,10,0,0" />
</StackPanel>
</Window>SelectedItem="{Binding MaterialSeleccionado}" es un binding en ambas direcciones: cuando
el usuario selecciona un elemento en la ListBox, WPF escribe automáticamente
MaterialSeleccionado en el ViewModel; no hace falta ningún evento SelectionChanged gestionado
a mano, como sí habría hecho falta en Windows Forms. Nótese también que ObservableCollection<T>
(de System.Collections.ObjectModel) es como List<T> pero notifica automáticamente a la vista
si se añaden o quitan elementos de la colección —aquí no hace falta esa notificación adicional
porque el catálogo no cambia de tamaño, solo cambia el estado interno de sus elementos, reflejado
mediante MensajeEstado—.
Errores Comunes y Consejos
- Escribir lógica de negocio en el code-behind (
.xaml.cs): rompe la separación MVVM; la regla práctica es que el code-behind debería limitarse, casi siempre, aInitializeComponent()y a asignarDataContext. - Olvidar
PropertyChanged?.Invoke(...)en elsetde una propiedad enlazada: sin esa notificación, la vista no se entera del cambio y sigue mostrando el valor antiguo, aunque el ViewModel ya tenga el valor nuevo internamente. - Usar
List<T>en vez deObservableCollection<T>cuando la colección puede crecer o encogerse dinámicamente:List<T>no notifica a la vista si se añaden o eliminan elementos después de asignar el binding inicial; laListBoxquedaría desactualizada. - Consejo: XAML permite depurar bindings rotos revisando la ventana de salida/depuración,
que suele mostrar un aviso cuando un
{Binding NombrePropiedad}no encuentra esa propiedad en elDataContextactual —un error silencioso en tiempo de compilación, pero visible en tiempo de ejecución.
Ejercicios
-
Añade al
CatalogoViewModeluna propiedadint TotalDisponibles(de solo lectura, calculada a partir deCatalogo.Count(m => m.Disponible)) y unTextBlocken el XAML que la muestre con{Binding TotalDisponibles}. Actualízala (con su propioPropertyChanged) cada vez que se ejecutePrestarMaterialSeleccionado. -
Explica, en un párrafo breve, por qué en MVVM la View no debería acceder nunca directamente al Model (
Biblioteca), y qué ventaja concreta aporta pasar siempre por el ViewModel.
Soluciones
private int _totalDisponibles;
public int TotalDisponibles
{
get => _totalDisponibles;
private set
{
_totalDisponibles = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TotalDisponibles)));
}
}
public CatalogoViewModel(Biblioteca biblioteca)
{
_biblioteca = biblioteca;
Catalogo = new ObservableCollection<MaterialBibliotecario>(_biblioteca.Catalogo);
ComandoPrestar = new ComandoRelay(PrestarMaterialSeleccionado);
ActualizarTotalDisponibles();
}
private void ActualizarTotalDisponibles()
{
TotalDisponibles = Catalogo.Count(m => m.Disponible);
}
private void PrestarMaterialSeleccionado()
{
// ... codigo existente ...
MaterialSeleccionado.Prestar();
MensajeEstado = $"Prestamo registrado: {MaterialSeleccionado.Titulo}";
ActualizarTotalDisponibles();
}
<TextBlock Text="{Binding TotalDisponibles}" Margin="0,10,0,0" />
Si la View accediera directamente a Biblioteca, quedaría acoplada a los detalles concretos
del dominio (cómo se llama cada método, qué comprobaciones hay que hacer antes de prestar), y
ese conocimiento tendría que repetirse en cada vista que necesitara la misma acción. Pasar
siempre por el ViewModel centraliza esa lógica en un solo sitio, permite probarla con pruebas
automatizadas sin abrir ninguna ventana (no depende de ningún control visual), y permite
cambiar la vista (por ejemplo, sustituir la ListBox por un DataGrid) sin tocar ni una
línea del ViewModel ni del Model.
Conclusión
En esta lección has visto cómo WPF sustituye el enfoque imperativo de Windows Forms por XAML
declarativo, y cómo el patrón MVVM separa con disciplina la vista (XAML) de la lógica
(ViewModel), comunicadas mediante data binding e INotifyPropertyChanged. El mismo escenario
de préstamo de la lección anterior —mostrar el catálogo, prestar un libro— se ha reconstruido
aquí sin un solo evento Click gestionado a mano en el code-behind, con ICommand y
ObservableCollection haciendo el trabajo de sincronización automáticamente.
Las dos lecciones siguientes cambian de plataforma: ASP.NET Core lleva BiblioTech a la web, exponiendo su lógica como una API que cualquier cliente HTTP puede consumir —incluida, más adelante, una versión web de este mismo catálogo con Blazor, que reutiliza buena parte de las ideas de data binding y componentes ya vistas aquí, pero ejecutándose en un navegador en lugar de una ventana de escritorio.
Curso de Programación en C#
Módulo 1: Introducción a C#
- Introducción a C#
- Configuración del Entorno de Desarrollo
- Programa Hola Mundo
- Sintaxis y Estructura Básica
- Variables y Tipos de Datos
- Arrays y Cadenas de Texto
Módulo 2: Estructuras de Control
Módulo 3: Programación Orientada a Objetos
- Clases y Objetos
- Métodos
- Constructores y Destructores
- Herencia
- Polimorfismo
- Encapsulamiento
- Abstracción
- Structs y Records: Tipos por Valor y por Referencia
Módulo 4: Conceptos Avanzados de C#
- Interfaces
- Delegados y Eventos
- Pattern Matching y Características Modernas de C#
- Genéricos
- Colecciones
- LINQ (Consulta Integrada en el Lenguaje)
- Programación Asíncrona
Módulo 5: Trabajando con Datos
- Entrada/Salida de Archivos
- Serialización
- Conectividad con Bases de Datos
- Entity Framework
- Trabajo con JSON y Consumo de APIs REST
Módulo 6: Temas Avanzados
- Reflexión
- Atributos
- Programación Dinámica
- Gestión de Memoria y Recolección de Basura
- Multihilo y Programación Paralela
Módulo 7: Construcción de Aplicaciones
- Formularios de Windows
- WPF (Windows Presentation Foundation)
- ASP.NET Core
- Blazor
- Xamarin y .NET MAUI
Módulo 8: Mejores Prácticas y Patrones de Diseño
- Estándares de Codificación y Mejores Prácticas
- Patrones de Diseño
- Inyección de Dependencias e Inversión de Control
- Pruebas Unitarias
- Revisión y Refactorización de Código
