Oi pessoal do MX Cursos, hoje vamos falar de PageContext, alguém já ouviu falar ? Não? Então, este foi um termo que utilizei já que estaremos tratando no próprio contexto das Pages no Xamarin.Forms e o nome parece ser bem fiel…
Em um rápido preview apresentaremos um diagrama simples para esboçar um projeto cujo sua camada de apresentação esteja sob o pattern MVVM ( Model-View-Viewmodel – intrínseco ao Xamarin ).
http://rehansaeed.com/tag/windows-phone/
View – Responsável por definir a estrutura, layout e aparência do que os usuários veem na tela. As definições das views podem ser feitas tanto em XAML ou Code API, com um code-behind sem lógicas de negócio e definições de eventos para os objetos visuais (utilizando XAML, teríamos somente a chamada do método InitializeComponent e no Code API, a instanciação e inicialização dos objetos visuais, e à conexão entre a View e a ViewModel através da propriedade de BindingContext, ….)
Viewmodel – Atua como um intermédio entre a view e a model, e é responsável por manipular a lógica de negócio. A viewmodel recupera e disponibiliza dados da model de forma que a view possa facilmente manipulá-los, esta por sua vez fornece Commands que um usuário da aplicação inicia na view.
Model – A model no MVVM é uma implementação do domínio da aplicação que incluem modelo de dados juntos com o negócio e lógica de validação. Exemplos de modelo de objetos incluem repositories, objetos de negócio, DTO (Data Transfer Objects ), POCOs ( Plain Old CLR Objects ), entidades e objetos proxy.
Agora com as definições compreendidas, vamos fluir na construção da nossa Sample Application ( Estamos utilizando o Visual Studio Community 2015 – ( Sensacional ) ).
Crie um novo projeto em Cross-Platform selecionando o ItemTemplate Blank App ( à escolha da abordagem Shared ou Portable fica por sua conta…. ).
Com o projeto gerado a nossa Solution ficará da seguinte forma.
Fazendo um rápido entendimento sobre o que será o helper PageContext, sabemos que o modelo de navegão utilizado no Xamarin.Forms se basea na interface INavigation membro da classe VisualElement e as mensagens pop-ups ( o Xamarin.Forms as implementa nativamente em cada plataforma ) através dos métodos DisplayAlert() e DisplayActionSheet presentes na classe Page. De certa forma poderíamos utilizar estes elementos através do Code-Behind das nossas classes que representam as Pages da nossa aplicação, já que os mesmos estão presentes nas classes ancestrais; mas, como a intenção é utilizar o modelo MVVM e como dito anteriormente que as Views se conectam às ViewModels através da propriedade BindingContext ( membro da classe BindableObject ), não faremos chamadas aos métodos de navegação ou mensagens, pois os objetos visuais utilizarão o mecanismo de DataBinding com Commands para designar tais ações. Os commands são membros da ViewModel, e esta por sua vez não possui ou não deve ter referências às Pages ( como por exemplo manipular seus objetos visuais), assim mantemos as responsabilidades das ViewModels bem definidas. Tudo soa bastante interessante, mas vai uma pergunta, como não utilizaremos Code-Behind e as ViewModels por não conhecerem as Pages, como utilizaremos a Navegação ou Mensagens em nossa aplicação ? Neste ponto entra o PageContext, que garantirá a independência entre as Views e ViewModels, utilizando Injeção de Dependência para executar a navegação e mensagens. Como resultado final nossa aplicação ficará da seguinte forma:
Windows Phone:
Android:
iOS:
Vamos utilizar em nosso exemplo o contâiner de IoC conhecido com Autofac (http://autofac.org/ ), para isto, utilize a opção Manage Nuget Packages, ou através do menu Tools -> Nuget Package Manager -> Package Manager Console.
Após a adição do package, vamos iniciar com a criação de nossas Pages, a abordagem utilizada será XAML. Por uma escolha pessoal gosto sempre de separar as classes em folders para que fiquem mais legíveis à solution.
Expandindo a folder Pages, obtemos os seguintes arquivos:
A folder Context, examinaremos com mais detalhes um pouco mais adiante, já a folder Interfaces contêm as interfaces que nossas Pages implementarão e que serão registradas em nosso container.
public interface IPage { /// <summary> /// Objeto que contêm propriedades que estarão no contexto do Binding. /// </summary> object BindingContext { get; set; } /// <summary> /// Exibe uma lista de ações que permitindo ao usuário sua escolha ( Apresentação de forma nativa ). /// </summary> /// <param name="title">Título</param> /// <param name="cancel">Texto apresentado na opção de cancelar</param> /// <param name="destruction"></param> /// <param name="buttons">Opções</param> /// <returns></returns> Task<string> DisplayActionSheet(string title, string cancel, string destruction, params string[] buttons); /// <summary> /// Apresenta uma caixa de diálogo com um única opção de cancelar. /// </summary> /// <param name="title">Título</param> /// <param name="message">Mensagem</param> /// <param name="cancel">Texto exibido no botão de cancelar.</param> /// <returns></returns> Task DisplayAlert(string title, string message, string cancel); /// <summary> /// Apresenta uma caixa de diálogo com um única opção de cancelar. /// </summary> /// <param name="title">Título</param> /// <param name="message">Mensagem</param> /// <param name="accept">Texto exibido no botão de confirmação</param> /// <param name="cancel">Texto exibido no botão de cancelar.</param> /// <returns></returns> Task<bool> DisplayAlert(string title, string message, string accept, string cancel); }
A interface IPage apresenta alguns membros como propriedade e métodos que as próprias Pages do Xamarin já implementam, estes por sua vez são os membros necessários que precisamos para as chamadas em nosso PageContext.
public interface IFirstPage : IPage { } public interface ISecondPage : IPage { } public interface IThirdPage : IPage { }
Estas são as demais interfaces da folder Pages/Interfaces que serão utilizadas em nosso container. Na estrutura das XAML Pages, temos:
FirstPage.xaml <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="XamarinSample.Pages.FirstPage"> <StackLayout VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" BackgroundColor="Black"> <Image Source="microsoftHubRocks.png" /> <StackLayout Padding="10"> <Label Text="First Page »" /> <Button Text="Go to Second Page" BackgroundColor="Green" Command="{Binding GoToSecondPageCommand}" /> </StackLayout> </StackLayout> </ContentPage> FirstPage.xaml.cs namespace XamarinSample.Pages { public partial class FirstPage : ContentPage, IFirstPage { #region Constructor public FirstPage() { InitializeComponent(); } #endregion } }
Conectamos o behavior de Click do nosso Button através da propriedade Command.
SecondPage.xaml <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="XamarinSample.Pages.SecondPage"> <StackLayout VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" BackgroundColor="Black"> <Image Source="microsoftHubRocks.png" /> <StackLayout Padding="10"> <Label Text="Second Page »" /> <Button Text="Go to Third Page" BackgroundColor="Green" Command="{Binding GoToThirdPageCommand}" /> </StackLayout> </StackLayout> </ContentPage> SecondPage.xaml.cs namespace XamarinSample.Pages { public partial class SecondPage : ContentPage, ISecondPage { #region Constructor public SecondPage() { InitializeComponent(); } #endregion } }
ThirdPage.xaml <?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" x:Class="XamarinSample.Pages.ThirdPage"> <StackLayout VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand" BackgroundColor="Black"> <Image Source="microsoftHubRocks.png" /> <StackLayout Padding="10"> <Label Text="Third Page »" /> <Button Text="Show Message" BackgroundColor="Green" Command="{Binding ShowCustomMessageCommand}"/> </StackLayout> </StackLayout> </ContentPage> ThirdPage.xaml.cs using Xamarin.Forms; using XamarinSample.Pages.Interfaces; namespace XamarinSample.Pages { public partial class ThirdPage : ContentPage, IThirdPage { #region Constructor public ThirdPage() { InitializeComponent(); } #endregion } }
Agora que nossas Pages estão construídas, vamos visualizar as viewmodels que se encontram na seguinte estrutura:
Lembrando que para que haja uma viewmodel a mesma deve implementar a interface INotifyPropertyChanged , para que a mesma possa responder às notificações; para evitar o retrabalho a ViewModelBase se encarrega desta implementação.
public abstract class ViewModelBase : IViewModel { #region Properties /// <summary> /// Contexto de navegação e mensagens pop-up. /// </summary> public IPageContext Context { get; private set; } #endregion #region Constructor public ViewModelBase(IPageContext context) { Context = context; } #endregion #region INotifyPropertyChanged members /// <summary> // Ocorre quando há mudanças em propriedades. /// </summary> public event PropertyChangedEventHandler PropertyChanged; /// <summary> /// Notifica às modificações. /// </summary> /// <typeparam name="T">Tipo da Propriedade.</typeparam> /// <param name="expression">Propriedade</param> protected void RaisedPropertyChanged<T>(Expression<Func<T>> expression) { var memberExpression = expression.Body as MemberExpression; if (memberExpression == null) return; var propertyInfo = memberExpression.Member as PropertyInfo; if (propertyInfo == null) return; RaisedPropertyChanged(propertyInfo.Name); } /// <summary> /// Notifica às modificações. /// </summary> /// <param name="propertyName">Nome da Propriedade</param> protected void RaisedPropertyChanged([CallerMemberName] string propertyName = null) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } #endregion }
Seguindo com as demais:
public class FirstViewModel : ViewModelBase, IFirstViewModel { #region Fields private Command _goToSecondPageCommand; #endregion #region Constructor public FirstViewModel(IPageContext context) : base(context) { } #endregion #region IFistViewModel members /// <summary> /// Commando responsável por navegar para a segunda página. /// </summary> public ICommand GoToSecondPageCommand { get { return _goToSecondPageCommand ?? (_goToSecondPageCommand = new Command(async() => await Context.NavigateTo<ISecondPage, ISecondViewModel>())); } } #endregion } public interface IFirstViewModel { /// <summary> /// Commando responsável por navegar para a segunda página. /// </summary> ICommand GoToSecondPageCommand { get; } }
public class SecondViewModel : ViewModelBase, ISecondViewModel { #region Fields private Command _goToThirdPageCommand; #endregion #region Constructor public SecondViewModel(IPageContext context) : base(context) { } #endregion #region ISecondViewModel members /// <summary> /// Commando responsável por navegar para a terceira página. /// </summary> public ICommand GoToThirdPageCommand { get { return _goToThirdPageCommand ?? (_goToThirdPageCommand = new Command(async() => await Context.NavigateTo<IThirdPage, IThirdViewModel>())); } } #endregion } public interface ISecondViewModel { /// <summary> /// Commando responsável por navegar para a terceira página. /// </summary> ICommand GoToThirdPageCommand { get; } }
public class ThirdViewModel : ViewModelBase, IThirdViewModel { #region Fields private Command _showCustomMessageCommand; #endregion #region Constructor public ThirdViewModel(IPageContext context) : base(context) { } #endregion #region IThirdViewModel members /// <summary> /// Commando responsável por exibir mensagem. /// </summary> public ICommand ShowCustomMessageCommand { get { return _showCustomMessageCommand ?? (_showCustomMessageCommand = new Command(async() => { await Context.ShowMessage("Mensagem", "PageContext com mensagem.", "Ok"); })); } } #endregion } public interface IThirdViewModel { /// <summary> /// Commando responsável por exibir mensagem. /// </summary> ICommand ShowCustomMessageCommand { get; } }
Podemos observar que as viewmodels não possuem instancias das Pages, temos somente uma propriedade chamada PageContext que é setada por injeção do container. Desta forma temos o PageContext apresentado na seguinte estrutura:
A interface IPageContext expõe os membros para Navigation e Messaging:
public interface IPageContext { // <summary> // Obtém a IPage atual. // </summary> IPage CurrentPage { get; } /// <summary> /// Navega para uma TPage conectada a uma TViewModel. /// Modelo de navegação baseado na NavigationStack. /// </summary> /// <typeparam name="TPage">Nova página</typeparam> /// <typeparam name="TViewModel">Viewmodel</typeparam> /// <returns></returns> Task NavigateTo<TPage, TViewModel>() where TPage : class, IPage where TViewModel : class; /// <summary> /// Navega para uma TPage conectada a uma TViewModel, fazendo uma injeção do valor para uma propriedade /// desta viewmodel. /// Modelo de navegação baseado na NavigationStack. /// </summary> /// <typeparam name="TPage">Nova página</typeparam> /// <typeparam name="TViewModel">Viewmodel</typeparam> /// <param name="property">Proprieda</param> /// <param name="value">Valor</param> /// <returns></returns> Task NavigateTo<TPage, TViewModel>(Expression<Func<object>> property, object value) where TPage : class, IPage where TViewModel : class; /// <summary> /// Navega para uma TPage conectada a uma TViewModel. /// Modelo de navegação baseado na ModalStack. /// </summary> /// <typeparam name="TPage">Nova página</typeparam> /// <typeparam name="TViewModel">Viewmodel</typeparam> /// <returns></returns> Task ModalNavigateTo<TPage, TViewModel>() where TPage : class, IPage where TViewModel : class; /// <summary> /// Exibe uma lista de ações que permitindo ao usuário sua escolha ( Apresentação de forma nativa ). /// </summary> /// <param name="title">Título</param> /// <param name="cancel">Texto apresentado na opção de cancelar</param> /// <param name="destruction"></param> /// <param name="buttons">Opções</param> /// <returns></returns> Task<string> ShowConfirmationMessage(string title, string cancel, string destruction, params string[] buttons); /// <summary> /// Apresenta uma caixa de diálogo com um única opção de cancelar. /// </summary> /// <param name="title">Título</param> /// <param name="message">Mensagem</param> /// <param name="cancel">Texto exibido no botão de cancelar.</param> /// <returns></returns> Task ShowMessage(string title, string message, string cancel); /// <summary> /// Apresenta uma caixa de diálogo com um única opção de cancelar. /// </summary> /// <param name="title">Título</param> /// <param name="message">Mensagem</param> /// <param name="accept">Texto exibido no botão de confirmação</param> /// <param name="cancel">Texto exibido no botão de cancelar.</param> /// <returns></returns> Task<bool> ShowMessage(string title, string message, string accept, string cancel); }
E sua implementação:
public class PageContext : IPageContext { #region Fields private IComponentContext _componentContext; private IPage _page; #endregion #region Properties /// <summary> /// Obtém a IPage atual. /// </summary> public IPage CurrentPage { get { var xamarinPage = _page as Page; if (xamarinPage == null) return null; var stack = xamarinPage.Navigation.NavigationStack; return stack.Any() ? stack.Last() as IPage : _page; } } #endregion #region Constructor public PageContext(IComponentContext componentContext, IPage page) { _componentContext = componentContext; _page = page; } #endregion #region Methods #region Navigation /// <summary> /// Navega para uma TPage conectada a uma TViewModel. /// </summary> /// <typeparam name="TPage">Nova página</typeparam> /// <typeparam name="TViewModel">Viewmodel</typeparam> /// <returns></returns> public async Task NavigateTo<TPage, TViewModel>() where TPage : class, IPage where TViewModel : class { //Resolvendo dependências. var newPage = _componentContext.Resolve<TPage>(); var viewmodel = _componentContext.Resolve<TViewModel>(); if (newPage != null && viewmodel != null) { //Conectando a Nova View com a Viewmodel. newPage.BindingContext = viewmodel; //Empilhando a página atual. await ((Page)CurrentPage).Navigation.PushAsync(newPage as Page); } } /// <summary> /// Navega para uma TPage conectada a uma TViewModel, fazendo uma injeção do valor para uma propriedade /// desta viewmodel. /// </summary> /// <typeparam name="TPage">Nova página</typeparam> /// <typeparam name="TViewModel">Viewmodel</typeparam> /// <param name="property">Proprieda</param> /// <param name="value">Valor</param> /// <returns></returns> public async Task NavigateTo<TPage, TViewModel>(Expression<Func<object>> property, object value) where TPage : class, IPage where TViewModel : class { //Resolvendo dependências. var newPage = _componentContext.Resolve<TPage>(); var viewmodel = _componentContext.Resolve<TViewModel>(); if (newPage != null && viewmodel != null) { //Conectando a Nova View com a Viewmodel. newPage.BindingContext = viewmodel; //Definindo valor para uma propriedade da Viewmodel. SetPropertyValue<TViewModel>(property.Body, viewmodel, value); //Empilhando a página atual. await ((Page)CurrentPage).Navigation.PushAsync(newPage as Page); } } /// <summary> /// Navega para uma TPage conectada a uma TViewModel. /// Modelo de navegação baseado na ModalStack. /// </summary> /// <typeparam name="TPage">Nova página</typeparam> /// <typeparam name="TViewModel">Viewmodel</typeparam> /// <returns></returns> public async Task ModalNavigateTo<TPage, TViewModel>() where TPage : class, IPage where TViewModel : class { //Resolvendo dependências. var newPage = _componentContext.Resolve<TPage>(); var viewmodel = _componentContext.Resolve<TViewModel>(); if (newPage != null && viewmodel != null) { //Conectando a Nova View com a Viewmodel. newPage.BindingContext = viewmodel; //Empilhando a página atual. await ((Page)CurrentPage).Navigation.PushModalAsync(newPage as Page); } } private void SetPropertyValue<T>(Expression property, object root, object value) { var member = property as MemberExpression; if (member == null) return; var propertyInfo = member.Member as PropertyInfo; if (propertyInfo == null) return; var propertySetter = root.GetType() .GetTypeInfo() .DeclaredProperties .First(c => c.Name.Equals(propertyInfo.Name, StringComparison.OrdinalIgnoreCase)); if (propertySetter != null) propertySetter.SetValue(root, value); } #endregion #region Messaging /// <summary> /// Exibe uma lista de ações que permitindo ao usuário sua escolha ( Apresentação de forma nativa ). /// </summary> /// <param name="title">Título</param> /// <param name="cancel">Texto apresentado na opção de cancelar</param> /// <param name="destruction"></param> /// <param name="buttons">Opções</param> /// <returns></returns> public async Task<string> ShowConfirmationMessage(string title, string cancel, string destruction, params string[] buttons) { var result = await CurrentPage.DisplayActionSheet(title, cancel, destruction, buttons); return result; } /// <summary> /// Apresenta uma caixa de diálogo com um única opção de cancelar. /// </summary> /// <param name="title">Título</param> /// <param name="message">Mensagem</param> /// <param name="cancel">Texto exibido no botão de cancelar.</param> /// <returns></returns> public async Task ShowMessage(string title, string message, string cancel) { await CurrentPage.DisplayAlert(title, message, cancel); } /// <summary> /// Apresenta uma caixa de diálogo com um única opção de cancelar. /// </summary> /// <param name="title">Título</param> /// <param name="message">Mensagem</param> /// <param name="accept">Texto exibido no botão de confirmação</param> /// <param name="cancel">Texto exibido no botão de cancelar.</param> /// <returns></returns> public async Task<bool> ShowMessage(string title, string message, string accept, string cancel) { var result = await CurrentPage.DisplayAlert(title, message, accept, cancel); return result; } #endregion #endregion }
Vamos detalhar nossa PageContext:
- A propriedade de CurrentPage retorna a IPage atual;
- O construtor da classe PageContext, recebe IComponentContext, que é o própria instância do container, para que possamos recuperar as implementações tardiamente, e recebemos também uma IPage que é inserida como a primeira página da Stack de navegação.
- Nos métodos de navegação recebemos nos placeholders uma page (TPage) e uma viewmodel (TViewmodel), ambas são referências para as instâncias recuperadas no container, as conectamos pelo BindingContext da Page e em seguida utilizamos o sistema de Navegação do Xamarin.Forms;
- O método SetPropertyValue é utilizado para setarmos uma propriedade da viewmodel.
- Por final temos os métodos de Messaging que são um wrapper para os métodos utilizados no Xamarin.Forms que estão presentes dentro de Page.
Desta forma em nossas implementações nas viewmodels temos o Context chamando o método de NavigateTo passando a interface da Page e Viewmodel, que serão resolvidas pelo container e o método ShowMessage para apresentação das mensagens.
public ICommand GoToThirdPageCommand { get { return _goToThirdPageCommand ?? (_goToThirdPageCommand = new Command(async() => await Context.NavigateTo<IThirdPage, IThirdViewModel>())); } }
/// <summary> /// Commando responsável por exibir mensagem. /// </summary> public ICommand ShowCustomMessageCommand { get { return _showCustomMessageCommand ?? (_showCustomMessageCommand = new Command(async() => { await Context.ShowMessage("Mensagem", "PageContext com mensagem.", "Ok"); })); } }
Antes de finalizarmos vamos à configuração do nosso container.
public class AutofacConfig { #region Fields private static IContainer _container; #endregion #region Properties /// <summary> /// Recupera uma page do contâiner. /// </summary> public static TPage GetPage<TService, TPage>() where TService : IPage where TPage : class { if (_container == null || !_container.IsRegistered<TService>()) return default(TPage); var pageContext = _container.Resolve<IPageContext>(); return (TPage)pageContext.CurrentPage; } #endregion #region Methods /// <summary> /// Responsável por registras em dependências ( Autofac ). /// </summary> public static void ConfigureContainer() { var builder = new ContainerBuilder(); //Pages builder.Register(c => new FirstPage()).As<IFirstPage>() .OnActivated(e => e.Instance.BindingContext = e.Context.Resolve<IFirstViewModel>()); builder.RegisterType<SecondPage>().As<ISecondPage>(); builder.RegisterType<ThirdPage>().As<IThirdPage>(); //Viewmodels builder.RegisterType<FirstViewModel>().As<IFirstViewModel>(); builder.RegisterType<SecondViewModel>().As<ISecondViewModel>(); builder.RegisterType<ThirdViewModel>().As<IThirdViewModel>(); //PageContext builder.Register((c, p) => new PageContext(c.Resolve<IComponentContext>(), c.Resolve<IFirstPage>())) .As<IPageContext>() .SingleInstance(); _container = builder.Build(); } #endregion }
O método GetPage nos auxilia na inicialização do PageContext e definir à Page de inicialização na classe Application.
Lembre-se de inicializar a configuração do container.
Este helper não contempla todas as possíveis chamadas e navegação do Xamarin.Forms, mas para isso basta implementá-los de acordo com à necessidade. A utilização do PageContext é interessante pois inibe a utilização da Page e seus membros, o que poderia deixar as viewmodels completamente amarradas. Espero que curtam PageContext, saudações MXCursos !