En este post vamos a revisar algunos conceptos básicos de Caliburn.Micro que nos servirán para poder construir aplicaciones compuestas.

¿Qué es una aplicación compuesta?

Una aplicación compuesta es una aplicación que está dividida en módulos, y estos módulos son independientes entre sí, es decir, un módulo no necesita de otro módulo para funcionar, o al menos no deberían.

Existen varios frameworks para construir aplicaciones compuestas, como Prism, pero en esta ocasión nos vamos a centrar en cómo hacerlo con Caliburn.Micro.

Para ir empezando primero vamos a explicar qué son las clases Screen y ConductorScreen y después, cuando tenemos varios ViewModels en la aplicación, veremos cómo hacer que se comuniquen entre sí evitando acoplamiento entre las clases.

Screens y Conductors

Para profundizar más en estas clases es recomendable la lectura de la documentación oficial de Caliburn.Micro

¿Qué son?

Un objeto de la clase Screen es una parte de la aplicación que tiene un ciclo de vida propio, es decir, se crea, se activa, se desactiva dentro del ciclo de vida de nuestra aplicación, por ejemplo si tuviéramos un editor de texto, sería cada una de las ventanas que se usan para abrir un archivo de texto, se activa cuando un usuario abre un archivo, se desactiva cuando el usuario cambia el documento que está visualizando o decide cerrar la ventana que contiene el archivo de texto. Cuando una ventana se crea, se activa o se desactiva, se propagan eventos para poder capturarlos en caso que sea necesario para realizar alguna funcionalidad.

Un objeto de la clase Conductor es la responsable de gestionar el ciclo de vida de uno o varios objetos de la clase Screen, es el responsable de activar, desactivar o cerrar los objetos de la clase Screen que administra.

Existen los siguientes tipos de Conductors:

· Conductor<T>: Gestiona una única pantalla, si mientras hay una pantalla activa se activa otra, la pantalla previa se desactiva, cierra y es olvidada por el Conductor. Puede usarse en algunos escenarios muy simples de navegación y detalle.

· Conductor<T>.Collection.OneActive: Gestiona varias pantallas, pero solo una puede ser la ventana activa. Cuando se activa una ventana, la ventana que estuviera activa se desactivará, pero seguirá bajo el control del Conductor. Las pantallas se pueden cerrar de forma explícita, esto también las quitará del control del Conductor. En caso que la ventana activa se cierre se encargará de activar otra de las vistas que gestiona.

· Conductor<T>.Collection.AllActive: como la anterior, salvo que permite tener varios elementos activos al mismo tiempo.

¡Veámoslo funcionando!

En este repositorio está el código que vamos a comentar a continuación, para ver como configurar el proyecto y ponerlo en marcha puedes leer este otro post.

En este ejemplo vamos a tener una ventana principal que va a ser el Conductor, en este caso vamos a utilizar la primera implementación, la que permite una única pantalla al tiempo. El proyecto no es más que un pequeño asistente que según pulsemos a los botones Next o Back va a ir navegando entre las pantallas.

Como podemos ver la clase MainPageViewModel, es el ViewModel de la pantalla principal, que va a ser el contenedor de todas las pantallas de la aplicación, podríamos decir que es nuestro Shell. Esta clase es la que hereda de Conductor<Screen>.

[code language=»csharp»]
public class MainPageViewModel : Conductor<Screen>
[/code]

MainPageView contiene un ContentControl que está enlazado a la propiedad ActiveItem, que es el elemento que está activo en este momento.

[code language=»xml»]
<ContentControl x:Name="ActiveItem" HorizontalAlignment="Center" VerticalAlignment="Center">

</ContentControl>
[/code]

El resto no son mas que 3 vistas por las que iremos navegando.

Dentro de la lógica de la clase MainPageViewModel vemos que según vayamos navegando entre las vistas, únicamente tenemos que llamar al método ActivateItem, y es el Conductor el que se encarga de desactivar la vista que estuviera activa hasta ese momento, y activar la que le estamos pidiendo.

[code language=»csharp»]
public void Next()
{
_currentPage++;
UpdateActiveView();
}

public void Back()
{
_currentPage–;
UpdateActiveView();
}

private void UpdateActiveView()
{
Screen activeScreen = null;
switch (_currentPage)
{
case 1:
activeScreen = _screen1ViewModel;
break;
case 2:
activeScreen = _screen2ViewModel;
break;
case 3:
activeScreen = _screen3ViewModel;
break;
default:
_currentPage = 1;
activeScreen = _screen1ViewModel;
break;
}
ActivateItem(activeScreen);
NotifyOfPropertyChange(() => CanBack);
NotifyOfPropertyChange(() => CanNext);
}
[/code]

En la próxima sección utilizaremos un Conductor del tipo AllActive. Pero de momento, con esto hemos aprendido a utilizar de forma básica un Conductor.

Comunicación entre ViewModels

Cuando hablamos de una aplicación compuesta partimos de la premisa que, en teoría, cada módulo debería ser una isla completamente independiente del resto de módulos.

Pero en algunos casos necesitaremos que nuestros módulos sean capaces de reaccionar ante posibles eventos que en nuestra aplicación puedan pasar. Por ejemplo, supongamos que tenemos una aplicación compuesta por un módulo de inicio de sesión, un módulo de clientes, y uno de contabilidad. Aunque ninguno tiene que saber nada del módulo que le acompaña, tanto el módulo de clientes como el de seguridad necesitarán saber cuando un usuario se ha logado correctamente o ha cerrado sesión  en la aplicación. Pero necesitamos evitar que los módulos se acoplen entre sí.

Esto se consigue utilizando el patrón de diseño EventAggregator. Este patrón consiste en que tenemos dos partes dentro de la aplicación, un publicador y un subscriptor, entre ellos no tienen nada en común, de echo, uno no sabe de la existencia del otro, pero estamos indicando que el subscriptor cada vez que se lance un evento de un tipo en concreto, se intercepte y se llame a un manejador. Y por la parte de publicador estamos indicando que “algo” ha ocurrido, pero no se preocupa de saber si hay alguien al otro lado esperando a ese evento.

Caliburn.Micro nos ofrece una implementación de este patrón siempre que resolvamos la interfaz IEventAggregator.

Ahora un poco de código.

En este repositorio se encuentra el código de la aplicación que vamos a comentar.

El proyecto contiene cuatro cinco vistas, la pantalla principal MainPage, y cuatro vistas para añadir al Conductor. La pantalla principal permite tener varios elementos activos al mismo tiempo.

[code language=»csharp»]
public class MainPageViewModel : Conductor<Screen>.Collection.AllActive
[/code]

Cuando crea la instancia de la pantalla pricipal, se resuelven todas las pantallas que se van a incluir en el Conductor, y el objeto del tipo IEventAggregator, que usaremos para publicar un evento a todo aquel que esté interesado en él, en este caso la pulsación de un botón en la AppBar.

[code language=»csharp»]
public MainPageViewModel(Screen1ViewModel screen1ViewModel, Screen2ViewModel screen2ViewModel, Screen3ViewModel screen3ViewModel, Screen4ViewModel screen4ViewModel, IEventAggregator eventAggregator)
{
_screen1ViewModel = screen1ViewModel;
_screen2ViewModel = screen2ViewModel;
_screen3ViewModel = screen3ViewModel;
_screen4ViewModel = screen4ViewModel;
_eventAggregator = eventAggregator;
}
[/code]

[code language=»csharp»]
public void Play()
{
_eventAggregator.PublishOnUIThread(new MainPageButtonPressedEvent());
}
[/code]

De las cuatro pantallas que hemos añadido queremos que tres sean subscriptoras, y que hagan algo cuando se publique el evento. Para ello hay que hacer dos cosas:

  • implementar la interfaz IHandle<T> donde T es la clase del evento que queremos capturar, en caso de tener que capturar varios eventos, deberíamos añadir una implementación por cada tipo que queramos manejar.
  • resolver la instancia del EventAggregator para llamar al método Subscribe para que podamos “escuchar” los eventos que se publiquen.

[code language=»csharp»]
public class Screen1ViewModel : Screen, IHandle<MainPageButtonPressedEvent>
{
private readonly IEventAggregator _eventAggregator;

private string _eventText;
public string EventText
{
get { return _eventText; }
set
{
_eventText = value;
NotifyOfPropertyChange(() => EventText);
}
}

public Screen1ViewModel(IEventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
}

protected override void OnActivate()
{
base.OnActivate();
_eventAggregator.Subscribe(this);
}

protected override void OnDeactivate(bool close)
{
base.OnDeactivate(close);
_eventAggregator.Unsubscribe(this);
}
public void Handle(MainPageButtonPressedEvent message)
{
EventText = "The button in the appbar has been clicked";
}
}
[/code]

El evento que vamos a publicar es una instancia de una clase,

[code language=»csharp»]
public class MainPageButtonPressedEvent
{
}
[/code]

Con estos sencillos pasos podremos comunicar mediante eventos los elementos de nuestra aplicación compuesta.

En el próximo post veremos como crear los módulos y cargarlos en una aplicación de Caliburn.

Happy coding!!!

Fernando de la Hermosa (@delahermosa)