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

  1. WPF frente a Windows Forms: XAML declarativo y separación de UI/lógica
  2. Sintaxis XAML básica: elementos, atributos y árbol de controles
  3. El patrón MVVM a nivel introductorio
  4. INotifyPropertyChanged: notificar cambios a la interfaz
  5. Data binding: conectar XAML con el ViewModel
  6. ICommand básico: comandos en vez de eventos Click
  7. DataContext: quién provee los datos a la vista
  8. Ejemplo completo: catálogo de BiblioTech con ObservableCollection y ViewModel

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

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

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

  1. INotifyPropertyChanged: notificar cambios a la interfaz

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

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

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

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:

<ListBox ItemsSource="{Binding Catalogo}"
         DisplayMemberPath="Titulo" />

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

  1. ICommand básico: comandos en vez de eventos Click

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

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

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.

  1. DataContext: quién provee los datos a la vista

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

  1. Ejemplo completo: catálogo de BiblioTech con ObservableCollection y ViewModel

Uniendo 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, a InitializeComponent() y a asignar DataContext.
  • Olvidar PropertyChanged?.Invoke(...) en el set de 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 de ObservableCollection<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; la ListBox quedarí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 el DataContext actual —un error silencioso en tiempo de compilación, pero visible en tiempo de ejecución.

Ejercicios

  1. Añade al CatalogoViewModel una propiedad int TotalDisponibles (de solo lectura, calculada a partir de Catalogo.Count(m => m.Disponible)) y un TextBlock en el XAML que la muestre con {Binding TotalDisponibles}. Actualízala (con su propio PropertyChanged) cada vez que se ejecute PrestarMaterialSeleccionado.

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

Módulo 2: Estructuras de Control

Módulo 3: Programación Orientada a Objetos

Módulo 4: Conceptos Avanzados de C#

Módulo 5: Trabajando con Datos

Módulo 6: Temas Avanzados

Módulo 7: Construcción de Aplicaciones

Módulo 8: Mejores Prácticas y Patrones de Diseño

Módulo 9: Proyecto Final

© Copyright 2026. Todos los derechos reservados