domingo, 24 de julio de 2016

Reto Xamarin (Premio: ¡Una Playera!): Agrega un Backend de Azure a tu aplicación móvil

¿Te quieres ganar una playera de Xamarin?


Si tu respuesta es sí, lo que tienes que hacer HOY (la fecha límite es mañana a las 11 am hora de México) es desarrollar una aplicación móvil con Xamarin utilizando un backend de Azure. Después, tienes que subir un video donde muestres el funcionamiento de tu app y tweetearlo. Todos los detalles de este concurso están aquí.

El reto está interesante y en este post te ayudo a que te ganes tu playera :-) ¿Listo? ¡Manos a la obra! (Tiempo estimado: ¡1 hora o menos!)

El primer paso es tener Xamarin instalado. Revisa este video si tienes alguna pregunta sobre la instalación de la herramienta.

A continuación, necesitas una cuenta de Azure, la plataforma en la nube de Microsoft.. Si no la tienes, puedes adquirir una cuenta gratuita con 25 dólares en créditos por mes durante un año gracias a los beneficios del programa Visual Studio Dev Essentials. Es muy fácil activar los beneficios, solo requieres una cuenta de Hotmail/Live/Outlook.





NOTA: Al activar el beneficio de Azure, deberás llenar un formulario donde, entre otras cosas, se te solicita tu número de celular para registrar un código y también datos de tu tarjeta de crédito. Es probable que se te cobre un dólar para verificar que la cuenta es válida, pero después de una semana o dos, se te reembolsa el dólar. Por otro lado, reitero que el beneficio es gratuito mientras no te pases de los 25 dólares mensuales. Si excedes el monto, no te preocupes, te llegará un correo informando que los servicios que has registrado en Azure se han bloqueado y tienes 2 opciones: esperar a que termine el mes para dar de alta nuevamente los servicios (no se te cobra nada pero tus servicios no son accesibles hasta entonces) o pagar (tus servicios se activan automáticamente, pero ya empieza a cobrarte a tu tarjeta). En pocas palabras, no hay nada qué temer, mientras no actives el pago de la cuenta, no habrá cargo alguno :-) ¡Si tienes dudas, con gusto te puedo ayudar!

Una vez que tienes tu cuenta de Azure, vamos a comenzar con el ejercicio real.

Vamos a crear un backend con Azure Mobile Services y Easy Tables. Para ello, primero entra al portal de Azure: portal.azure.com y selecciona New - Web + Mobile - Mobile App:


Esto va a generar un App Service (un espacio en la nube dedicado para apps webs y móviles que te permiten almacenar datos), pero primero tienes que configurarlo. Comienza con el nombre de tu app. Tiene que ser único (en mi caso, he colocado canciones-appservice). Crea un Resource group, que te permitirá almacenar varios servicios bajo un solo nombre (es más fácil administrarlos posteriormente). En mi caso, he colocado canciones-resourcegroup. Finalmente, crea un nuevo App Service plan, que es un alias para agrupar servicios en base a su localización. En nombre he colocado europa-plan y en ubicación selecciona el centro de datos más cercano a tí (Para México, recomiendo South Central US). El esquema de precio lo puedes dejar como está (D1/Shared) puesto que es el más económico (9.67 USD por mes). Da clic en OK y luego en Create.


Espera un poco en lo que se genera el AppService. Recibirás una notificación que te indicará el éxito de la operación.


Vamos a configurar el servicio que acabas de crear. Accede a él desde All resources y da clic (elige el que dice App Service, porque hay otro de tipo Application Insights con el mismo nombre, pero no lo utilizaremos de momento).


Copia la URL del servicio y pégala en el bloc de notas (la utilizaremos más adelante).


En el blade Settings que se genera del lado derecho, localiza la categoría Mobile. Hay 2 elementos que configuraremos: Data Connections y Easy Tables.


Comenzaremos con Data connections. Da clic en esta opción y luego en el botón Add para agregar una nueva conexión a una base de datos donde almacenaremos la información. El tipo de almacenamiento será SQL Database, para lo cual tienes que configurar parámetros requeridos.


Elige crear una nueva base de datos y coloca un nombre único (en mi caso es canciones-db). En el esquema de precio te recomiendo que selecciones el Plan Basico (4.99 USD mensuales).


Ahora configura el Servidor. Da clic en Server y escribe un nombre único (en mi caso es canciones-server). Escribe un nombre de usuario y su contraseña (con ellos podrás acceder a tu base de datos desde SQL Server Management Studio, por ejemplo). En ubicación selecciona el centro de datos más cercano a tí (South Central US para los de México). También selecciona la opción Permitir que servicios de Azure accedan al servidor. Finalmente da clic en OK en cada uno de los blades que han sido abiertos hasta llegar a Data Connections nuevamente.


Espera unos minutos en lo que se la conexión a datos. Recibirás una notificación nuevamente cuando esto suceda.



Ahora configuremos los Easy Tables, que es una forma simplificada de administrar una tabla de base de datos desde nuestras aplicaciones móviles. En realidad es muy sencillo. Comienza seleccionando Easy tables desde el blade Settings. Ahora, espera un poco y da clic en el botón azul que indica que es necesario configurar Easy Tables. El paso 1 que aparece ya está realizado, dado que ya configuraste una conexión a datos. Para el paso 2 simplemente checa la opción de Inicializar el App Service y da clic en el botón respectivo.


Nuevamente espera unos minutos en lo que se inicializa el App Service. Una notificación te indicará cuando el backend haya sido inicializado.


En la sección Easy Tables da clic en el botón Add. Crea una tabla (en mi caso, el nombre es Canciones). Dado que no implementaremos autenticación, los permisos de las operaciones CRUD se pueden dejar como están (acceso anónimo). Da clic en OK


Si accedes a la tabla Canciones, observarás que ya cuenta con una estructura predeterminada con campos como ID, Version, Deleted, entre otros. Dejaremos estos campos así como están y lo mejor de todo es que no tenemos que agregar los nuestros desde aquí. Lo haremos desde nuestra aplicación móvil.

Ya hemos terminado de configurar el servicio. A continuación vamos con la parte interesante, es decir, el codigo.

Abre Visual Studio o Xamarin Studio. Crea un nuevo proyecto de tipo Xamarin.Forms Shared Project (en mi caso se llama CancionesApp).


Una vez generado el proyecto, agregaremos paquetes Nuget a los proyectos de interés. En este caso, solamente mostraré la implementación de Android, pero bien puedes repetir el paso para UWP o iOS. Da clic derecho en el proyecto Droid y selecciona Administrar paquetes Nuget.

Selecciona Browse (Examinar) y agrega los siguientes paquetes en el orden presentado:

  • Azure Mobile SQLiteStore (version 2.1.0)
  • Newtonsoft.Json (versión 9.0.1)
  • Refractored.MvvmHelpers (versión 1.0.1)
  • Xam.Plugin.Connectivity (versión 2.2.12)




Acepta la licencia en caso de que algún paquete te lo solicite.

NOTA: Una de las librerías, Microsoft.Azure.Mobile.Client, se agrega con la versión 2.0.0, pero al momento de compilar la aplicación tal vez te mande un error. Para resolverlo, sugiero que actualices esta librería a la versión 2.1.0. Simplemente entra de nuevo al Nuget Package Manager y selecciona Microsoft.Azure.Mobile.Client dentro de las librerías instaladas, y actualízala a la última versión:



Ahora vamos al proyecto compartido CancionesApp. Crea 4 carpetas (clic derecho sobre el nombre del proyecto CancionesApp - Agregar nueva carpeta): 

  • Models
  • ViewModels
  • Services
  • Views

En la carpeta Models, crea una nueva clase llamada Canciones (clic derecho sobre la carpeta Models - Agregar nuevo elemento).


En esta clase vamos a modelar nuestra Easy Table, agregando nuestros campos con la información que queremos almacenar. Para este ejemplo, manejaremos 2 propiedades: el nombre de una canción y el nombre del artista que la interpreta. Simplemente hay que agregarlas a la clase. Las propiedades Id y AzureVersion son requeridas para cuestiones de Integridad con la tabla que se está modelando (indicadas por los atributos entre corchetes) El código de esta clase es el siguiente:

using System;
using System.Collections.Generic;
using System.Text;

namespace CancionesApp.Models
{
    public class Canciones
    {
        [Newtonsoft.Json.JsonProperty("Id")]
        public string Id { get; set; }

        [Microsoft.WindowsAzure.MobileServices.Version]
        public string AzureVersion { get; set; }

        public string NombreCancion { get; set; }
        public string NombreArtista { get; set; }
    }
}

Ahora vamos a la carpeta Services para manejar la conexión con la tabla en el Azure AppService, así como las operaciones de agregar y sincronizar. Crea una clase en la carpeta Services llamada AzureDataService con el código siguiente:

using System;
using Microsoft.WindowsAzure.MobileServices;
using Microsoft.WindowsAzure.MobileServices.Sync;
using System.Threading.Tasks;
using System.Collections.Generic;
using Microsoft.WindowsAzure.MobileServices.SQLiteStore;
using Xamarin.Forms;
using CancionesApp.Models;

namespace CancionesApp.Services
{
    public class AzureDataService
    {
        public MobileServiceClient MobileService { get; set; }
        IMobileServiceSyncTable<Canciones> tablaCanciones;

        bool isInitialized;

        public async Task Initialize()
        {
            if (isInitialized)
                return;

            MobileService = new MobileServiceClient("http://canciones-appservice.azurewebsites.net");

            const string path = "syncstore.db";

            var store = new MobileServiceSQLiteStore(path);
            store.DefineTable<Canciones>();
            await MobileService.SyncContext.InitializeAsync(store, new MobileServiceSyncHandler());

            tablaCanciones = MobileService.GetSyncTable<Canciones>();
            isInitialized = true;
        }

        public async Task<IEnumerable<Canciones>> ObtenerCanciones()
        {
            await Initialize();
            await SyncCanciones();
            return await tablaCanciones.OrderBy(c => c.NombreCancion).ToEnumerableAsync();
        }

        public async Task<Canciones> AgregarCancion(string cancion, string artista)
        {
            await Initialize();

            var item = new Canciones
            {
                NombreCancion = cancion,
                NombreArtista = artista
            };

            await tablaCanciones.InsertAsync(item);

            await SyncCanciones();
            return item;
        }

        public async Task SyncCanciones()
        {
            await tablaCanciones.PullAsync("Canciones", tablaCanciones.CreateQuery());
            await MobileService.SyncContext.PushAsync();
        }
    }
}

NOTA: En la línea 24 debes reemplazar la URL por la de tu AppService. Anteriormente, la copiamos y pegamos en el bloc de notas :-)

En el código anterior, estás creando un cliente (MobileService) que administrará las peticiones con nuestro AppService, mientras que tablaCanciones es nuestra referencia a la EasyTable que está dentro del servicio. El método Initialize realiza funciones de creación de la tabla e inicia un contexto de sincronización. Después, el método ObtenerCanciones retorna la tabla ordenada por canción, mientras que el método AgregarCanción inserta un nuevo registro en la tabla. En ambos métodos, se llama al método SyncCanciones, que sincroniza el almacenamiento local con el almacenamiento en la nube.

A continuación, agrega una nueva clase llamada CancionesViewModel dentro de la carpeta ViewModels. El código es el siguiente:

using System;
using MvvmHelpers;
using System.Windows.Input;
using System.Threading.Tasks;
using System.Diagnostics;
using Xamarin.Forms;
using System.Linq;
using Microsoft.WindowsAzure.MobileServices;
using CancionesApp.Services;
using CancionesApp.Models;

namespace CancionesApp.ViewModels
{
    public class CancionesViewModel : BaseViewModel
    {
        AzureDataService azureService;

        public CancionesViewModel()
        {
            azureService = new AzureDataService();
        }

        public ObservableRangeCollection<Canciones> Canciones { get; } = new ObservableRangeCollection<Canciones>();
        public ObservableRangeCollection<Grouping<string, Canciones>> CancionesAgrupadas { get; } = new ObservableRangeCollection<Grouping<string, Canciones>>();

        string loadingMessage;
        public string LoadingMessage
        {
            get { return loadingMessage; }
            set { SetProperty(ref loadingMessage, value); }
        }

        ICommand cargarCancionesCommand;
        public ICommand CargarCancionesCommand =>
            cargarCancionesCommand ?? (cargarCancionesCommand = new Command(async () => await EjecutarCargarCancionesCommand()));

        async Task EjecutarCargarCancionesCommand()
        {
            if (IsBusy)
                return;

            try
            {
                await azureService.Initialize();

                LoadingMessage = "Cargando canciones...";
                IsBusy = true;
                var canciones = await azureService.ObtenerCanciones();
                Canciones.ReplaceRange(canciones);

                OrdenarCanciones();
            }
            catch (Exception ex)
            {
                Debug.WriteLine("No se pudo sincronizar la tabla, tal vez estás offline!" + ex);
            }
            finally
            {
                IsBusy = false;
            }
        }

        void OrdenarCanciones()
        {
            var groups = from cancion in Canciones
                         orderby cancion.NombreCancion ascending
                         group cancion by cancion.NombreArtista
                into grupoCancion
                         select new Grouping<string, Canciones>($"{grupoCancion.Key} ({grupoCancion.Count()})", grupoCancion);


            CancionesAgrupadas.ReplaceRange(groups);
        }

        string cancion;
        public string Cancion
        {
            get { return cancion; }
            set { SetProperty(ref cancion, value); }
        }

        string artista;
        public string Artista
        {
            get { return artista; }
            set { SetProperty(ref artista, value); }
        }

        ICommand agregarCancionCommand;
        public ICommand AgregarCancionCommand =>
            agregarCancionCommand ?? (agregarCancionCommand = new Command(async () => await EjecutarAgregarCancionCommand()));

        async Task EjecutarAgregarCancionCommand()
        {
            if (IsBusy)
                return;

            try
            {
                await azureService.Initialize();

                LoadingMessage = "Agregando cancion...";
                IsBusy = true;

                var canciones = await azureService.ObtenerCanciones();
                Canciones.ReplaceRange(canciones);

                OrdenarCanciones();

                var item = await azureService.AgregarCancion(cancion, artista);
                Canciones.Add(item);
                OrdenarCanciones();
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex);
            }
            finally
            {
                IsBusy = false;
            }
        }
    }
}

Básicamente este archivo es la conexión entre nuestra futura vista y el modelo que ya tenemos creado. Mediante el objeto azureService (de la clase AzureDataService) se manejan todas las peticiones. Se tienen 2 colecciones: Canciones (todas las canciones de la tabla) y CancionesAgrupadas (las canciones se agrupan por artista y es la colección que se visualizará en una lista). Como todo ViewModel, se tienen comandos y propiedades. Las propiedades LoadingMessage e IsBusy permitirán al usuario conocer el estado de la aplicación (con un mensaje de que los datos se están cargando y con un ActivityIndicator, respectivamente). Con respecto a los comandos, CargarCancionesCommand llama al método ObtenerCanciones (del azureService) para mostrar la lista de canciones de la tabla, mientras que AgregarCancionesCommand inserta un nuevo registro en la tabla mediante una llamada al metodo AgregarCancion (del azureService) haciendo uso de las propiedades Cancion y Artista, que se enlazarán a 2 cajas de texto en la vista.

Finalmente, la vista: En la carpeta Views agrega un Forms Xaml Page llamado PaginaCancion.


Esta página tiene 2 códigos, el de la vista de diseño (XAML) y el del code-behind (C#). Comenzamos con el diseño de la página. Este código va entre las etiquetas <Contentpage> </Contentpage>:

<AbsoluteLayout HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
    <Grid AbsoluteLayout.LayoutFlags="All"
          AbsoluteLayout.LayoutBounds="0,0,1,1"
          RowSpacing="0">
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="Auto"/>
      </Grid.RowDefinitions>
      <StackLayout Orientation="Horizontal" Padding="16" Spacing="16" BackgroundColor="#F2C500">
        <StackLayout Orientation="Vertical">
          <Entry Text="{Binding Cancion}" Placeholder="Cancion" WidthRequest="100"/>
          <Entry Text="{Binding Artista}" Placeholder="Artista" WidthRequest="100"/>
        </StackLayout>
        <Button Text="Agregar Cancion"
                Command="{Binding AgregarCancionCommand}"
                VerticalOptions="Center"
                TextColor="White"
                BackgroundColor="#F2C500"/>
      </StackLayout>
      <ListView
          Grid.Row="1"
          GroupDisplayBinding="{Binding Key}"
          IsGroupingEnabled="true"
          HasUnevenRows ="true"
          ItemsSource="{Binding CancionesAgrupadas}"
          IsPullToRefreshEnabled="true"
          IsRefreshing="{Binding IsBusy, Mode=OneWay}"
          RefreshCommand="{Binding CargarCancionesCommand}"
          x:Name="ListViewCanciones">
        <ListView.ItemTemplate>
          <DataTemplate>
            <ViewCell>
              <Grid Padding="16,12">
                <Grid.ColumnDefinitions>
                  <ColumnDefinition Width="Auto"/>
                  <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
                <Label Text="{Binding NombreCancion}" FontSize="16" Style="{DynamicResource ListItemTextStyle}" VerticalTextAlignment="Center"/>
                <Label Grid.Column="1" HorizontalOptions="End" FontSize="16" Text="{Binding NombreArtista}" Style="{DynamicResource ListItemTextStyle}" TextColor="#979797" VerticalTextAlignment="Center"/>
              </Grid>
            </ViewCell>
          </DataTemplate>
        </ListView.ItemTemplate>
      </ListView>
      <StackLayout Grid.Row="2" x:Name="OfflineStack" Padding="8" IsVisible="false" BackgroundColor="#F2C500">
        <Label TextColor="White" Text="No Connection - Offline Mode" HorizontalOptions="Center" VerticalOptions="Center"/>
      </StackLayout>
    </Grid>
    <StackLayout IsVisible="{Binding IsBusy}" Padding="32"
                 AbsoluteLayout.LayoutFlags="PositionProportional"
                 AbsoluteLayout.LayoutBounds="0.5,0.5,-1,-1"
                 BackgroundColor="#90000000">
      <ActivityIndicator IsRunning="{Binding IsBusy}" Color="#3CB9A8"/>
      <Label Text="{Binding LoadingMessage}" HorizontalOptions="Center" TextColor = "White" Style="{DynamicResource ListItemTextStyle}"/>
    </StackLayout>
  </AbsoluteLayout>


En el code-behind de PaginaCancion, coloca el siguiente código:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using Xamarin.Forms;
using CancionesApp.ViewModels;
using Plugin.Connectivity;

namespace CancionesApp.Views
{
 public partial class PaginaCancion : ContentPage
 {
        CancionesViewModel vm;

        public PaginaCancion()
        {
            InitializeComponent();

            BindingContext = vm = new CancionesViewModel();

            ListViewCanciones.ItemTapped += (sender, e) =>
            {
                if (Device.OS == TargetPlatform.iOS || Device.OS == TargetPlatform.Android)
                    ListViewCanciones.SelectedItem = null;
            };

            if (Device.OS != TargetPlatform.iOS && Device.OS != TargetPlatform.Android)
            {
                ToolbarItems.Add(new ToolbarItem
                {
                    Text = "Refresh",
                    Command = vm.CargarCancionesCommand
                });
            }
        }

        protected override void OnAppearing()
        {
            base.OnAppearing();

            CrossConnectivity.Current.ConnectivityChanged += ConnecitvityChanged;
            OfflineStack.IsVisible = !CrossConnectivity.Current.IsConnected;
            if (vm.Canciones.Count == 0)
                vm.CargarCancionesCommand.Execute(null);
        }

        protected override void OnDisappearing()
        {
            base.OnDisappearing();
            CrossConnectivity.Current.ConnectivityChanged -= ConnecitvityChanged;
        }

        void ConnecitvityChanged(object sender, Plugin.Connectivity.Abstractions.ConnectivityChangedEventArgs e)
        {
            Device.BeginInvokeOnMainThread(() =>
            {
                OfflineStack.IsVisible = !e.IsConnected;
            });
        }

 }
}

El código anterior hace el binding de la vista con el ViewModel que creamos (CancionesViewModel) para realizar las diferentes operaciones, tales como cargar las canciones, agregarlas a la base de datos, etc. También implementa un plugin de conectividad que nos dice si estamos conectados a Internet o no.

Por último (¡por fin!), modifica 2 archivos. Primero, App.cs, para que la página principal sea la que acabamos de crear:

public App ()
{
  // The root page of your application
  MainPage = new NavigationPage(new CancionesApp.Views.PaginaCancion());
}

Ahora ve al archivo MainActivity que está en el proyecto de Android (CancionesApp.Droid). Simplemente tienes que agregar una llamada al método Init de los servicios móviles de Azure dentro del método OnCreate:

protected override void OnCreate (Bundle bundle)
{
 base.OnCreate (bundle);
 global::Xamarin.Forms.Forms.Init (this, bundle);
 Microsoft.WindowsAzure.MobileServices.CurrentPlatform.Init();
 LoadApplication (new CancionesApp.App ());
}

¡Listo! Compila y ejecuta tu aplicación. Si todo sale según lo planeado, primero verás esto en tu aplicación:


¡En estos momentos ya se han agregado los campos a tu tabla en la nube! Pero primero, agreguemos unas tres canciones:


Y si regresas al panel de Azure y accedes a la tabla Canciones, verás esto:


NOTA: Si en la app utilizas el emulador y te indica que estás offline (aunque tu computadora tenga Internet), conecta tu computadora a Internet mediante WiFi y reinicia el emulador.

¡Felicidades! El backend de Azure ha sido integrado exitosamente en tu aplicación móvil de Xamarin. Ahora, para que tu esfuerzo se vea recompensado con tu playera, no olvides hacer un video (en inglés), te recomiendo lo siguiente:


  • Muestra tu aplicación corriendo (en el emulador o dispositivo físico). Menciona tu nombre e inserta varios datos.
  • Accede al portal de Azure con la Easy Table que muestre los datos que acabas de agregar
  • De ser posible, también muestra Visual Studio para que se vea que codificaste la aplicación


Este video lo tienes que tweetear a @xamarinhq con los hashtags #Xamarin y #AzureMobile.

EDIT: Aqui puedes ver mi tweet de participación con el video, por si te sirve de ayuda :-)


Si esta entrada fue útil para tí, deja un comentario :-) Si tienes alguna pregunta, con mucho gusto te puedo ayudar. Recuerda que el límite es el Lunes 25 a las 12 pm EST (aproximadamente a las 11 am hora de México)

¡Gracias por tu atención y éxito!

4 comentarios:

  1. Genial muchas gracias, son casi las 9 pero llegare a terminar en la madrugada, solo una duda como sabe estoy en México como seria el envío de mi playerita en el tweet me pedirán mis datos?

    ResponderEliminar
    Respuestas
    1. Qué tal Armando. Mucho éxito con la app, cualquier duda me comentas :-) Después de unos días te mandan un mensaje directo los de Xamarin solicitando tu dirección para lo del envío.

      Eliminar
  2. Genial muchas gracias, son casi las 9 pero llegare a terminar en la madrugada, solo una duda como sabe estoy en México como seria el envío de mi playerita en el tweet me pedirán mis datos?

    ResponderEliminar
  3. Que tal Amigo muy buen tutorial! solo me sale un error en la parte de MainActivity cuando pongo la linea Microsoft.WindowsAzure.MobileServices.CurrentPlatform.Init();
    me sale el siguiente error:

    'CurrentPlatform' no es accesible debido a su nivel de protección
    Me podrías ayudar a solucionarlo

    ResponderEliminar