[Windows 10] Navegación en Apps Windows

Introducción

Casi cualquier aplicación que realices tendrá más de una página. Por lo que es vital saber como navegar entre ellas.

En este artículo, vamos a aprender como se realiza la navegación entre páginas en Apps Windows
primero desde un punto de vista teórica para terminar realizando un
ejemplo paso a paso cubriendo todos los aspectos relacionados con la
navegación utilizando el patrón MVVM.

¿Te apuntas?

Navegación entre páginas

El Frame, se crea en el arranque de la aplicación y es el encargado de contener y gestionar cada una de las páginas (Page). Contamos con un modelo de navegación integrado que usa controles Frame y Page y funciona de manera similar a un navegador web. El control Frame hospeda controles Page
y tiene un historial de navegación que se puede utilizar para ir hacia
atrás y hacia adelante por las páginas que ya visitaste. Tras obtener el
Frame correspondiente, utilizamos el método Navigate para realizar la navegación a otra página. Tenemos dos sobrescrituras del método Navigate:

  • Navigate(TypeName). Provoca que el Frame cargue el contenido especificado por el tipo pasado como parámetro.
  • Navigate(TypeName, Object).
    En este caso, además de indicar el tipo del contenido a cargar (primer
    parámetro), podemos pasar un parámetro a la página que se navega(segundo
    parámetro).

Navegar atrás

Utilizamos primero la propiedad CanGoBack que
nos devuelve un bool indicando si hay páginas en el historial de
navegación o no. Si existe historial de navegación utilizamos el método GoBack que navega al elemento más inmediato del historial de navegación.

NOTA: También contamos con la propiedad CanGoForward. Indica si en el historial de navegación existe una página delante de la actual.

Pasar información entre páginas

Hemos visto la teoría necesaria para navegar hacia delante y hacia
atrás. Sin embargo, en ocasiones, necesitamos pasar información entre
las páginas. Ya hemos visto que el método Navigate cuenta con una sobrescritura que nos permite pasar parámetros.

Navigate(TypeName, Object).
En este caso, además de indicar el tipo del contenido a cargar (primer
parámetro), podemos pasar un parámetro a la página que se navega(segundo
parámetro).

Al navegar hacia o desde una página se ejecutan los métodos:

Para recuperar el objeto pasado como parámetro vamos a utilizar el método OnNavigatedTo que se ejecutará al entrar en la segunda página. El argumento de tipo NavigationEventArgs  cuenta con una propiedad Parameter.

Manos a la obra!

Para probar todas las posibilidades de navegación crearemos un nuevo proyecto UAP:

Nuestro objetivo en el ejemplo sera crear múltiples páginas y navegar entre ellas.

Realmente podemos mantener una implementación MVVM
muy similar a como hacíamos hasta ahora en Apps Universales para Windows
y Windows Phone 8.1 salvando las diferencias a nivel de proyectos (tres
VS uno). Comenzamos por la base. Contamos con dos ficheros clave en
nuestro proyecto para implementar el patrón MVVM:

  • PageBase
  • ViewModelBase

PageBase

Como revelamos con su nombre, será una clase de la que heredaran
todas las páginas de nuestra aplicación. Y si, comentamos que serán
todas las páginas de la aplicación (ya sean del proyecto Windows o del
proyecto Windows Phone). Las páginas en ambas plataformas es exactamente
igual, un objeto de tipo Page. Esta clase cuenta con varios objetivos:

  • Establecer el Frame activo en todo momento para que el acceso al mismo sea sencillo.
  • Permitir el acceso de los eventos de navegación desde nuestras viewmodels.

Además podría interesarnos además:

  • Gestionar las transiciones entre páginas.
  • Gestionar estados (Loading, etc.)
  • Gestionar el estado segun la conexión de red.

Con la funcionalidad básica (facilitarnos el acceso a Frame y eventos de navegación) quedaría:

public class PageBase : Page
{
        private ViewModelBase _vm;
 
        protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            base.OnNavigatedTo(e);
 
            _vm = (ViewModelBase)this.DataContext;
            _vm.SetAppFrame(this.Frame);
            _vm.OnNavigatedTo(e);
        }
 
        protected override void OnNavigatedFrom(NavigationEventArgs e)
        {
            base.OnNavigatedFrom(e);
            _vm.OnNavigatedFrom(e);
        }
}

Gestionamos los eventos básicos de navegación (al entrar y al salir de la página) de modo que, en el método OnNavigateTo obtenemos la viewmodel de la página y asignamos el Frame.

ViewModelBase

Continuamos con la segunda de nuestras clases base. En esta ocasión
trataremos la clase base de la que heredarán todos los viewmodels. Los
objetivos básicos de la clase son:

  • Notificar cambios (implementar INotifyPropertyChanged).
  • Acceso al objeto Frame que nos permitirá realizar la navegación.
  • Permitir el acceso a los eventos de navegación.
public abstract class ViewModelBase : INotifyPropertyChanged
{
        private Frame appFrame;
        private bool isBusy;
 
        public ViewModelBase()
        {
        }
 
        public Frame AppFrame
        {
            get { return appFrame; }
        }
 
        public bool IsBusy
        {
            get { return isBusy; }
            set
            {
                isBusy = value;
                RaisePropertyChanged();
            }
        }
 
        public event PropertyChangedEventHandler PropertyChanged;
 
        public abstract Task OnNavigatedFrom(NavigationEventArgs args);
 
        public abstract Task OnNavigatedTo(NavigationEventArgs args);
 
        public void RaisePropertyChanged([CallerMemberName]string propertyName = "")
        {
            var Handler = PropertyChanged;
            if (Handler != null)
                Handler(this, new PropertyChangedEventArgs(propertyName));
        }
 
        internal void SetAppFrame(Frame viewFrame)
        {
            appFrame = viewFrame;
        }
}

El acceso a los eventos de navegación lo lograremos implementando en la viewmodel los métodos abstractos OnNavigatedFrom y OnNavigatedTo. Esto no permite guardar y recuperar parámetros o estados.

Utilizando PageBase

En la carpeta Views añadiremos nuestras páginas. En nuestro ejemplo, tendremos dos páginas:

  • FirstView
  • SecondView

Cada página será un objeto de tipo PageBase, tanto en XAML:

<base:PageBase
    x:Class="Navigation.Views.FirstView"
    xmlns:local="using:Navigation"
    xmlns:base="using:Navigation.Views.Base"
    mc:Ignorable="d"
    DataContext="{Binding FirstViewModel, Source={StaticResource Locator}}">
    <Grid>
    </Grid>
</base:PageBase>

Como en el code-behind:

public sealed partial class FirstView : PageBase
{
     public FirstView()
     {
         this.InitializeComponent();
     }
}

Añadimos en la primera vista (FirstView) dos botones que permita navegar a la segunda vista sin pasar y pasándo parámetro:

<Grid
     Background="LightBlue">
     <StackPanel
          HorizontalAlignment="Center"
      VerticalAlignment="Center">
      <Button
        Content="Go to Second View"
        Command="{Binding GoToSecondCommand}" />
      <Button
        Content="Pass parameter"
        Command="{Binding ParameterCommand}"
        Margin="0, 10"/>
     </StackPanel>
</Grid>

El resultado visual:

Para permitir navegar a la segunda vista, necesitamos definir dos
comandos (uno navegará sin pasar parámetro y otro pasándolo) en la
viewmodel. Creamos una clase derivada de ViewModelBase:

public class FirstViewModel : ViewModelBase
{
 
}

Implementamos los métodos de navegación definidos en ViewModelBase:

public class FirstViewModel : ViewModelBase
{
    public override Task OnNavigatedFrom(NavigationEventArgs args)
    {
        return null;
    }
 
    public override Task OnNavigatedTo(NavigationEventArgs args)
    {
        return null;
    }
}

Y añadimos comandos que permitan navegar de la página principal a la segunda página:

public class FirstViewModel : ViewModelBase
{
        private ICommand _goToSecondCommand;
        private ICommand _parameterCommand;
 
    public override Task OnNavigatedFrom(NavigationEventArgs args)
    {
        return null;
    }
 
    public override Task OnNavigatedTo(NavigationEventArgs args)
    {
        return null;
    }
 
    public ICommand GoToSecondCommand
    {
        get { return _goToSecondCommand = _goToSecondCommand ?? new DelegateCommand(GoToSecondCommandExecute); }
    }
 
    public ICommand ParameterCommand
    {
        get { return _parameterCommand = _parameterCommand ?? new DelegateCommand(ParameterCommandExecute); }
    }
 
    private void GoToSecondCommandExecute()
    {
        AppFrame.Navigate(typeof(SecondView));
    }
 
    private void ParameterCommandExecute()
    {
        var rnd = new Random();
        AppFrame.Navigate(typeof(SecondView), rnd.Next(1, 100));
    }
}

Recordamos que el control Frame hospeda controles Page
y tiene un historial de navegación que se puede utilizar para ir hacia
atrás y hacia adelante por las páginas que ya visitaste. Tras obtener el
Frame correspondiente, utilizamos el método Navigate para realizar la navegación a otra página. Tenemos dos sobrescrituras del método Navigate:

  • Navigate(TypeName). Provoca que el Frame cargue el contenido especificado por el tipo pasado como parámetro.
  • Navigate(TypeName, Object).
    En este caso, además de indicar el tipo del contenido a cargar (primer
    parámetro), podemos pasar un parámetro a la página que se navega(segundo
    parámetro).

En nuestro ejemplo hemos utilizado la segunda sobrescritura del método pasando un parámetro.

Ahora ,necesitamos conectar nuestra vista con nuestro viewmodel.
Podemos hacerlo de múltiples formas, desde el constructor de la vista,
instanciandola en App, usando Ioc. En nuestro caso, utilizaremos Ioc.
Usaremos Unity.

Una vez añadida la referencia correspondiente en cada proyecto (Windows y Windows Phone) creamos una nueva clase en la carpeta Base dentro de la carpeta ViewModels llamada ViewModelLocator:

public class ViewModelLocator
{
     readonly IUnityContainer _container;
 
     public ViewModelLocator()
     {
            _container = new UnityContainer();
 
            _container.RegisterType<FirstViewModel>();
            _container.RegisterType<SecondViewModel>();
     }
 
     public FirstViewModel FirstViewModel
     {
            get { return _container.Resolve<FirstViewModel>(); }
     }
 
     public SecondViewModel SecondViewModel
     {
            get { return _container.Resolve<SecondViewModel>(); }
     }
}

Sencillamente registramos nuestras viewmodels y creamos un par de
propiedades públicas por cada viewmodel para poder resolverlas y acceder
a ellas desde las vistas. A continuación, registramos nuestro locator en App.xaml:

<locator:ViewModelLocator x:Key="Locator" />

Asignamos la viewmodel como DataContext de nuestra viewmodel:

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

Y todo preparado!. Nuestra segunda página sera similar a la primera (tipo PageBase):

<base:PageBase
    x:Class="Navigation.Views.SecondView"
    xmlns:local="using:Navigation.Views"
    xmlns:base="using:Navigation.Views.Base"
    mc:Ignorable="d"
    DataContext="{Binding SecondViewModel, Source={StaticResource Locator}}">
    <Grid
        Background="LightGreen">
        <StackPanel
            HorizontalAlignment="Center"                    
            VerticalAlignment="Center">
            <Button
                Content="Go Back"
                Command="{Binding BackCommand}" />
            <TextBlock
                Text="{Binding Parameter}"
                Visibility="{Binding Parameter, Converter={StaticResource IntToVisibilityConverter}}"
                FontSize="48" />
        </StackPanel>
    </Grid>
</base:PageBase>

Contará con su propia viewmodel:

public class SecondViewModel : ViewModelBase
{
    // Variables
    private int _parameter;
 
    // Commands
    private ICommand _backCommand;
 
    public override Task OnNavigatedFrom(NavigationEventArgs args)
    {
        return null;
    }
 
    public override Task OnNavigatedTo(NavigationEventArgs args)
    {
            if (args.Parameter != null)
        {
            Parameter = Convert.ToInt32(args.Parameter);
                }
 
        return null;
    }
 
    public int Parameter
    {
        get { return _parameter; }
        set
        {
            _parameter = value;
            RaisePropertyChanged();
        }
    }
 
    public ICommand BackCommand
    {
        get { return _backCommand = _backCommand ?? new DelegateCommand(BackCommandExecute); }
    }
 
    private void BackCommandExecute()
    {
        if (AppFrame.CanGoBack)
            AppFrame.GoBack();
    }
}

Utilizamos el método GoBack del
Frame que navega al elemento más inmediato del historial de navegación,
la página anterior. Por otro lado, en caso de recibir parámetro, lo
asignamos a una propiedad bindeada en la vista. En nuestro ejemplo
pasamos un valor aleatorio entero entre 1 y 100.

En caso de utilizar el segundo botón, pasaremos el parámetro, se capturará en el método OnNavigatedTo y se bindeará en la vista:

Gestión de la tecla física volver

En nuestra aplicación, ejecutándola en Windows Phone y estando en la
segunda página, podemos utilizar también la tecla física como hacíamos
en Windows Phone 8 para volver atrás. Si la pulsamos… Ops!, ¿que ha ocurrido?

No hemos vuelto a la página anterior. En aplicaciones Windows XAML, al pulsar la tecla física atrás provoca una navegación a la App anterior no a la página anterior.

NOTA: En aplicaciones Windows Phone Silverlight 8.1 la navegación atrás es dentro de la propia aplicación.

Podemos incluir código personalizado para anular este comportamiento y
causar una navegación atrás dentro de la propia aplicación.

Tras añadir la extensión Mobile:

Windows Mobile Extension SDK

En App.xaml.cs:

if (ApiInformation.IsTypePresent("Windows.Phone.UI.Input.HardwareButtons"))
     HardwareButtons.BackPressed += HardwareButtons_BackPressed;

Utilizamos el método IsTypePresent de la clase estática ApiInformation incluida dentro del namespace Windows.Foundation.Metadata.

void HardwareButtons_BackPressed(object sender, BackPressedEventArgs e)
{
     Frame frame = Window.Current.Content as Frame;
  
     if (frame == null)
          return;
  
     if (frame.CanGoBack)
     {
          frame.GoBack();
          e.Handled = true;
     }
}

Analizamos que hemos hecho. Cada vez que se pulse la tecla física
volver, entramos en nuestro código personalizado. Accedemos al Frame de
la aplicación, y si no es nulo, navegamos atrás y anulamos el
comportamiento por defecto.

Podéis descargar el ejemplo realizado a continuación:

También podéis acceder al código fuente directamente en GitHub:

Ver GitHub

Recordar que cualquier tipo de duda o sugerencia la podéis dejar en los comentarios de la entrada.

Más información

Deja un comentario

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