Guía de MVVM Cross para Windows 8.1

Esta semana me ha tocado portar una App con un MVVM propio y varias capas a MVVM Cross. El proceso no ha sido trivial así que paso a explicar todos los cambios / añadidos que he hecho. Esto, por supuesto, sirve también para todos aquellos que os queráis meter en el interesante mundo del MVVM Cross comenzando o portando una de vuestras apps.

El ejemplo va para Windows 8, pero no debería ser muy diferente en el resto de plataformas, sobretodo en Windows Phone 8.

Capa de Windows 8

Paso 1: Instalar de nuget el Starter Pack de MVVM Cross.

Paso 2: Setup

La idea es crear una clase Setup que registre lo necesario para la plataforma. En mi caso, como ya trabajaba con Servicios, no he hecho aquí ningún cambio, básicamente he tenido que registrar dichos servicios desde aquí al ViewModelLocator (lo que en MVVM Cross se conoce como App, responsable de inicializar la aplicación y la gestión/inyección de dependencias). He respetado el nombre porque siempre lo habíamos llamado así, pero sed libres de nombrarlo App, de hecho ya se añade con este nombre al instalar el paquete de nuget en la capa del ViewModel.

public class Setup : MvxStoreSetup
{
    public Setup(Frame rootFrame)
        : base(rootFrame)
    {
    }

    protected override IMvxApplication CreateApp()
    {
        var app = new Apps.Core.Mvvm.ViewModel.ViewModelLocator();
        app.RegisterType(typeof(SConnectionService), typeof(IConnectionService));
        app.RegisterType(typeof(SEncryptionService), typeof(IEncryptionService));
        app.RegisterType(typeof(SGeolocationService), typeof(IGeolocationService));
        app.RegisterType(typeof(SLaunchersService), typeof(ILaunchersService));
        app.RegisterType(typeof(SMapsService), typeof(IMapsService));
        app.RegisterType(typeof(SMessagingService), typeof(IMessagingService));
        app.RegisterType(typeof(SNavigationService), typeof(INavigationService));
        app.RegisterType(typeof(SNotificationService), typeof(INotificationService));
        app.RegisterType(typeof(SResourceService), typeof(IResourceService));
        app.RegisterType(typeof(SSettingsService), typeof(ISettingsService));

        app.RegisterType(typeof(DataAccess), typeof(IDataAccess));

        return app;
    }

    protected override IMvxTrace CreateDebugTrace()
    {
        return new DebugTrace();
    }
}

Paso 3: Modificar el App.xaml.cs

Al final del launched y antes del Windows.Current.Activate(); hay que añadir lo siguiente…

if (rootFrame.Content == null)
{
    var setup = new Setup(rootFrame);
    setup.Initialize();
    var startup = Cirrious.CrossCore.Mvx.Resolve();
    startup.Start();
}

Obviamente tendremos que borrar la línea que hace que se navegue a la primera página.

Paso 4: Vistas

Las vistas han de utilizar el using

xmlns:views="using:Cirrious.MvvmCross.WindowsStore.Views"

y construirse en el XAML como 

views:MvxStorePage

También tendremos que ir al cs de la vista y hacer que la vista herede de MvxStorePage.

Si queremos, no obstante, podemos usar una clase PageBase (que quizás ya usábamos antes) que herede de la clase MvxStorePage.

*Nota: Si tienes algún override y te has cargado el base puede que algo no vaya bien.

Paso 5: UserControls

Podemos crear UserControls con su propio ViewModel. Para ello asignaremos el ViewModel al DataContext (en el XAML, por ejemplo)

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

Si además tenemos una DependencyProperty y queremos asignarla a alguna propiedad del ViewModel haremos lo siguiente:

Creamos una propiedad de tipo DependencyProperty

public static readonly DependencyProperty HeaderPropery =
            DependencyProperty.Register("Header", typeof(string), typeof(UCTopBar), new PropertyMetadata(default(string)));

Creamos una propiedad de tipo el ViewModel que queramos.

IndexViewModel viewModel;

La asignamos en el constructor (en este caso se asigna la que hemos ido a buscar al ViewModelLocator desde la asignación al DataContext del XAML.

this.InitializeComponent();
viewModel = (IndexViewModel)this.DataContext;

Y finalmente enlazamos el DependencyProperty al ViewModel.

public string Header
{
    get
    {
        return viewModel.HeaderText;
    }
    set
    {
        viewModel.HeaderText = value;
    }
}

Desde el XAML, obviamente, enlazaríamos con la propiedad del ViewModel, haciendo un:

Text="{Binding HeaderText, Mode=OneWay}"

Hay que tener en cuenta que al cargar el ViewModel con la llamada al ViewModelLocator no seguirá el ciclo de vida habitual de MVVM Cross, ya que ni siquiera entrará en el InitBundle (lo veremos más adelante).

Capa del MVVM

Paso 1: Archivo ViewModelLocator (el ya nombrado App.cs de MVVM Cross).

Añadimos un método para cargar los servicios desde el Setup de la capa de plataforma.

public void RegisterType(Type tService, Type tInterface)
{
    Mvx.RegisterType(tInterface, tService);
}

En el Initialize cargamos las clases que queramos que se nos gestione mediante inyección de dependencias.

Por ejemplo, cargamos los ViewModels de esta capa mediante:

CreatableTypes()
                .EndingWith("ViewModel")
                .AsTypes()
                .RegisterAsLazySingleton();

Podemos cargar archivos de otras capas haciendo algo así:

typeof(IAddressRepository).Assembly.CreatableTypes()
                            .EndingWith("Repository")
                            .AsInterfaces()
                            .RegisterAsLazySingleton();

Y finalmente registramos el ViewModel que queramos como inicio de la App.

RegisterAppStart();

También deberemos hacer un método, como hacíamos con el antiguo ViewModelLocator, para los ViewModels que no carguemos de forma habitual, como por ejemplo lo mencionado anteriormente de los UserControls.

public IndexViewModel IndexViewModel
{
    get
    {
        try
        {
            return Mvx.Resolve();
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine(ex.Message);
        }
        return Mvx.Resolve();
    }
}

Paso 2: ViewModels

El ViewModel ha de heredar ahora de MvxViewModel, cosa que no impide tener un ViewModelBase si queremos.

En cuanto al constructor, en el caso del ViewModelBase haríamos algo así.

public ViewModelBase(IMessagingService mesService, INotificationService notService, INavigationService navService, IResourceService resService)
{
    this._messagingService = mesService;
    this._notificationService = notService;
    this._navigationService = navService;
    this._resourceService = resService;
            
    InitializeCommands();
}

Los commands muy parecidos a los habituales.

private Lazy _cmdGoBack;

public ICommand CmdGoBack
{
    get
    {
        return this._cmdGoBack.Value;
    }
}

Y el InitializeCommands

private void InitializeCommands()
{
    this._cmdGoBack = new Lazy(() => new MvxCommand(() =>
    { 
        Close(this);
    }));
}

Éste en concreto cierra el ViewModel actual para ir atrás en la navegación.

Un ejemplo de ViewModel más completo que hereda de ViewModelBase

public CustomerViewModel(IConnectionService conService, IGeolocationService geoService,
    IManagementCustomer customerManagement, IManagementAgent agentManagement,
    IMessagingService mesService, INotificationService notService, INavigationService navService, IResourceService resService) : base(mesService, notService,navService,resService)
{
    this._connectionService = conService;
    this._geolocationService = geoService;
    this._managementCustomer = customerManagement;
    this._managementAgent = agentManagement;
    InitializeCommands();
}

Navegación

ShowViewModel(new { LexId = 0 });

Con esto vamos al AddressViewModel pasándole un parámetro con nombre “LexId”.

Para recoger estos parámetros…

protected async override void InitFromBundle(Cirrious.MvvmCross.ViewModels.IMvxBundle parameters)
{ 
    int param = Convert.ToInt16(parameters.Data["LexId"]);
    await InitializeMapHeaderAndParamDependences(param);

    base.InitFromBundle(parameters);
}

Éste método también nos servirá para inicializar nuestro ViewModel.

Si queremos, no obstante, podemos usar el método Start.

public override void Start()
{
    base.Start();
}

Y para ir atrás se usa el ya citado Close.

Close(this);

Estado de navegación

Si quieres tener algunos datos cargados por si vuelves a la vista…

Usa el SaveStateToBundle para guardar todo lo que quieras (pasará automáticamente por aquí antes de ir a otro ViewModel).

protected override void SaveStateToBundle(IMvxBundle bundle)
{
    bundle.Data["FilterIsVisible"] = _filterIsVisible.ToString();
    bundle.Data["GridItem"] = _gridItem.ToString();
    bundle.Data["HorizontalOffset"] = _horizontalOffset.ToString();
    bundle.Data["SortOption"] = _sortOption.ToString();
    bundle.Data["SearchText"] = _searchText;
    bundle.Data["FirstSortOption"] = _firstSortOption.ToString();
            
    base.SaveStateToBundle(bundle);
}

Y luego carga los datos mediante el ReloadFromBundle (pasará por aquí después del InitFromBundle y antes del Start (aunque si tienes algún await en alguno de estos métodos, esto no tiene porqué ser del todo cierto).

protected override void ReloadFromBundle(IMvxBundle state)
{ 
    FilterIsVisible = Convert.ToBoolean(state.Data["FilterIsVisible"]);
    GridItem = Convert.ToInt16(state.Data["GridItem"]);
    HorizontalOffset = Convert.ToDouble(state.Data["HorizontalOffset"]);
    SortOption = Convert.ToInt16(state.Data["SortOption"]);
    SearchText = state.Data["SearchText"];
    _firstSortOption = Convert.ToBoolean(state.Data["FirstSortOption"]);
}

Hay que tener en cuenta que si hacemos un Close de un ViewModel y luego lo volvemos a abrir no pasará por el SaveStateToBundle ni por el ReloadFromBundle.

Convención de nombres

Si la vista se llama CustomerView el ViewModel debe llamarse CustomerViewModel, de no ser

así no lo cargará (a no ser que asignemos un método del ViewModelLocator al DataContext de

la vista, como hemos ejemplificado en el caso los UserControls).

Algunos enlaces de interés

blog comments powered by Disqus