Navegación de aplicaciones empresariales

Nota:

Este libro se publicó en la primavera de 2017 y no se ha actualizado desde entonces. Hay mucho en el libro que sigue siendo valioso, pero algunos de los materiales están obsoletos.

Xamarin.Forms incluye compatibilidad con la navegación por páginas, que normalmente surge a partir de la interacción del usuario con la UI o de la propia aplicación, como resultado de los cambios de estado internos causados por la lógica. Sin embargo, la navegación puede ser compleja de implementar en aplicaciones que usan el patrón Modelo-Vista-Modelo de vista (MVVM), ya que se deben superar los siguientes desafíos:

  • Cómo identificar la vista a la que se va a navegar mediante un enfoque que no introduce acoplamientos y dependencias estrictos entre las vistas.
  • Cómo coordinar el proceso por el que se crea y se inicializa una instancia de la vista a la que se va a navegar. Al usar MVVM, es necesario crear instancias del modelo de vista y de la vista y asociarlas entre sí mediante el contexto de enlace de la vista. Cuando una aplicación usa un contenedor de inserción de dependencias, la creación de instancias de vistas y modelos de vistas podría requerir un mecanismo de construcción específico.
  • Saber si se va a realizar la navegación con prioridad para la vista o para el modelo de vista. Si se prioriza la vista, la página a la que se navega hace referencia al nombre del tipo de vista. Durante la navegación, se crea una instancia de la vista especificada, junto con su modelo de vista correspondiente y otros servicios dependientes. Un enfoque alternativo consiste en priorizar el modelo de vista. Así, la página a la que se navega hace referencia al nombre del tipo de modelo de vista.
  • Cómo separar limpiamente el comportamiento de navegación de la aplicación entre las vistas y los modelos de vistas. El patrón MVVM proporciona una separación entre la interfaz de usuario de la aplicación y su presentación y lógica de negocios. Sin embargo, el comportamiento de navegación de una aplicación suele abarcar la UI y las partes de presentación de la aplicación. El usuario suele iniciar la navegación desde una vista y la vista se reemplazará como resultado de la navegación. Sin embargo, es posible que. con cierta asiduidad, la navegación también tenga que iniciarse o coordinarse desde dentro del modelo de vista.
  • Cómo pasar parámetros durante la navegación con fines de inicialización. Por ejemplo, si el usuario navega a una vista para actualizar los detalles de una orden, los datos de la orden tendrán que pasarse a la vista para que pueda mostrar los datos correctos.
  • Cómo coordinar la navegación para garantizar que se cumplen determinadas reglas del negocio. Por ejemplo, es posible que, antes de salir de una vista, se solicite a los usuarios que corrijan datos no válidos o que envíen o descarten los cambios de datos realizados en la vista.

En este capítulo se abordan estos desafíos mediante la presentación de una clase denominada NavigationService, que se usa para realizar la navegación de la página con prioridad para el modelo de vista.

Nota:

El NavigationService usado por la aplicación está diseñado solo para realizar la navegación jerárquica entre las instancias de ContentPage. El uso del servicio para navegar entre otros tipos de página podría dar lugar a un comportamiento inesperado.

La lógica de navegación puede residir en el código subyacente de una vista o en un modelo de vista enlazado a datos. Aunque colocar la lógica de navegación en una vista podría ser el enfoque más sencillo, no se puede probar fácilmente con pruebas unitarias. Si se coloca la lógica de navegación en las clases de modelo de vista, la lógica se puede revisar mediante pruebas unitarias. Además, el modelo de vista puede implementar lógica para controlar la navegación y así asegurar que se aplican determinadas reglas del negocio. Por ejemplo, es posible que una aplicación no permita que el usuario salga de una página sin asegurarse primero de que los datos especificados sean válidos.

Normalmente, una clase NavigationService se invoca desde los modelos de vista para promover la capacidad de prueba. Sin embargo, navegar a las vistas desde los modelos de vistas requeriría que los modelos de vistas hagan referencia a las vistas y, particularmente, a las vistas con las que el modelo de vista activo no está asociado, algo que no es recomendable. Por lo tanto, el elemento NavigationService que se presenta aquí especifica el tipo de modelo de vista como destino al que navegar.

La aplicación móvil eShopOnContainers usa la clase NavigationService para proporcionar navegación con prioridad para el modelo de vista. Esta clase implementa la interfaz INavigationService, que se muestra en el siguiente código de ejemplo:

public interface INavigationService  
{  
    ViewModelBase PreviousPageViewModel { get; }  
    Task InitializeAsync();  
    Task NavigateToAsync<TViewModel>() where TViewModel : ViewModelBase;  
    Task NavigateToAsync<TViewModel>(object parameter) where TViewModel : ViewModelBase;  
    Task RemoveLastFromBackStackAsync();  
    Task RemoveBackStackAsync();  
}

Esta interfaz especifica que una clase de implementación debe proporcionar los siguientes métodos:

Método Propósito
InitializeAsync Realiza la navegación a una de las dos páginas cuando se inicia la aplicación.
NavigateToAsync Realiza la navegación jerárquica a una página especificada.
NavigateToAsync(parameter) Realiza la navegación jerárquica a una página especificada y pasa un parámetro.
RemoveLastFromBackStackAsync Quita la página anterior de la pila de navegación.
RemoveBackStackAsync Quita todas las páginas anteriores de la pila de navegación.

Además, la interfaz INavigationService especifica que una clase de implementación debe proporcionar una propiedad PreviousPageViewModel. Esta propiedad devuelve el tipo de modelo de vista asociado a la página anterior de la pila de navegación.

Nota:

Normalmente, una interfaz INavigationService también especificaría un método GoBackAsync, que se usa para volver mediante programación a la página anterior de la pila de navegación. Sin embargo, falta este método en la aplicación móvil eShopOnContainers porque no es necesario.

Creación de la instancia NavigationService

La clase NavigationService, que implementa la interfaz INavigationService, se registra como singleton con el contenedor de inserción de dependencias de Autofac, como se muestra en el ejemplo de código siguiente:

builder.RegisterType<NavigationService>().As<INavigationService>().SingleInstance();

La interfaz INavigationService se resuelve en el constructor de clase ViewModelBase, como se muestra en el ejemplo de código siguiente:

NavigationService = ViewModelLocator.Resolve<INavigationService>();

Esto devuelve una referencia al objeto NavigationService almacenado en el contenedor de inserción de dependencias Autofac, que el método InitNavigation crea en la clase App. Para obtener más información, consulte Navegar cuando se inicia la aplicación.

La clase ViewModelBase almacena la instancia NavigationService en una propiedad NavigationService, de tipo INavigationService. Por lo tanto, todas las clases de modelos de vistas, que se derivan de la clase ViewModelBase, pueden usar la propiedad NavigationService para tener acceso a los métodos especificados por la interfaz INavigationService. Esto evita la sobrecarga de insertar el objeto NavigationService desde el contenedor de inserción de dependencias de Autofac en cada clase de modelo de vista.

Control de solicitudes de navegación

Xamarin.Forms proporciona la clase NavigationPage, que implementa una experiencia de navegación jerárquica en la que el usuario puede navegar por páginas, hacia delante y hacia atrás, según sea necesario. Para obtener más información sobre la navegación jerárquica, consulte Navegación jerárquica de .

En lugar de usar directamente la clase NavigationPage, la aplicación eShopOnContainers ajusta la clase NavigationPage en la clase CustomNavigationView, como se muestra en el ejemplo de código siguiente:

public partial class CustomNavigationView : NavigationPage  
{  
    public CustomNavigationView() : base()  
    {  
        InitializeComponent();  
    }  

    public CustomNavigationView(Page root) : base(root)  
    {  
        InitializeComponent();  
    }  
}

El propósito de este ajuste es facilitar el estilo de la instancia de NavigationPage dentro del archivo XAML para la clase.

La navegación se realiza dentro de las clases de modelo de vista invocando uno de los métodos NavigateToAsync, especificando el tipo de modelo de vista para la página a la que se navega, como se muestra en el ejemplo de código siguiente:

await NavigationService.NavigateToAsync<MainViewModel>();

En el siguiente código de ejemplo se muestra el método NavigateToAsync proporcionado por la clase NavigationService:

public Task NavigateToAsync<TViewModel>() where TViewModel : ViewModelBase  
{  
    return InternalNavigateToAsync(typeof(TViewModel), null);  
}  

public Task NavigateToAsync<TViewModel>(object parameter) where TViewModel : ViewModelBase  
{  
    return InternalNavigateToAsync(typeof(TViewModel), parameter);  
}

Cada método permite que cualquier clase de modelo de vista que derive de la clase ViewModelBase realice la navegación jerárquica invocando el método InternalNavigateToAsync. Además, el segundo método NavigateToAsync permite especificar los datos de navegación como argumento que se pasa al modelo de vista al que se navega, donde normalmente se usa para realizar la inicialización. Para más información, consulte Pasar parámetros durante la navegación.

El método InternalNavigateToAsync ejecuta la solicitud de navegación y se muestra en el ejemplo de código siguiente:

private async Task InternalNavigateToAsync(Type viewModelType, object parameter)  
{  
    Page page = CreatePage(viewModelType, parameter);  

    if (page is LoginView)  
    {  
        Application.Current.MainPage = new CustomNavigationView(page);  
    }  
    else  
    {  
        var navigationPage = Application.Current.MainPage as CustomNavigationView;  
        if (navigationPage != null)  
        {  
            await navigationPage.PushAsync(page);  
        }  
        else  
        {  
            Application.Current.MainPage = new CustomNavigationView(page);  
        }  
    }  

    await (page.BindingContext as ViewModelBase).InitializeAsync(parameter);  
}  

private Type GetPageTypeForViewModel(Type viewModelType)  
{  
    var viewName = viewModelType.FullName.Replace("Model", string.Empty);  
    var viewModelAssemblyName = viewModelType.GetTypeInfo().Assembly.FullName;  
    var viewAssemblyName = string.Format(  
                CultureInfo.InvariantCulture, "{0}, {1}", viewName, viewModelAssemblyName);  
    var viewType = Type.GetType(viewAssemblyName);  
    return viewType;  
}  

private Page CreatePage(Type viewModelType, object parameter)  
{  
    Type pageType = GetPageTypeForViewModel(viewModelType);  
    if (pageType == null)  
    {  
        throw new Exception($"Cannot locate page type for {viewModelType}");  
    }  

    Page page = Activator.CreateInstance(pageType) as Page;  
    return page;  
}

El método InternalNavigateToAsync realiza la navegación a un modelo de vista llamando primero al método CreatePage. Este método busca la vista que corresponde al tipo de modelo de vista especificado y crea y devuelve una instancia de este tipo de vista. La ubicación de la vista que corresponde al tipo de modelo de vista usa un enfoque basado en convención, que supone que:

  • Las vistas están en el mismo ensamblado que los tipos de modelo de vista.
  • Las vistas están en un espacio de nombres secundario .Views.
  • Los modelos de vista están en un espacio de nombres secundario .ViewModels.
  • Los nombres de vista corresponden a los nombres de modelo de vista, con "Modelo" quitado.

Cuando se crea una instancia de una vista, está asociada a su modelo de vista correspondiente. Para obtener más información sobre cómo se produce esto, vea Crear automáticamente un modelo de vista con un localizador de modelos de vista.

Si la vista que se crea es un LoginView, se ajusta dentro de una nueva instancia de la clase CustomNavigationView y se asigna a la propiedad Application.Current.MainPage. De lo contrario, se recupera la instancia de CustomNavigationView y, si no es null, se invoca el método PushAsync para insertar la vista que se crea en la pila de navegación. Sin embargo, si la instancia CustomNavigationView recuperada es null, la vista que se crea se ajusta dentro de una nueva instancia de la clase CustomNavigationView y se asigna a la propiedad Application.Current.MainPage. Este mecanismo garantiza que durante la navegación, las páginas se agregan correctamente a la pila de navegación cuando está vacía y cuando contiene datos.

Sugerencia

Considere la posibilidad de almacenar en caché las páginas. El almacenamiento en caché de páginas da como resultado el consumo de memoria para las vistas que no se muestran actualmente. Sin embargo, sin el almacenamiento en caché de páginas, significa que el análisis y la construcción de XAML de la página y su modelo de vista se producirán cada vez que se navega a una página nueva, lo que puede tener un impacto en el rendimiento de una página compleja. Para una página bien diseñada que no usa un número excesivo de controles, el rendimiento debe ser suficiente. Sin embargo, el almacenamiento en caché de páginas puede ayudar si se encuentran tiempos de carga de página lentos.

Una vez creada y navegada a la vista, se ejecuta el método InitializeAsync del modelo de vista asociado de la vista. Para más información, consulte Pasar parámetros durante la navegación.

Cuando se inicia la aplicación, se invoca el método InitNavigation de la clase App. El siguiente ejemplo de código muestra este método:

private Task InitNavigation()  
{  
    var navigationService = ViewModelLocator.Resolve<INavigationService>();  
    return navigationService.InitializeAsync();  
}

El método crea un nuevo objeto NavigationService en el contenedor de inserción de dependencias de Autofac y devuelve una referencia a él, antes de invocar su método InitializeAsync.

Nota:

Cuando la clase ViewModelBase resuelve la interfaz INavigationService, el contenedor devuelve una referencia al objeto NavigationService que se creó cuando se invoca el método InitNavigation.

El siguiente ejemplo de código muestra el método NavigationServiceInitializeAsync:

public Task InitializeAsync()  
{  
    if (string.IsNullOrEmpty(Settings.AuthAccessToken))  
        return NavigateToAsync<LoginViewModel>();  
    else  
        return NavigateToAsync<MainViewModel>();  
}

Se navega a MainView si la aplicación tiene un token de acceso almacenado en caché, que se usa para la autenticación. De lo contrario, se navega a LoginView.

Para obtener más información sobre el contenedor de inserción de dependencias de Autofac, consulte Introducción a la inserción de dependencias.

Pasar parámetros durante la navegación

Uno de los métodos NavigateToAsync, especificado por la interfaz INavigationService, permite especificar datos de navegación como un argumento que se pasan al modelo de vista al que se navega, donde normalmente se usa para realizar la inicialización.

Por ejemplo, la clase ProfileViewModel contiene un OrderDetailCommand que se ejecuta cuando el usuario selecciona una orden en la página ProfileView. Después, esto sirve para ejecutar el método OrderDetailAsync, que se muestra en el siguiente código de ejemplo:

private async Task OrderDetailAsync(Order order)  
{  
    await NavigationService.NavigateToAsync<OrderDetailViewModel>(order);  
}

Este método invoca la navegación al OrderDetailViewModel, pasando una instancia de Order que representa el orden seleccionado por el usuario en la página ProfileView. Cuando la clase NavigationService crea el OrderDetailView, se crea una instancia de la clase OrderDetailViewModel y se asigna a la BindingContext de la vista. Después de navegar al OrderDetailView, el método InternalNavigateToAsync ejecuta el método InitializeAsync del modelo de vista asociado de la vista.

El método InitializeAsync se define en la clase ViewModelBase como un método que se puede invalidar. Este método especifica un argumento object que representa los datos que se van a pasar a un modelo de vista durante una operación de navegación. Por lo tanto, las clases de modelo de vista que desean recibir datos de una operación de navegación proporcionan su propia implementación del método InitializeAsync para realizar la inicialización necesaria. En el ejemplo de código siguiente se muestra el método InitializeAsync de la clase OrderDetailViewModel:

public override async Task InitializeAsync(object navigationData)  
{  
    if (navigationData is Order)  
    {  
        ...  
        Order = await _ordersService.GetOrderAsync(  
                        Convert.ToInt32(order.OrderNumber), authToken);  
        ...  
    }  
}

Este método recupera la instancia Order que se pasó al modelo de vista durante la operación de navegación y la usa para recuperar los detalles de orden completos de la instancia OrderService.

Invocación de la navegación mediante comportamientos

Normalmente, la interacción del usuario desencadena la navegación desde una vista. Por ejemplo, LoginView realiza la navegación después de autenticarse correctamente. En el siguiente código de ejemplo se muestra la forma en que un comportamiento invoca la navegación:

<WebView ...>  
    <WebView.Behaviors>  
        <behaviors:EventToCommandBehavior  
            EventName="Navigating"  
            EventArgsConverter="{StaticResource WebNavigatingEventArgsConverter}"  
            Command="{Binding NavigateCommand}" />  
    </WebView.Behaviors>  
</WebView>

En tiempo de ejecución, EventToCommandBehavior responderá a la interacción con WebView. Cuando WebView navega a una página web, se activará el evento Navigating, que ejecutará NavigateCommand en LoginViewModel. De manera predeterminada, los argumentos de evento para el evento se pasarán al comando. Estos datos se convierten a medida que se pasan entre el origen y el destino mediante el convertidor especificado en la propiedad EventArgsConverter, que devuelve Url desde WebNavigatingEventArgs. Por lo tanto, cuando NavigationCommand se ejecuta, la URL de la página web se pasa como un parámetro a la Action registrada.

Después, NavigationCommand ejecuta el método NavigateAsync, que se muestra en el siguiente código de ejemplo:

private async Task NavigateAsync(string url)  
{  
    ...          
    await NavigationService.NavigateToAsync<MainViewModel>();  
    await NavigationService.RemoveLastFromBackStackAsync();  
    ...  
}

Este método invoca la navegación a la MainViewModel y, después, quita la página LoginView de la pila de navegación.

Confirmación o cancelación de la navegación

Es posible que una aplicación tenga que interactuar con el usuario durante una operación de navegación para que el usuario pueda confirmar o cancelar la navegación. Esto puede ser necesario, por ejemplo, cuando el usuario intenta navegar antes de haber completado del todo una página de entrada de datos. En esta situación, una aplicación debe enviar una notificación que permita al usuario salir de la página o cancelar la operación de navegación antes de que se produzca. Esto se puede lograr en una clase de modelo de vista, usando la respuesta de una notificación para controlar si se invoca o no la navegación.

Resumen

Xamarin.Forms incluye compatibilidad con la navegación por páginas, que normalmente surge a partir de la interacción del usuario con la UI o de la propia aplicación, como resultado de los cambios de estado internos causados por la lógica. Sin embargo, la navegación puede ser compleja de implementar en aplicaciones que usan el patrón MVVM.

En este capítulo se ha descrito una clase NavigationService, que se usa para realizar la navegación de la página con prioridad para el modelo de vista desde modelos de vistas. Si se coloca la lógica de navegación en las clases de modelo de vista, la lógica se puede revisar mediante pruebas unitarias. Además, el modelo de vista puede implementar lógica para controlar la navegación y así asegurar que se aplican determinadas reglas del negocio.