Alessandro Del Sole's Blog

{ A programming space about Microsoft® .NET® }
posts - 1909, comments - 2047, trackbacks - 352

My Links

News

Your host

This is me! Questo spazio è dedicato a Microsoft® .NET®, di cui sono molto appassionato :-)

Cookie e Privacy

Disabilita cookie ShinyStat

Microsoft MVP

My MVP Profile

Microsoft Certified Professional

Microsoft Specialist

Xamarin Certified Mobile Developer

Il mio libro su VB 2015!

Pre-ordina il mio libro su VB 2015 Pre-ordina il mio libro "Visual Basic 2015 Unleashed". Clicca sulla copertina per informazioni!

Il mio libro su WPF 4.5.1!

Clicca sulla copertina per informazioni! E' uscito il mio libro "Programmare con WPF 4.5.1". Clicca sulla copertina per informazioni!

These postings are provided 'AS IS' for entertainment purposes only with absolutely no warranty expressed or implied and confer no rights.
If you're not an Italian user, please visit my English blog

Le vostre visite

I'm a VB!

Guarda la mia intervista a Seattle

Follow me on Twitter!

Altri spazi

GitHub
I miei progetti open-source su GitHub

Article Categories

Archives

Post Categories

Image Galleries

Privacy Policy

Xamarin.Forms e la validazione dell'input lato client - prima parte

Premessa
Ho avuto la necessità di implementare la validazione dell'input dell'utente in un'app scritta con Xamarin.Forms. Per esempio, dire all'utente che un campo non può essere vuoto o più lungo di un certo numero di caratteri. L'obiettivo è quindi mostrare un messaggio all'utente per dire che qualcosa non va, all'interno della UI. In questa prima parte vi parlo dell'implementazione della validazione tramite behavior, nella seconda parte di come associarla alla UI e allo XAML.

Problema...
Ho molti difetti ma a cercare su Internet o sui libri me la cavo abbastanza bene e, a meno di sviste, la situazione di Xamarin.Forms (ieri e) oggi è la seguente:

  • Non esiste il concetto di ValidationRule come in WPF
  • Non esiste supporto a IDataErrorInfo
  • Non esiste supporto a INotifyDataErrorInfo
  • Non esistono ErrorTemplate
  • Le Data Annotations non hanno effetto sulla UI e richiedono diversi barbatrucchi per poter essere installate dal mitico NuGet. I progetti Windows 8.1 e Phone 8.1 non le supportano, bisogna toglierli, cambiare il target, installare la libreria.. ma comunque non hanno effetto.

Quindi avere una validazione nel nostro model non ha praticamente effetto sul data-binding. Il libro del grande Petzold parla di Trigger o di Behavior. Entrambi consentono di raggiungere l'obiettivo, ma i Trigger non hanno effetto in UWP e a me piace ragionare in modo universale (dove possibile).

In pratica, un Behavior è una classe che estende un controllo, anzi un oggetto che eredita da BindableObject, intercettando qualche comportamento e modificandone l'aspetto.

Ed è qui il punto: possiamo modificare l'aspetto di un controllo a seconda dell'input dell'utente, ma ogni logica di vera e propria validazione spetta a noi. E può starci, quindi rimbocchiamoci le maniche.

Obiettivo
Implementare due behavior, uno per impedire che una casella di testo sia vuota e uno per impedire che contenga più di 255 caratteri.

Implementazione: un'interfaccia e una classe base
Sul Web troverete tanti esempi, io ho voluto fare un passo in più ragionando per astrazione e riutilizzo di codice. Nel caso in esame, abbiamo l'obiettivo di due tipologie di validazione sullo stesso tipo di controllo, la Entry di Xamarin.Forms. Conviene quindi ragionare per astrazione e definire dapprima un'interfaccia come questa:

public interface IValidatorBehavior
{
    bool IsValid { getset; }
}
La proprietà IsValid ci servirà per determinare se il contenuto di una Entry è valido o meno. Ciò vuol dire che ogni behavior che valida una Entry dovrà avere questa proprietà, per cui conviene creare una classe base come la seguente, che eredita da Behavior<T> per forza di cose e che vuole che T sia un BindableObject. La proprietà è implementata come BindableProperty perché nella UI, come vedremo nella seconda parte, verrà utilizzata per fare binding di una Label al risultato del behavior. Ecco la classe:

public class BaseValidatorBehavior<T> : Behavior<T>,
    IValidatorBehavior where T : BindableObject
{
    static readonly BindablePropertyKey IsValidPropertyKey =
        BindableProperty.CreateReadOnly("IsValid"typeof(bool),
        typeof(BaseValidatorBehavior<T>), false);
 
    public static readonly BindableProperty IsValidProperty = IsValidPropertyKey.BindableProperty;
 
    public bool IsValid
    {
        get { return (bool)base.GetValue(IsValidProperty); }
        set { SetValue(IsValidPropertyKey, value); }
    }
}

Se avete un minimo di dimestichezza con le dependency property di WPF/UWP, qui il concetto è identico.

Implementazione: un behavior per la lunghezza massima
Di base un behavior ha un evento OnAttachedTo che riceve il controllo cui si applica e che a sua volta va a specificare un gestore per l'evento da intercettare, nel nostro caso il TextChanged della Entry. In questo secondo gestore, intercetteremo la stringa e vedremo se rispetta la logica. Non di meno, la lunghezza massima della stringa non sarà hard-coded, ma saremo noi (o chi per noi) a deciderla direttamente in binding e per questo implementeremo un'apposita BindableProperty come segue:

public class FieldLengthValidatorBehavior : BaseValidatorBehavior<Entry>
{
    public static readonly BindableProperty MaxLengthProperty =
        BindableProperty.Create("MaxLength"typeof(int),
            typeof(FieldLengthValidatorBehavior), 0);
 
    public int MaxLength
    {
        get { return (int)GetValue(MaxLengthProperty); }
        set { SetValue(MaxLengthProperty, value); }
    }
 
    protected override void OnAttachedTo(Entry bindable)
    {
        bindable.TextChanged += bindable_TextChanged;
    }
 
    private void bindable_TextChanged(object sender, TextChangedEventArgs e)
    {
 
        try
        {
            if (e.NewTextValue.Length > 0 && e.NewTextValue.Length > MaxLength)
            {
                IsValid = false;
                ((Entry)sender).Text = e.NewTextValue.Substring(0, MaxLength);
            }
            IsValid = true;
        }
        catch (Exception)
        {
 
 
        }
    }
 
    protected override void OnDetachingFrom(Entry bindable)
    {
        bindable.TextChanged -= bindable_TextChanged;
    }
}
Riepilogando:
  • il behavior eredita da quello base, prendendosi IsValid
  • il behavior si "attacca" alla Entry
  • ne ascolta l'evento TextChanged
  • verifica la lunghezza della stringa immessa e la confronta con la lunghezza massima fornita dallo sviluppatore nell'espressione di binding verso MaxLength. In questo caso non cambio il colore della Entry perché mostrerò il messaggio nella UI per varie altre ragioni, come vedremo nella seconda parte, ma potremmo farlo in fase di "analisi logica". Qui mi limito semplicemente a troncare la lunghezza del testo in base a quella specificata

E gli errori di validazione?

Saggia domanda. Nel primo behavior sto troncando la lunghezza della stringa, ma nel prossimo behavior, in cui valido il contenuto di un campo, devo fare diversamente. In sostanza devo avere un errore di validazione per ogni controllo a cui applicherò il behavior e consentire a tutti di conoscere lo stato degli errori. Questo perché potrei anche voler cambiare il behavior precedente. Una buona idea, soprattutto se pensiamo che gli oggetti da validare siano una manciata, è creare un Dictionary in questo modo:

public static class Validation
{
    /// <summary>
    /// A list of validation errors. Key of type object is typically the View
    /// while Value of type string is an error message
    /// </summary>
    public static Dictionary<objectstring> ValidationErrors { getset; }
}


Questa collection ci torna subito utile, per esempio nel behavior successivo il cui compito è quello di verificare che un campo non sia vuoto, modificando lo stato del placeholder della Entry.

public class FieldEmptyValidatorBehavior : BaseValidatorBehavior<Entry>
{
    protected override void OnAttachedTo(Entry bindable)
    {
        bindable.TextChanged += HandleTextChanged;
        if (Validation.ValidationErrors == nullValidation.ValidationErrors = new Dictionary<objectstring>();
        // TextChanged isn't raised at first time, so manually adding an error
        if (bindable.IsVisible == true)
            Validation.ValidationErrors.Add(bindable, "Field cannot be empty");
        else
        {
            if (Validation.ValidationErrors.Keys.Contains(bindable)) Validation.ValidationErrors.Remove(bindable);
        }
    }
 
    /// <summary>
    /// Perform the validation logic
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void HandleTextChanged(object sender, TextChangedEventArgs e)
    {
        try
        {
            IsValid = !string.IsNullOrWhiteSpace(e.NewTextValue);
            var entry = (Entry)sender;
            if (IsValid)
            {
                entry.PlaceholderColor = Color.Default;
                entry.Placeholder = "";
                if (Validation.ValidationErrors.Keys.Contains(sender))
                {
                    Validation.ValidationErrors.Remove(sender);
                }
                return;
            }
            else
            {
                entry.PlaceholderColor = Color.Red;
                entry.Placeholder = "Field cannot be empty";
                if (Validation.ValidationErrors.Keys.Contains(sender))
                {
                    return;
                }
                else
                {
                    Validation.ValidationErrors.Add(sender, "Field cannot be empty");
                }
            }
        }
        catch (Exception)
        {
 
        }
    }
 
    protected override void OnDetachingFrom(Entry bindable)
    {
        bindable.TextChanged -= HandleTextChanged;
    }
}

Ogni altra logica di validazione andrebbe nel HandleTextChanged, anche se avrebbe poco senso colorare di rosso una casella vuota, a meno di voler personalizzare il bordo.

Ancora sulla collection di errori e considerazioni sulla validazione

Di fatto, l'attivazione o meno del behavior non impedisce ai dati di tornare alla sorgente in fase di data-binding. Il behavior è solo un'estensione del comportamento della UI. E' per tale ragione che abbiamo definito una collection che ci consente di memorizzare la presenza di un errore e che, alla sua verifica, consentirà alla UI di bloccare i passaggi successivi.
Come vedete nel secondo behavior, la prima volta devo scatenare a mano l'aggiunta di un errore alla collection perché il primo TextChanged non viene intercettato. Poi aggiungo errori quando il campo è vuoto, li rimuovo quando si digita e faccio un doppio controllo perché.. non si sa mai

La prossima volta vedremo come utilizzare questi behavior per mostrare all'utente che qualcosa non va. Ripeto "mostrare all'utente".

Alessandro

Print | posted on mercoledì 21 settembre 2016 00:00 | Filed Under [ Xamarin ]

Powered by:
Powered By Subtext Powered By ASP.NET