[Entrada original publicada en proyecto-mono.org]
MvvmCross 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.
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.
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
20 febrero, 2013 at 4:35 pm
UPDATE: MvvmCross es totalmente compatible con Xamarin 2.0, de hecho ahora no es necesario usar pluggins de terceros para programar desde Visual Studio.