Navegação entre páginas no Windows Phone: Problemas comuns e soluções

Muitos aplicativos Windows Phone são compostos por várias páginas e a navegação entre elas é relativamente simples, exigindo pouco esforço de programação. Mas no decorrer do desenvolvimento do projeto nos deparamos com problemas e comportamentos indesejáveis e, depois de algum tempo de investigação, acabamos descobrindo que esses problemas estão relacionados justamente com a forma como implementamos a navegação entre páginas.

Portanto, apesar de ser simples, a navegação entre páginas pode trazer um pouco de dor de cabeça para o desenvolvedor, principalmente quando este esquece de testar alguns cenários comuns como aquele em que seu aplicativo é suspenso por causa de uma ligação recebida pelo usuário.

A seguir iremos contextualizar os aspectos relacionados com a suspensão do aplicativo e navegação entre páginas. Depois iremos abordar como realizar a passagem de parâmetros entre páginas. E por fim citaremos problemas normalmente encontrados pelo desenvolvedor:

Gerenciando a suspensão do aplicativo e a navegação entre páginas

Para facilitar o trabalho do desenvolvedor, o Visual Studio 2013 (para desenvolvimento de aplicações Windows e Windows Phone 8.1) oferece, em alguns dos seus templates, classes que facilitam o controle de navegação entre páginas e também o gerenciamento de informações quando seu aplicativo é suspenso.

Essas classes são automaticamente incluídas no seu projeto quando você o cria usando os seguintes templates: Hub App (Universal Apps), Hub App (Windows Phone), Pivot App (Windows Phone).

Essas classes são:

  • Common.NavigationHelper.cs: Possui recursos para navegação entre páginas, já tratando o salvamento e carga do estado da página.
  • Common.SuspensionManager.cs: Fornece código para salvar informações do aplicativo quando o mesmo é suspenso e depois retomado.

Além disso, para esses templates são incluídas alterações pontuais no arquivo App.xaml.cs para preparar seu aplicativo para usar tanto o NavigationHelper como o SuspensionManager:

  • No método OnLaunched é incluído o modificador async e também o seguinte código:
    protected async override void OnLaunched(LaunchActivatedEventArgs e)
    {
    #if DEBUG
        if (System.Diagnostics.Debugger.IsAttached)
        {
            this.DebugSettings.EnableFrameRateCounter = true;
        }
    #endif
    
        Frame rootFrame = Window.Current.Content as Frame;
    
        // Do not repeat app initialization when the Window already has content,
        // just ensure that the window is active
        if (rootFrame == null)
        {
            // Create a Frame to act as the navigation context and navigate to the first page
            rootFrame = new Frame();
    
            // Added: Associate the frame with a SuspensionManager key                                
            SuspensionManager.RegisterFrame(rootFrame, "AppFrame");
    
            // TODO: change this value to a cache size that is appropriate for your application
            rootFrame.CacheSize = 1;
        // ...
      

    Nota: Nós devemos registrar o Frame da aplicação no SuspensionManager. A partir deste momento, sempre que a aplicação for suspensa, o SuspensionManager fornece um meio simplificado do desenvolvedor prover código para salvar o estado da aplicação. Dessa forma, quando a aplicação retornar, o desenvolvedor pode recuperar o estado salvo.

  • No método OnSuspending é incluído o modificador async e também o seguinte código:
    private async void OnSuspending(object sender, SuspendingEventArgs e)
    {
        var deferral = e.SuspendingOperation.GetDeferral();
                
        // Added
        await SuspensionManager.SaveAsync();
    
        deferral.Complete();
    }
    

Adicionando recursos de suspensão e navegação em um código já existente

É indicado a criação de um projeto a partir de um template pois este já oferece os recursos básicos que facilitam o desenvolvimento. Mas caso você tenha criado sua aplicação não usando os templates citados acima e queira incluir o SuspensionManager e NavigationHelper no código, você poderá fazê-lo incluindo uma página usando o template Basic Page.

Nesse caso o próprio Visual Studio inclui as classes no seu projeto. Mas lembre-se que você precisa, neste caso, incluir manualmente as alterações na classe App.xaml.cs.

Passando parâmetros entre páginas

Para navegar entre as páginas, devemos usar o método Navigate(Type pageType). E para incluir parâmetros, usamos o método Navigate(Type pageType, object param).

Por exemplo, considere uma página chamada BasicPage1. Desejamos a partir dela navegar para outra página passando um parâmetro. Para tanto:

  • Na classe BasicPage1:
    private void HyperlinkButton_Click(object sender, RoutedEventArgs e)
    {
        this.Frame.Navigate(typeof(BasicPage2), tb1.Text);
    }
    

Notas:

  • A BasicPage2 é a classe da página para a qual desejamos navegar.
  • O tb1 é um TextBox incluído na página de origem para que o usuário forneça um texto que será enviado como parâmetro. Segue o trecho do código XAML da página de origem que inclui esse campo e o hyperlink:
    <TextBox HorizontalAlignment="Stretch" Name="tb1" Margin="0"/>
    <HyperlinkButton Content="Click to go to page 2" Click="HyperlinkButton_Click"/>
    
  • Na classe BasicPage2:
    private void NavigationHelper_LoadState(object sender, LoadStateEventArgs e) {
        string message = e.NavigationParameter as string;
        if (!string.IsNullOrWhiteSpace(name)) {
            tb1.Text = "Hello, " + name;
        }
    }
    

Notas:

  • Implementamos um método para manipular o evento LoadState que é disponibilizado pela classe NavigationHelper. Nesse método obtemos o parâmetro e fazemos o cast para o tipo correspondente. No exemplo esse tipo é string.
  • O tb1 é um TextBlock que foi incluído na página BasicPage2.xaml para visualizar o valor do parâmetro.
Navegação entre páginas
Navegação entre páginas

Problemas Comuns de Desenvolvimento

Os problemas normalmente encontrados na passagem de parâmetros entre páginas são:

  1. Erro “GetNavigationState doesn’t support serialization of a parameter type which was passed to Frame.Navigate

    Isso está relacionado com o tipo de parâmetro que tentamos passar entre páginas. Um problema muito comum é não usar um tipo que seja serializável (isto é, convertido em texto e depois recuperado do texto).

    Para evitar este problema, lembre-se sempre de passar como parâmetros objetos de tipos não complexos. Os tipos básicos como string e int já são serializáveis. E se criamos uma classe nossa e desejamos passar como parâmetro na navegação entre páginas, devemos garantir que as informações nela contidas sejam serializáveis. Um exemplo de tipo que deve ser evitado é o ObservableCollection. Mais detalhes sobre esse problema podem ser obtidos na página de descrição do método Frame.Navigate.

    Uma forma que as vezes encontramos como sugestão para passar parâmetros de diferentes tipos entre página é serializar o objeto usando a biblioteca Json.Net. Dessa forma, passamos como parâmetro a string que representa o objeto e depois, na outra página, obtermos esse mesmo string e deserializamos usando a própria biblioteca Json.Net.

  2. Erro ao resumir a aplicação, sendo a mesma encerrada sem nenhum erro aparente.

    Isso está relacionado mais com o tipo do parâmetro fornecido nas navegações entre páginas. Quando navegamos fornecendo um parâmetro, devemos garantir que o tipo é conhecido pela classe SuspensionManager. Para isso, devemos disponibilizar o seguinte código no construtor da nossa aplicação, no App.xaml.cs:

    public App()
    {
        this.InitializeComponent();
        this.Suspending += this.OnSuspending;
        SuspensionManager.KnownTypes.Add(typeof(MyCustomType));
    }
    

    Lembre-se que esse erro só será verificado quando a aplicação for resumida (após ter sido suspensa ou terminada). E é aí que reside o problema. Esse teste muitas vezes passa desapercebido pelo desenvolvedor. Além disso, não importa a quantidade de páginas que temos na nossa aplicação, se pelo menos 1 página passar como parâmetro um tipo desconhecido, o erro ocorrerá quando a aplicação tentar voltar depois de ter sido suspensa.

    Para testar esse caso, podemos usar o próprio recurso do Visual Studio de ciclo de vida da aplicação enquanto fazemos depuração de nosso código:

    Opções de eventos de Ciclo de Vida da aplicação durante a depuração.
    Opções de eventos de Ciclo de Vida da aplicação durante a depuração.

    Segue procedimento para testar contra este erro:

    1. Iniciar a depuração da sua aplicação (no emulador ou no dispositivo).
    2. Para cada página que você navegar, selecione o evento de ciclo de vida “Suspend”. Seu aplicativo será suspenso no dispositivo ou emulador.
    3. Após suspender a aplicação, tentar retornar para ela através da opção “Resume” ou ainda acessando a aplicação a partir da lista de aplicativos instalados no dispositivo ou emulador. Como resultado esperado, o seu aplicativo deve retornar sem problemas.

Portanto, antes de publicar seu aplicativo, tenha certeza que seu aplicativo não possui nenhum desses problemas citados acima.

Até mais! Continue nos acompanhando pelo blog talkitbr.

5 comentários

  1. Olá fiz o exemplo que vc passou usando o frame.navigate para enviar deu tudo certo, mas ao receber os parâmetros na tela que estou direcionando quando tento enviar um segundo objeto da erro, minha pergunta é como tratar esse recebimento de parâmetros ?

    Curtir

    • Olá Rodrigo, obrigado pelo contato!

      Quando se repassa parâmetros para outra página, o que você recebe do outro ládo é um object. Você precisa fazer o cast do objeto para o tipo correto no outro lado. Por exemplo, usando o template padrão do HubApp, na página HubPage posso especificar o seguinte:

      private void GroupSection_ItemClick(object sender, ItemClickEventArgs e)
      {
          SampleDataGroup group = ((SampleDataGroup)e.ClickedItem);
          object[] parameters = new object[2];
          parameters[0] = group.UniqueId;
          parameters[1] = group.ImagePath;
          if (!Frame.Navigate(typeof(SectionPage), parameters))
          {
              throw new Exception(this.resourceLoader.GetString("NavigationFailedExceptionMessage"));
          }
      }
      

      Já na página destino, que é o GroupPage, especifico o seguinte:

      private async void NavigationHelper_LoadState(object sender, LoadStateEventArgs e)
      {
          object[] parameters = e.NavigationParameter as object[];
          var groupId = (string)parameters[0];
          var groupImagePath = (string)parameters[1];
          var group = await SampleDataSource.GetGroupAsync(groupId);            
          this.DefaultViewModel["Group"] = group;
      }
      

      Att,
      João

      Curtir

  2. Olá João Boa Noite e obg pelo retorno.

    Essa parte que vc explicou eu acho que já estou fazendo. No total de telas são 3 a de pedido(principal), a de cliente e a de produtos. Vou colocar o exemplo.

    pagina Cliente ao ser clicado:

    private void btTelaPedido(object sender, TappedRoutedEventArgs e){
                Cliente cliente = new Cliente();
                cliente.idCliente = Convert.ToInt32(tbCodCliente.Text);
                cliente.razao = tbNomeCliente.Text;
    
                Frame.Navigate(typeof(pedidoTelaCadastro), cliente );
    
            }
    

    pagina pedido (recebe o objeto)

     protected override void OnNavigatedTo(NavigationEventArgs e){
                this.navigationHelper.OnNavigatedTo(e);
    
                Cliente clienteRecebido = (Cliente)e.Parameter;
                    tbIdCliente.Text = Convert.ToString(clienteRecebido.idCliente);
                    tbCliente.Text = clienteRecebido.razao;
                    tbDataPedido.Text = "01/01/2015";
    
            }
    

    dentro dessa tela pedido eu vou pra outra tela buscar os produtos:

    evento gerando na tela produtos que envia outro objeto para a tela de pedidos.

      private void listaProduto_SelectionChanged(object sender, SelectionChangedEventArgs e){
                Produto produtoEscolhido = (sender as ListView).SelectedItem as Produto;
                Frame.Navigate(typeof(pedidoTelaCadastro), produtoEscolhido);
            }
    

    retorno para tela pedido paea receber o objeto produto

    protected override void OnNavigatedTo(NavigationEventArgs e){
                this.navigationHelper.OnNavigatedTo(e);
    
                Cliente clienteRecebido = (Cliente)e.Parameter;
                    tbIdCliente.Text = Convert.ToString(clienteRecebido.idCliente);
                    tbCliente.Text = clienteRecebido.razao;
                    tbDataPedido.Text = "01/01/2015";
    
                Produto produtoRecebido = (Produto)e.Parameter;
                tbProduto.Text = Convert.ToString(produtoRecebido.descricao);
    
            }
    

    se eu fizer o mesmo procedimento de recebimento de objeto que fiz com o cliente na tela de pedido da um erro pq já estou recebendo o objeto cliente primeiro, minha duvida era como tratar esse segundo objeto na tela de pedidos.

    Não sei se consegui passar minha duvida corretamente mais é isso. Mais uma vez obrigado pela atenção e desculpe pelo abuso.

    Curtir

  3. Olá Rodrigo,

    Entendi o problema. Considerando sua abordagem, é possível contornar seu problema da seguinte forma:

    1. Crie uma classe Pedido (ou algum outro nome similar) que contém as propriedades Cliente e Produto.

    2. Na página Cliente você cria esse objeto, define a propriedade cliente e então repassa o objeto pedido como parâmetro para a tela pedido.

    3. Na tela Pedido, extraia o objeto pedido do parâmetro de navegação e atribua a uma variável local ou num ViewModel.

    4. Quando você for navegar para a tela Produto, repasse o mesmo objeto pedido (que já contem o cliente) como parâmetro de navegação.

    5. Na tela Produto, extraia o objeto pedido do parâmetro de navegação e, quando o usuário selecionar o produto, defina a propriedade produto no objeto pedido.

    4. Quando você for navegar de volta para a tela Pedido, repasse o mesmo objeto pedido (que agora tem cliente e produto) como parâmetro de navegação.

    A única questão que me preocupa na sua solução é como fica a pilha de navegação no final de todo o processo. Quando o usuário navega novamente para a página Pedido com o produto selecionado, se ele clicar em Voltar, me parece que a aplicação retornará para a tela Produto, depois Pedido e depois Cliente. Se for esse o caso, sugiro a seguinte abordagem:

    Salvar o Pedido (cliente e produto) no SuspensionManager.SessionState. Nesse caso não precisa manter a informação no parâmetro de navegação. E então, na tela Produto, usar o Frame.GoBack(); para sair da página;

    Curtir

  4. Olá João segui o que vc falou e deu certo , mais pesquisando achei um outro jeito que tbm resolveu meu problema. Na minha pagina principal(pedido) eu faço o seguinte tratamento no onNavigateTo.

    if (e.Parameter.GetType() == typeof(Cliente)) {
    //Faz o que deve ser feito para o cliente
    else if (e.Parameter.GetType() == typeof(Produto)) {
    //Faz o que deve ser feito com o produto
    }
    else {
    //Recebeu um tipo não esperado
    }

    Obg pela orientação.

    OBS:

    A plataforma de desenvolvimento para Windowsphone parece ser bem promissora mais ainda é bem complicado arranjar informações quando se tem duvidas, se não for com pessoas como vc fica muito difícil. Acho que a microsoft deveria disponibilizar um material (tutoriais, vídeo aulas no youtube, etc) para motivar o desenvolvimento eles até tem um canal mais além de ser em inglês é pouco pratico e muito teórico.

    Curtir

Deixe um comentário