[Windows 10] MVVM en Apps Windows

Introducción

Con la llegada de las herramientas de desarrollo de Windows 10 Technical Preview tenemos la culminación en el viaje hacia la convergencia en el desarrollo entre plataformas Windows.

Ahora hablamos de Apps Universales escritas una
única vez con un código comun tanto para la lógica de negocio como para
la interfaz de usuario. Además, generamos un único paquete que mantendrá una interfaz consistente y familiar para el usuario pero adaptada a cada plataforma.

Además de lanzarnos de pleno a analizar nuevos controles, APIs y
herramientas, debemos abordar como implementar el patrón MVVM en Apps
UAP para conseguir una buena arquitectura y compartir código.

El patrón MVVM

Model-View-ViewModel (MVVM) es un patrón de diseño de aplicaciones que permite desacoplar el código de interfaz de usuario del código que no sea de interfaz de usuario.

El patrón MVVM consta de 3 partes:

  • La vista (View) contiene la interfaz de usuario y su lógica.
  • El vista-modelo (ViewModel) contiene la lógica de presentación.
  • El modelo (Model) contiene la lógica de negocio junto a los datos.

La vista interactúa con el vista-modelo mediante enlace a datos (Data Binding) y mediante comandos:

Las ventajas conseguidas utilizando el patrón son significativas:

  • Nos permite dividir el trabajo de manera muy sencilla (diseñadores – desarrolladores)
  • El mantenimiento es más sencillo.
  • Permite realizar Test a nuestro código.
  • Permite una más fácil reutilización de código.

MVVM en escena!

Realmente podemos mantener una implementación MVVM muy similar a como hacíamos hasta ahora en Apps Universales Windows y Windows Phone 8,1.

Crearemos un nuevo proyecto UAP:

Comenzamos por la base. Contamos con dos ficheros clave en nuestro proyecto Universal para implementar el patrón MVVM:

  • PageBase
  • ViewModelBase

PageBase

Como revelamos con su nombre, será una clase de la que heredaran
todas las páginas de nuestra aplicación. Y si, comentamos que serán
todas las páginas de la aplicación (ya sean del proyecto Windows o del
proyecto Windows Phone). Las páginas en ambas plataformas es exactamente
igual, un objeto de tipo Page. Esta clase cuenta con varios objetivos:

  • Establecer el Frame activo en todo momento para que el acceso al mismo sea sencillo.
  • Permitir el acceso de los eventos de navegación desde nuestras viewmodels.

Además podría interesarnos además:

  • Gestionar las transiciones entre páginas.
  • Gestionar estados (Loading, etc.)
  • Gestionar el estado segun la conexión de red.

Con la funcionalidad básica (facilitarnos el acceso a Frame y eventos de navegación) quedaría:

public class PageBase : Page
{
        private ViewModelBase _vm;
 
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);
 
            _vm = (ViewModelBase)this.DataContext;
            _vm.SetAppFrame(this.Frame);
            _vm.OnNavigatedTo(e);
        }
 
        protected override void OnNavigatedFrom(NavigationEventArgs e)
        {
            base.OnNavigatedFrom(e);
            _vm.OnNavigatedFrom(e);
        }
}

Gestionamos los eventos básicos de navegación (al entrar y al salir de la página) de modo que, en el método OnNavigateTo obtenemos la viewmodel de la página y asignamos el Frame.

ViewModelBase

Continuamos con la segunda de nuestras clases base. En esta ocasión
trataremos la clase base de la que heredarán todos los viewmodels. Los
objetivos básicos de la clase son:

  • Notificar cambios (implementar INotifyPropertyChanged).
  • Acceso al objeto Frame que nos permitirá realizar la navegación.
  • Permitir el acceso a los eventos de navegación.
public abstract class ViewModelBase : INotifyPropertyChanged
{
        private Frame appFrame;
        private bool isBusy;
 
        public ViewModelBase()
        {
        }
 
        public Frame AppFrame
        {
            get { return appFrame; }
        }
 
        public bool IsBusy
        {
            get { return isBusy; }
            set
            {
                isBusy = value;
                RaisePropertyChanged();
            }
        }
 
        public event PropertyChangedEventHandler PropertyChanged;
 
        public abstract Task OnNavigatedFrom(NavigationEventArgs args);
 
        public abstract Task OnNavigatedTo(NavigationEventArgs args);
 
        public void RaisePropertyChanged([CallerMemberName]string propertyName = "")
        {
            var Handler = PropertyChanged;
            if (Handler != null)
                Handler(this, new PropertyChangedEventArgs(propertyName));
        }
 
        internal void SetAppFrame(Frame viewFrame)
        {
            appFrame = viewFrame;
        }
}

El acceso a los eventos de navegación lo lograremos implementando en la viewmodel los métodos abstractos OnNavigatedFrom y OnNavigatedTo. Esto no permite guardar y recuperar parámetros o estados.

Definiendo Vistas. Utilizando PageBase

En la carpeta Views añadiremos nuestras vistas. Cada página será un objeto de tipo PageBase, tanto en XAML:

<base:PageBase
    x:Class="Navigation.Views.MainView"
    xmlns:local="using:Navigation"
    xmlns:base="using:Navigation.Views.Base"
    mc:Ignorable="d"
    DataContext="{Binding FirstViewModel, Source={StaticResource Locator}}">
    <Grid>
    </Grid>
</base:PageBase>

Como en el code-behind:

public sealed partial class MainView : PageBase
{
     public MainView()
     {
         this.InitializeComponent();
     }
}

La VistaModelo

Creamos una clase derivada de ViewModelBase:

public class FirstViewModel : ViewModelBase
{
 
}

Implementamos los métodos de navegación definidos en ViewModelBase:

public class FirstViewModel : ViewModelBase
{
    public override Task OnNavigatedFrom(NavigationEventArgs args)
    {
        return null;
    }
 
    public override Task OnNavigatedTo(NavigationEventArgs args)
    {
        return null;
    }
}

Asociando la Vista con la VistaModelo

Ahora, necesitamos conectar nuestra vista con nuestro viewmodel.
Podemos hacerlo de múltiples formas, desde el constructor de la vista,
instanciandola en App, usando Ioc. En nuestro caso, utilizaremos Ioc.
Usaremos Unity.

Una vez añadida la referencia correspondiente en cada proyecto (Windows y Windows Phone) creamos una nueva clase en la carpeta Base dentro de la carpeta ViewModels llamada ViewModelLocator:

public class ViewModelLocator
{
     readonly IUnityContainer _container;
 
     public ViewModelLocator()
     {
            _container = new UnityContainer();
 
            _container.RegisterType<MainViewModel>();
     }
 
     public MainViewModel MainViewModel
     {
            get { return _container.Resolve<MainViewModel>(); }
     }
}

Sencillamente registramos nuestras viewmodels y creamos un par de
propiedades públicas por cada viewmodel para poder resolverlas y acceder
a ellas desde las vistas. A continuación, registramos nuestro locator en App.xaml:

<locator:ViewModelLocator x:Key="Locator" />

Asignamos la viewmodel como DataContext de nuestra viewmodel:

DataContext="{Binding MainViewModel, Source={StaticResource Locator}}"

Y todo preparado!.

Hel10 World!

Vamos a añadir un botón a nuestra interfaz que al ser pulsado muestre un mensaje en pantalla.

Añadimos el botón en nuestra vista (MainView.xaml):

<Button            
     Content="Hel10 World"            
     Command="{Binding HelloCommand}"
     HorizontalAlignment="Center" />

Nuestro botón cuenta con un comando definido en la ViewModel:

private ICommand _helloCommand;
 
public ICommand HelloCommand
{
     get { return _helloCommand = _helloCommand ?? new DelegateCommand(HelloCommandExecute); }
}
 
private void HelloCommandExecute()
{
     // TODO:    
}

Al ejecutar el comando debemos mostrar el mensaje. De momento, no hemos añadido lógica. Utilizaremos para mostrar el mensaje un servicio.

Servicios

Introducimos el concepto de servicio. Básicamente serán clases que cubrirán un escenario muy específico. Por ejemplo:

  • Gestionar Tiles
  • Enviar un email
  • Llamar por teléfono
  • Mostrar un mensaje

En lugar de escribir el código específico para esas acciones directamente en la ViewModel, lo haremos en clases separadas, nuestros servicios. Las ViewModels interactuarán con estos servicios para poder realizar y gestionar las acciones necesarias.

Primero definiremos nuestro servicio de diálogo mediante una sencilla interfaz:

public interface IDialogService
{
     void Show(string message);
}

Realizamos la implementación de nuestro servicio:

public class DialogService : IDialogService
{
     public void Show(string message)
     {
    var messageDialog = new MessageDialog(message);
    var result = messageDialog.ShowAsync();
     }
}

Muy simple. Utilizamos el MessageDialog para mostrar el mensaje recibido como parámetro en nuestro servicio.

En el ViewModelLocator registramos el servicio:

_container.RegisterType<IDialogService, DialogService>(new ContainerControlledLifetimeManager());

Visto como crear el servicio y como registrarlo queda la duda de como se resuelve y utiliza en la ViewModel.
Bastará con declarar la interfaz de la clase como parámetro del
constructor público de la ViewModel. Registramos el servicio y la
ViewModel en nuestro contenedor de Unity, Unity lo resolverá
automáticamente. Cada servicio necesario en la ViewModel es “inyectado” con la implementación declarada en nuestro contenedor.

De esta forma conseguimos:

  • No tener que crear nuevas instancias de nuestro servicio cada vez que sea necesario.
  • Tener el código para una acción concreta en una única clase, sin repeticiones similares en múltiples ViewModels.
  • Tenemos la capacidad de modificar la implementación a “inyectar” en la ViewModel de un servicio con suma facilidad.
public MainViewModel(IDialogService dialogService)
{
     _dialogService = dialogService;
}

Modificamos la ejecución del comando utilizando nuestro servicio de diálogo:

private void HelloCommandExecute()
{
     _dialogService.Show("Hello World!");
}

Si ejecutamos la App veremos algo como lo siguiente:

Al pulsar el botón, se ejecutará el comando definido en la ViewModel
asignada como DataContext de la vista, que utilizará el servicio de
dialogo inyectado en el constructor para mostrar el dialogo:

Podéis descargar el ejemplo realizado a continuación:

También podéis acceder al código fuente directamente en GitHub:

Ver GitHub

Recordar que cualquier tipo de duda o sugerencia la podéis dejar en los comentarios de la entrada.

Más información

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *