En este artículo vamos a ver cómo utilizar Caliburn en una aplicación universal de Windows 8.1 y Windows Phone 8.1. Cómo configurarlo, y utilizarlo en nuestra aplicación.

Código

El código de la aplicación que se muestra en este post se puede descargar aquí.

¿Qué es Caliburn.Micro?

Caliburn.Micro es un framework diseñado para construir aplicaciones para todas las plataformas basadas en XAML. Con soporte para el patrón MVVM.

Configurando Caliburn.Micro

Añadir Caliburn.Micro es algo muy sencillo, tan sencillo como instalar un paquete de NuGet. Para ello en nuestra solución hacemos click con el botón secundario, y seleccionamos “Manage NuGet packages for solution…” y añadimos el paquete.

screenshot1

screenshot2

Una vez que pulsemos sobre el botón “Install”, el paquete se instalará en todos los proyectos que lo necesiten, en nuestro caso el proyecto de Windows 8.1 y Windows Phone 8.1

Lo siguiente que tenemos que hacer es configurar nuestra aplicación para que sea una aplicación de Caliburn. Para ello cambiamos el archivo App.xaml que de esta forma:

[code language=»xml»]
<micro:CaliburnApplication
x:Class="Caliburn101.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:micro="using:Caliburn.Micro"
xmlns:local="using:Caliburn101">

</micro:CaliburnApplication>
[/code]

Ahora tenemos que cambiar el archivo del code behind, App.xaml.cs. Debería tener un aspecto similar a éste:

[code language=»csharp»]
using Caliburn.Micro;
using Caliburn101.Services;
using Caliburn101.ViewModels;
using Caliburn101.Views;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Animation;
using Windows.UI.Xaml.Navigation;

// The Blank Application template is documented at http://go.microsoft.com/fwlink/?LinkId=234227

namespace Caliburn101
{
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public sealed partial class App : CaliburnApplication
{
private WinRTContainer _container;

public App()
{
InitializeComponent();
}

protected override void Configure()
{
_container = new WinRTContainer();
_container.RegisterWinRTServices();

_container.PerRequest<MainPageViewModel>();
_container.PerRequest<ContactDetailViewModel>();
_container.Singleton<IContactsService, ContactsService>();
}

protected override void OnLaunched(LaunchActivatedEventArgs args)
{
DisplayRootView<MainPageView>();
}

protected override object GetInstance(Type service, string key)
{
return _container.GetInstance(service, key);
}

protected override IEnumerable<object> GetAllInstances(Type service)
{
return _container.GetAllInstances(service);
}

protected override void BuildUp(object instance)
{
_container.BuildUp(instance);
}

protected override void PrepareViewFirst(Frame rootFrame)
{
_container.RegisterNavigationService(rootFrame);
}

}
}

[/code]

Como se puede ver, en el método Configure configuramos el contenedor para que sea capaz de resolver todas las dependencias que tiene nuestra aplicación. Así como sobrescribir los métodos necesarios para que cuando se vaya a realizar la resolución se utilice nuestro contenedor, esos son los métodos GetInstance, GetAllInstances, BuildUp.

En Caliburn existen dos formas de cargar las pantallas, por su ViewModel (ViewModel first) o por la View (View first). En la sobrescritura de la función OnLaunched indicamos a qué vista queremos navegar, finalmente, en la sobrescritura del método PrepareViewFirst registramos el servicio de navegación (NavigationService) que nos será útil más adelante para poder navegar entre vistas.

Como dijimos anteriormente, en este punto es en el que tenemos que configurar nuestro contenedor, a la hora de registrar una clase en el contenedor tenemos que decidir cómo queremos que se comporte, si se creará una nueva instancia cada vez que se resuelva la dependencia, o tendremos un singleton que será el objeto que se entregue en cada resolución. Para registrar un tipo y obtener en cada ocasión una nueva instancia usaremos la función PerRequest, y para que sea un singleton utilizaremos la función Singleton.

Cuando registremos un tipo en el contenedor, tenemos dos opciones, registrar directamente el tipo, como en esta línea:

[code language=»csharp»]
_container.PerRequest<MainPageViewModel>();
[/code]

O podemos indicar una interfaz e indicar cuál es la clase que la implementa:

[code language=»csharp»]
_container.Singleton<IContactsService, ContactsService>();
[/code]

Enlazando Views con sus ViewModels

Una de las grandes ventajas que tiene Caliburn es que el enlace entre View y ViewModel se hace de forma automática, sólo hay que seguir unas convenciones.

1. Las vistas tienen que estar en un directorio que se llame Views.

2. Los ViewModels tienen que estar en un directorio que se llame ViewModels.

3. View y ViewModel tienen que llamarse igual, y cada uno tienen que tener el sufijo View y ViewModel.
Por ejemplo, si la vista se llama MainPageView.xaml, su ViewModel se llamará MainPageViewModel.cs.

Siguiendo estas convenciones, el enlace entre View y ViewModel se hace de forma automática.

La estructura del proyecto será similar a esta:
screenshot3

Enlazando controles

Para enlazar controles de la vista a su contexto de datos Caliburn también nos echa una mano, basta con que el control tenga la propiedad x:Name con el mismo nombre que la propiedad, si vemos la pantalla MainPageView.xaml y su correspondiente ViewModel MainPageViewModel.cs, en la vista tenemos esta línea:

[code language=»xml»]
<ListView x:Name="Contact" SelectionMode="None" IsItemClickEnabled="True"

micro:Message.Attach="[Event ItemClick] = [Action Navigate($eventArgs)]"/>

[/code]

que automáticamente se enlaza a la propiedad del mismo nombre de su DataContext.

[code language=»csharp»]
private IEnumerable<ContactListViewModel> _contact;

public IEnumerable<ContactListViewModel> Contact
{
get { return _contact; }
set
{
_contact = value;
NotifyOfPropertyChange(() => Contact);
}
}

[/code]

Enlazando comandos

Para enlazar comandos a botones, ya sean botones dentro de la pantalla o dentro de la AppBar, la convención es exactamente la misma, se hace por el nombre del control. Por ejemplo, la AppBar de la página MainPage.xaml:

[code language=»xml»]
<Page.BottomAppBar>
<CommandBar>
<CommandBar.PrimaryCommands>
<AppBarButton Icon="Add" Label="Add"
x:Name="Add" />
</CommandBar.PrimaryCommands>
</CommandBar>
</Page.BottomAppBar>

[/code]

Aquí tenemos un botón que se llama Add, cuando se presione ese botón, se llamará al método Add del DataContext:

[code language=»csharp»]
public void Add()
{
_navigationService.NavigateToViewModel<ContactDetailViewModel>(Guid.Empty);
}

[/code]

Si además añadimos un método que se llame CanAdd, este método será el que determine si el comando se puede ejecutar o no. Si analizamos el código de la clase ContactDetailViewModel.cs vemos que existe un método que es CanDelete que habilita o deshabilita ese comando.

[code language=»csharp»]
public class ContactDetailViewModel : Screen
{
private readonly IContactsService _contactsService;
private readonly INavigationService _navigationService;

public Guid Parameter { get; set; }

private Guid _id;
private string _forename;
public string Forename
{
get { return _forename; }
set
{
_forename = value;
NotifyOfPropertyChange(() => Forename);
}
}
private string _surname;

public string Surname
{
get { return _surname; }
set
{
_surname = value;
NotifyOfPropertyChange(() => Surname);
}
}
private string _address;

public string Address
{
get { return _address; }
set
{
_address = value;
NotifyOfPropertyChange(() => Address);
}
}
private string _phoneNumber;
public string PhoneNumber
{
get { return _phoneNumber; }
set
{
_phoneNumber = value;
NotifyOfPropertyChange(() => PhoneNumber);
}
}

public ContactDetailViewModel(IContactsService contactsService, INavigationService navigationService)
{
_contactsService = contactsService;
_navigationService = navigationService;
}

protected override void OnInitialize()
{
base.OnInitialize();
LoadContactData();
}

private void LoadContactData()
{
var contact = _contactsService.GetContact(Parameter);
if (contact == null)
{
_id = Guid.Empty;
}
else
{
_id = contact.Id;
Forename = contact.Forename;
Surname = contact.Surname;
PhoneNumber = contact.PhoneNumber;
Address = contact.Address;
}
}

public void Save()
{
_contactsService.Save(_id, Forename, Surname, Address, PhoneNumber);
_navigationService.GoBack();
}

public void Delete()
{
_contactsService.Delete(_id);
_navigationService.GoBack();
}

public bool CanDelete()
{
return _id != Guid.Empty;
}
}

[/code]

También se pueden enlazar comandos a eventos de controles, para ello hay que utilizar la AttachedProperty Message.Attach indicando el evento que queremos enlazar y el manejador de ese evento, así como pasarle por parámetros los argumentos del evento, por ejemplo, en la vista MainPageView.xaml

[code language=»xml»]
<ListView x:Name="Contact" SelectionMode="None" IsItemClickEnabled="True"
micro:Message.Attach="[Event ItemClick] = [Action Navigate($eventArgs)]"/>

[/code]

La clase Screen

Como ya hemos dicho, Caliburn es un framework que nos aporta alguna funcionalidad para implementar MVVM ya hecha, entre otras trae una implementación de la interfaz INotifyPropertyChanged que podremos utilizar en nuestros ViewModels. Para implementar esta interfaz lo podemos hacer de dos maneras, hacer que nuestra clase herede de PropertyChangedBase, con la que podremos notificar a la UI los cambios que haya habido, o, si además el ViewModel necesita más información de su vista, de la clase Screen.

La clase Screen nos ofrece unas sobrescrituras muy interesantes, ya que permiten al ViewModel saber entre otras cosas:

· Cuando se ha inicializado la clase con la sobrescritura OnInitialize.

· Cuando la vista se muestra con la sobrescritura OnActivate.

· Cuando se oculta la vista con la sobrescritura OnDeactivate.

· Cuando se ha realizado en enlace entre View y ViewModel con la sobrescritura de OnViewAttached.

Navegación entre vistas.

Caliburn tiene incorporado un servicio de navegación que viene definido por la interfaz INavigationService, este servicio permite navegar tanto utilizando la aproximación ViewFirst o la ViewModelFirst.

Para utilizarlo tendremos que añadir al constructor de nuestro ViewModel un parámetro de tipo INavigationService para que Caliburn lo resuelva mediante inyección de dependencias.

Para navegar a sólo tendremos que hacer:

[code language=»csharp»]
_navigationService.NavigateToViewModel<ContactDetailViewModel>();
[/code]

En esta navegación podemos utilizar parámetros:

[code language=»csharp»]
_navigationService.NavigateToViewModel<ContactDetailViewModel>(contact.Id);
[/code]

Para recuperar este parámetro en la clase de destino tenemos que tener una propiedad pública que se llame Parameter que sea del mismo tipo que el parámetro que queremos pasar de un ViewModel a otro.

[code language=»csharp»]
public class ContactDetailViewModel : Screen
{
private readonly IContactsService _contactsService;
private readonly INavigationService _navigationService;
public Guid Parameter { get; set; }

private Guid _id;
}
[/code]

Proyecto de demo

El proyecto que vamos a ver todo lo que hemos comentado anteriormente se puede descargar desde GitHub.

Lo primero que podemos ver es que el proyecto sigue la estructura recomendada para utilizar Caliburn, las vistas están en los directorios Views de sus respectivos proyectos, y los ViewModels están en el directorio ViewModels de su proyecto.
screenshot4

Nuestro proyecto consta de 2 páginas, la página principal, que contiene un listado de contactos, y una página para ver el detalle del contacto seleccionado.

App.xaml

[code language=»xml»]
<micro:CaliburnApplication
x:Class="Caliburn101.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:micro="using:Caliburn.Micro"
xmlns:local="using:Caliburn101">

</micro:CaliburnApplication>
[/code]

Aquí hemos cambiado el objeto App por CaliburnApplication para que la aplicación al arrancar sea capaz de configurar y cargar todos los elementos de Caliburn.

App.xaml.cs

[code language=»csharp»]
using Caliburn.Micro;
using Caliburn101.Services;
using Caliburn101.ViewModels;
using Caliburn101.Views;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Animation;
using Windows.UI.Xaml.Navigation;

// The Blank Application template is documented at http://go.microsoft.com/fwlink/?LinkId=234227

namespace Caliburn101
{
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public sealed partial class App : CaliburnApplication
{
private WinRTContainer _container;

public App()
{
InitializeComponent();
}

protected override void Configure()
{
_container = new WinRTContainer();
_container.RegisterWinRTServices();

_container.PerRequest<MainPageViewModel>();
_container.PerRequest<ContactDetailViewModel>();
_container.Singleton<IContactsService, ContactsService>();
}

protected override void OnLaunched(LaunchActivatedEventArgs args)
{
DisplayRootView<MainPageView>();
}

protected override object GetInstance(Type service, string key)
{
return _container.GetInstance(service, key);
}

protected override IEnumerable<object> GetAllInstances(Type service)
{
return _container.GetAllInstances(service);
}

protected override void BuildUp(object instance)
{
_container.BuildUp(instance);
}

protected override void PrepareViewFirst(Frame rootFrame)
{
_container.RegisterNavigationService(rootFrame);
}

}
}

[/code]

Configuramos el contenedor para que Caliburn sea capaz de resolver todas las dependencias de las clases de nuestro proyecto. Registramos los tipos MainPageViewModel y ContactDetailViewModel para que cada vez que se resuelvan se cree una nueva instancia, y el tipo ContactsService como la clase que implementa la interfaz IContactsService y además la registramos como singleton.

Finalmente cargamos la vista MainPageView como vista de inicio.

MainPageView.xaml

[code language=»xml»]
<Page
x:Class="Caliburn101.Views.MainPageView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Caliburn101"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:micro="using:Caliburn.Micro"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid>
<ListView x:Name="Contact" SelectionMode="None" IsItemClickEnabled="True"
micro:Message.Attach="[Event ItemClick] = [Action Navigate($eventArgs)]"/>
</Grid>
<Page.BottomAppBar>
<CommandBar>
<CommandBar.PrimaryCommands>
<AppBarButton Icon="Add" Label="Add"
x:Name="Add" />
</CommandBar.PrimaryCommands>
</CommandBar>
</Page.BottomAppBar>
</Page>
[/code]

La página principal contiene un listado que hemos llamado Contact, que es el mismo nombre que tendrá la propiedad que será el ItemsSource del control. La lista también tiene enlazado el evento ItemClick del control con el método Navigate de la clase MainPageViewModel.

En la parte inferior tenemos definido un botón cuyo Command está asociado al método Add de la clase MainPageViewModel.

MainPageViewModel.cs

[code language=»csharp»]
using Caliburn.Micro;
using Caliburn101.Services;
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using Windows.UI.Xaml.Controls;

namespace Caliburn101.ViewModels
{
public class MainPageViewModel : Screen
{
private readonly IContactsService _contactsService;
private readonly INavigationService _navigationService;

private IEnumerable<ContactListViewModel> _contact;

public IEnumerable<ContactListViewModel> Contact
{
get { return _contact; }
set
{
_contact = value;
NotifyOfPropertyChange(() => Contact);
}
}

public MainPageViewModel(IContactsService contactsService, INavigationService navigationService)
{
_contactsService = contactsService;
_navigationService = navigationService;
}

protected override void OnActivate()
{
base.OnActivate();
Contact = _contactsService.GetContacts().Select(c => new ContactListViewModel(c));
}

public void Navigate(ItemClickEventArgs args)
{
var contact = args.ClickedItem as ContactListViewModel;
_navigationService.NavigateToViewModel<ContactDetailViewModel>(contact.Id);
}

public void Add()
{
_navigationService.NavigateToViewModel<ContactDetailViewModel>(Guid.Empty);
}

public void Back()
{
_navigationService.GoBack();
}

public bool CanBack()
{
return _navigationService.CanGoBack;
}
}
}
[/code]

DataContext para la página principal, en su constructor tenemos dos argumentos, de los tipos IContactsService e INavigationService, Caliburn automáticamente resuelve estas dos dependencias y las carga en el constructor. Luego con la sobrescritura del método OnActivated cada vez que la vista asociada a esta clase se muestra se cargan los datos y se asignan a la propiedad Contact, que al llamarse igual que el control de la vista MainPageView se pintan en la lista.

Tenemos el método Add que es el comando del botón de la AppBar de la vista y el método Navigate que habíamos enlazado al evento ElementClick de la lista.

Ahora vamos a prestarle atención a propiedad Contact, esta propiedad es un IEnumerable<ContactListViewModel>. Ya que los objetos tienen el sufijo ViewModel cuando se van a intentar pintar, Caliburn busca una clase en la carpeta Views que se corresponda en nombre, es decir, que se llame ContactListView, como esta vista solo la empleamos para mostrar datos estáticos, no vamos a profundizar en ella.

ContactDetailsView

[code language=»xml»]
<Page
x:Class="Caliburn101.Views.ContactDetailView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Caliburn101.Views"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">

<StackPanel>
<TextBlock Text="Forename" Style="{StaticResource TitleTextBlockStyle}" />
<TextBox x:Name="Forename" />
<TextBlock Text="Surname" Style="{StaticResource TitleTextBlockStyle}" />
<TextBox x:Name="Surname" />
<TextBlock Text="PhoneNumber" Style="{StaticResource TitleTextBlockStyle}" />
<TextBox x:Name="PhoneNumber" />
<TextBlock Text="Address" Style="{StaticResource TitleTextBlockStyle}" />
<TextBox x:Name="Address" />
</StackPanel>

<Page.BottomAppBar>
<CommandBar>
<CommandBar.PrimaryCommands>
<AppBarButton Icon="Save" Label="Save"
x:Name="Save" />
<AppBarButton Icon="Delete" Label="Delete"
x:Name="Delete" />
</CommandBar.PrimaryCommands>
</CommandBar>
</Page.BottomAppBar>
</Page>

[/code]

En esta pantalla mostramos y permitimos modificar los datos de los contactos, vemos que tenemos varios TextBox cuyo nombre coinciden con propiedades de la clase ContactDetailsViewModel, así que Caliburn hace el enlace de forma automática. También tenemos dos botones en la parte inferior, para guardar y para borrar el contacto. Vamos a ver cómo están implementados en su ViewModel.

ContactDetailsViewModel

[code language=»csharp»]
using Caliburn.Micro;
using Caliburn101.Services;
using System;
using System.Collections.Generic;
using System.Text;

namespace Caliburn101.ViewModels
{
public class ContactDetailViewModel : Screen
{
private readonly IContactsService _contactsService;
private readonly INavigationService _navigationService;

public Guid Parameter { get; set; }

private Guid _id;
private string _forename;
public string Forename
{
get { return _forename; }
set
{
_forename = value;
NotifyOfPropertyChange(() => Forename);
}
}
private string _surname;

public string Surname
{
get { return _surname; }
set
{
_surname = value;
NotifyOfPropertyChange(() => Surname);
}
}
private string _address;

public string Address
{
get { return _address; }
set
{
_address = value;
NotifyOfPropertyChange(() => Address);
}
}
private string _phoneNumber;

public string PhoneNumber
{
get { return _phoneNumber; }
set
{
_phoneNumber = value;
NotifyOfPropertyChange(() => PhoneNumber);
}
}

public ContactDetailViewModel(IContactsService contactsService, INavigationService navigationService)
{
_contactsService = contactsService;
_navigationService = navigationService;
}

protected override void OnInitialize()
{
base.OnInitialize();
LoadContactData();
}

private void LoadContactData()
{
var contact = _contactsService.GetContact(Parameter);
if (contact == null)
{
_id = Guid.Empty;
}
else
{
_id = contact.Id;
Forename = contact.Forename;
Surname = contact.Surname;
PhoneNumber = contact.PhoneNumber;
Address = contact.Address;
}
}

public void Save()
{
_contactsService.Save(_id, Forename, Surname, Address, PhoneNumber);
_navigationService.GoBack();
}

public void Delete()
{
_contactsService.Delete(_id);
_navigationService.GoBack();
}

public bool CanDelete()
{
return _id != Guid.Empty;
}

public void Back()
{
_navigationService.GoBack();
}

public bool CanBack()
{
return _navigationService.CanGoBack;
}
}
}
[/code]

Esta clase contiene todas las propiedades que se enlazan a los TextBox de la vista, además de los métodos Save, que responde al botón del mismo nombre de la vista, y Delete que responde al otro botón que tenemos en la UI. La última función interesante es CanDelete que al hacer el enlace entre el botón y la función se lanza para determinar si el botón se muestra habilitado o deshabilitado, si pulsamos en el botón Add de la pantalla principal es muestra deshabilitado.

Conclusión

Espero que con este post hayas visto lo sencillo que resulta añadir Caliburn a un proyecto, y lo rápido que puedes estar aprovechando algunas de sus características. En futuros posts veremos cómo utilizarlo en Aplicaciones Universales de Windows 10, y también alguna funcionalidad un poco más avanzada.

Happy coding!!!

Fernando de la Hermosa (@delahermosa)