Dot.Blog

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

Xamarin.Forms : Ordre de saisie des Entry

Une application pro est une application qui répond à de nombreux critères mais l’un des premiers est la gestion du Focus ! Voyons cela de plus près…

Le Focus

C’est tout bête le focus tellement qu’on n’y prête pas assez attention. Et pourtant ! C’est l’un des critères les plus importants pour juger si une application est pro ou non, si elle est terminée ou pas. Et même de façon inconsciente.

Un exemple du quotidien, depuis un moment le site web “Deepl” (un traducteur de bonne qualité, un peu mieux que Google je trouve) incite à installer une application équivalente de type Windows Store. J’ai fini par craqué pour voir. Anecdotiquement on notera qu’appeler et charger l’appli UWP prend autant de temps que d’appeler le site web ce qui rend la chose assez inutile (pour le reste les temps de réponse sont les mêmes car ils imposent des aller-retours sur leur serveur). Mais là n’est pas la question. L’appli s’affiche on est content. Et là on commence tout de suite à taper son texte à traduire ou bien on fait un Ctrl-V. Et ? Deux possibilités : soit rien ne se passe, soit il se passe des trucs louches.

Raison : L’appli s’affiche mais Windows lui donne mal le focus ou bien l’appli le gère mal, peu importe. Et retenez bien ce dicton totalement inventé “Focus mal géré, Appli pétée !”

Je pourrai vous parler de milles autres applications qui gèrent mal le focus : le premier champ focussé n’est pas le premier champ de saisie, quand on valide une entrée ça ne bouge pas ou ça va n’importe où, etc..

L’effet sur l’utilisateur est désastreux. Moi c’est pire, je sais comment c’est fait, ou plutôt pas fait, et j’ai envie de fouetter les développeurs et toute leur chaîne de commandement jusqu’au big boss pour n’avoir pas fait leur boulot ! (N’oublions jamais que si les mauvais développeurs existent ce n’est jamais à cause d’eux qu’un soft est mauvais, c’est toujours de la faute de la hiérarchie qui n’est pas fait son travail de sélection à l’embauche, de surveillance, de supervision, de contrôle de qualité, etc. A gros salaire, grosses responsabilités j’aime ajouter… histoire d’aller à contre-courant des gros bonnets qui aiment rejeter la faute sur les gars qui sont en dessous…).

Bref si dans la vie on évite les focus (en deux mots), en informatique on a intérêt à les soigner !

Première leçon à retenir : faites en sorte que le premier champ ayant le focus dans une page soit le plus pertinent.

L’ordre de saisie

Lorsque plusieurs champs s’enchaînent dans une saisie, type “nom”, “prénom”, “adresse” etc… là aussi le focus a intérêt à être bien géré !

Ce n’est plus la nature statique du focus qui nous intéresse ici mais sa dynamique au contraire. Comment il se promène de champ en champ.

Les environnements de développement proposent en général un moyen de fixer l’ordre des contrôles visuels dans la chaîne des focus. Sous Xamarin.Forms on peut être tenté d’utiliser la propriété TabIndex par exemple qui appartient à VisualElement donc à une classe assez haute dans la hiérarchie des objets visuels.

Toutefois cela ne serait s’appliquer convenablement qu’aux environnements possédant une touche TAB donc un clavier donc les PC ou les Mac.

De plus il semble qu’il y ait des bugs dans le support Android de cette fonction.

Il faut certainement l’utiliser pour les environnements qui le supportent mais ce n’est pas suffisant (même une fois le bug corrigé).

En effet on ne change pas forcément de champ avec TAB même sur un PC. Une saisie se termine le plus souvent par le réflexe d’appuyer sur Entrer ou son équivalent sur le clavier virtuel Android ou iOS.

C’est là qu’entre en scène la ReturnCommand de l’objet Entry

C’est une ICommand, donc elle peut exécuter n’importe quel code sur l’appui de Entrer (ou son équivalent virtuel). Ce qui est très pratique dans pas mal de situations.

Mais on peut aussi s’en servir pour s’assurer que la dynamique du focus sera correcte, que l’ordre de saisie sera celui qui a un sens…

S’agissant d’un problème d’affichage on peut parfaitement utiliser le code-behind pour cela.

Exemple

Prenons le code XAML 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" 
             x:Class="samples.core.Views.EntryMoveNextView" 
             xmlns:ctrl="clr-namespace:samples.core.Controls;assembly=samples.core">
    <ContentPage.Resources>
        <ResourceDictionary>
            <Style x:Name="MyEntry" TargetType="Entry">
                <Setter Property="HeightRequest" Value="45" />
            </Style>
        </ResourceDictionary>
    </ContentPage.Resources>
    <ContentPage.Content>
        <StackLayout Padding="15,25">
            <Entry x:Name="Entry_First" ReturnType="Next" Placeholder="First Entry" HeightRequest="45"/>
            <Entry x:Name="Entry_Second" ReturnType="Next" Placeholder="Second Entry" HeightRequest="45"/>
            <Entry x:Name="Entry_Third" ReturnType="Done" Placeholder="Third Entry" HeightRequest="45"/>
            <BoxView/>
            <Button Text="Save" VerticalOptions="End"/>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

Et codons dans le code-behind les quelques lignes suivantes (après l’initialisation des contrôles) :

using System;
using System.Collections.Generic;

using Xamarin.Forms;

namespace samples.core.Views
{
    public partial class EntryMoveNextView : ContentPage
    {
        public EntryMoveNextView()
        {
            InitializeComponent();

            Entry_First.ReturnCommand = new Command(() => Entry_Second.Focus());
            Entry_Second.ReturnCommand = new Command(() => Entry_Third.Focus());
        }
    }
}

Dès lors, l’appui de Entrer (virtuel ou non) sur le premier et le second Entry entraînera le passage du focus au suivant selon un ordre établi et régulé par le développeur lui-même.

Et cela peut se compliquer un peu. Car l’avantage de contrôler ce changement par le code permet de s’adapter à des conditions changeantes. Si on imagine une fiche Contact qui demande par exemple le nom du conjoint si on a coché “marié” il est évident que ce champ devient le suivant dans la chaîne de focus après la case à cocher, mais si la case n’est pas cochée il est plus ergonomique de faire “sauter” le focus au champ d’après. Faire s’arrêter la saisie sur “nom du conjoint” si on vient juste de dire qu’on n’a pas de conjoint… qu’en dire… Sortez le fouet !

Un Entry adapté

Taper du code n’est jamais une bonne idée, on augmente systématiquement les chances d’introduire un bug… Mais comme certains besoins existent (comme celui exposé plus haut) le seul moyen d’éviter de répéter sans cesse le code consiste à fédérer tout cela d’une façon ou d’une autre. Behaviors ou autre astuces sont de bons candidats, mais ils peuvent être verbeux en XAML et de fait on ne gagne pas grand chose…

Ici le plus simple est de créer un Entry qui sait gérer la commande de passage au suivant de telle façon à pouvoir mettre uniquement que le nécessaire dans la balise XAML du contrôle (ou dans le code behind).

Cela pourrait donner cela :

using System;
using System.Runtime.CompilerServices;
using Xamarin.Forms;

namespace samples.core.Controls
{
    public class EntryMoveNextControl : Entry
    {
        public static readonly BindableProperty NextEntryProperty = BindableProperty.Create(nameof(NextEntry), typeof(View), typeof(Entry));
        public View NextEntry
        {
            get => (View)GetValue(NextEntryProperty);
            set => SetValue(NextEntryProperty, value);
        }

        protected override void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            base.OnPropertyChanged(propertyName);

            this.Completed += (sender, e) => 
            {
                this.OnNext();
            };
        }

        public void OnNext()
        {
            NextEntry?.Focus();
        }
    }
}

Rien de bien extravagant, on créer une classe dérivée de Entry et on lui ajoute une propriété de dépendance “NextEntry” de type “View”.

Si on conserve la logique d’un code-behind (on a vu que cela pouvait être utile pour s’auto adapter à des conditions de saisie changeantes) on écrira maintenant derrière l’initialisation de la page :

Entry_First.NextEntry = Entry_Second;
Entry_Second.NextEntry = Entry_Third;

C’est vraiment très simple.

Et bien entendu pour des cas sans variante ni adaptation on écrira alors tout cela directement dans la balise XAML de chaque Entry concerné.

Jouer avec le clavier virtuel

Une fois qu’on en est arrivé à ce niveau de détail on peut vouloir aller un cran plus loin : s’assurer que sur le clavier virtuel la touche Entrée est bien représentée par Entrée ou par l’invitation à aller “au suivant” (Next / Done selon les claviers par exemple).

On peut utiliser des Effects Xamarin.Forms pour cela. L’effet sera attaché à chaque Entry modifié possédant une propriété NextEntry (code ci-dessus).

Sous Android cela donnera :

using System;
using samples.core.Controls;
using samples.Droid.Effects;
using Xamarin.Forms;
using Xamarin.Forms.Platform.Android;

[assembly: ExportEffect(typeof(EntryMoveNextEffect), nameof(EntryMoveNextEffect))]
namespace samples.Droid.Effects
{
    public class EntryMoveNextEffect : PlatformEffect
    {
        protected override void OnAttached()
        {
            // Check if the attached element is of the expected type and has the NextEntry
            // property set. if so, configure the keyboard to indicate there is another entry
            // in the form and the dismiss action to focus on the next entry
            if (base.Element is EntryMoveNextControl xfControl && xfControl.NextEntry != null)
            {
                var entry = (Android.Widget.EditText)Control;

                entry.ImeOptions = Android.Views.InputMethods.ImeAction.Next;
                entry.EditorAction += (sender, args) =>
                {
                    xfControl.OnNext();
                };
            }
        }

        protected override void OnDetached()
        {
            // Intentionally empty
        }
    }
}

Sous iOS c’est un peu plus long :

using System;
using CoreGraphics;
using samples.core.Controls;
using samples.iOS.Effects;
using UIKit;
using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: ExportEffect(typeof(EntryMoveNextEffect), nameof(EntryMoveNextEffect))]
namespace samples.iOS.Effects
{
    public class EntryMoveNextEffect : PlatformEffect
    {
        protected override void OnAttached()
        {
            var entry = (UITextField)Control;

            // Change return key to Don key
            entry.ReturnKeyType = UIReturnKeyType.Done;

            // Add the "done" button to the toolbar easily dismiss the keyboard
            // when it may not have a return key
            if (entry.KeyboardType == UIKeyboardType.DecimalPad || 
                entry.KeyboardType == UIKeyboardType.NumberPad)
            {
                entry.InputAccessoryView = BuildDismiss();
            }

            // Check if the attached element is of the expected type and has the NextEntry
            // property set. if so, configure the keyboard to indicate there is another entry
            // in the form and the dismiss action to focus on the next entry
            if (base.Element is EntryMoveNextControl xfControl && xfControl.NextEntry != null)
            {
                entry.ReturnKeyType = UIReturnKeyType.Next;
            }
        }

        protected override void OnDetached()
        {
            // Intentionally empty
        }

        private UIToolbar BuildDismiss()
        {
            var toolbar = new UIToolbar(new CGRect(0.0f, 0.0f, Control.Frame.Size.Width, 44.0f));

            // Set default state of buttonAction/appearance
            UIBarButtonItem buttonAction = new UIBarButtonItem(UIBarButtonSystemItem.Done, delegate { Control.ResignFirstResponder(); });

            // If we have a next element, swap out the default state for "Next"
            if (base.Element is EntryMoveNextControl xfControl && xfControl.NextEntry != null)
            {
                buttonAction = new UIBarButtonItem("Next", UIBarButtonItemStyle.Plain, delegate
                {
                    xfControl.OnNext();
                });
            }

            toolbar.Items = new[]
            {
                new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace),
                buttonAction,
            };

            return toolbar;
        }
    }
}

Conclusion

Des choses aussi simples que le focus, sa dynamique ou l’adaptation du clavier virtuel permettent de faire du premier coup d’œil la différence entre un bricolage d’amateur et une application terminée de niveau pro.

Peu importe le code proposé ici, ce ne sont que des exemples pour illustrer le propos, à vous de réfléchir à comment vous souhaitez gérer le focus dans vos apps et quel code vous allez écrire pour le faire selon vos critères et ceux de vos utilisateurs.

Mais une chose est sûre : ne négligez pas la gestion de focus !

et … Stay Tuned !

blog comments powered by Disqus