lunes, 27 de marzo de 2017

Validación de datos usando Behaviors en Xamarin.Forms

¡Hola! Hace unos días me escribieron al correo preguntando sobre cómo validar mediante Behaviors la entrada de datos del usuario, por ejemplo, que sólo haya números en un Entry; también había duda sobre cómo limitar la cantidad de caracteres en el Entry, por ejemplo, que solo acepte 10 caracteres.

Investigando, encontré el siguiente enlace, en el cual está basado este post con ciertas modificaciones que voy a mencionar. La idea de esta publicación es validar las entradas de datos que el usuario realiza típicamente en un formulario. En la siguiente publicación veremos cómo validar correos electrónicos, fechas, números telefónicos e incluso verificar la relación entre dos controles (verificando que el texto introducido en dos Entrys sean iguales, por ejemplo). Para ello, utilizaremos Behaviors, que son elementos que permiten añadir funcionalidad a nuestros controles y que los podemos reutilizar, lo cual ayuda a tener un código más compacto y disponible donde lo necesitemos. También haremos uso de otras técnicas, por ejemplo expresiones regulares.

Básicamente, lo que hay que hacer es declarar una clase que herede de la clase Behavior, donde T es un control (por ejemplo, un Entry, un DatePicker, etc). Esta clase puede contener una BindableProperty, es decir, un elemento que se desee enlazar al control para interactuar con él desde el código XAML. Lo que si debe realizarse es hacer un override de 2 métodos: OnAttachedTo y OnDetachingFrom, en los cuales nos suscribimos y desuscribimos respectivamente a los manejadores de evento que vamos a considerar para el control. También se pueden hacer operaciones de inicialización y finalización del control. Es importante sobre todo remover las referencias a manejadores de eventos que no vamos a manejar más (mediante OnDetachingFrom) por cuestiones de manejo de memoria en nuestras aplicaciones móviles. Finalmente, otro código opcional (pero casi seguro que lo agregamos) pues es precisamente el de los manejadores de evento agregados desde OnAttachedTo.


public class MiClase : Behavior<Control>
{
    public static readonly BindableProperty MiPropiedad = BindableProperty.Create("Propiedad", typeof(tipo), typeof(MiClase), valorInicial);

    public int Propiedad
    {
        get { return (tipo)GetValue(MiPropiedad); }
        set { SetValue(MiPropiedad, value); }
    }

    protected override void OnAttachedTo(Control c)
    {
    }

    protected override void OnDetachingFrom(Control c)
    {
    }
}





Vamos a ver el código específico ¡Manos a la obra!

Paso 1. Crea un proyecto de tipo Xamarin.Forms Portable. Para este caso, he puesto el nombre Validaciones.


Paso 2. En el proyecto compartido, crea una carpeta llamada Behaviors, en la cual agregarás varias clases, las cuales describiremos a continuación:


EmailValidatorBehavior.cs: Esta clase sirve para validar que el correo electrónico introducido en un Entry sea válido. En este caso, utilizo una expresión regular que cubre la mayoría de casos válidos de un correo electrónico (tal vez se me escape alguno). La idea es utilizar el evento TextChanged del Entry. Cada vez que el usuario introduzca un caracter, se valida la cadena de texto completa. En el momento en que el patrón de correo electrónico se cumpla, el color cambiará a verde; mientras que no se cumpla esa condición, será rojo. Cuando ya no se necesite más el control (por ejemplo, que se navegue a otra página), se desuscribe el evento. El código es el siguiente:


using System;
using System.Text.RegularExpressions;
using Xamarin.Forms;

namespace Validaciones.Behaviors
{
    public class EmailValidatorBehavior : Behavior<Entry>
    {
        const string emailRegex = @"^[\w!#$%&'*+\-/=?\^_`{|}~]+(\.[\w!#$%&'*+\-/=?\^_`{|}~]+)*" + "@" + @"((([\-\w]+\.)+[a-zA-Z]{2,4})|(([0-9]{1,3}\.){3}[0-9]{1,3}))$";

        protected override void OnAttachedTo(Entry entry)
        {
            entry.TextChanged += TextChanged;
            base.OnAttachedTo(entry);
        }

        // Valida si el texto introducido es un correo electrónico
        void TextChanged(object sender, TextChangedEventArgs e)
        {
            bool valido = (Regex.IsMatch(e.NewTextValue, emailRegex, RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(250)));
            ((Entry)sender).TextColor = valido ? Color.Green : Color.Red;
        }

        protected override void OnDetachingFrom(Entry entry)
        {
            entry.TextChanged -= TextChanged;
            base.OnDetachingFrom(entry);
        }
    }
}

FechaValidatorBehavior.cs: Esta clase sirve para validar que la fecha seleccionada en un DatePicker no sea menor a 100 años máximo (es solo un ejemplo, tú puedes modificar el valor o la condición, por ejemplo que esté entre el año 1980 y 2000) y su código es el siguiente:


using System;
using Xamarin.Forms;

namespace Validaciones.Behaviors
{
    public class FechaValidatorBehavior : Behavior<DatePicker>
    {
        protected override void OnAttachedTo(DatePicker dp)
        {
            dp.DateSelected += DateSelected;
            base.OnAttachedTo(dp);
        }

        // Valida una fecha menor al año actual en 100 años máximo
        private void DateSelected(object sender, DateChangedEventArgs e)
        {
            int resultado = DateTime.Now.Year - e.NewDate.Year;
            bool valido = (resultado <= 100 && resultado >= 0);
            ((DatePicker)sender).BackgroundColor = valido ? Color.Green : Color.Red;
        }

        protected override void OnDetachingFrom(DatePicker dp)
        {
            dp.DateSelected -= DateSelected;
            base.OnDetachingFrom(dp);
        }
    }
}

MaxLengthValidatorBehavior.cs: Esta clase sirve para limitar la cantidad de caracteres aceptados en un Entry. Cabe mencionar que todos los ejemplos que encontré utilizan TextChanged y funcionan en Android, pero no en UWP. La única forma de que funcionara en ambos fue suscribiendo en su lugar el evento Unfocused. La ventaja de usar TextChanged es que no te dejará introducir más de N caracteres (es decir, cuando tengas 10 caracteres, todo lo demás que presiones no se agregará a la caja de texto), mientras que para Unfocused necesitas hacer Tap en otra parte del formulario (es decir, que el Entry pierda el foco) para que toda la cadena sea validada y se remuevan los caracteres adicionales. Por un lado, lo ideal sería que funcionara como TextChanged, pero no encontré cómo hacerlo funcionar en UWP, pues siempre dejaba introducir más de 10 caracteres. En fin, con Unfocused funcionó, jeje. El código es el siguiente:


using Xamarin.Forms;

namespace Validaciones.Behaviors
{
    public class MaxLengthValidatorBehavior : Behavior<Entry>
    {
        public static readonly BindableProperty MaxLengthProperty = BindableProperty.Create("MaxLength", typeof(int), typeof(MaxLengthValidatorBehavior), 0);

        public int MaxLength
        {
            get { return (int)GetValue(MaxLengthProperty); }
            set { SetValue(MaxLengthProperty, value); }
        }

        protected override void OnAttachedTo(Entry entry)
        {
            entry.Unfocused += Unfocused;
            base.OnAttachedTo(entry);
        }

        // Devuelve una cadena de máximo una longitud permitida
        private void Unfocused(object sender, FocusEventArgs e)
        {
            Entry entry = (Entry)sender;
            entry.Text = entry.Text.Substring(0, MaxLength);
        }

        protected override void OnDetachingFrom(Entry entry)
        {
            entry.Unfocused -= Unfocused;
            base.OnDetachingFrom(entry);
        }
    }
}

NumeroValidatorBehavior.cs: Esta clase sirve para validar que el Entry solo contenga dígitos, y su código es el siguiente, apoyado en una expresión regular:


using System;
using System.Text.RegularExpressions;
using Xamarin.Forms;

namespace Validaciones.Behaviors
{
    public class NumeroValidatorBehavior : Behavior<Entry>
    {
        const string digitosRegEx = @"^[0-9]+$";

        protected override void OnAttachedTo(Entry entry)
        {
            entry.TextChanged += TextChanged;
            base.OnAttachedTo(entry);
        }

        // Solo dígitos
        void TextChanged(object sender, TextChangedEventArgs e)
        {
            bool valido = (Regex.IsMatch(e.NewTextValue, digitosRegEx, RegexOptions.IgnoreCase, TimeSpan.FromMilliseconds(250)));
            ((Entry)sender).TextColor = valido ? Color.Green : Color.Red;
        }

        protected override void OnDetachingFrom(Entry entry)
        {
            entry.TextChanged -= TextChanged;
            base.OnDetachingFrom(entry);
        }
    }
}

PasswordValidatorBehavior.cs: Esta clase sirve para validar que el contenido de un Entry cumpla ciertas reglas. Para este caso específico, yo quiero que contenga 10 caracteres, de los cuales al menos exista una mayúscula, una minúscula, un dígito y un caracter especial (por ejemplo, P@$$w0rd123 es una cadena válida, mientras que password123 no lo es). Su código es el siguiente:


using System.Text.RegularExpressions;
using Xamarin.Forms;

namespace Validaciones.Behaviors
{
    public class PasswordValidatorBehavior : Behavior<Entry>
    {
        const string passwordRegex = @"^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{10,}$";

        protected override void OnAttachedTo(Entry entry)
        {
            entry.TextChanged += TextChanged;
            base.OnAttachedTo(entry);
        }

        //10 caracteres, con al menos: 1 digito, 1 minúscula, 1 mayúscula, 1 caracter especial
        void TextChanged(object sender, TextChangedEventArgs e)
        {
            bool valido = (Regex.IsMatch(e.NewTextValue, passwordRegex));
            ((Entry)sender).TextColor = valido ? Color.Green : Color.Red;
        }

        protected override void OnDetachingFrom(Entry entry)
        {
            entry.TextChanged -= TextChanged;
            base.OnDetachingFrom(entry);
        }
    }
}

CompareTextsValidatorBehavior.cs: Esta clase es la primera que utilizo para relacionar dos controles y es muy sencilla de entender. Básicamente, sirve para validar que el texto introducido en dos Entry sea el mismo, por ejemplo cuando queremos confirmar una contraseña. En este caso, se utilizan BindableProperty que vamos a asignar desde el XAML. Solo se requiere la propiedad de Text (el texto del otro control) pero a mi me interesa una referencia cruzada, es decir, una referencia a otro control (Ent) porque quiero que los dos controles aparezcan del mismo color dependiendo la validación. Es decir, si no pusiera esta referencia, cuando la cadena se modifique, solo un control cambiará su color, generando inconsistencias. Su código es el siguiente:


using Xamarin.Forms;

namespace Validaciones.Behaviors
{
    public class CompareTextsValidatorBehavior : Behavior<Entry>
    {
        public static BindableProperty TextProperty = BindableProperty.Create<CompareTextsValidatorBehavior, string>(tc => tc.Text, string.Empty, BindingMode.TwoWay);

        public string Text
        {
            get { return (string)GetValue(TextProperty); }
            set { SetValue(TextProperty, value); }
        }

        public static BindableProperty EntProperty = BindableProperty.Create<CompareTextsValidatorBehavior, Entry>(tc => tc.Ent, null, BindingMode.TwoWay);

        public Entry Ent
        {
            get { return (Entry)GetValue(EntProperty); }
            set { SetValue(EntProperty, value); }
        }

        protected override void OnAttachedTo(Entry entry)
        {
            entry.TextChanged += TextChanged;
            base.OnAttachedTo(entry);
        }

        // Compara que el texto de este control sea el mismo que el de otro control
        void TextChanged(object sender, TextChangedEventArgs e)
        {
            bool valido = (e.NewTextValue == Text);
            ((Ent == null) ? (Entry)sender : Ent).TextColor = valido ? Color.Green : Color.Red;
        }

        protected override void OnDetachingFrom(Entry entry)
        {
            entry.TextChanged -= TextChanged;
            base.OnDetachingFrom(entry);
        }
    }
}

CompareDatesValidatorBehavior.cs: Por último, esta clase sirve para validar que la fecha seleccionada en un control DatePicker sea menor a la fecha de otro DatePicker. Al igual que en el caso anterior, solo se requiere una propiedad (Date), pero yo agrego otra propiedad (DatePick) para tener la referencia al otro control, y poder modificar su color si la fecha de cualquiera de los dos cambia y evitar inconsistencias. Su código es el siguiente:


using System;
using Xamarin.Forms;

namespace Validaciones.Behaviors
{
    public class CompareDatesValidatorBehavior : Behavior<DatePicker>
    {
        public static BindableProperty DateProperty = BindableProperty.Create<CompareDatesValidatorBehavior, DateTime>(dt => dt.Date, DateTime.Now, BindingMode.TwoWay);

        public DateTime Date
        {
            get { return (DateTime)GetValue(DateProperty); }
            set { SetValue(DateProperty, value); }
        }

        public static BindableProperty OrderProperty = BindableProperty.Create<CompareDatesValidatorBehavior, int>(dt => dt.Order, 0, BindingMode.TwoWay);

        public int Order
        {
            get { return (int)GetValue(OrderProperty); }
            set { SetValue(OrderProperty, value); }
        }

        public static BindableProperty DatePickProperty = BindableProperty.Create<CompareDatesValidatorBehavior, DatePicker>(dt => dt.DatePick, null, BindingMode.TwoWay);

        public DatePicker DatePick
        {
            get { return (DatePicker)GetValue(DatePickProperty); }
            set { SetValue(DatePickProperty, value); }
        }

        protected override void OnAttachedTo(DatePicker dp)
        {
            dp.DateSelected += DateSelected;
            base.OnAttachedTo(dp);
        }

        // Compara que la fecha seleccionada sea mayor a la de otro control
        private void DateSelected(object sender, DateChangedEventArgs e)
        {
            bool valido = (Order == 1) ? Date > e.NewDate : (e.NewDate > Date);
            ((DatePicker)sender).BackgroundColor = valido ? Color.Green : Color.Red;
            DatePick.BackgroundColor = valido ? Color.Green : Color.Red;
        }

        protected override void OnDetachingFrom(DatePicker dp)
        {
            dp.DateSelected -= DateSelected;
            base.OnDetachingFrom(dp);
        }
    }
}

Paso 3. Lo que sigue es crear una página a manera de formulario donde enlacemos varios controles a los Behaviors creados. Agregamos una carpeta llamada Paginas y dentro, una Forms Xaml Page llamada Formulario.


El código XAML de esta página se muestra a continuación. Básicamente es un formulario con varios controles Entry, Label y DatePicker, pero observa que algunos de ellos definen uno o varios Behavior. Debes agregar el espacio de nombre que referencia la carpeta Behaviors (lo identificamos como local en el XAML) para que puedan ser localizados.



<?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="Validaciones.Paginas.Formulario"
             xmlns:local="clr-namespace:Validaciones.Behaviors">
  <StackLayout BackgroundColor="White">
    <Label HorizontalTextAlignment="Center" Text="Formulario" FontAttributes="Bold" TextColor="Blue" />

    <Label Text="Nombre: " TextColor="Blue"/>
    <Entry x:Name="txtNombre" Placeholder="Ingresa tu nombre" TextColor="Black"/>
  
    <Label Text="Telefono: " TextColor="Blue"/>
    <Entry x:Name="txtTelefono" Placeholder="Ingresa tu telefono (máximo 10 dígitos)" TextColor="Black" Keyboard="Numeric" >
      <Entry.Behaviors>  
        <local:MaxLengthValidatorBehavior  MaxLength="10"/>
        <local:NumeroValidatorBehavior/>
      </Entry.Behaviors>  
    </Entry>  

    <Label Text="Fecha de nacimiento: " TextColor="Blue"/>
    <DatePicker x:Name="dtpFechaNacimiento">
      <DatePicker.Behaviors>
        <local:FechaValidatorBehavior/>  
      </DatePicker.Behaviors>
    </DatePicker>
    
    <Label Text="Correo: " TextColor="Blue"/>
    <Entry x:Name="txtCorreo" Placeholder="Ingresa tu correo" TextColor="Black" >
      <Entry.Behaviors>  
        <local:EmailValidatorBehavior/>
      </Entry.Behaviors>  
    </Entry>  

    <Label Text="Password: " TextColor="Blue"/>
    <Entry x:Name="txtPassword" IsPassword="True" Placeholder="Ingresa tu contraseña">
      <Entry.Behaviors>  
        <local:PasswordValidatorBehavior />  
        <local:CompareTextsValidatorBehavior BindingContext="{x:Reference txtConfirmaPassword}" Text="{Binding Text}" Ent="{x:Reference txtConfirmaPassword}"/>
      </Entry.Behaviors>
    </Entry>
  
    <Label Text="Repite tu password: " TextColor="Blue"/>
    <Entry x:Name="txtConfirmaPassword" IsPassword="True" Placeholder="Repite tu contraseña">
      <Entry.Behaviors>
        <local:PasswordValidatorBehavior />  
        <local:CompareTextsValidatorBehavior BindingContext="{x:Reference txtPassword}" Text="{Binding Text}"/>
      </Entry.Behaviors>
    </Entry>

    <Label Text="Fecha de inicio:" TextColor="Blue"/>
    <DatePicker x:Name="dtpFechaInicio">
      <DatePicker.Behaviors>
        <local:CompareDatesValidatorBehavior BindingContext="{x:Reference dtpFechaFin}" Date="{Binding Date}" Order="1" DatePick="{x:Reference dtpFechaFin}" />
      </DatePicker.Behaviors>
    </DatePicker>

    <Label Text="Fecha de fin: " TextColor="Blue"/>
    <DatePicker x:Name="dtpFechaFin">
      <DatePicker.Behaviors>
        <local:CompareDatesValidatorBehavior BindingContext="{x:Reference dtpFechaInicio}" Date="{Binding Date}" Order="2" DatePick="{x:Reference dtpFechaInicio}" />
      </DatePicker.Behaviors>
    </DatePicker>
  </StackLayout>
</ContentPage>


Paso 4. ¡Listo! Ya solo tienes que indicar en tu archivo App.cs del proyecto de Xamarin.Forms que inicie la página Formulario para mostrar la página mediante el código siguiente:

public App()
{
     MainPage = new Paginas.Formulario();
}

Ahora solo resta ejecutar la aplicación. En primer lugar, observa cómo se visualiza en un teléfono con Windows 10:

  • El teléfono está limitado a 10 caracteres.
  • La fecha de nacimiento, como no es menor a la fecha actual, se pone en rojo.
  • El correo está en formato correcto.
  • La contraseña utilizada es password123, es decir, es inválida y por eso aparece en rojo.
  • Aunque la confirmación es la misma cadena, la contraseña no obedece al formato y por eso aparece en rojo.
  • La fecha de inicio es menor a la fecha de fin, por eso están en verde.

Ahora hagamos una prueba en un dispositivo Android:



  • El teléfono tiene un punto, es decir, un caracter que no es dígito, por tanto aparece en rojo.
  • La fecha de nacimiento es válida (menor a la fecha actual)
  • El correo electrónico no es válido
  • El password introducido es P@$$w0rd123, por tanto es válido.
  • La confirmación es la misma cadena, por tanto, válida
  • La fecha de fin es menor a la fecha de inicio, por tanto son no válidas.

¡Listo! Como puedes ver, no es complicado utilizar Behaviors para añadir funcionalidad de validación a tus controles. Lo mejor de todo es que puedes reutilizarlos (simplemente llamándolos en el XAML) en otras páginas. Puedes crear tantos como gustes y un mismo control puede tener más de un Behavior enlazado (por ejemplo, el Entry del teléfono tiene los Behaviors MaxLengthValidatorBehavior y NumeroValidatorBehavior). Así mismo, la condición de validación tú mismo la defines.

El código fuente de este proyecto está disponible en mi GitHub

Ahora que has entendido cómo hacerlo y si quieres manejar algo más avanzado, por ejemplo que no se modifique el color de fondo sino que se muestre un mensaje de error para el usuario o hacer una interfaz de usuario más atractiva a la vista, te sugiero revisar este enlace del blog oficial de Xamarin, el cual también implementa Behaviors.

Espero que esta entrada haya sido de tu interés. Compártela con tus amigos si así fue, y espero te sea útil en tus desarrollos.

¡Saludos!

7 comentarios:

  1. Muy buen aporte. Muy claro y muy útil a la hora de desarrollar nuestras app..
    Muchas gracias por compartir!!

    ResponderEliminar
    Respuestas
    1. Gracias, que bueno que te haya gustado la entrada. Seguiré compartiendo mis aportes, saludos!

      Eliminar
  2. Gracias Luis excelente información, saludos!!

    ResponderEliminar
    Respuestas
    1. Gracias por leer la publicación, espero te sea de utiliadd :) Saludos!

      Eliminar
  3. Buen tutorial Luis. He seguido tu ejemplo, pero en el xaml del formulario me aparece el siguiente error:

    No se puede convertir un objeto de tipo 'Xamarin.Forms.Xaml.ListNode' al tipo 'Xamarin.Forms.Xaml.IElementNode'.

    ResponderEliminar
  4. Muy buen articulo,que me recomiendan si deseo enviar notificaciones a usuarios de mi app, por ejemplo avisarle a x usuario que su compra ha sido enviada.

    ResponderEliminar
  5. Justo lo que buscaba estimado, feliz tarde . Excelente artivulo amigo.

    ResponderEliminar