Navigator para WP7 en MVVM

Otro tema que estaba pendiente…

Cuando empecé a estudiar y testear cosas de WP7 con MVVM, una de los detalles que más me molestaban era tener que navegar entre páginas usando el code behind.

Es verdad que desde el ViewModel puedes tener acceso al NavigationService, pero esto sería una muy mala práctica de cara a los test unitarios. Por otra parte, desde dónde puedo capturar los parámetros pasados a una vista es siempre el método OnNavigatedTo que tenemos en el Code Behind.

Una tarde escuchando una charla de Josue sobre WP7, comentó que se encontraba desarrollando una aplicación llamada WPControla. Soy de los que piensa que una de las mejores maneras de tomar buenas prácticas en los desarrollos es mirar mucho código, cualquiera, no importa de quien sea porque siempre te ayudan a comparar y sacar conclusiones sobre lo que se debe o no hacer. 

En la propia charla le pregunté a Josue si podríamos descargarnos el código de la aplicación y como siempre, nos dio acceso al repositorio. Winking smile

Mirando el código de esta aplicación descubrí algo que me llamó mucho la atención, dentro del namespace WPControla.Client.Services había una clase llamada NavigationService. Me imaginé que esta clase, al implementar una interfaz sería una solución a lo que tanto me había molestado hasta el momento, poder navegar entre Views desde el ViewModel sin afectar los test.

Me costó entender cómo funcionaba porque en el repo no está toda la funcionalidad aún implementada, así que le pregunté directamente a Josue si esto era lo que me imaginaba y cómo había pensado en resolver esto, su respuesta no se hizo esperar. Gracias Josue! Smile

Con permiso del Master, vamos a intentar explicar la solución que implementa  WPControla.

Todo parte de una clase llamada NavController en la cual se empieza definiendo un singleton para el acceso a una única instancia. También contamos con un Dictionary que nos permite registrar y referenciar las vistas mediante “alias”.

registeredViews.Add("Start", new Uri(@"/MainPage.xaml", UriKind.Relative));

En mi caso, cambié el Dictionary utilizado:

static Dictionary<String, Uri> registeredViews = new Dictionary<String, Uri>();

por:

private static readonly Dictionary<Pages, Uri> _registeredViews = new Dictionary<Pages, Uri>();

Pages es un enumerado, esto no implica ninguna mejora a la solución planteada por Josue, más bien es un gusto personal, no me gusta trabajar con string porque soy muy despistado Sad smile y siempre los escribo mal, así que me ahorro errores de ejecución usando enumerados.

La clase define  tres sobrecargas sobre el método NavigateTo. Una sobrecarga nos permite navegar a una vista sin pasar parámetros, en la segunda, podemos incorporar parámetros pasados por QueryString y en la tercera nos vamos a detener:

public void NavigateTo(String navigationTarget, Action<NavigationEventArgs> onNavigated)

Esta sobrecarga incluye un Action<NavigationEventArgs> como parámetro, el objetivo es poder tomar una acción en el momento en que se navegue de una vista a otra. Internamente la clase navController captura el evento Navigated de PhoneApplicationFrame y cuando este ocurre, ejecuta nuestra acción.

/// <summary>
/// Este evento se lanza cuando hemos navegado a una página.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void root_Navigated(object sender, NavigationEventArgs e)
{
    if (NavigationMethod != null)
    {
        NavigationMethod(e);
        NavigationMethod = null;
    }
}

Este método de la forma que está escrito, no nos permite capturar el Navigated cuando el NavigationMode es Back, o sea, cuando vamos de regreso. Para solucionarlo solo necesitamos eliminar la línea de código que asigna null al NavigationMethod y no olvidar desasignar el evento cuando regresemos, porque de lo contrario navegando hacia atrás y hacia adelante multiplicará la cantidad de veces que se ejecuta nuestra acción.

void RootNavigated(object sender, NavigationEventArgs e)
{
    if (_navigationMethod == null) return;

    _navigationMethod(e);

    if (e.NavigationMode != NavigationMode.Back) return;
    var rootFrame = (PhoneApplicationFrame)Application.Current.RootVisual;
        
    rootFrame.Navigated -= RootNavigated;
}

Otra particularidad de esta sobrecarga es que mediante el parámetro NavigationEventArgs tenemos acceso a la View que estamos navegando mediante la propiedad Content, a su vez, mediante la View tenemos acceso al DataContext que en una arquitectura MVVM sería el ViewModel. Esta estructura pudiera ser utilizada para inicializar objetos complejos cuando navegamos de una vista a otra (luego veremos un ejemplo).

Ya que estamos creando métodos de navegación, adicioné a esta clase un método que nos permita hacer Back por código.

public void NavigateToBack()
{
    var rootFrame = Application.Current.RootVisual as PhoneApplicationFrame;
    if (rootFrame == null || !rootFrame.CanGoBack) return;

    rootFrame.GoBack();
}

La magia para que todo esto se integre con nuestros ViewModels llega ahora:

Definimos una interfaz con los distintos métodos para la navegación que necesitamos en nuestra aplicación, hacemos que un servicio implemente dicha interfaz y lo usamos en el constructor de los distintos ViewModels.

Un servicio de navegación que utilice NavController quedaría como el siguiente:

public interface INavigatorService
{
    void GotoMvvmView1();
    void GotoMvvmView1(DataItem param);

    void GoBack();
}

public class NavigatorService : INavigatorService
{
    public void GotoMvvmView1()
    {
        NavController.Current.NavigateTo(NavController.Pages.MvvmView1);
    }

    public void GotoMvvmView1(DataItem param)
    {
        NavController.Current.NavigateTo(NavController.Pages.MvvmView1, 
            args =>
            {
                if (args.NavigationMode != NavigationMode.New) return;

                var view1Model = ((FrameworkElement) (args.Content)).DataContext as View1ViewModel;
                if (view1Model != null) view1Model.Initialize(param);
            });
    }

    public void GoBack()
    {
        NavController.Current.NavigateToBack();
    }
}

Observen como estamos ejecutando una acción en el momento en que llegamos a la vista a la que deseamos navegar. Esta acción accede al ViewModel e inicializa parámetros necesarios con los datos que se pasan al método GotoMvvmView1.

A mi este mecanismo no me gusta  (cuestión personal), no sé si será buena o mala práctica, pero me cuesta tener que acceder a métodos del ViewModel desde este punto.

Para solucionar este problema he incorporado a la clase NavController un Dicctionary para pasar datos entre vistas.

/// <summary>
/// Navigation Parameters 
/// </summary>
public static IDictionary<string, object> Parameters { get; set; }

Como NavNavigator es independiente de nuestros ViewModels mediante una interfaz, tenemos que adicionar a la misma la utilización del Dictionary. La interfaz vista anteriormente ahora nos quedaría así:

public interface INavigatorService
{
    IDictionary<string, object> Parameters { get; set; }

    void GotoMvvmView1();
    void GotoMvvmView1(DataItem param);

    void GoBack();
}

y su implementación sería…

public class NavigatorService : INavigatorService
{
    public IDictionary<string, object> Parameters
    {
        get { return NavController.Parameters; }
        set { NavController.Parameters = value; }
    }

    public void GotoMvvmView1()
    {
        NavController.Current.NavigateTo(NavController.Pages.MvvmView1);
    }

    public void GotoMvvmView1(DataItem param)
    {
        NavController.Current.NavigateTo(NavController.Pages.MvvmView1, 
            args =>
            {
                if (args.NavigationMode == NavigationMode.Back) return;

                NavController.Parameters = new Dictionary<string, object> { { "item", param} };
            });
    }

    public void GoBack()
    {
        NavController.Current.NavigateToBack();
    }
}

Después de tener esto solo nos queda pasar el servicio de navegación en el constructor de nuestros ViewModels y acceder a los distintos métodos para navegar.

/// <summary>
/// Initializes a new instance of the MainViewModel class.
/// </summary>
public MainViewModel(IDataService dataService, INavigatorService navigator)
private void GotoView1Command()
{
    _navigator.GotoMvvmView1(_item);
}

Quizás pienses que a esta altura, cuando ya WP7 solo tendrá una próxima actualización y las cosas cambiarán de cara a WP8 y a la nueva interfaz de W8 (antes Metro), ya no tienen mucho sentido. Puedes echarle una ojeada a este post sobre W8 del propio Josue y seguramente habrá cosas que te resulten familiar. Winking smile

Salu2

Código del ejemplo

Deja un comentario

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