Dot.Blog

C#, XAML, Xamarin, UWP/Android/iOS

INotifyDataErrorInfo sous WPF 4.5, validation asynchrone et MVVM

[new:30/03/2014]La notification en asynchrone des erreurs par le ViewModel est l'une des choses les plus importantes pour suivre le pattern MVVM, avec INotifyPropertyChanged. WPF 4.5 introduit cette interface qui n'existait que sous Silverlight, regardons de plus près comment cela fonctionne…

INotifyDataErrorInfo

Durant l'été 2012 j'avais écrit un papier « Validation des données sous Silverlight » (que vous retrouvez aussi dans le tome 8 « Silverlight » de la série de livres gratuits « All Dot.Blog »). J'y présentais l'interface IDataErrorInfo tout en promettant de vous parler de INotifyDataErrorInfo dans un prochain billet. L'histoire de Silverlight étant ce qu'elle est je n'avais pas tenu cette promesse préférant vous parler du développement cross-plateforme notamment…

Mais je tiens toujours mes paroles ! Même avec un peu de retard… Surtout lorsque cela se justifie !

En effet s'il n'est pas question aujourd'hui de creuser les méandres de Silverlight il ne faudrait surtout pas oublier que l'extraordinaire, le magnifique, l'exceptionnel (j'en fais peut-être un peu trop ok…) que dis-je l'énormissime WPF est toujours bien vivant et que sa version 4.5 a amené son lot de nouveautés, et pour ce qui nous intéresse aujourd'hui, le support de INotifyDataErrorInfo qui se trouvait déjà dans Silverlight.

Quel est l'intérêt de cette interface ?

L'intérêt principal est de pouvoir gérer les erreurs de validation des propriétés de façon simple, aussi bien côté ViewModel que View puisque XAML prend en charge certains comportements automatiques pour peu que le VM implémente l'interface.

Le second intérêt est qu'à la différence de IDataErrorInfo, interface ancienne déjà présente sous Windows Forms, INotifyDataErrorInfo est moderne, donc conçue pour fonctionner en asynchrone.

Je ne m'étendrai pas sur les avantages de l'asynchronisme qui se trouve aujourd'hui au cœur de tout logiciel bien conçu, par choix éclairé du développeur ou bien par la force des choses (comme par exemple sous WinRT où tout est asynchrone ou presque).

Pourquoi parler de MVVM ?

INotifyDataErrorInfo n'a pas été spécialement conçue pour être utilisée dans un contexte MVVM, même si on peut penser que l'idée se trouvait tout de même dans les arrière-pensées de ceux qui l'ont ajoutée au Framework .NET.

Donc bien que conçue dans un esprit indépendant de MVVM, il s'avère que cette interface rend le travail de validation des données bien plus simple et plus conforme au pattern. D'une part parce qu'elle permet d'écrire les validations au niveau du ViewModel sans rien coder dans l'UI (la View), et d'autre part parce que XAML prend en charge des comportements visuels automatiques lorsqu'il détecte un DataContext supporter cette interface.

Mais il y a un avantage de plus, c'est bien entendu l'asynchronisme.

Comme je le disais plus haut un logiciel moderne bien conçu utilise l'asynchronisme. Ce n'est même plus une question de choix sous WinRT pour Windows 8.x par exemple.

Donc, pouvoir centraliser les validations de données (propriétés) dans le ViewModel est un atout important pour une implémentation correcte de MVVM. Si l'asynchronisme est pris en compte (ce qui est le cas) cela facilite encore plus les choses !

De fait, bien qu'utilisable en dehors du contexte de MVVM, l'interface INotifyDataErrorInfo prend toute sa valeur dans ce dernier.

Un exemple !

Comme d'habitude je vais partir d'un exemple simple pour que tout le monde puisse suivre s'en trop s'ennuyer… C'est l'un des avantages de Dot.Blog, être accessible et sympathique tout en parlant de choses sérieuses J

Nous allons ici imaginer une fiche de saisie sous WPF ne possédant qu'un seul champ à saisir.

Ce que nous aimerions c'est valider ce champ au fur et à mesure qu'il est tapé. L'utilisateur recevra un retour permanent sur l'état de sa frappe grâce aux mécanismes visuels automatiques de XAML et parce que, bien entendu, nous allons implémenter INotifyDataErrorInfo dans notre ViewModel.

Pour que l'exemple ne se perde pas dans les méandres de tel ou tel autre Framework MVVM nous réaliserons la séparation entre View et ViewModel « à la main », c'est-à-dire que l'objet visuel, la fiche XAML, sera la Vue, et que le ViewModel sera juste une instance d'une classe séparée qui sera directement placée dans la propriété DataContext de la Vue. Il sera ensuite facile au lecteur d'extrapoler cette situation à tous les Frameworks MVVM existants (et plus particulièrement à celui ou ceux qu'il utilise !).

Le principe

Avant d'aborder l'exemple il est nécessaire de parler un peu plus de INotifyDataErrorInfo. Cette interface repose sur un principe simple vu de l'extérieur, d'ailleurs sa définition est la suivante :

public interface INotifyDataErrorInfo

{    

 bool HasErrors { get; }

 event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

 IEnumerable GetErrors(string propertyName);

}

 

Comme on peut le constater rien de bien sophistiqué. On remarque tout de même deux ou trois choses intéressantes.

La première est que GetErrors retourne une liste (IEnumerable) et se base sur un nom de propriété. Cela signifie que le mécanisme est capable de demander la liste des erreurs d'une propriété en particulier. Ce qui signifie en d'autres termes qu'une propriété peut présenter plusieurs messages d'erreur à la fois ce qui est plus proche de la réalité. Cela permet ainsi de prévenir tout de suite l'utilisateur de l'ensemble des problèmes au lieu de lui faire croire qu'il n'y en a qu'un puis de lui sortir un nouveau message d'erreur, etc, mécanisme très frustrant.

On note aussi la présence d'un événement ErrorsChanged. Cela signifie que le mécanisme est très dynamique : lorsque la liste des erreurs change en temps réel il est possible d'avertir l'UI qui se sera abonnée à l'événement en question et qui réagira immédiatement. Nul besoin d'attendre qu'une boucle ou autre procédé barbare ne parcoure l'ensemble des propriétés de temps en temps pour savoir si un champ n'est pas valide ou si au contraire un champ en erreur ne vient à repasser à un état normal.

La validation se fait avec la granularité la mieux adaptée : si GetErrors est appelé sans aucun nom de propriété ce sont toutes les erreurs de l'entité qui sont retournées. Sinon seules celles de la propriété indiquée le sont.

Pour que le mécanisme fonctionne on comprend bien que l'entité source du binding (en général le ViewModel et ses propriétés) doit supporter l'interface INotifyDataErrorInfo. Mais cela ne serait pas suffisant. Il faut aussi, côté vue, avertir l'objet de Binding qu'il devra prendre en compte cette gestion particulière des erreurs. Cela se fait en plaçant sa propriété ValidatesOnNotifyDataErrors à true (elle ne l'est pas par défaut).

Le moteur de binding XAML de WPF 4.5 s'abonne donc à tous les événements ErrorsChanged des propriétés dont le binding déclare le ValidatesOnNotifyDataErrors. On comprend mieux les noms de toutes ces interfaces et de leurs propriétés. C'est un mécanisme basé principalement sur des notifications passant via des messages de type événement.

Dans cette explication il manque un élément de l'interface : HasErrors. C'est un booléen dont on comprend le rôle, s'il retourne true c'est qu'il y a des erreurs à gérer, sinon il n'y en a pas. Le moteur pourrait se contenter de demander le Count de l'énumérable retourné par GetErrors, c'est d'ailleurs ce que fait le plus souvent le code d'implémentation côté ViewModel, mais cet appel peut être couteux. Il peut y avoir des moyens plus rapides d'obtenir cette information. Comme cela est spécifique au code d'implémentation, à lui de retourner quelque chose du genre « MaListeDErreurs.Count > 0 » ou bien un simple flag qui évite l'appel du Count.

La validation et l'asynchronisme

Arrivé ici on a bien compris comment INDEI (l'acronyme de l'interface, comme on utilise INPC pour la célèbre INotifyPropertyChanged) fonctionne. Reste à voir comment tout cela vient se mêler avec la notion d'asynchronisme…

D'abord il faut se rendre compte que telle que INDEI est définie elle laisse une liberté quasi-totale à l'implémentation… Il y a donc milles façons de la programmer mais comme toujours quelques-unes sont de meilleures qualité que les autres.

Ainsi chaque champ aura la responsabilité de se valider et le meilleur endroit pour le faire (en général) c'est dans le setter de la propriété elle-même. Mais on se rappelle que INDEI peut réclamer les erreurs de toutes les propriétés… Donc le code qui valide une propriété ne doit pas être dans le setter de cette dernière sinon il faudra le doubler pour répondre à la demande des erreurs de toute l'entité. On comprend immédiatement que les setters des propriétés feront plutôt appel à une méthode externe chargée d'effectuer la véritable validation.

Ainsi une propriété Name aura dans son setter certainement quelque chose comme « validateName(_name) ; ». S'il s'agit de valider une chaîne vide ou non il existe d'autres procédés comme les annotations qui passent par des attributs, mais pour des validations véritablement adaptées à des propriétés particulières il y aura systématiquement en face une méthode de validation. Cette dernière peut en revanche être exploitée par plusieurs propriétés si elles sont de même nature. Imaginons deux propriétés de type numéro de sécu dans une entité, il n'y aura bien entendu qu'une seule méthode de validation utilisée pour les deux propriétés.

Certaines validations sont rapides. Détecter un champ vide ou la présence d'un caractère interdit dans une chaîne par exemple. Pour de tels cas l'asynchronisme n'a pas beaucoup d'intérêt.

Mais dans de vraies applications qui dépassent l'exemple simplificateur il arrive assez souvent qu'une validation puisse réclamer ne serait-ce qu'un accès à un SGBD pour obtenir le résultat de validation. Une clé d'identification par exemple, la validation d'une URL qui réclame des accès Web, un ping pour déterminer si une IP est joignable ou non, etc.

Bref il existe des tas de cas où valider une propriété peut prendre « un certain temps ». Et les bonnes UX passent par l'instantanéité. Les UI qui se figent pour faire une validation c'est de la programmation à grand papa… Dès lors on commence à percevoir l'intérêt de l'asynchronisme : une propriété est changée par la saisie de l'utilisateur la validation est demandée immédiatement mais celle-ci va se dérouler en arrière-plan dans un thread à part ce qui laisse l'utilisateur libre de commencer la saisie d'une autre zone. Quand la validation demandée sera disponible, et si sa réponse est une erreur, il sera alors temps de mettre à jour l'UI pour que l'utilisateur puisse être averti.

C'est fluide, dynamique donc moderne…

Bien entendu dans certains cas, certains domaines, une bonne UX peut réclamer que l'utilisateur ne puisse passer aux propriétés suivantes que si les précédentes sont validées, quitte à ce qu'il y ait une attente. Des cas extrêmes où la beauté de la fluidité doit être sacrifiée sur l'autel du fonctionnel et de ces impératifs. Dans ces cas on traite la validation dans la méthode de validation et puis c'est tout… Mais ces cas sont très rares ! Ne vous protégez pas derrière ces exceptions pour éviter de raisonner en asynchrone…

Les annotations

Les annotations, des attributs qu'on trouve dans l'espace de noms System.ComponentModel.DataAnnotations, peuvent être utilisées en même temps que le procédé ici décrit ce qui permet de bénéficier de validations déjà écrites comme la validation d'une URL bien formée par exemple.

Il n'y a aucune obligation d'utiliser les annotations, c'est juste un petit plus parfaitement compatible avec INDEI et c'est bon à savoir.

Le code

Je l'ai promis il faut bien que je le sorte de mon escarcelle maintenant…

XAML

Nous allons partir d'un exemple très simple, une simple URL à saisir. Le code XAML sera donc trivial : une grille (Grid) et un texte à saisir (TextBox).

Toutefois nous allons un peu compliqué les choses pour pimenter le jeu. Par exemple nous allons écrire un Style qui, en plus d'utiliser le mécanisme d'encerclement en rouge de la zone en erreur va récupérer les messages d'erreur pour les placer dans un Tooltip. Ce n'est pas très compliqué mais c'est pratique pour retourner une information plus complète à l'utilisateur sans pour autant réserver de la place à l'écran pour les éventuels messages d'erreur. En termes d'UX cela se discute, le Tooltip peut ne pas être « instinctif » car il faut qu'il apparaisse et que l'œil s'attende à y trouver un message d'erreur. Mais faisons fi des grandes discussions philosophiques pour cet exemple cela sera parfait.

La deuxième chose que nous ferons consistera à initialiser correctement le binding de la TextBox pour qu'elle gère INDEI. Mais comme nous voulons une validation immédiate, caractère par caractère, nous ajouterons une indication à l'UpdateSourceTrigger pour qu'il se déclenche sur chaque PropertyChanged. Sinon il faudrait attendre que la zone perde le focus pour qu'elle soit validée. Une bonne UX étant réactive il est préférable d'éviter les validations qui se font sur la perte de focus. D'une part une zone ne perd pas forcément le focus (comme ici car il n'y aura qu'un seul champ !) et d'autre part l'erreur d'un champ s'active donc lorsqu'utilisateur entre dans un autre champ. Ce qui donne « un coup de retard » parfaitement stupide et qui personnellement m'insupporte… Si j'entre une bêtise je veux le savoir tout de suite, pas quand je suis passé au champ suivant.

On se rappellera aussi que nous initialisons directement dans la fenêtre le DataContext, c'est-à-dire le ViewModel. Cela en raison du fait que nous n'utilisons ici aucun framework MVVM mais que nous suivons le pattern malgré tout…

Tout cela nous donne un XAML comme suit :

<Window x:Class="ValidationExample.MainWindow"

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns:viewModel="clr-namespace:ValidationExample.ViewModel"

Title="MainWindow" Height="350" Width="525">

 

<Window.DataContext>

<viewModel:MainWindowViewModel></viewModel:MainWindowViewModel>

</Window.DataContext>

 

<Window.Resources>

<Style x:Key="UrlValidationStyle" TargetType="TextBox">

<Style.Triggers>

<Trigger Property="Validation.HasError" Value="True">

<Setter Property="ToolTip">

<Setter.Value>

<Binding Path="(Validation.Errors).CurrentItem.ErrorContent" 
RelativeSource="{x:Static RelativeSource.Self}" />

</Setter.Value>

</Setter>

</Trigger>

<Trigger Property="Validation.HasError" Value="False">

<Setter Property="ToolTip">

<Setter.Value>

L'URL a été atteinte.

</Setter.Value>

</Setter>

</Trigger>

</Style.Triggers>

</Style>

</Window.Resources>

 

<Grid>

<Border BorderBrush="Black" BorderThickness="1">

<TextBox Style="{StaticResource UrlValidationStyle}" TextWrapping="Wrap" Height="20" Width="200"

Text="{Binding Url ,

UpdateSourceTrigger=PropertyChanged,

ValidatesOnNotifyDataErrors=True,

Mode=TwoWay,

NotifyOnValidationError=True}" />

</Border>

</Grid>

</Window>

 

On remarque bien au début : la création de la classe MainWindowViewModel dont l'instance est directement stockée dans la propriété DataContext de l'objet Window.

Suit la définition du style UrlValidationStyle qu'on place naturellement pour cet exemple dans les ressources de l'objet Window (le Style pourrait être défini dans App.Xaml pour être global à toute l'application, généralement via un dictionnaire de ressources).

On trouve enfin la grille principale qui englobe un Border qui contient la TextBox.

Je ne vais pas détailler toutes les subtilités XAML de ce code ce n'est pas le but de ce billet, mais lisez-le bien, tout n'est pas forcément évident.

Le ViewModel

Il se complique un peu puisqu'il gère INPC et INDEI et qu'en plus j'utilise des annotations pour valider la propriété… Et comme je voulais vous montrer un peu plus qu'un exemple de base, l'une de ces annotations est un attribut custom.

Les validations seront ainsi effectuées en balayant les attributs de validation. C'est une stratégie plus complexe que celle exposée de base en général pour l'utilisation de INDEI. Je le répète une fois encore utilisez les annotations n'est en rien obligatoire, c'est juste histoire de pimenter un peu l'exemple et de vous faire découvrir peut-être des choses auxquelles vous ne pensiez pas.

using System;

using System.Collections;

using System.Collections.Concurrent;

using System.Collections.Generic;

using System.ComponentModel;

using System.ComponentModel.DataAnnotations;

using System.Linq;

using System.Runtime.CompilerServices;

using System.Security.AccessControl;

using System.Security.Policy;

using System.Text;

using System.Threading.Tasks;

using ValidationExample.Attributes;

 

 

namespace ValidationExample.ViewModel

{

public class MainWindowViewModel : INotifyPropertyChanged, INotifyDataErrorInfo

{

 

// fields

 

private readonly ConcurrentDictionary<string, List<ValidationResult>> _errors =

new ConcurrentDictionary<string, List<ValidationResult>>();

 

private string _url;

 

// property

 

[ReachableUrl]

[UrlAttribute]

public string Url

{

get { return _url; }

set

{

if (value != _url)

{

_url = value;

OnPropertyChanged();

ValidateUrl(_url);

}

}

}

 

 

// Validation methods

 

private async void ValidateUrl(string urlString, [CallerMemberName] String propertyName = "")

{

await Task.Factory.StartNew(() =>

{

// Remove the existing errors for this property

List<ValidationResult> existingErrors;

_errors.TryRemove(propertyName, out existingErrors);

 

// Check for valid URL

var results = new List<ValidationResult>();

var vc = new ValidationContext(this) { MemberName = propertyName };

if (Validator.TryValidateProperty(urlString, vc, results) || results.Count <= 0) return;

_errors.AddOrUpdate(propertyName, new List<ValidationResult>

{ new ValidationResult(results[0].ErrorMessage)},

(key, existingVal) => new List<ValidationResult>

{ new ValidationResult(results[0].ErrorMessage)});

NotifyErrorsChanged(propertyName);

});

}

 

 

 

// Data Validation Interface

 

public IEnumerable GetErrors(string propertyName)

{

List<ValidationResult> propertyErrors = null;

_errors.TryGetValue(propertyName, out propertyErrors);

return propertyErrors;

}

 

public bool HasErrors { get { return _errors.Count > 0; } }

 

public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

 

protected virtual void NotifyErrorsChanged([CallerMemberName] String propertyName = "")

{

var e = ErrorsChanged;

if (e == null) return;

e(this, new DataErrorsChangedEventArgs(propertyName));

}

 

// Property Changed

 

public event PropertyChangedEventHandler PropertyChanged;

 

protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)

{

var handler = PropertyChanged;

if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));

}

 

}

}

 

Si on fait abstraction du code des interfaces INPC et INDEI qui est un passage obligé, notre VM ne contient rien d'autre qu'une simple propriété de type String s'appelant Url. Cette propriété est codée de façon très classique avec un setter et un getter.

La seule petite différence avec un code standard gérant INPC est la présence, en plus, de l'appel à la validation du champ. Cet appel est classique et n'a rien d'asynchrone en lui-même ValidateUrl(_url) ;

On remarque bien entendu les deux attributs qui décorent la propriété Url : ReachableUrl et UrlAttribute

Le dernier est un attribut issu de DataAnnotation et il se charge de vérifier si une URL est bien formée. Le premier est un attribut custom qui s'assure que l'URL peut être jointe, c'est-à-dire qu'elle existe vraiment sur le Net et qu'elle est active.

La méthode de validation utilise une liste d'erreurs sous la forme d'un dictionnaire de type concurrent. C'est un nouveau type de dictionnaire qui est thread-safe ce qui s'avère indispensable ici puisque plusieurs threads vont tourner en même temps. Cela évite au passage la gestion de Lock pas toujours effectuée correctement par nombre de développeurs.

La méthode de validation est marquée async car elle utilise dans son corps await. Je passerai rapidement sur ces mots clés dont la puissance et l'intérêt sont tellement énormes qu'il vaut mieux ne rien dire que dire trois mots. Mais ici nous sommes en plein dans l'asynchronisme… Après avoir vider le dictionnaire des erreurs pour la propriété testée la méthode utilise le Validator qui est un Helper permettant d'exploiter les attributs de DataAnnotation (de base ou custom). C'est ici que nous pourrions avoir un code de test dans un thread classique. J'ai remplacé ce code classique de validation par un balayage des attributs d'annotation car cela permet de voir comment écrire une méthode de validation générique (elle ne change pas en fonction des tests à effectuer) qui tire profit des annotations existantes tout autant que de celles qu'on peut créer soi-même.

Au lieu d'avoir des méthodes du type « ValideNom(nom) ou ValidePrénom(prénom) » on a une seule méthode de validation valable pour toutes les propriétés et les tests individuels sont codés sous la forme d'attributs custom.

J'aime bien cette façon de rendre le processus de validation plus générique même si, de prime abord au moins, cela complique légèrement le code.

L'attribut custom

La façon de procéder que je vous propose utilise donc des attributs custom pour tester la validité des propriétés. Ces attributs décorent les propriétés, c'est propre, clair, l'intention est évidente, c'est modulable et on peut ainsi classer ses tests par type ou famille sans être obligé d'avoir un code répétitif (un code pour valider le nom, un autre pour le prénom, un autre pour les chaines qui n'autorise pas les espaces, etc…). On peut créer des tests simples et les « empiler » sur une propriété pour la valider.

L'attribut custom que nous utilisons ici est responsable du test d'existence d'une URL, c'est-à-dire qu'il doit nous dire non pas si elle est bien formée (c'est l'attribut standard UrlAttribute qui le fait) mais si l'Url en question est joignable. Pour ce faire le code va être obligé d'accéder au Web et seule façon de s'assurer que l'adresse est valide il va tenter de télécharger le code HTML que cette adresse retourne.

Autant dire que cette opération peut être un peu lente. D'où l'intérêt bien entendu de faire fonctionner tout cela en mode asynchrone.

using System;

using System.ComponentModel.DataAnnotations;

using System.Net.Http;

using System.Text.RegularExpressions;

using System.Threading.Tasks;

 

 

namespace ValidationExample.Attributes

{

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]

public class ReachableUrl : DataTypeAttribute

{

private static readonly Lazy<HttpClient> LazyHttpClient = new Lazy<HttpClient>(() => new HttpClient());

private static readonly Regex Regex = new Regex(@"^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture);

 

public ReachableUrl()

: base(DataType.Url)

{

ErrorMessage = "URL non valide !";

}

 

public override bool IsValid(object value)

{

if (value == null)

{

return true;

}

 

var valueAsString = value as string;

if (valueAsString == null || Regex.Match(valueAsString).Length <= 0) return false;

var isReachableTask = IsReachableUrl(value);

isReachableTask.Wait();

return isReachableTask.Result;

}

 

private async Task<bool> IsReachableUrl(object value)

{

var isReachableUrl = false;

 

try

{

LazyHttpClient.Value.CancelPendingRequests();

var uri = new Uri(value.ToString());

var responseBody = await LazyHttpClient.Value.GetStringAsync(uri);

isReachableUrl = responseBody.Length > 0;

}

catch (Exception e)

{

ErrorMessage = e.Message;

}

return isReachableUrl;

}

}

}

 

Code le code est bien fait, il ne cherche sur Internet que si l'adresse lui semble « possible ». Pour ce faire une gigantesque expression régulière se charge de valider la forme de l'Url testée.

Si c'est le cas, la méthode IsReachableUrl sera appelée, elle utilise une tâche et quelques astuces comme le LazzyHttpClient pour obtenir une réponse de l'Url. Si le contenu retourné à une longueur supérieure à 0 l'Url est considérée comme ok.

Ce test est largement discutable. Une page retournée vide fera considérer l'Url comme non accessible, ce qui n'est pas le cas (mais correspond à un cas d'erreur le plus souvent). De même une adresse valide sur un serveur en maintenance ou en panne sera retournée en erreur même si cela est temporaire, etc.

Mais dans la grande majorité des cas le test est plutôt efficace : si l'URL retourne quelque chose c'est qu'elle est accessible (méfiance, encore, avec une page d'erreur disant que la page demandée n'existe pas… Il faudrait aussi tester les codes des statuts retournés pour parfaire les choses).

Un peu de visuel

Lorsqu'on tape une URL en erreur voici ce qui se passe :

 

La TextBox est en rouge, indiquant que la zone est en erreur. La capture d'écran efface le Tooltip, j'en suis désolé car sinon vous verriez le message « URL non valide ! » tel que défini dans l'attribut custom ReachableUrl.

Lorsque nous complétons l'URL de façon convenable :

 

La zone reprend sa couleur normale (ici un bord bleu) et le Tooltip indique « L'URL a été atteinte ! », et ce message est celui qui est défini dans le Style XAML lorsqu'il n'y a pas d'erreur…

Projet source

Voici le code du projet source sous Visual Studio 2013 (j'utilise temporairement Word pour publier les billets et je ne suis pas certain que l'intégration du zip va fonctionner… Si cela ne fonctionne pas ne soyez pas trop déçu, vous avez 100% du code dans l'article !).

 

 ValidationExample.zip (12,71 kb)

Conclusion

Valider des propriétés est aussi important que d'en permettre la saisie… Garbage in Garbage out dit un proverbe de notre profession… Faire en sorte que l'utilisateur ne puisse pas entrer de « garbage » est donc un pas important en direction d'une bonne UX.

WPF est une plateforme magnifique, XAML est une merveille, ça tourne sous tous les Windows en circulation et désormais le Windows Store accepte aussi les applications de ce type, preuve que l'époque du WinRT n'a été qu'une passade et que Microsoft fait œuvre de réalisme.

WPF est donc bien vivant et en tirer le meilleur parti est essentiel !

Stay Tuned !

blog comments powered by Disqus