Navegação de aplicativos corporativos

Observação

Este e-book foi publicado na primavera de 2017 e não foi atualizado desde então. Há muito no livro que permanece valioso, mas parte do material está desatualizado.

Xamarin.Forms inclui suporte para navegação de página, que normalmente resulta da interação do usuário com a interface do usuário ou do próprio aplicativo como resultado de alterações de estado controladas por lógica interna. No entanto, a navegação pode ser complexa de implementar em aplicativos que usam o padrão MVVM (Model-View-ViewModel), pois os seguintes desafios devem ser atendidos:

  • Como identificar a exibição a ser navegada, usando uma abordagem que não introduza acoplamento rígido e dependências entre as exibições.
  • Como coordenar o processo pelo qual a exibição a ser navegada é instanciada e inicializada. Ao usar o MVVM, a exibição e o modelo de exibição precisam ser instanciados e associados entre si por meio do contexto de associação da exibição. Quando um aplicativo está usando um contêiner de injeção de dependência, a instanciação de exibições e modelos de exibição pode exigir um mecanismo de construção específico.
  • Se a navegação de exibição inicial deve ser executada ou a navegação de exibição de modelo inicial. Com a navegação de exibição primeiro, a página para a qual navegar se refere ao nome do tipo de exibição. Durante a navegação, a exibição especificada é instanciada, juntamente com seu modelo de exibição correspondente e outros serviços dependentes. Uma abordagem alternativa é usar a navegação de exibição que prioriza o modelo, em que a página para navegar se refere ao nome do tipo de modelo de exibição.
  • Como separar de forma limpa o comportamento de navegação do aplicativo entre as exibições e os modelos de exibição. O padrão MVVM fornece uma separação entre a interface do usuário do aplicativo e sua apresentação e lógica de negócios. No entanto, o comportamento de navegação de um aplicativo geralmente abrange as partes da interface do usuário e das apresentações do aplicativo. O usuário geralmente iniciará a navegação de uma exibição e a exibição será substituída como resultado da navegação. No entanto, a navegação também pode precisar ser iniciada ou coordenada de dentro do modelo de exibição.
  • Como passar parâmetros durante a navegação para fins de inicialização. Por exemplo, se o usuário navegar até uma exibição para atualizar os detalhes do pedido, os dados do pedido precisarão ser passados para a exibição para que possam exibir os dados corretos.
  • Como coordenar a navegação, para garantir que certas regras de negócios sejam obedecidas. Por exemplo, os usuários podem ser solicitados antes de navegar para longe de uma exibição para que corrijam dados inválidos ou para enviar ou descartar alterações de dados que foram feitas dentro da exibição.

Este capítulo aborda esses desafios apresentando uma NavigationService classe usada para executar a navegação de página do modelo de exibição primeiro.

Observação

O NavigationService usado pelo aplicativo foi projetado apenas para executar a navegação hierárquica entre instâncias de ContentPage. Usar o serviço para navegar entre outros tipos de página pode resultar em um comportamento inesperado.

A lógica de navegação pode residir no code-behind de uma exibição ou em um modelo de exibição associado a dados. Embora colocar a lógica de navegação em uma exibição possa ser a abordagem mais simples, ela não é facilmente testável por meio de testes de unidade. Colocar a lógica de navegação em classes de modelo de exibição significa que a lógica pode ser exercida por meio de testes de unidade. Além disso, o modelo de exibição pode implementar lógica para controlar a navegação para garantir que determinadas regras de negócios sejam impostas. Por exemplo, um aplicativo pode não permitir que o usuário navegue para longe de uma página sem primeiro garantir que os dados inseridos sejam válidos.

Normalmente, uma NavigationService classe é invocada de modelos de exibição para promover a capacidade de teste. No entanto, navegar para exibições de modelos de exibição exigiria que os modelos de exibição fizessem referência a exibições e, particularmente, exibições às quais o modelo de exibição ativo não está associado, o que não é recomendado. Portanto, o NavigationService apresentado aqui especifica o tipo de modelo de exibição como o destino para o qual navegar.

O aplicativo móvel eShopOnContainers usa a NavigationService classe para fornecer navegação de exibição no modelo primeiro. Essa classe implementa a interface INavigationService, que é mostrada no seguinte exemplo de código:

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();  
}

Essa interface especifica que uma classe de implementação deve fornecer os seguintes métodos:

Método Finalidade
InitializeAsync Executa a navegação para uma das duas páginas quando o aplicativo é iniciado.
NavigateToAsync Executa a navegação hierárquica para uma página especificada.
NavigateToAsync(parameter) Executa a navegação hierárquica para uma página especificada, passando um parâmetro.
RemoveLastFromBackStackAsync Remove a página anterior da pilha de navegação.
RemoveBackStackAsync Remove todas as páginas anteriores da pilha de navegação.

Além disso, a INavigationService interface especifica que uma classe de implementação deve fornecer uma PreviousPageViewModel propriedade. Essa propriedade retorna o tipo de modelo de exibição associado à página anterior na pilha de navegação.

Observação

Uma interface INavigationService normalmente também especificaria um método GoBackAsync, que é usado para retornar programaticamente à página anterior na pilha de navegação. No entanto, esse método está ausente do aplicativo móvel eShopOnContainers porque não é necessário.

Criando a instância do NavigationService

A NavigationService classe, que implementa a INavigationService interface, é registrada como um singleton com o contêiner de injeção de dependência Autofac, conforme demonstrado no exemplo de código a seguir:

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

A INavigationService interface é resolvida no construtor de ViewModelBase classe, conforme demonstrado no exemplo de código a seguir:

NavigationService = ViewModelLocator.Resolve<INavigationService>();

Isso retorna uma referência ao NavigationService objeto armazenado no contêiner de injeção de dependência do Autofac, que é criado pelo InitNavigation método na App classe. Para obter mais informações, consulte Navegando quando o aplicativo é iniciado.

A classe ViewModelBase armazena a instância NavigationService em uma propriedade NavigationService, do tipo INavigationService. Portanto, todas as classes de modelo de exibição, que derivam da ViewModelBase classe, podem usar a NavigationService propriedade para acessar os métodos especificados pela INavigationService interface. Isso evita a sobrecarga de injetar o NavigationService objeto do contêiner de injeção de dependência Autofac em cada classe de modelo de exibição.

Manipulando solicitações de navegação

Xamarin.Forms fornece a NavigationPage classe, que implementa uma experiência de navegação hierárquica na qual o usuário pode navegar pelas páginas, para frente e para trás, conforme desejado. Para obter mais informações sobre navegação hierárquica, veja Navegação hierárquica.

Em vez de usar a NavigationPage classe diretamente, o aplicativo eShopOnContainers encapsula a NavigationPage CustomNavigationView classe na classe, conforme mostrado no exemplo de código a seguir:

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

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

A finalidade desse encapsulamento é facilitar o NavigationPage estilo da instância dentro do arquivo XAML para a classe.

A navegação é executada dentro de classes de modelo de exibição invocando um dos NavigateToAsync métodos, especificando o tipo de modelo de exibição para a página que está sendo navegada, conforme demonstrado no exemplo de código a seguir:

await NavigationService.NavigateToAsync<MainViewModel>();

O exemplo de código a seguir mostra os NavigateToAsync métodos fornecidos pela NavigationService classe:

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 qualquer classe de modelo de exibição derivada da ViewModelBase classe execute navegação hierárquica invocando o InternalNavigateToAsync método. Além disso, o segundo NavigateToAsync método permite que os dados de navegação sejam especificados como um argumento que é passado para o modelo de exibição que está sendo navegado, onde normalmente é usado para executar a inicialização. Para obter mais informações, consulte Passando parâmetros durante a navegação.

O InternalNavigateToAsync método executa a solicitação de navegação e é mostrado no exemplo de código a seguir:

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;  
}

O InternalNavigateToAsync método executa a navegação para um modelo de exibição chamando primeiro o CreatePage método. Esse método localiza a exibição que corresponde ao tipo de modelo de exibição especificado e cria e retorna uma instância desse tipo de exibição. Localizar a exibição que corresponde ao tipo de modelo de exibição usa uma abordagem baseada em convenção, que pressupõe que:

  • As vistas estão na mesma montagem que os tipos de modelo de vista.
  • As visualizações estão em um arquivo . Exibe o namespace filho.
  • Os modelos de vista estão em um arquivo . Namespace filho ViewModels.
  • Os nomes de vista correspondem aos nomes dos modelos de vista, com "Modelo" removido.

Quando uma exibição é instanciada, ela é associada ao seu modelo de exibição correspondente. Para obter mais informações sobre como isso ocorre, consulte Criando automaticamente um modelo de vista com um localizador de modelo de vista.

Se a exibição que está sendo criada for um LoginView, ela será encapsulada em uma nova instância da CustomNavigationView classe e atribuída à Application.Current.MainPage propriedade. Caso contrário, a CustomNavigationView instância será recuperada e, desde que não seja nula, o PushAsync método será invocado para enviar a exibição que está sendo criada para a pilha de navegação. No entanto, se a instância recuperada CustomNavigationView for null, a exibição que está sendo criada será encapsulada em uma nova instância da CustomNavigationView classe e atribuída à Application.Current.MainPage propriedade. Esse mecanismo garante que, durante a navegação, as páginas sejam adicionadas corretamente à pilha de navegação quando ela estiver vazia e quando contiver dados.

Dica

Considere armazenar páginas em cache. O cache de página resulta no consumo de memória para exibições que não são exibidas no momento. No entanto, sem o cache de página, isso significa que a análise XAML e a construção da página e seu modelo de exibição ocorrerão sempre que uma nova página for navegada, o que pode ter um impacto no desempenho de uma página complexa. Para uma página bem projetada que não usa um número excessivo de controles, o desempenho deve ser suficiente. No entanto, o cache de página pode ajudar se forem encontrados tempos de carregamento de página lentos.

Depois que a exibição é criada e navegada, o InitializeAsync método do modelo de exibição associado à exibição é executado. Para obter mais informações, consulte Passando parâmetros durante a navegação.

Quando o aplicativo é iniciado, o InitNavigation App método na classe é invocado. O seguinte exemplo de código mostra esse método:

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

O método cria um novo NavigationService objeto no contêiner de injeção de dependência Autofac e retorna uma referência a ele, antes de invocar seu InitializeAsync método.

Observação

Quando a INavigationService interface é resolvida pela ViewModelBase classe, o contêiner retorna uma referência ao NavigationService objeto que foi criado quando o método InitNavigation é invocado.

O exemplo de código a seguir mostra o NavigationService InitializeAsync método:

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

O MainView é navegado para se o aplicativo tiver um token de acesso armazenado em cache, que é usado para autenticação. Caso contrário, o LoginView é navegado para.

Para obter mais informações sobre o contêiner de injeção de dependência do Autofac, consulte Introdução à injeção de dependência.

Passando parâmetros durante a navegação

Um dos NavigateToAsync métodos, especificado pela interface, permite que os INavigationService dados de navegação sejam especificados como um argumento que é passado para o modelo de exibição que está sendo navegado, onde normalmente é usado para executar a inicialização.

Por exemplo, a classe ProfileViewModel contém um OrderDetailCommand que é executado quando o usuário seleciona uma ordem na página ProfileView. Por sua vez, isso executa o método OrderDetailAsync, que é mostrado no seguinte exemplo de código:

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

Esse método invoca a navegação para o OrderDetailViewModel, passando uma Order instância que representa a ordem que o usuário selecionou na ProfileView página. Quando a NavigationService classe cria o OrderDetailView, a OrderDetailViewModel classe é instanciada e atribuída ao .BindingContext Depois de navegar até o OrderDetailView, o InternalNavigateToAsync método executa o InitializeAsync método do modelo de exibição associado à exibição.

O InitializeAsync método é definido na ViewModelBase classe como um método que pode ser substituído. Esse método especifica um object argumento que representa os dados a serem passados para um modelo de exibição durante uma operação de navegação. Portanto, as classes de modelo de exibição que desejam receber dados de uma operação de navegação fornecem sua própria implementação do InitializeAsync método para executar a inicialização necessária. O exemplo de código a seguir mostra o método InitializeAsync da classe OrderDetailViewModel:

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

Esse método recupera a Order instância que foi passada para o modelo de exibição durante a operação de navegação e a usa para recuperar os detalhes completos do pedido da OrderService instância.

Invocando a navegação usando comportamentos

A navegação geralmente é disparada de uma exibição por uma interação do usuário. Por exemplo, o LoginView executa a navegação após a autenticação bem-sucedida. O seguinte exemplo de código mostra como a navegação é invocada por um comportamento:

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

Em runtime, EventToCommandBehavior responderá à interação com WebView. Quando o WebView navega para uma página da Web, o Navigating evento é acionado, o que executa o NavigateCommand arquivo .LoginViewModel Por padrão, os argumentos do evento para o evento serão passados para o comando. Esses dados são convertidos conforme são passados entre a origem e o destino pelo conversor especificado na propriedade EventArgsConverter, que retorna o Url do WebNavigatingEventArgs. Portanto, quando o NavigationCommand é executado, a URL da página da web é passada como um parâmetro para o .Action

Por sua vez, o NavigationCommand executa o método NavigateAsync, que é mostrado no seguinte exemplo de código:

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

Esse método invoca a navegação para o , e a MainViewModelnavegação a seguir, remove a LoginView página da pilha de navegação.

Confirmando ou cancelando a navegação

Um aplicativo pode precisar interagir com o usuário durante uma operação de navegação para que o usuário possa confirmar ou cancelar a navegação. Isso pode ser necessário, por exemplo, quando o usuário tenta navegar antes de preencher totalmente uma página de entrada de dados. Nessa situação, um aplicativo deve fornecer uma notificação que permita que o usuário navegue para longe da página ou cancele a operação de navegação antes que ela ocorra. Isso pode ser obtido em uma classe de modelo de exibição usando a resposta de uma notificação para controlar se a navegação é invocada ou não.

Resumo

Xamarin.Forms inclui suporte para navegação de página, que normalmente resulta da interação do usuário com a interface do usuário ou do próprio aplicativo, como resultado de alterações de estado controladas por lógica interna. No entanto, a navegação pode ser complexa de implementar em aplicativos que usam o padrão MVVM.

Este capítulo apresentou uma NavigationService classe, que é usada para executar a navegação do modelo de exibição a partir de modelos de exibição. Colocar a lógica de navegação em classes de modelo de exibição significa que a lógica pode ser exercida por meio de testes automatizados. Além disso, o modelo de exibição pode implementar lógica para controlar a navegação para garantir que determinadas regras de negócios sejam impostas.