Xamarin Forms: Creando un Sistema de Navegación sin frameworks

Hola amigos, en esta ocasión, escribo esta entrada para celebrar el 2do. Calendario de Adviento de Xamarin organizado por Luis Beltrán.

El tema de hoy, es un tema que estoy seguro, te has preguntado alguna vez, tanto si eres nuevo en el tema de Xamarin, o si ya tienes algunos años de experiencia. Se trata del tema, de cómo crear un Sistema de Navegación desde cero con Xamarin Forms. Vamos a ello.

¡Adquiere el Máster en Xamarin Forms!

Antes de iniciar, te invito a visitar la página de la Membresía de mi academia, donde podrás encontrar:

Cursos y talleres de Xamarin
Cursos y talleres en C#
Cursos y talleres de Blazor
– Cursos y talleres de ASP.NET
– Cursos y talleres en muchas otras tecnologías

¡Todo al precio más bajo posible por tiempo limitado!

curso de xamarin

Video sobre Navegación en Xamarin Forms

A continuación, te dejo el contenido de esta publicación en formato video, por si te apetece ver el paso a paso de lo que se describe, ¡No olvides suscribirte al canal!

Creación del Proyecto de Navegación con Xamarin Forms

Vamos a crear un proyecto sencillo para iniciar la demostración. Crearemos las clásicas carpetas “Views” y “ViewModels”, y crearemos dentro de la carpeta Views dos archivos tipo ContentPage, con el siguiente contenido:

Contenido de la página “Page1View”:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
    x:Class="NavigationDemo.Views.Page1View"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
    <ContentPage.Content>
        <StackLayout>
            <Button
                Clicked="Button_Clicked"
                HorizontalOptions="CenterAndExpand"
                Text="Go to Page 2!"
                VerticalOptions="CenterAndExpand" />
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

Contenido de la página “Page2View”:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
    x:Class="NavigationDemo.Views.Page2View"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
    <ContentPage.Content>
        <StackLayout>
            <Label
                HorizontalOptions="CenterAndExpand"
                Text="Welcome to Page 2!"
                VerticalOptions="CenterAndExpand" />
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

Como tal vez hayas hecho alguna vez, podemos utilizar el manejador de eventos creado del botón para navegar a la página 2, de una forma extremandamente sencilla:

        private async void Button_Clicked(object sender, EventArgs e)
        {
            await Navigation.PushAsync(new Page2View());
        }

El problema viene, cuando agregamos los ViewModels de cada página (Sin contenido para no saturar el ejemplo):

Contenido de Page1ViewModel

    public class Page1ViewModel
    {        
    }

Contenido de Page2ViewModel:

    public class Page2ViewModel
    {
    }

Ya sabemos que debemos ligar un ViewModel a una ContentPage, en este ejemplo, Page1ViewModel al BindingContext de Page1View:

        public Page1View()
        {
            InitializeComponent();
            BindingContext = new Page1ViewModel();
        }

Con esto, podemos quitar el manejador de eventos y utilizar la propiedad Command del botón en Page1View:

            <Button
                Command="{Binding ButtonCommand}"
                HorizontalOptions="CenterAndExpand"
                Text="Go to Page 2!"
                VerticalOptions="CenterAndExpand" />

Podemos declarar el Comando, pero, ¿Cómo navegamos a otro ViewModel?

    public class Page1ViewModel : ViewModelBase
    {
        public ICommand ButtonCommand { get; set; }

        public Page1ViewModel()
        {
            ButtonCommand = new Command(async () =>
            {
                //????                
            });
        }
    }

Implementando un Sistema de Navegación en Xamarin Forms

Lo primero que tenemos que hacer, es crear una carpeta “Services”, donde crearemos una interfaz, donde podremos definir todos los métodos pensados hacia la parte de navegación. Por motivos de simplicidad, en esta entrega únicamente implementaremos el método que nos permita navegar hacia otro ViewModel, pero si quieres que profundicemos en otros métodos, deja tu comentario.

    public interface INavigationService
    {
        Task NavigateToAsync<TViewModel>();
    }

Una vez creada la interfaz, la implementaremos en una clase llamada “NavigationService”:

    public class NavigationService : INavigationService
    {
        public Task NavigateToAsync<TViewModel>()
        {
            throw new NotImplementedException();
        }
    }

Para implementar este método, vamos a crear, en primer lugar, una funcionalidad que nos permita obtener el tipo del objeto de nuestra página de interfaz gráfica. Ahora bien, si has podido notar, hemos mantenido una nomenclatura en el proyecto, con el propósito de lograr esto de manera sencilla, por lo que hacer esta tarea se facilita con este método:

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

Este método, permite reemplazar la palabra Model por una cadena vacía, de tal forma, que si el model se llama “Page1ViewModel”, el nombre de la vista será “Page1View”, lo cual coincide con lo que estamos haciendo.

Lo siguiente que haremos, será crear la página como tal, a partir de un tipo. Afortunadamente, contamos con una clase llamada Activator, que nos permite crear precisamente instancias a partir de tipos:

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

Una vez que obtenemos una referencia a una página ó elemento Page correspondiente a un ViewModel, vamos a realizar una navegación interna:

        private async Task InternalNavigateToAsync(Type viewModelType, object parameter)
        {
            Page page = CreatePage(viewModelType, parameter);
            var navigationPage = Application.Current.MainPage as NavigationPage;

            if (navigationPage != null)
            {
                await navigationPage.PushAsync(page);
            }
            else
            {
                Application.Current.MainPage = new NavigationPage(page);
            }
           }

Ya con estos métodos listos, lo único que tendremos que hacer, es completar el método “NavigateTo” que hace falta:

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

Si has sido observador, hemos dejado las preparaciones para que nuestro sistema de navegación sea capaz de recibir objetos que podamos pasar entre ViewModels, pero eso quedará para otra ocasión.

La siguiente tarea, será utilizar un Contenedor de Inyección de Dependencias, en mi caso, utilizaré el contenedor alojado en un repositorio de Github: https://github.com/grumpydev/TinyIoC, el cual, voy a copiar y pegar en una carpeta llamada TinyIoC, y crearé la clase “TinyIoC”, donde pegaré el contenido del contenedor.

Una vez implementado el contendor, voy a proceder a crear una clase llamada “ViewModelLocator”, la cual nos permitirá inicializar con contenedor con los ViewModels y el sistema de Navegación:

    public static class ViewModelLocator
    {
        private static TinyIoCContainer _container;

        static ViewModelLocator()
        {
            _container = new TinyIoCContainer();
            _container.Register<Page1ViewModel>();
            _container.Register<Page2ViewModel>();

            _container.Register<INavigationService, NavigationService>();
        }

        public static T Resolve<T>() where T : class
        {
            return _container.Resolve<T>();
        }
    }

Ahora, para tener acceso a la parte de navegación desde cada ViewModel, debemos definir un ViewModel base y ahí manejar el servicio de Navegación:

    public class ViewModelBase
    {
        protected readonly INavigationService NavigationService;
        public ViewModelBase()
        {
            NavigationService = ViewModelLocator.Resolve<INavigationService>();
        }
    }

Con todo esto listo, regresamos a Page1ViewModel, y llevamos a cabo el proceso de herencia de ViewModelBase, lo que nos permitirá invocar el método para llevar a cabo la navegación a la página 2:

    public class Page1ViewModel : ViewModelBase
    {
        public ICommand ButtonCommand { get; set; }

        public Page1ViewModel()
        {
            ButtonCommand = new Command(async() =>
            {
                //ir a botón 2???
                await NavigationService.NavigateToAsync<Page2ViewModel>();
            });
        }
    }

Listo, con esto ya hemos implementado un sistema de navegación para ir desde un ViewModel hacia otro. Te queda como tarea, implementar funcionalidad adicional para llevar a cabo por ejemplo, el pase de información entre ViewModels, o bien, implementar la funcionalidad para ir una página atrás.

¡Saludos!

Deja un comentario

Tu dirección de correo electrónico no será publicada.