Software Crafters® 2025 | Creado con 🖤 para elevar el nivel de la conversación sobre programación en español | Legal
En mi anterior post Xamarin.Forms, introducción a las aplicaciones multiplataforma vimos cómo podíamos crear nuestra primera aplicación nativa multiplataforma. Ahí nos quedamos en una simple aplicación que mostraba un "Hello World" con un diseño muy rudimentario.
En este post vamos a empezar a desarrollar una aplicación un poco más compleja que tenga una funcionalidad más cercana a una aplicación real. Para ello vamos a apoyarnos en el patrón MVVM (Model-View-ViewModel).
Este patrón tiene una gran popularidad en el desarrollo nativo de aplicaciones, especialmente en Xamarin y Windows (donde se originó). Pero también es utilizado en otros frameworks, como Angular o jQuery.
Para ir afianzando conceptos vamos a realizar un ejemplo: una aplicación para gestionar una lista de vinos. El usuario podrá ver los vinos que posee e interactuar con esta lista añadiendo, modificando o eliminando vinos.
MVVM es un patrón arquitectónico de Presentación, que surgió originalmente en Microsoft como una variación del patrón de presentación, para aprovecharse de las características específicas de Windows Presentation Foundation y Silverlight. Se usa principalmente para separar datos y lógica de presentación de la Interfaz de Usuario.
Estos patrones de Presentación nos ofrecen una gran ventaja: la colaboración entre equipos, ya que facilitan que analistas se encarguen de la parte de la Vista mientras en paralelo los desarrolladores se encargan de la parte de 'backend' (modelo y controlador principalmente). Además nos permiten hacer tests automatizados con mayor facilidad, en comparación con otros patrones menos rígidos de UI.
Os dejo una imagen explicativa del patrón MVVM que está muy bien, y no quería dejarla escapar:
Una vez explicado esto, uno de los puntos fuertes y más característicos de MVVM es el DataBinding, y es lo que lo diferencia de otros patrones de presentación, como puede ser MVC.
El DataBinding es una herramienta que usamos normalmente desde la Vista hacia ViewModel, y nos permite que cuando un valor se modifique en un sitio, se modifique automáticamente en el otro.
Si nos vamos hacia la Vista, es una representación visual de la aplicación. Lo que hace aquí el DataBinding es conectarla con las Clases de Modelo y los ViewModel, en concreto con sus propiedades. Por ejemplo, es capaz de asignar los datos de una propiedad a un control. El DataBinding también nos ayudara a sincronizar los cambios que pueden ocurrir en ambas direcciones.
Las Views sólo conocen el tipo de propiedades del ViewModel y no saben nada acerca del modelo. Por su parte, el ViewModel es el que interactúa con el Modelo. De esa manera, el ViewModel dispone de métodos, comandos y otras propiedades que ayudan a mantener el estado de la vista, así como ser el responsable de manipular los Modelos como respuesta a las acciones en la vista y la que desencadena eventos en ella.
Como veis, una parte muy importante del MVVM es el ViewModel, que es el que nos ayuda a compartir el código entre Vistas. Para asegurar que está funcionando correctamente se puede implementar la interfaz
INotifyPropertyChanged
, que nos proporciona un evento para PropertyChanged
que indica que ha habido un cambio. Xamarin Forms nos provee una clase llamada BindableObjects
que, cuando la utilizamos como clase base para nuestro ViewModel, evita que necesitemos implementar INotifyPropertyChanged
manualmente.
Para nuestro ViewModel, algunas de las cosas que harán que nuestros proyectos funcionen mejor y podamos organizarlos de manera modular, son:
INotifyPropertyChanged
en todos los ViewModels que queramos enlazar (o heredar de clases ya preparadas para ello).Ahora vamos a un ejemplo completo para ver cómo aplicar este patrón. La aplicación que vamos a construir será para gestionar una lista de vinos. Seguiremos el patrón MVVM para facilitar el mantenimiento, la testabilidad y la separación de preocupaciones. Crearemos un modelo de vino, un ViewModel para gestionar la lista y vistas para mostrar e interactuar con los datos.
Para empezar, vamos a crear un nuevo proyecto de Xamarin.Forms como una aplicación multiplataforma. Una vez creado, vamos a organizarlo para seguir el patrón MVVM:
- WineApp (Proyecto compartido) - Models/ - Wine.cs - ViewModels/ - BaseViewModel.cs - WineListViewModel.cs - WineDetailViewModel.cs - Views/ - WineListPage.xaml - WineDetailPage.xaml - Services/ - IWineService.cs - MockWineService.cs - App.xaml - WineApp.Android - WineApp.iOS
Primero, definamos nuestro modelo de datos. Crearemos una clase
Wine
que represente un vino:
namespace WineApp.Models { public class Wine { public string Id { get; set; } public string Name { get; set; } public string Winery { get; set; } public string Type { get; set; } // Tinto, Blanco, Rosado, etc. public int Year { get; set; } public string Region { get; set; } public string Notes { get; set; } public double Rating { get; set; } // Valoración de 0 a 5 public Wine() { Id = Guid.NewGuid().ToString(); } } }
Ahora crearemos un ViewModel base que implementará
INotifyPropertyChanged
para permitir el DataBinding:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Runtime.CompilerServices;
namespace WineApp.ViewModels { public class BaseViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty(ref T backingStore, T value, [CallerMemberName]string propertyName = "", Action onChanged = null) { if (EqualityComparer.Default.Equals(backingStore, value)) return false; backingStore = value; onChanged?.Invoke(); OnPropertyChanged(propertyName); return true; } protected void OnPropertyChanged([CallerMemberName] string propertyName = "") { var changed = PropertyChanged; if (changed == null) return; changed.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
}
Vamos a crear un servicio que se encargará de gestionar los datos de los vinos. Primero definiremos una interfaz:
using System.Collections.Generic; using System.Threading.Tasks; using WineApp.Models;
namespace WineApp.Services { public interface IWineService { Task<List> GetWinesAsync(); Task GetWineAsync(string id); Task AddWineAsync(Wine wine); Task UpdateWineAsync(Wine wine); Task DeleteWineAsync(string id); } }
Y ahora implementaremos un servicio mock para simular una base de datos:
using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using WineApp.Models;
namespace WineApp.Services { public class MockWineService : IWineService { readonly List wines;
public MockWineService() { wines = new List() { new Wine { Id = "1", Name = "Protos Crianza", Winery = "Bodegas Protos", Type = "Tinto", Year = 2016, Region = "Ribera del Duero", Notes = "Aromas a fruta madura y regaliz", Rating = 4.5 }, new Wine { Id = "2", Name = "Marqués de Riscal", Winery = "Marqués de Riscal", Type = "Tinto", Year = 2015, Region = "Rioja", Notes = "Equilibrado con buen paso por boca", Rating = 4.2 }, new Wine { Id = "3", Name = "Albariño Martín Códax", Winery = "Martín Códax", Type = "Blanco", Year = 2018, Region = "Rías Baixas", Notes = "Fresco y afrutado", Rating = 4.0 } }; } public async Task<List> GetWinesAsync() { return await Task.FromResult(wines); } public async Task GetWineAsync(string id) { return await Task.FromResult(wines.FirstOrDefault(w => w.Id == id)); } public async Task AddWineAsync(Wine wine) { wines.Add(wine); await Task.CompletedTask; } public async Task UpdateWineAsync(Wine wine) { var index = wines.FindIndex(w => w.Id == wine.Id); if (index != -1) wines[index] = wine; await Task.CompletedTask; } public async Task DeleteWineAsync(string id) { var wine = wines.FirstOrDefault(w => w.Id == id); if (wine != null) wines.Remove(wine); await Task.CompletedTask; } }
}
Ahora creamos el ViewModel que será utilizado por la vista de lista de vinos:
using System; using System.Collections.ObjectModel; using System.Diagnostics; using System.Threading.Tasks; using Xamarin.Forms; using WineApp.Models; using WineApp.Services; using WineApp.Views;
namespace WineApp.ViewModels { public class WineListViewModel : BaseViewModel { private readonly IWineService _wineService; public ObservableCollection Wines { get; } public Command LoadWinesCommand { get; } public Command AddWineCommand { get; } public Command WineTapped { get; }
public WineListViewModel(IWineService wineService) { _wineService = wineService; Wines = new ObservableCollection(); LoadWinesCommand = new Command(async () => await ExecuteLoadWinesCommand()); AddWineCommand = new Command(OnAddWine); WineTapped = new Command(OnWineSelected); } async Task ExecuteLoadWinesCommand() { try { Wines.Clear(); var wines = await _wineService.GetWinesAsync(); foreach (var wine in wines) { Wines.Add(wine); } } catch (Exception ex) { Debug.WriteLine(ex); } } private async void OnAddWine() { await Application.Current.MainPage.Navigation.PushAsync(new WineDetailPage(new Wine())); } private async void OnWineSelected(Wine wine) { if (wine == null) return; await Application.Current.MainPage.Navigation.PushAsync(new WineDetailPage(wine)); } public void OnAppearing() { IsBusy = true; LoadWinesCommand.Execute(null); } private bool isBusy = false; public bool IsBusy { get { return isBusy; } set { SetProperty(ref isBusy, value); } } }
}
También necesitamos un ViewModel para manejar los detalles de un vino específico:
using System; using System.Diagnostics; using Xamarin.Forms; using WineApp.Models; using WineApp.Services; namespace WineApp.ViewModels { public class WineDetailViewModel : BaseViewModel { private readonly IWineService _wineService; private string id; private string name; private string winery; private string type; private int year; private string region; private string notes; private double rating; public string Id { get => id; set => SetProperty(ref id, value); } public string Name { get => name; set => SetProperty(ref name, value); } public string Winery { get => winery; set => SetProperty(ref winery, value); } public string Type { get => type; set => SetProperty(ref type, value); } public int Year { get => year; set => SetProperty(ref year, value); } public string Region { get => region; set => SetProperty(ref region, value); } public string Notes { get => notes; set => SetProperty(ref notes, value); } public double Rating { get => rating; set => SetProperty(ref rating, value); } public Command SaveCommand { get; } public Command CancelCommand { get; } public Command DeleteCommand { get; } public WineDetailViewModel(Wine wine, IWineService wineService) { _wineService = wineService; Id = wine.Id; Name = wine.Name; Winery = wine.Winery; Type = wine.Type; Year = wine.Year; Region = wine.Region; Notes = wine.Notes; Rating = wine.Rating; SaveCommand = new Command(OnSave, ValidateSave); CancelCommand = new Command(OnCancel); DeleteCommand = new Command(OnDelete); PropertyChanged += (_, __) => SaveCommand.ChangeCanExecute(); } private bool ValidateSave() { return !String.IsNullOrWhiteSpace(name) && !String.IsNullOrWhiteSpace(winery); } private async void OnCancel() { await Application.Current.MainPage.Navigation.PopAsync(); } private async void OnSave() { Wine wine = new Wine() { Id = Id, Name = Name, Winery = Winery, Type = Type, Year = Year, Region = Region, Notes = Notes, Rating = Rating }; if (string.IsNullOrEmpty(wine.Id)) { await _wineService.AddWineAsync(wine); } else { await _wineService.UpdateWineAsync(wine); } await Application.Current.MainPage.Navigation.PopAsync(); } private async void OnDelete() { await _wineService.DeleteWineAsync(id); await Application.Current.MainPage.Navigation.PopAsync(); } } }
Ahora vamos a crear las vistas XAML que mostrarán nuestros datos. Primero, la vista de la lista de vinos:
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="WineApp.Views.WineListPage" Title="Mi Colección de Vinos"> <ContentPage.ToolbarItems> <ToolbarItem Text="Añadir" Command="{Binding AddWineCommand}" /> </ContentPage.ToolbarItems> <RefreshView Command="{Binding LoadWinesCommand}" IsRefreshing="{Binding IsBusy, Mode=TwoWay}"> <CollectionView x:Name="WinesCollectionView" ItemsSource="{Binding Wines}" SelectionMode="None"> <CollectionView.ItemTemplate> <DataTemplate> <Grid Padding="10" x:DataType="model:Wine"> <Frame BorderColor="LightGray" CornerRadius="5"> <StackLayout Orientation="Horizontal" Padding="5"> <StackLayout VerticalOptions="Center" HorizontalOptions="StartAndExpand"> <Label Text="{Binding Name}" FontSize="Medium" FontAttributes="Bold" /> <Label Text="{Binding Winery}" FontSize="Small" /> <StackLayout Orientation="Horizontal"> <Label Text="{Binding Type}" FontSize="Small" /> <Label Text="{Binding Year}" FontSize="Small" /> </StackLayout> </StackLayout> <Label Text="{Binding Rating, StringFormat='{0:F1}'}" VerticalOptions="Center" HorizontalOptions="End" FontSize="Large" FontAttributes="Bold" /> </StackLayout> </Frame> <Grid.GestureRecognizers> <TapGestureRecognizer NumberOfTapsRequired="1" Command="{Binding Source={RelativeSource AncestorType={x:Type local:WineListViewModel}}, Path=WineTapped}" CommandParameter="{Binding .}"> </TapGestureRecognizer> </Grid.GestureRecognizers> </Grid> </DataTemplate> </CollectionView.ItemTemplate> </CollectionView> </RefreshView> </ContentPage>
Y la vista de detalle:
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="WineApp.Views.WineDetailPage" Title="{Binding Name}"> <ContentPage.ToolbarItems> <ToolbarItem Text="Cancelar" Command="{Binding CancelCommand}" /> <ToolbarItem Text="Guardar" Command="{Binding SaveCommand}" /> </ContentPage.ToolbarItems> <ScrollView> <StackLayout Spacing="20" Padding="15"> <Label Text="Nombre" FontSize="Medium" /> <Entry Text="{Binding Name}" FontSize="Medium" /> <Label Text="Bodega" FontSize="Medium" /> <Entry Text="{Binding Winery}" FontSize="Medium" /> <Label Text="Tipo" FontSize="Medium" /> <Picker x:Name="TypePicker" Title="Seleccionar tipo" SelectedItem="{Binding Type}"> <Picker.ItemsSource> <x:Array Type="{x:Type x:String}"> <x:String>Tinto</x:String> <x:String>Blanco</x:String> <x:String>Rosado</x:String> <x:String>Espumoso</x:String> <x:String>Dulce</x:String> </x:Array> </Picker.ItemsSource> </Picker> <Label Text="Año" FontSize="Medium" /> <Entry Text="{Binding Year}" FontSize="Medium" Keyboard="Numeric" /> <Label Text="Región" FontSize="Medium" /> <Entry Text="{Binding Region}" FontSize="Medium" /> <Label Text="Notas" FontSize="Medium" /> <Editor Text="{Binding Notes}" AutoSize="TextChanges" FontSize="Medium" Margin="0" /> <Label Text="Valoración (0-5)" FontSize="Medium" /> <Slider Minimum="0" Maximum="5" Value="{Binding Rating}" MinimumTrackColor="Red" MaximumTrackColor="Gray" /> <Label Text="{Binding Rating, StringFormat='{0:F1}'}" HorizontalOptions="Center" FontSize="Large" /> <StackLayout Orientation="Horizontal"> <Button Text="Eliminar" Command="{Binding DeleteCommand}" HorizontalOptions="FillAndExpand" IsVisible="{Binding Id, Converter={StaticResource StringToBoolConverter}}" BackgroundColor="Red" TextColor="White" /> </StackLayout> </StackLayout> </ScrollView> </ContentPage>
Finalmente, vamos a configurar nuestra aplicación para usar nuestros servicios y ViewModels. Modificaremos el App.xaml.cs:
using System; using Xamarin.Forms; using WineApp.Services; using WineApp.Views; namespace WineApp { public partial class App : Application { public static IWineService WineService { get; private set; } public App() { InitializeComponent(); WineService = new MockWineService(); MainPage = new NavigationPage(new WineListPage()); } protected override void OnStart() { } protected override void OnSleep() { } protected override void OnResume() { } } }
Y las clases code-behind de nuestras vistas:
// WineListPage.xaml.cs using System; using Xamarin.Forms; using WineApp.ViewModels; namespace WineApp.Views { public partial class WineListPage : ContentPage { WineListViewModel _viewModel; public WineListPage() { InitializeComponent(); BindingContext = _viewModel = new WineListViewModel(App.WineService); } protected override void OnAppearing() { base.OnAppearing(); _viewModel.OnAppearing(); } } } // WineDetailPage.xaml.cs using System; using Xamarin.Forms; using WineApp.Models; using WineApp.ViewModels; namespace WineApp.Views { public partial class WineDetailPage : ContentPage { public WineDetailPage(Wine wine) { InitializeComponent(); BindingContext = new WineDetailViewModel(wine, App.WineService); } } }
Con este ejemplo, hemos implementado una aplicación completa siguiendo el patrón MVVM. La aplicación permite a los usuarios ver una lista de vinos, añadir nuevos vinos, editar los existentes y eliminarlos. Todo esto se ha logrado mediante la separación de responsabilidades: * Los **Modelos** definen la estructura de nuestros datos (Wine) * Las **Vistas** definen la interfaz de usuario (páginas XAML) * Los **ViewModels** conectan los modelos con las vistas y manejan la lógica de presentación * Los **Servicios** manejan la lógica de acceso a datos Si ejecutas la aplicación, podrás ver una lista de vinos y navegar a la pantalla de detalles para editarlos o crear nuevos. El DataBinding garantiza que la interfaz de usuario se actualice automáticamente cuando cambien los datos y viceversa.
El patrón MVVM es extremadamente útil para desarrollar aplicaciones en Xamarin.Forms, ya que permite una clara separación de responsabilidades y facilita el mantenimiento, la testabilidad y la reutilización del código. Además, el DataBinding nos permite conectar fácilmente la interfaz de usuario con los datos, sin necesidad de escribir código adicional. Con lo aprendido en este artículo, deberías ser capaz de empezar a desarrollar aplicaciones más complejas y estructuradas con Xamarin.Forms siguiendo el patrón MVVM. ¡Feliz codificación!