Pages

samedi 8 octobre 2011

Validation asynchrone en Silverlight avec INotifyDataErrorInfo

Aperçu d'une interface, quelles possibilités ?

Dans une première partie, nous allons découvrir succinctement la nouvelle validation apportée par Silverlight 4, ses possibilités, son fonctionnement.

D'où sort cette interface ?


Silverlight 4 est arrivé avec son lot de nouveautés, toutes plus ou moins intéressantes, mais l'une d'entre elles, une simple interface, est venue bousculer un dinosaure datant de la genèse du .Net.
En ces temps reculés, lorsque l'on voulait faire de la validation de formulaire à base de binding (ou pas) en WinForm (oui, entendre ça aujourd'hui peu faire sourire !), il était une interface de référence : IDataErrorInfo. Elle répondait à 2 simples questions : "Mon objet est-il en erreur ?" et "Quel est le message d'erreur rapporté par cette propriété ?". Un peu rudimentaire, mais avait on besoin de plus ? Non, nous n'étions alors pas dans un environnement "full asynchrone" façon Silverlight !
Arrive aujourd'hui avec Silverlight 4 l'interface INotifyDataErrorInfo, décidée à enterrer son prédécesseur. Alors, quoi de neuf ? L'apparition d'un évènement "ErrorsChanged", et un retour d'info plus précis, faisant passer la réponse à la question "Quel est le message d'erreur rapporté par cette propriété ?" d'une simple chaine de caractères à une énumération d'objets.
La team Silverlight a propulsé cette interface en avant, et c'est aujourd'hui la méthode de validation par défaut lors de l'utilisation du Binding, plus besoin de ValidateOnQuelqueChose à ajouter en paramètre, tout est automatique !
Mais en quoi ces 2 petits changements révolutionnent la validation en Silverlight ? Rappelez vous la dernière fois que vous avez du gérer la validation d'un objet avec le serveur pour ensuite en informer l'utilisateur ... vous y êtes ? Maintenant, imaginez que vos infos de validation reviennent du serveur, là, d'un simple lancement de l'évènement ErrorsChanged, votre interface s'illumine ! Des cadres rouges pour de belles erreurs bien sûr ! Mais pourquoi pas des jaunes pour les warning ! Trop compliqué ? Non, enfantin avec INotifyDataErrorInfo ! Bref, assez parlé, observons cette interface d'un peu plus près !

Alors, comment ça marche ?


Prenons les différents membres de cette interface point par point, et voyons en quoi ils peuvent nous êtes utiles.
bool HasErrors { get; }
Pas besoin d'un dessin, absente de IDataErrorInfo, pourtant si simple et efficace, cette propriété vous évitera d'itérer sur les différentes propriétés pour savoir si tout va bien.
IEnumerable GetErrors(string propertyName);
On trouve ici la fonction clef de la validation. Via cette méthode, on demande à partir d'un nom de propriété quelles sont les erreurs sur celle-ci. On parles ici d'erreurs, et non de message d'erreur, petite subtilité : on ne retourne pas un message texte à afficher dans une popup (bien que rien ne nous en empêche...) mais potentiellement plusieurs objets d'erreur, contenant par exemple un niveau d'erreur, une version short, et une version détaillée, et tout ce que vous voulez ! Pour bien faire, la méthode ToString() nous retournera un bon vieux message bien "human readable" !
Si le paramètre est null ou vide, on retournera alors une erreur globale, ou pourquoi pas un résumé de toutes nos erreurs, bref : à ma guise !
event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
Lorsque l'état de validation d'une propriété change, on lance alors l'évènement ErrorsChanged en lui passant en argument le nom de la propriété concernée. (ou rien s'il s'agit de la validation globale de l'objet) Le contrôle est alors notifié de cette modification, et se charge de gérer l'affichage de l'erreur.
Par défaut, les contrôles de Silverlight (et du Toolkit il me semble) vont aller chercher l'énumération d'erreur, prendre la première d'entre elles, et l'afficher via la méthode ToString() dans une magnifique popup rouge. (j'insiste sur le magnifique, car faisant actuellement du WPF, la validation Silverlight me manque ;)

Bref, après ce petit aperçu, je crois qu'il est clair qu'utiliser IDataErrorInfo dans un projet Silverlight 4+ vous ferait partir avec un sacré handicap ! (mais je reste ouvert aux arguments qui pencherait en faveur de son utilisation !)

Et dans mon application, ça donne quoi ?

Bonne question, attelons nous à la réalisation d'une petite démo ...

Par quoi on commence ?


Pour moi qui suis un habitué des applications de gestion, et pour un besoin de simplicité pour une démo plus efficace, je vais avoir besoin d'afficher des messages de type "erreur" ou "warning", en modifiant un minimum de code, et en faisant en sorte que ce soit transparent pour les développeurs. Mais sans pour autant tomber dans la facilité de la mise en place d'usine à gaz ou de "boites noires" cachant tout le mécanisme de validation dans un code trop complexe, ou illisible !
Dans un premiers temps, il va falloir définir l'interface de notre objet d'erreur, créer les éventuelles dépendances puis enfin implémenter notre interface. Ensuite, on s'attaquera à l'UI, on redéfinira le template du composant que l'on veut améliorer. Nous créeront après une entité, ainsi que son validateur. Pour enfin brancher le tout dans un UserControl de démo.

Définissons notre interface.


Pour cela, rien de bien compliqué, je vais juste avoir besoin d'un message texte et d'un niveau d'erreur, qui sera une simple enum contenant "error" et "warning".
public interface IError{
    string Message { get; }

    ErrorLevel Level { get; }
}

public enum ErrorLevel{
    Error = 0,
    Warning
}
Notre interface terminée, il ne reste plus qu’à l’implémenter dans notre objet CustomError le plus simplement du monde, en lui créant un constructeur prenant nos 2 paramètres, et sans oublier de surcharger la méthode ToString pour la compatibilité !
public class CustomError : IError{
    public CustomError(string message)
    {
        this.Message = message;
    }

    public CustomError(string message, ErrorLevel level)
        : this(message)
    {
        this.Level = level;
    }

    public string Message { get; set; }

    public ErrorLevel Level { get; set; }

    public override string ToString()
    {
        return string.Format("[{0}] {1}", this.Level, this.Message);
    }
}
Voilà, notre objet est prêt, il n’attend plus que d’être utilisé !

Passons à l'UI, et aux templates !


Pour ceux qui ne sont pas familiers avec la modifications des styles, des templates, et tout ce genre de truc pour lesquels vous vous dîtes "je suis développeur moi, pas designer !", il va falloir suivre un minimum. Ce n'est pas compliqué, d'autant que l'on peut s'aider de ce superbe soft qu'est Expression Blend !! Si si, faîtes pas cette tête, ce logiciel va réellement vous sauver la vie si vous voulez modifier un style/template sans prise de tête. De plus, il existe des dizaines de vidéos sur le sujet sur le site officiel de Silverlight, réalisées en grande partie par notre Jesse Liberty !
Pour simplifier les choses, je vais ajouter quelques converters dont le code n’apparaitra pas ici, le projet de démo sera téléchargeable …

Nous voulons donc que notre TextBox (et son Label, je prend ici l’exemple du DataForm) affiche au lieu de son habituelle popup rouge, une popup qui sera jaune en cas de warning, et qui affichera tous les messages d’erreur.
Pour ma TextBox, le travail est simple, je redéfinis son template à partir de l’original, et je vais me contenter d’aller supprimer la valeur en dur dans la couleur de la bordure ValidationErrorElement pour passer du rouge à un binding sur Validation.Errors, qui prendra évidement un converter chargé de définir si la bordure doit être rouge ou jaune. Idem pour le petit triangle qui apparait dans le coin façon Excel.
Evidement, le gros du travail se fait sur son Tooltip ! J’ai donc son template (merci Blend) d’extrait, et il me faut maintenant ajouter ma couleur, et mes erreurs … Pour cela, je retrouve la belle couleur rouge que je remplace par mon binding (toujours le même) et son converter, mais pour ce qui est de l’affichage, on remarque que par défaut, on va chercher l’élément d’index 0 dans la liste des erreurs. Je remplace donc ça par une ListBox, dont les éléments seront customisés pour répondre aux paramètres de mon objet CustomError. Et voilà la résultat (je vous passe les détails du VisualStateManager, trop bavard mais tellement utile !) :
<ControlTemplate x:Key="ValidationToolTipTemplate">
    <Grid x:Name="Root" Margin="5,0" Opacity="0" RenderTransformOrigin="0,0">
        <Grid.RenderTransform>
            <TranslateTransform x:Name="xform" X="-25"/>
        </Grid.RenderTransform>
        <VisualStateManager.VisualStateGroups>
            <[...]>
        </VisualStateManager.VisualStateGroups>
        <Border Background="#052A2E31" CornerRadius="5" Margin="4,4,-4,-4"/>
        <Border Background="#152A2E31" CornerRadius="4" Margin="3,3,-3,-3"/>
        <Border Background="#252A2E31" CornerRadius="3" Margin="2,2,-2,-2"/>
        <Border Background="#352A2E31" CornerRadius="2" Margin="1,1,-1,-1"/>
        <!--<Border Background="#FFDC000C" CornerRadius="2"/>-->
        <Border Background="{Binding (Validation.Errors), Converter={StaticResource ErrorListToGlobalColorConverter}}" CornerRadius="2"/>
        <ListBox Margin="1" ItemsSource="{Binding (Validation.Errors)}" BorderThickness="0" Background="{x:Null}" IsHitTestVisible="False">
            <ListBox.ItemContainerStyle>
                <Style TargetType="ListBoxItem">
                    <Setter Property="Padding" Value="0" />
                    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
                </Style>
            </ListBox.ItemContainerStyle>
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Border Background="{Binding ErrorContent, Converter={StaticResource ErrorLevelToBackgroundColorConverter}}">
                        <TextBlock Foreground="{Binding ErrorContent, Converter={StaticResource ErrorLevelToForegroundColorConverter}}" MaxWidth="250" Margin="8,4,8,4" TextWrapping="Wrap" Text="{Binding ErrorContent.Message}" UseLayoutRounding="false" />
                    </Border>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</ControlTemplate>
Voilà, les converters parlent d’eux-même, je ne pense pas qu’il faille commenter plus que ça. De toute façon, comme j'ai eu l'occasion de le constater trop de fois, les personnes hermétiques aux styles iront honteusement pomper le code sans essayer de le comprendre. (bah oui, on en connait tous ;)
Notre erreur est prête, notre composant aussi, reste le principale !

Place enfin à l'entité …


Pour faire court, notre entité sera implémentée de manière a renvoyer constamment true lorsqu’on lui demandera si elle est en erreur, et dans cette liste d’erreur, on ajoutera en dur quelques valeur d’exemple. Le mieux étant ensuite de faire un objet Validator qui contiendra la logique de validation. Mais restons sur notre exemple que voilà :
public class Person : INotifyPropertyChanged, INotifyDataErrorInfo{
    private string lastName;

    [Display(Name = "Nom")]
    public string LastName [...]

    private string firstName;

    [Display(Name = "Prénom")]
    public string FirstName [...]

    private string birthDay;

    [Display(Name = "Date de naissance")]
    public string BirthDay [...]

    private string address;

    [Display(Name = "Adresse")]
    public string Address [...]

    #region INotifyDataErrorInfo

    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    private void NotifyErrorsChanged(string propertyName)
    {
        if (this.ErrorsChanged != null)
        {
            this.ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
        }
    }

    public IEnumerable GetErrors(string propertyName)
    {
        if ("FirstName".Equals(propertyName))
        {
            return new List<CustomError>()
            {
                new CustomError("Message d'erreur niveau \"Error\"", ErrorLevel.Error)
            };
        }
        else if ("LastName".Equals(propertyName))
        {
            return new List<CustomError>()
            {
                new CustomError("Message d'erreur niveau \"Error\"", ErrorLevel.Error),
                new CustomError("Message d'erreur niveau \"Warning\"", ErrorLevel.Warning),
                new CustomError("Second message d'erreur niveau \"Warning\"", ErrorLevel.Warning),
            };
        }
        else if ("BirthDay".Equals(propertyName))
        {
            return new List<CustomError>()
            {
                new CustomError("Message d'erreur niveau \"Warning\"", ErrorLevel.Warning),
                new CustomError("Second message d'erreur niveau \"Warning\"", ErrorLevel.Warning),
            };
        }
        else
        {
            return null;
        }
    }

    public bool HasErrors
    {
        get { return true; }
    }

    #endregion INotifyDataErrorInfo

    [INotifyPropertyChanged...]
}

Et finalement, ça donne quoi ?


Et bien voilà le résultat !



 SilverlightPowerValidation.zip