La propriété BindableLayout sur un exemple concret

Added by Martin Toutant 4 months ago

Objectif

L’objectif de ce tutoriel est de se familiariser avec l’élément BindableLayout pour afficher, avec beaucoup de liberté, les éléments d’une liste.
Au cours de ce tutoriel nous allons créer l’interface suivante:

Nous allons travailler avec le modèle Animal qui possède trois propriétés :
  • Id (int)
  • Elevage (string)
  • Date de traitement (DateTime).

La première page affiche cette liste d’animaux avec l’id et l’élevage, et lors du tap sur un des animaux, une nouvelle page comportant toutes les informations est affichée.

Création du projet

.
├── BindableLayoutSample
│   ├── App.xaml
│   ├── App.xaml.cs
│   ├── AssemblyInfo.cs
│   ├── BindableLayoutSample.csproj
│   ├── DAO
│   │   └── AnimalDAO.cs
│   ├── Model
│   │   └── Animal.cs
│   ├── View
│   │   ├── AnimalDetailsView.xaml
│   │   │   └── AnimalDetailsView.xaml.cs
│   │   └── AnimalListView.xaml
│   │       └── AnimalListView.xaml.cs
│   └── ViewModel
│       ├── AnimalDetailsViewModel.cs
│       ├── AnimalItemViewModel.cs
│       ├── AnimalListViewModel.cs
│       └── Base
│           └── ViewModelBase.cs
└── BindableLayoutSample.Android
    └── ...
La première étape est de créer une application vide Xamarin.Forms.
  • Supprimer les fichiers Main.xaml et Main.xaml.cs, c'est une page par défaut dont on ne se servira pas
  • Créer les répertoires DAO, Model, View, ViewModel et le sous-répertoire Base dans ViewModel
  • Créer le modèle Animal :
    // Le modèle Animal avec ses 3 propriétés et accesseurs
    public class Animal
    {
        public int AnimalID { get; set; }
        public string Elevage { get; set; }     
        public DateTime DateTraitement { get; set; }
    }
    
  • Créer l’objet AnimalDAO :
    // La classe d'accès aux données sur les animaux
    public class AnimalDAO
    {
        public async Task<List<Animal>> GetAllAnimals()
        {
            // Génération de "fausses" données
            List<Animal> ret = new List<Animal>()
            {
                new Animal()
                {
                    AnimalID = 27852,
                    DateTraitement = DateTime.Parse("2022-08-05 14:26"),
                    Elevage = "PEIMA" 
                },
                new Animal()
                {
                    AnimalID = 2421,
                    DateTraitement = DateTime.Parse("2022-08-05 12:33"),
                    Elevage = "PEIMA" 
                },
                new Animal()
                {
                    AnimalID = 13569,
                    DateTraitement = DateTime.Parse("2022-08-06 08:14"),
                    Elevage = "Lees-Athas" 
                },
                new Animal()
                {
                    AnimalID = 45555,
                    DateTraitement = DateTime.Parse("2022-08-07 16:21"),
                    Elevage = "Donzacq" 
                },
                new Animal()
                {
                    AnimalID = 8956,
                    DateTraitement = DateTime.Parse("2022-08-07 18:01"),
                    Elevage = "PEIMA" 
                },
            };
    
            // Simulation d'attente d'accès aux données (WS, BDD, ...)
            await Task.Delay(1000);
            return ret;
        }
    }
    
  • Créer les Views et ViewModels en ajoutant public avant chaque déclaration mais en les laissant vides (pour l’instant…)
  • Faire le binding pour chaque View - ViewModel:

Dans AnimalDetailsView.xaml.cs:

public AnimalDetailsView()
{
    InitializeComponent();
    BindingContext = new AnimalDetailsViewModel();
}

Dans AnimalListView.xaml.cs:
public AnimalListView()
{
    InitializeComponent();
    BindingContext = new AnimalListViewModel();
}

A noter que nous avons un 3ème ViewModel AnimalViewModel qui n’a pas de View liée : c’est normal, on s’en occupera plus tard.
  • Implémenter le ViewModelBase qui implémente INPC :
    public class ViewModelBase : INotifyPropertyChanged
    {
        // Propriété de notification de changement
        public event PropertyChangedEventHandler PropertyChanged;
    
        // Procédure de changement de valeur
        // Déclenche la fonction de notification
        protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyname = null)
        {
            if (Equals(storage, value))
            {
                return false;
            }
    
            storage = value;
            OnPropertyChanged(propertyname);
            return true;
        }
    
        // Fonction de notification 
        // Déclenche l'évènement de changement de valeur
        // Avec en argument le nom de la propriété qui a changé
        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    
  • Et finalement : définir la MainPage dans App.xaml.cs
    public App()
    {
        InitializeComponent();
    
        // Point d'entrée de l'application
        MainPage = new NavigationPage(new AnimalListView());
    }
    

La page d’accueil et chargement des données

Création de la liste

Dans le XAML, nous allons itérer sur une liste d’animaux que l'on nommera AnimalList. Cette liste contient les données que l’on souhaite afficher. On devra donc définir comment ces informations doivent être affichées. De plus, sur chaque tap on veut naviguer vers une page de détail de l’animal concerné.

Tout d'abord, on souhaite itérer sur AnimalList depuis le fichier XAML. Chaque objet de liste comportera des éléments auxquels on pourra se lier à l’aide d’un Binding. En utilisant un Binding, on doit utiliser un nouveau ViewModel pour chaque objet de la liste (les Views ne peuvent être bindées qu’à des ViewModels, c’est la règle MVVM!).
On obtient donc plusieurs ViewModel liés à la même page… et c’est valide ! La règle étant qu’un ViewModel ne doit être lié qu’à une seule page, rien ne nous empêche d’avoir plusieurs ViewModel par page, et c’est même la manière de faire pour notre problématique.

Pour pouvoir utiliser le Binding et ajouter la fonctionnalité de navigation sans toucher au modèle animal, nous allons créer un nouveau ViewModel : AnimalItemViewModel. Ce ViewModel comportera les informations de l’animal ainsi que la commande de navigation vers la page de détail de cet animal.

Le code de AnimalItemViewModel.cs donne donc :

public class AnimalItemViewModel : ViewModelBase
{
    private Animal animal;

    // Les infos de l'animal
    public Animal Animal
    {
        get => animal;
        set => SetProperty(ref animal, value);
    }

    // La commande de navigation est asynchrone,
    // Pour ne pas bloquer le Thread principal en charge de l'affichage
    // pendant l'instanciation de la nouvelle Page
    private async Task NavigateToAnimalDetails()
    {
        // Petite attente pour rendre la transition plus fluide
        await Task.Delay(150);

        // L'utilisation de la navigation page nous permet d'avoir une navigation verticale (push & pop)
        await Application.Current.MainPage.Navigation.PushAsync(new AnimalDetailsView(Animal));
    }

    public ICommand NavigateToAnimalCommand { private set; get; }

    public AnimalItemViewModel(Animal _animal)
    {
        Animal = _animal;
        NavigateToAnimalCommand = new Command(async () => await NavigateToAnimalDetails());
    }

}

On instancie ensuite une liste observable (ObservableCollection) de ce modèle. Cette liste est ensuite alimentée avec les données de chaque animal chargé.

Et comment ça se passe du côté de l’affichage (le XAML) ?

On veut créer une liste dans le XAML liée à notre AnimalList et en même temps gérer l’affichage en itérant chaque élément de la liste. Pour cela on utilise la propriété BindableLayout de la balise StackLayout.

En liant la propriété BindableLayout.ItemSource à une liste observable (d’où ObservableCollection), on peut itérer sur notre liste dans le XAML et gérer l’affichage pour chaque itération.
Grâce à l'enchaînement de balises suivantes on peut personnaliser l’affichage de chaque élément:

<StackLayout Padding="0,5" BindableLayout.ItemsSource="{Binding AnimalList}">
    <!--
        On entre dans une itération sur BindableLayout.ItemSource donc AnimalList
    -->
    <BindableLayout.ItemTemplate>
        <!--
            Dans le dataTemplate, le BindingContext est lié à l'AnimalItemViewModel
            de l'itération
        -->
        <DataTemplate>
            <!--
                Lors du "tap", on utilise la commande de navigation définie
                dans AnimalItemViewModel
            -->
            <Grid
                Padding="5" 
                xe:Commands.Tap="{Binding NavigateToAnimalCommand}" 
                xe:TouchEffect.Color="#4b6da6" 
                ColumnDefinitions="*,2*" 
                RowDefinitions="Auto, Auto" 
                RowSpacing="0">
                <!--
                    Les infos qu'on cherche se trouve dans l'objet Animal de
                    AnimalItemViewModel
                -->
                <Label
                    Grid.Row="0" 
                    Grid.Column="0" 
                    FontAttributes="Bold" 
                    HorizontalOptions="CenterAndExpand" 
                    Text="{Binding Animal.AnimalID}" 
                    TextColor="DarkCyan" 
                    VerticalOptions="CenterAndExpand" />
                <Label
                    Grid.Row="0" 
                    Grid.Column="1" 
                    FontSize="Medium" 
                    HorizontalOptions="CenterAndExpand" 
                    Text="{Binding Animal.Elevage}" 
                    TextColor="DarkGray" />
            </Grid>
        </DataTemplate>
    </BindableLayout.ItemTemplate>
</StackLayout>

Dans la balise DataTemplate, le BindingContext change, au lieu d’être lié à la page AnimalListView, il s’attache à l’élément traité par l’itération sur BindableLayout.ItemSource donc à chaque AnimalItemViewModel. Ce fonctionnement est représenté sur le diagramme ci-dessous:

Le reste de l'application pour obtenir les pages des captures d'écran ci-dessus est du développement Xamarin, décrit dans d'autres tutoriels et formations de ce wiki.
L'ensemble du code source est disponible sur ce dépôt Git : https://forge-dga.jouy.inra.fr/projects/xamarin/repository/tuto_bindablelayout
L'adresse du .git est : https://forge-dga.jouy.inra.fr/git/xamarin.tuto_bl.git

orga_bc_700.png (169 KB)

Resultat_700.png (205 KB)