Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Xamarin.Forms : montrer/cacher un mot de passe

C’est une fonction indispensable surtout avec la nécessité de mots de passe complexes, il faut pouvoir vérifier ce qu’on tape et donc afficher ce qui est frappé. Un moyen simple : utiliser les Trigger d’action. Comment ?

Les mots de passe cet enfer moderne

Je comptabilise plus de 500 logins différents dans mon soft de gestion des mots de passe. Chaque mot de passe doit aujourd’hui être une suite longue de signes, symboles, majuscules, minuscules, chiffres complètement incompréhensibles. Nous vivons en ce moment l’enfer des mots des passe. Et plus les exigences deviennent grandes et plus les sites deviennent sécurisés, plus il faut gérer de mots de passe, différents bien entendu. C’est pourquoi plus cette exigence grandit plus les gens utilisent le même mot de passe simple pour être sûr de s’y retrouver… Totalement contreproductif !

Mais hélas ce n’est pas une entrée de blog, même du prestigieux Dot.Blog (!), qui pourra faire changer cette tendance délirante.

En revanche nous pouvons essayer d’aider nos utilisateurs. Comment ? En rendant systématiquement affichables les mots de passe tapés dans nos applications.

On pourrait d’ailleurs prôner l’abandon de cette pratique idiote de vouloir “cacher” les mots de passe à l’écran. Sur PC dans un bureau open space pourquoi pas, on ne sait jamais qui passent derrière vous et espionne ce que vous tapez. Mais sur un smartphone ou une tablette, honnêtement… si je me sens espionné et serré de trop près par quelqu’un, je ne vais pas m’amuser à visiter mon compte en banque. A la limite c’est ce qui va s’afficher ensuite qui est plus “secret” que le mot de passe !

Mais là encore, je n’ai pas le pouvoir de faire changer les choses. Mais je l’exprime car sait-on jamais cela pourra peut-être avoir une vague influence qui fera son chemin… il faut généraliser un système de dongle biométrique utilisable partout, c’est la seule solution valable.

Donc revenons à ce qui est possible de faire : laisser le mot de passe en mode “étoiles”, donc caché, mais offrir un moyen simple de le rendre visible pour que l’utilisateur puisse vérifier sa frappe.

Comment rendre visible ce qui ne l’est pas ?

Savoir ce qu’on veut et pourquoi

En informatique on ne prend pas des décisions au doigt mouillé… Si on opte pour une solution on doit pouvoir la justifier par une argumentation solide. Et c’est difficile car se mêlent nécessités techniques, contextes humains, psychologie, voire éthique ou politique !

Donc avant de savoir comment nous allons rendre visible le mot de passe, regardons les différentes options qui peuvent être appliquées et choisissons celle qui nous semble la meilleure, ou la moins pire, puis nous chercherons à l’implémenter.

Les mots de passe cachés sont le fait d’un Entry en mode “IsPassword=True”. Il “suffit” donc de basculer cette propriété à False pour afficher le contenu. Et de nouveau à true pour le cacher encore. Mais il y a plein de façon de le faire…

Différentes stratégies s’offrent ainsi à nous. On peut par exemple opter pour un affichage de contrôle temporisé. Un appui sur un bouton affiche 2 ou 3 secondes le mot de passe et le re-cache immédiatement sans aucune action supplémentaire. C’est une méthode intéressante dans le sens où elle respecte l’esprit du mot de passe caché. Même après l’action de l’utilisateur de toute façon la sécurité reprendra ses droits et le texte sera à nouveau caché sans que l’utilisateur ne le demande. Mais si le mot de passe est complexe, long, voire les deux, le réglage de la temporisation risque d’être trop court. Et si on allonge de trop ce temps, il sera assez long pour que le regard d’un intrus discret puisque en capter l’essentiel. Dilemme…

Autre option que je trouve intéressante aussi : le bouton poussoir instantané. C’est à dire que tant que l’utilisateur laisse son doigt sur le bouton le mot de passe s’affiche, dès qu’il lève le doigt le mot de passe est caché. Ici on règle le problème de temporisation évoqué plus haut. On conserve l’aspect sécuritaire aussi. Mais en fait on ne règle rien on rejette juste la responsabilité sur l’utilisateur… mais soit, c’est toujours mieux de laisser ce dernier décider ce qui est bon pour lui plutôt que de le faire à sa place, autoritarisme à la mode chez nos politiques et nos informaticiens mais assez néfaste je trouve. Chacun doit prendre ses responsabilités plutôt que de se voir imposer des choix normatifs et arbitraires (dans la solution précédente imposer 2 secondes ou 3 est totalement arbitraire par exemple). Mais ce qui gêne le plus dans cette seconde approche c’est que l’utilisateur voit son doigt bloqué sur le bouton ! L’ergonomie, le côté pratique des choses doit guider l’UX, pas l’idéologie. Ici impossible de taper en regardant le code, il faut choisir… J’ai déjà expérimenté ce genre d’astuce et je déteste.

Problème de temporisation, choix éthiques nous emmenant dans des territoires philosophiques et politiques hors de propos ici, ou solution ubuesque bloquant le doigt qui devrait servir à taper… Ces solutions sont mauvaises à plus d’un titre. Et pourtant elles sont très fréquentes !

Peut-ont faire mieux ?

Pas si sûr… Mais quitte à choisir entre fausse sécurité (car tout ceci est illusoire) et bonne UX je vais opter pour cette dernière.

On peut alors se dire qu’un bouton en fin de Entry permettra tout simplement de basculer entre le mode visible et le mode caché. Sous la responsabilité entière de l’utilisateur. Sans lui bloquer le doigt sur le bouton, sans lui imposer de temporisation mal taillée. A lui de savoir si le contexte lui permet de laisser son mot de passe visible ou non. Laissons-le prendre ses responsabilités. C’est plus reposant pour nous, et l’UX sera meilleure car l’utilisateur ne se sentira pas soumis à une volonté extérieure qu’il ne contrôle pas alors même que la solution imposée ne lui convient peut-être pas.

Je sais que mes digressions philosophico-informatiques peuvent laisser de marbre les acharnés de la frappe qui veulent du code, et tout de suite. Sans réfléchir. Heureusement les lecteurs de Dot.Blog dans cet état d’esprit ne sont pas nombreux. Les fidèles en tout cas. Mais il y a tous ceux de passage qui veulent juste une recette immédiate à appliquer, peu importe si elle est bonne ou non. Alors je le répète la création d’une bonne App c’est avant tout une bonne UX et l’UX est à la croisée des chemins entre pragmatisme, psychologie, design, art, technique et éthique. Et une bonne décision d’UX impose d’avoir une réflexion poussée dans tous ces domaines à la fois. Si mes papiers ne reflètent pas cet effort alors ils n’ont aucun intérêt. Publier un livre de recette n’est pas ma tasse de thé désolé. Je cherche à faire passer toujours beaucoup plus que la simple recette technique que j’expose. Donc c’est long. Tant pis pour ceux qui veulent du McDo à emporter, ici on ne sert qu’à table en prenant le temps d’expliquer ce que le chef à mis dans la sauce ! Dot.Blog se veut à la programmation ce que la slow-food est à l’alimentation …

Mise en oeuvre

Bon, maintenant que nous savons ce que nous voulons faire et pourquoi nous voulons le faire, encore faut-il savoir comment nous allons l’implémenter.

J’ai laissé un gros indice dans le sous titre… Nous allons utiliser un Trigger d’action. (Action Trigger).

Là aussi il faut justifier son choix car il y a plusieurs façon d’arriver au même résultat. Par exemple créer un userControl. mais je n’aime pas la profusion de ces contrôles personnalisés difficiles à maintenir quand d’autres solutions sont applicables, ce qui est le cas. On peut aussi utiliser des custom Renderers, des Effects, des behaviors… ces derniers se rapprochent beaucoup de la solution du jour, les précédents sont trop bas niveau à mon goût. Alors pourquoi pas un behavior ? Parce qu’ici j’essaye aussi de vous faire voir des choses que vous utilisez trop peu… Et les Action Triggers en font partie. Donc plutôt qu’un behavior dont j’ai déjà parlé plusieurs fois, lançons nous dans l’utilisation moins fréquente des Action Triggers !

Voilà… La solution visuelle et pratique, l’UX, a été validée et justifiée et le choix d’implémentation aussi, on peut enfin, et seulement, envisager de coder… C’est ça l’informatique. Programmer en sautant ces deux étapes c’est sauter en parachute sans vérifier que ce qu’on a dans le dos ce n’est pas le sac à dos avec le duvet et la gourde mais bien le parachute…

Event Trigger et Action Trigger

Un Event Trigger est un déclencheur (trigger) associé à un événement précis (event). Il déclenche donc l’exécution d’un code lorsqu’un événement ciblé se produit. XAML est plein d’événements associés à des changements d’état des contrôles (TextChanged sur un Entry quand le texte est modifié par exemple).

Un Event Trigger se crée en utilisant la classe TriggerAction<ViewType>

Cette dernière impose l’écriture d’un code pour sa méthode Invoke (invocation), et c’est ce que code qui exécutera la partie utile quand l’événement ciblé se produira. On note aussi que le TriggerAction est de type générique et qu’il ne peut porter que sur une classe de View précise.

C’est lors de l’utilisation du code TriggerAction<ViewType> en XAML que nous déclarerons le nom de l’événement qui sera ausculté. Cette information n’est pas codée dans le trigger lui-même ce qui permet de créer des actions réutilisables selon des conditions de déclenchement différentes.

Cible du trigger

Nous pourrions ciblé la classe Entry puisque c’est sur un élément de ce type que nous voulons agir. Mais en réalité nous souhaitons afficher un bouton de type image montrant classiquement un oeil ouvert ou barré selon que le mot de passe est caché ou non. C’est donc finalement sur ImageButton que nous allons coder le trigger et non sur le Entry.

Alors pourquoi tout simplement ne pas coder le tap du bouton ?

Bonne question, qui rallonge les explications mais qui se doit d’être traitée… encore une fois navré pour les pressés !

Où mettriez-vous le code ?

Ceux qui pensent trop MVVM diront dans le ViewModel. Mais c’est une erreur, ici nous parlons d’un code qui manipule l’UI uniquement et qui n’a rien à faire dans le ViewModel.

Ceux qui pensent avec fierté avoir échappé à ce piège en répondant dans le code-behind commettent aussi une erreur. Pas la même. Ici c’est le problème de maintenance et de réutilisation du code qui est soulevé. DRY! Don’t Repeat Yourself. Ne Vous Répétez Pas ! Principe élémentaire qu’on retrouve dans le principe plus général SOLID (que j’ai traité, cherchez dans les archives).

En effet, à chaque fois qu’un mot de passe devra être saisi il faudra recopier le code, c’est terriblement sale.

En plaçant le code utile dans un Trigger, code placé dans son fichier C# facilement réutilisable dans plein de projets, dans une librairie commune etc, nous nous assurons d’un seul point de maintenance (en cas de débogue ou d’évolution) et nous évitons les copies inutiles, nous respectons DRY. Et nous ne coderons du visuel que sous XAML, exploitant parfaitement la séparation code / visuel propre à MVVM.

ImageButton

C’est la vue que nous utiliserons, en fin de zone de l’Entry. C’est un simple bouton mais qui supporte une image au lieu d’un texte. Et nous allons utiliser deux images ici, l’une par défaut, oeil fermé (mot de passe caché) et l’autre, oeil ouvert (mot de passe visible) qui sera positionnée quand le trigger est actif.

La page de test

Je vais utiliser ici la page par défaut du projet de base  : MainPage.xaml, une ContentPage.

Le code complet de cette page sera le suivant :

<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
              xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
              xmlns:d="http://xamarin.com/schemas/2014/forms/design"
              xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
              mc:Ignorable="d"
              x:Class="ShowHidePasswordTrigger.MainPage"
              xmlns:local="clr-namespace:ShowHidePasswordTrigger">
     <StackLayout VerticalOptions="Center"
                  Padding="20">
         <Grid>
          <Entry Placeholder="Password"
                 IsPassword="{Binding Source={x:Reference ShowPasswordActualTrigger},
Path=HidePassword}"/>

         <ImageButton VerticalOptions="Center"
                       Margin="0,0,10,0"
                      HeightRequest="20"
                      HorizontalOptions="End"
                      Source="ic_eye_hide">
                          <ImageButton.Triggers>
                               <EventTrigger Event="Clicked">
                                    <local:ShowPasswordTriggerAction
ShowIcon="ic_eye"
                                      HideIcon="ic_eye_hide"
                                      x:Name="ShowPasswordActualTrigger"/>
                                </EventTrigger>
                         </ImageButton.Triggers>
           </ImageButton>
         </Grid>
     </StackLayout>
</ContentPage>

Cette page reste ultra simple et ne contient que le nécessaire au test. On y trouve le Entry dont la propriété “IsPassword” est bindée en local par un “x:Reference” à notre trigger et plus particulièrement à sa propriété “HidePassword”. C’est une technique parmi d’autres. Les références locales peuvent parfois être très utiles et leur syntaxe n’est pas toujours maîtrisée, c’est donc l’occasion de la montrer.

On trouve ensuite l’ImageButton avec son image source “ic_eye_hide”, l’oeil fermé ou barré selon les icônes qu’on utilise. Car par défaut le mot de passe est caché. Mais c’est dans le corps de la déclaration de l’ImageButton que se cache l’utilisation du trigger : Dans la propriété Triggers de l’ImageButton on ajoute un EventTrigger qui auscultera l’événement “Clicked”. Et ensuite nous précision la classe de notre Action Trigger avec ses paramètres (le nom des deux images à utiliser).

Et c’est tout. Le reste va être réalisé par le trigger.

Le Trigger

Voici le code du Trigger. Les assoiffés de code vont enfin se rassasier Smile 

using System.ComponentModel;
using Xamarin.Forms;

namespace ShowHidePasswordTrigger
{
     public class ShowPasswordTriggerAction : TriggerAction<ImageButton>, INotifyPropertyChanged
     {
         public string ShowIcon { get; set; }
         public string HideIcon { get; set; }

        private bool hidePassword = true;

         public bool HidePassword
         {
             set
             {
                 if (hidePassword == value) return;
                 hidePassword = value;
                 PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HidePassword)));
             }
             get => hidePassword;
         }

         protected override void Invoke(ImageButton sender)
         {
             HidePassword = !HidePassword;             sender.Source = HidePassword ? HideIcon : ShowIcon;

         }

         public event PropertyChangedEventHandler PropertyChanged;
    } }

Ce code est terriblement simple aussi. Il déclare deux propriété qui serviront à recevoir le nom des deux images (ShowIcon et HideIcon) et expose une propriété HidePassword qui joue le rôle de bascule pour cacher ou montrer le mot de passe. C’est cette propriété qui est référencée par le Entry. Notre trigger est donc bien le pilote. Il ne fait pas qu’échanger les images de l’ImageButton, il contient aussi l’état associé à la visibilité du mot de passe. Il prend donc en charge 100% de la fonction qui lui est attribué. C’est un détail important. Il n’est pas rare de voir ce genre d’information se balader ici et là. Le principe de responsabilité unique et localisée est essentiel à respecter.

On voit la méthode Invoke héritée de TriggerAction<> . Elle se contente d’inverser la valeur de HidePassword puis d’assigner à l’ImageButton (sender) l’icone qui convient.

Visuel

Caché

image

Visible

image

Conclusion

Comme d’habitude la solution technique proposée aujourd’hui est surtout l’occasion d’insister sur des fondamentaux de la programmation respectueuse des règles du métier et de l’utilisateur. Mais la bonne utilisation des Trigger d’action est souvent négligée et en voir une mise en œuvre pourra, je l’espère, vous donner envie de vous en servir plus…

Stay Tuned !

blog comments powered by Disqus