[Entrada original publicada en proyecto-mono.org]

mvvmMvvmCross nace de una necesidad, una necesidad de llevar el desarrollo móvil multiplataforma al siguiente nivel. La idea principal de MvvmCross es llevar el patrón MVVM al desarrollo para iOS y Android, manteniendo tu código (Model y ViewModel) en una sola librería y reimplementando la Vista en cada plataforma. Como os decía, hoy voy a hablaros del siguiente nivel en el desarrollo móvil multiplataforma.

 

De dónde venimos y a dónde vamos

MvvmCross es un fork de MonoCross. MonoCross grosso modo es un excelente proyecto, que nos permite utilizar el patrón MVC en nuestras aplicaciones de manera muy similar a cómo desarrollaríamos para ASP.NET MVC, es decir, usando el patrón MVC y un sistema de rutas para comunicarnos con la vista.
Otros proyectos en los que se basa MvvmCross son: MvvmLight, ASP.NET MVC y OpenNetCF.

Por supuesto para poder ejecutar nuestras aplicaciones con diferentes targets, en nuestro caso iOS y Android (además de Windows Phone) necesitaremos MonoTouch y MonoForAndroid. Si esto te parece poco, actualmente hay soporte para OSX, WPF, Windows forms  y Windows RT que aunque no lo veremos aquí, la forma de proceder es análoga.

MVVM Nuestro aliado

Si has desarrollado para desktop o móvil con las tecnologías de Microsoft, seguramente ya conoces el patrón MVVM. Si por el contrario vienes del mundo iOS o Android quizá no te resulta tan familiar, para ellos, lo explico brevemente:

El patrón MVVM (Model View ViewModel) es una variación del patrón MVC (Model View Controller). El modelo y la vista básicamente son los mismos que en  MVC, donde encontramos la diferencia principal es en Controller vs ViewModel. El ViewModel es un mediador entre el modelo y la vista, es el responsable de exponer los datos a la vista para que ésta los muestre a través de comandos (basados en eventos) que la vista puede utilizar para comunicarse con el ViewModel.

Es muy fácil deducir el alto grado de desacoplamiento que tenemos utilizando el patrón arquitectónico MVVM, lo que nos proporciona dos grandes ventajas: Facilidad para realizar UnitTest (muy fácil usar DI + Repository) y sobre todo, facilita construir aplicaciones sin ninguna responsabilidad en la vista más que mostrar datos e interactuar con el usuario.

imgres

Show me the code

Vamos a basarnos en un ejemplo que puedes encontrar en el github del proyecto, en concreto el Sample Tutorial. De esta manera podré centrarme en las partes interesantes y el que quiera investigar por su cuenta, tiene un camino por el que seguir.

Bien, nuestro ViewModel es el siguiente:

 

public class TipViewModel
 
: MvxViewModel
 
{
 
private float _tipValue;
 
public float TipValue
 
{
 
get { return _tipValue; }
 
private set { _tipValue = value; RaisePropertyChanged(() => TipValue); }
 
}
 
private float _total;
 
public float Total
 
{
 
get { return _total; }
 
private set { _total = value; RaisePropertyChanged(() => Total); }
 
}
 
private float _subTotal;
 
public float SubTotal
 
{
 
get { return _subTotal; }
 
set { _subTotal = value; RaisePropertyChanged(() => SubTotal); Recalculate(); }
 
}
 
private int _tipPercent;
 
public int TipPercent
 
{
 
get { return _tipPercent; }
 
set { _tipPercent = value; RaisePropertyChanged(() => TipPercent); Recalculate(); }
 
}
 
public TipViewModel()
 
{
 
SubTotal = 60.0f;
 
TipPercent = 12;
 
Recalculate();
 
}
 
private void Recalculate()
{
 
TipValue = ((int)Math.Round(SubTotal * TipPercent)) / 100.0f;
 
Total = TipValue + SubTotal;
}
}
}

 

Lo primero que destaca es que nuestra clase hereda de MvxViewModel, obligatorio para indicar que se trata de un ViewModel. También tenemos una serie de propiedades que serán las que mostraremos en nuestra vista. Cada propiedad después de cambiar su valor, hace una llamada a RaisePropertyChanged que será el encargado de avisar a la vista de que el valor actual ha cambiado. Por último, en el constructor asignamos unos valores predeterminados y tenemos un método que cambia los valores de algunas de nuestras propiedades.
Además de este ViewModel, tendremos un ViewModel principal desde el que navegaremos hacia este:

 

public class MainMenuViewModel
       : MvxViewModel
   {
       public List Items { get; set; }
 
       public ICommand ShowItemCommand
       {
           get
           {
               return new MvxRelayCommand((type) => DoShowItem(type));
           }
       }
 
       public void DoShowItem(Type itemType)
       {
           this.RequestNavigate(itemType);
       }
 
       public MainMenuViewModel()
       {
           Items = new List()
                       {
                          // typeof(Lessons.SimpleTextPropertyViewModel),
                          // typeof(Lessons.PullToRefreshViewModel),
                           typeof(Lessons.TipViewModel),
                          // typeof(Lessons.CompositeViewModel),
                          // typeof(Lessons.LocationViewModel)
                       };
       }
   }

Como vemos hemos vuelto a heredar de MvxViewModel, ya que volvemos a necesitar un ViewModel. Esta vez tenemos una lista de ítems, del tipo Type que en nuestro caso sólo contendrá un elemento puesto que sólo hemos definido TipViewModel (recomiendo bajarse el proyecto entero para ver los demás tipos), nótese que la lista de ítems podría haber sido perfectamente una lista del tipo ObservableCollection, pero como no va a cambiar no es necesario. También tenemos  un ICommand que ejecutará la acción DoShowItem que, como se puede ver en el código, hace una llamada a RequestNavigate(), con este método le decimos a MvvmCross que queremos ir a otra vista. Esto es maravilloso, nos abstrae del sistema de navegación de la plataforma y de una forma realmente trivial. Por último, en el constructor asignamos la lista de ítems (están comentados todos menos los que usamos en este ejemplo).

CrossPlatform

En el repo podréis ver más plataformas (Android, WinRT y Windows Phone) pero en este ejemplo, por cuestiones de espacio sólo voy a mostrar la de iOS. Y es que iOS tiene un serio inconveniente ya que se suele utilizar para diseñar las interfaces el editor de XCode (aunque yo personalmente prefiero escribir el código). El problema que tiene XCode para diseñar las interfaces es que el XML que genera no es muy agradable a la vista y por tanto no se puede manipular, lo que nos obligará a hacer los bindings en code-behind. Los bindings que haremos con nuestra vista en Windows Phone quedarían en el mismo XAML (de manera muy similar a como trabajaríamos normalmente si somos desarrolladores Windows Phone). En Android, puesto que también se basa en XML para definir la interfaz (y es amigable) el binding también lo podemos introducir como si fueran propiedades de nuestros controles. Pero como he dicho, en iOS no es así y por ello hay que hacer los bindings en code-behind.

Nuestra vista principal:

 

public class MainMenuView
        : MvxBindingTouchTableViewController
    {
        public MainMenuView(MvxShowViewModelRequest request)
            : base(request)
        {
        }
 
        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
 
            Title = "Views";
 
            var tableSource = new TableViewSource(TableView);
            tableSource.SelectionChanged += (sender, args) => ViewModel.DoShowItem((Type)args.AddedItems[0]);
 
            this.AddBindings(
                new Dictionary<object, string>()
                    {
                        { tableSource, "{'ItemsSource':{'Path':'Items'}}" }
                    });
 
            TableView.Source = tableSource;
            TableView.ReloadData();
        }
 
        #region Nested classes for the table
 
        public sealed class TableViewCell
            : MvxBindableTableViewCell
        {
            public const string BindingText = @"{'TitleText':{'Path':'Name'},'DetailText':{'Path':'FullName'}}";
 
            public TableViewCell(UITableViewCellStyle cellStyle, NSString cellIdentifier)
                : base(BindingText, cellStyle, cellIdentifier)
            {
                Accessory = UITableViewCellAccessory.DisclosureIndicator;
            }
        }
 
        public class TableViewSource : MvxBindableTableViewSource
        {
            static readonly NSString CellIdentifier = new NSString("TableViewCell");
 
            public TableViewSource(UITableView tableView)
                : base(tableView)
            {
            }
 
            protected override UITableViewCell GetOrCreateCellFor(UITableView tableView, NSIndexPath indexPath, object item)
            {
                var reuse = tableView.DequeueReusableCell(CellIdentifier);
                if (reuse != null)
                    return reuse;
 
                var toReturn = new TableViewCell(UITableViewCellStyle.Subtitle, CellIdentifier);
                return toReturn;
            }
        }
 
        #endregion
    }

Esta es la vista principal, como vemos hereda de   MvxBindingTouchTableViewController que es una subclase de UITableViewController que nos proporciona el framework e implementa ciertas funcionalidades (como el permitirnos hacer bindings). Para no entrar en demasiadas especificaciones de la plataforma en sí, y mantener el artículo crossPlatform voy a centrarme en explicar, brevemente, lo realmente importante aquí:

Con el método AddBindings, que recibe un diccionario como parámetro, le decimos a MvvmCross sobre qué propiedades queremos hacer bindings:
{ tableSource, “{‘ItemsSource’:{‘Path’:’Items’}}” }

Lo que le está diciendo es, “al objeto tableSource (key,object) busca la propiedad ItemsSource y le asignas la propiedad de nuestro ViewModel, Items.

Esto hará que la tabla genere una celda para cada elemento de Items (uno en nuestro caso y cinco en el ejemplo en el que nos basamos). Para asignar las datos que queremos mostrar en cada celda usamos @”{‘TitleText’:{‘Path’:’Name’},’DetailText’:{‘Path’:’FullName’}}”
que como antes, asigna a cada propiedad de la celda un valor del ViewModel. Nótese que está vez no hace falta especificar explícitamente el target porque lo estamos haciendo sobre la celda, antes lo estamos haciendo sobre la clase de la vista en sí y queríamos hacer binding en el objeto tableSource.

La vista donde se expondrá el otro ViewModel es la que sigue:

 

public partial class TipView : MvxBindingTouchViewController
    {
        public TipView (MvxShowViewModelRequest request) : base (request, "TipView", null)
        {
        }
 
        public override void ViewDidLoad ()
        {
            base.ViewDidLoad ();
 
            this.AddBindings(
                new Dictionary<object, string>()
                    {
                        { TipValueLabel, "{'Text':{'Path':'TipValue'}}" },
                        { TotalLabel, "{'Text':{'Path':'Total'}}" },
                        { TipPercentText, "{'Text':{'Path':'TipPercent','Converter':'Int','Mode':'TwoWay'}}" },
                        { TipPercentSlider, "{'Value':{'Path':'TipPercent','Converter':'IntToFloat','Mode':'TwoWay'}}" },
                        { SubTotalText, "{'Text':{'Path':'SubTotal','Converter':'Float','Mode':'TwoWay'}}" },
                    });
        }
 
        public override void ViewDidUnload ()
        {
            base.ViewDidUnload ();
 
            // Clear any references to subviews of the main view in order to
            // allow the Garbage Collector to collect them sooner.
            //
            // e.g. myOutlet.Dispose (); myOutlet = null;
 
            ReleaseDesignerOutlets ();
        }
 
        public override bool ShouldAutorotateToInterfaceOrientation (UIInterfaceOrientation toInterfaceOrientation)
        {
            // Return true for supported orientations
            return (toInterfaceOrientation != UIInterfaceOrientation.PortraitUpsideDown);
        }
    }

Esta vez heredamos de MvxBindingTouchViewController puesto que esta vez hemos diseñado nuestra vista con XCode (también está soportado MonoTouch.Dialog). Como vemos en el diccionario de bindings, añadimos varias labels y la propiedad que queremos exponer. Seguro que os habéis fijado, pero si no os lo digo yo, en esas propiedades se observan algunos converters. Y es que sí, lógicamente también podemos definir nuestros propios converters, no lo he querido incluir por el tamaño de la entrada. Pero comentar que se crean de manera análoga a como lo haríamos en Silverlight.

 Screen Shot 2013-02-19 at 7.37.14 PM

Más allá de MVVM

Como hemos visto MvvmCross nos permite implementar el patrón MVVM en nuestras aplicaciones. Si bien hay particularidades inherentes a cada plataforma (como que en iOS tenemos que ponerlo en code-behind), el resultado en general es sobresaliente. Pero MvvmCross es más que MVVM, MvvmCross incorpora un contenedor de inyección de dependencias de serie, multitud de librerías (como JSON.NET) y lo que es más importante, nos abstrae del acceso a los sensores de cada plataforma (gps y demás). Tiene un sistema de pluggins que por sí solo daría para varias entradas.

Enlaces interesantes

Proyecto en git

Blog de creador

Lista de enlaces interesantes

Tutorial

Desarrollar con MonoTouch desde Visual Studio

Utilizando librerías de clase portables