Objectif Dans cet exercice, vous allez apprendre à créer une application MAUI qui change dynamiquement l'affichage des drapeaux en fonction de la langue sélectionnée par l'utilisateur.
Resources/Images:
-
Ajoutez vos images de drapeaux dans le dossier Resources/Images du projet.
-
Les images doivent avoir les noms suivants :
- france.png
- angleterre.png
- espagne.png
- allemagne.png
Resources/Raw/Locales:
Créez les fichiers:
- en.json
- fr.json
- es.json
- de.json
en.json:
{
"FormTitle": "Form",
"nomEntryPlaceholder": "Name",
"prenomEntryPlaceholder": "First Name",
"ageEntryPlaceholder": "Age",
"dateNaissanceEntryPlaceholder": "Date of Birth mm/dd/yyyy",
"LanguagePickerTitle": "Choose a language",
"CountryPickerTitle": "Country of Birth",
"ReservationPickerTitle": "Reservation Type",
"CancelButtonText": "Cancel",
"ApplyButtonText": "Apply",
"Image": "angleterre.png",
"DisplayAlertTitle": "Reservation confirmation",
"DisplayAlertBody": "Do you want to confirm the reservation?",
"DisplayAlertOK": "OK",
"DisplayAlertCancel": "Cancel"
}fr.json:
{
"FormTitle": "Formulaire",
"nomEntryPlaceholder": "Nom",
"prenomEntryPlaceholder": "Prénom",
"ageEntryPlaceholder": "Âge",
"dateNaissanceEntryPlaceholder": "Date de naissance jj/mm/aaaa",
"LanguagePickerTitle": "Choisir une langue",
"CountryPickerTitle": "Pays de naissance",
"ReservationPickerTitle": "Type de réservation",
"CancelButtonText": "Annuler",
"ApplyButtonText": "Réserver",
"Image": "france.png",
"DisplayAlertTitle": "Confirmation de réservation",
"DisplayAlertBody": "Voulez vous confirmer la reservation?",
"DisplayAlertOK": "D'accord'",
"DisplayAlertCancel": "Annulation"
}es.json:
{
"FormTitle": "Formulario",
"nomEntryPlaceholder": "Nombre",
"prenomEntryPlaceholder": "Apellido",
"ageEntryPlaceholder": "Edad",
"dateNaissanceEntryPlaceholder": "Fecha de nacimiento dd/mm/aaaa",
"LanguagePickerTitle": "Elige un idioma",
"CountryPickerTitle": "País de nacimiento",
"ReservationPickerTitle": "Tipo de reserva",
"CancelButtonText": "Cancelar",
"ApplyButtonText": "Reservar",
"Image": "espagne.png",
"DisplayAlertTitle": "Confirmación de reserva",
"DisplayAlertBody": "¿Quieres confirmar la reserva?",
"DisplayAlertOK": "Vale",
"DisplayAlertCancel": "Cancelar"
}de.json:
{
"FormTitle": "Formular",
"nomEntryPlaceholder": "Name",
"prenomEntryPlaceholder": "Vorname",
"ageEntryPlaceholder": "Alter",
"dateNaissanceEntryPlaceholder": "Geburtsdatum tt/mm/jjjj",
"LanguagePickerTitle": "Sprache wählen",
"CountryPickerTitle": "Geburtsland",
"ReservationPickerTitle": "Reservierungsart",
"CancelButtonText": "Abbrechen",
"ApplyButtonText": "Reservieren",
"Image": "allemagne.png",
"DisplayAlertTitle": "Reservierungsbestätigung",
"DisplayAlertBody": "Möchten Sie die Reservierung bestätigen?",
"DisplayAlertOK": "Ya",
"DisplayAlertCancel": "Stornieren"
}Il faut ensuite présenter ces fichiers comme des ressources intégrées à l'application:
Ajouter une configuration dans le fichier *.csproj pour rendre ces fichiers comme resources intégrées:
<ItemGroup>
<EmbeddedResource Include="Resources\Raw\locales\de.json" />
<EmbeddedResource Include="Resources\Raw\locales\en.json" />
<EmbeddedResource Include="Resources\Raw\locales\es.json" />
<EmbeddedResource Include="Resources\Raw\locales\fr.json" />
</ItemGroup>Créez un dossier Services dans votre projet et ajoutez un fichier LocalizationService.cs.
**LocalizationService.cs: **
using System.Reflection;
using System.Text.Json;
public static class LocalizationService
{
private static Dictionary<string, string> _localizedResources;
public static async Task LoadLocalizationResourcesAsync(string languageCode)
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = $"MauiApp1.Resources.Raw.locales.{languageCode}.json";
using (Stream stream = assembly.GetManifestResourceStream(resourceName))
{
if (stream == null)
{
throw new FileNotFoundException($"Resource '{resourceName}' not found.");
}
using (StreamReader reader = new StreamReader(stream))
{
var json = await reader.ReadToEndAsync();
_localizedResources = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
}
}
}
public static string Translate(string key)
{
return _localizedResources.TryGetValue(key, out var value) ? value : key;
}
}using MauiApp1.Services;
[ContentProperty(nameof(Key))]
public class TranslateExtension : IMarkupExtension
{
public string Key { get; set; }
public object ProvideValue(IServiceProvider serviceProvider)
{
if (Key == null)
return "";
return LocalizationService.Translate(Key);
}
}Ajouter ces lignes pour definir la culture de l'application et initialiser le service de localisation
var cultureInfo = CultureInfo.CurrentCulture;
LocalizationService.LoadLocalizationResourcesAsync(cultureInfo.TwoLetterISOLanguageName).Wait(); <ContentPage.Resources>
<local:TranslateExtension x:Key="Translate" />
</ContentPage.Resources>
<ScrollView>
<StackLayout Padding="20">
<!-- Grand titre -->
<Label Text="{local:Translate Key=FormTitle}"
FontSize="36"
HorizontalOptions="Center" />
<!-- Image du pays -->
<Image x:Name="flagImg" Source="{local:Translate Key=image}" VerticalOptions="Center" HorizontalOptions="Center"
HeightRequest="200" WidthRequest="200" />
<!-- Picker pour les langues -->
<Picker x:Name="langPkr"
SelectedIndexChanged="langPkr_SelectedIndexChanged"
Title="{local:Translate Key=LanguagePickerTitle}">
<Picker.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>Langue française</x:String>
<x:String>English language</x:String>
<x:String>Lingua Española</x:String>
<x:String>Deutsche Sprache</x:String>
</x:Array>
</Picker.ItemsSource>
</Picker>
<!-- Champs du formulaire -->
<Entry x:Name="nomEntry" Placeholder="{local:Translate Key=nomEntryPlaceholder}" />
<Entry x:Name="prenomEntry" Placeholder="{local:Translate Key=prenomEntryPlaceholder}" />
<Entry x:Name="ageEntry" Placeholder="{local:Translate Key=ageEntryPlaceholder}" Keyboard="Numeric" />
<Entry x:Name="dateniassancePkr" Placeholder="{local:Translate Key=dateNaissanceEntryPlaceholder}" />
<!-- Picker pour le pays de naissance -->
<Picker x:Name="paysnaissancePkr" Title="{local:Translate Key=CountryPickerTitle}">
<Picker.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>France</x:String>
<x:String>Allemagne</x:String>
<x:String>Espagne</x:String>
<!-- Ajoutez plus de pays ici -->
</x:Array>
</Picker.ItemsSource>
</Picker>
<!-- Picker pour la réservation -->
<Picker x:Name="reservationPkr" Title="{local:Translate Key=ReservationPickerTitle}">
<Picker.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>VIP</x:String>
<x:String>Standard</x:String>
</x:Array>
</Picker.ItemsSource>
</Picker>
<!-- Time Picker pour le temps de réservation -->
<TimePicker />
<!-- Boutons -->
<StackLayout Orientation="Horizontal" HorizontalOptions="CenterAndExpand">
<Button x:Name="btnCancel" Text="{local:Translate Key=CancelButtonText}" Margin="5" Clicked="btnCancel_Clicked" />
<Button x:Name="btnApply" Text="{local:Translate Key=ApplyButtonText}" Margin="5" Clicked="btnApply_Clicked" />
</StackLayout>
</StackLayout>
</ScrollView> Explication du code:
Ce code XAML définit la mise en page d'une page de contenu dans une application MAUI avec des ressources localisées et une interface utilisateur interactive. Voici une explication brève de ce qu'il fait :
Définition des Ressources:
<ContentPage.Resources>
<local:TranslateExtension x:Key="Translate" />
</ContentPage.Resources>
Déclare une ressource nommée Translate, utilisant une extension de balisage personnalisée TranslateExtension pour gérer la traduction des textes dans l'interface utilisateur.
Contenu de la Page:
<ScrollView>
<StackLayout Padding="20">Grand Titre:
<Label Text="{local:Translate Key=FormTitle}"
FontSize="36"
HorizontalOptions="Center" />Affiche un grand titre, dont le texte est traduit dynamiquement en utilisant la clé FormTitle, avec une taille de police de 36 et centré horizontalement.
Image du Pays:
<Image x:Name="flagImg" Source="{local:Translate Key=image}" VerticalOptions="Center" HorizontalOptions="Center"
HeightRequest="200" WidthRequest="200" />
Affiche une image de drapeau, dont la source est traduite dynamiquement avec la clé image. L'image est centrée horizontalement et verticalement, avec des dimensions de 200x200.
Picker pour les Langues: Déclare un Picker pour sélectionner la langue, avec un événement déclenché lors du changement de sélection (SelectedIndexChanged). Le titre du Picker est traduit dynamiquement.
<Picker x:Name="langPkr"
SelectedIndexChanged="langPkr_SelectedIndexChanged"
Title="{local:Translate Key=LanguagePickerTitle}">
<Picker.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>Langue française</x:String>
<x:String>English language</x:String>
<x:String>Lingua Española</x:String>
<x:String>Deutsche Sprache</x:String>
</x:Array>
</Picker.ItemsSource>
</Picker>Champs du Formulaire: Définit plusieurs champs de saisie (Entry) pour le nom, le prénom, l'âge et la date de naissance, avec des placeholders traduits dynamiquement.
<Entry x:Name="nomEntry" Placeholder="{local:Translate Key=nomEntryPlaceholder}" />
<Entry x:Name="prenomEntry" Placeholder="{local:Translate Key=prenomEntryPlaceholder}" />
<Entry x:Name="ageEntry" Placeholder="{local:Translate Key=ageEntryPlaceholder}" Keyboard="Numeric" />
<Entry x:Name="dateniassancePkr" Placeholder="{local:Translate Key=dateNaissanceEntryPlaceholder}" />
Picker pour le Pays de Naissance: Picker pour sélectionner le pays de naissance avec des options prédéfinies et un titre traduit dynamiquement.
<Picker x:Name="paysnaissancePkr" Title="{local:Translate Key=CountryPickerTitle}">
<Picker.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>France</x:String>
<x:String>Allemagne</x:String>
<x:String>Espagne</x:String>
</x:Array>
</Picker.ItemsSource>
</Picker>
Picker pour la Réservation:
Picker pour sélectionner le type de réservation, avec des options "VIP" et "Standard" et un titre traduit dynamiquement.
<Picker x:Name="reservationPkr" Title="{local:Translate Key=ReservationPickerTitle}">
<Picker.ItemsSource>
<x:Array Type="{x:Type x:String}">
<x:String>VIP</x:String>
<x:String>Standard</x:String>
</x:Array>
</Picker.ItemsSource>
</Picker>Time Picker pour le Temps de Réservation: Un sélecteur de temps (TimePicker) pour choisir l'heure de la réservation.
<TimePicker />Boutons:
Deux boutons, Annuler et Appliquer, avec des textes traduits dynamiquement et des événements de clic associés.
<StackLayout Orientation="Horizontal" HorizontalOptions="CenterAndExpand">
<Button x:Name="btnCancel" Text="{local:Translate Key=CancelButtonText}" Margin="5" Clicked="btnCancel_Clicked" />
<Button x:Name="btnApply" Text="{local:Translate Key=ApplyButtonText}" Margin="5" Clicked="btnApply_Clicked" />
</StackLayout>
using System.Globalization;
using System.Text.Json;
using MauiApp1.Services;
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
protected override void OnAppearing()
{
base.OnAppearing();
flagImg.Source= ImageSource.FromFile("angleterre.png");
}
async private void btnApply_Clicked(object sender, EventArgs e)
{
// Récupérer les données du formulaire
var formData = new
{
Nom = nomEntry.Text,
Prenom = prenomEntry.Text,
Age = ageEntry.Text,
DateNaissance = dateniassancePkr.Text,
Langue = langPkr.SelectedItem as string,
PaysNaissance = paysnaissancePkr.SelectedItem as string,
Reservation = reservationPkr.SelectedItem as string
};
// Convertir les données en JSON
string jsonString = JsonSerializer.Serialize(formData);
// Afficher un message de confirmation
var result = await DisplayAlert(LocalizationService.Translate("DisplayAlertTitle"),
LocalizationService.Translate("DisplayAlertBody"),
LocalizationService.Translate("DisplayAlertOK"),
LocalizationService.Translate("DisplayAlertCancel"));
if(result==true)
{
// Définir le chemin du fichier
var filePath = Path.Combine(FileSystem.Current.AppDataDirectory, $"formData{formData.GetHashCode()}.json");
// Écrire les données dans le fichier
await File.WriteAllTextAsync(filePath, jsonString);
}
else
{
// Vider les champs d'entrée
nomEntry.Text = string.Empty;
prenomEntry.Text = string.Empty;
ageEntry.Text = string.Empty;
dateniassancePkr.Text = string.Empty;
langPkr.SelectedIndex = -1;
paysnaissancePkr.SelectedIndex = -1;
reservationPkr.SelectedIndex = -1;
}
}
private void btnCancel_Clicked(object sender, EventArgs e)
{
// Vider les champs d'entrée
nomEntry.Text = string.Empty;
prenomEntry.Text = string.Empty;
ageEntry.Text = string.Empty;
dateniassancePkr.Text = string.Empty;
langPkr.SelectedIndex = -1;
paysnaissancePkr.SelectedIndex = -1;
reservationPkr.SelectedIndex = -1;
}
async private void langPkr_SelectedIndexChanged(object sender, EventArgs e)
{
Picker picker = sender as Picker;
string selectedLanguageCode = "en"; // Default to English if something goes wrong
switch (picker.SelectedIndex)
{
case 0:
selectedLanguageCode = "fr";
break;
case 1:
selectedLanguageCode = "en";
break;
case 2:
selectedLanguageCode = "es";
break;
case 3:
selectedLanguageCode = "de";
break;
}
// Change the current culture
CultureInfo.CurrentCulture = new CultureInfo($"{selectedLanguageCode}-{selectedLanguageCode.ToUpper()}");
// Load localization resources for the selected language
await LocalizationService.LoadLocalizationResourcesAsync(selectedLanguageCode);
// Update the UI
UpdateUI();
}
ImageSource imageSrc;
private void UpdateUI()
{
// Update the placeholders and titles with translated text
nomEntry.Placeholder = LocalizationService.Translate("nomEntryPlaceholder");
prenomEntry.Placeholder = LocalizationService.Translate("prenomEntryPlaceholder");
ageEntry.Placeholder = LocalizationService.Translate("ageEntryPlaceholder");
dateniassancePkr.Placeholder = LocalizationService.Translate("dateNaissanceEntryPlaceholder");
langPkr.Title = LocalizationService.Translate("LanguagePickerTitle");
paysnaissancePkr.Title = LocalizationService.Translate("CountryPickerTitle");
reservationPkr.Title = LocalizationService.Translate("ReservationPickerTitle");
btnCancel.Text = LocalizationService.Translate("CancelButtonText");
btnApply.Text = LocalizationService.Translate("ApplyButtonText");
// Update the flag image
string flagImageName = LocalizationService.Translate("Image");
flagImg.Source = ImageSource.FromFile(flagImageName);
// You may need to refresh other parts of the UI here
}
}-
Constructeur MainPage() :
- Initialise la page en appelant InitializeComponent(), qui charge les composants définis dans le fichier XAML associé.
-
Méthode OnAppearing() :
- Override de la méthode OnAppearing() qui est appelée lorsque la page devient visible. Ici, elle configure l'image du drapeau par défaut (angleterre.png).
-
Méthode btnApply_Clicked :
- Méthode asynchrone déclenchée lorsque le bouton "Appliquer" est cliqué.
- Récupère les données du formulaire et les convertit en objet anonyme.
- Sérialise cet objet en JSON.
- Affiche une boîte de dialogue de confirmation localisée.
- Si l'utilisateur confirme, les données sont enregistrées dans un fichier JSON dans le répertoire d'application.
- Si l'utilisateur annule, les champs du formulaire sont vidés.
- Méthode asynchrone déclenchée lorsque le bouton "Appliquer" est cliqué.
-
Méthode btnCancel_Clicked :
- Vider les champs du formulaire lorsque le bouton "Annuler" est cliqué.
-
Méthode langPkr_SelectedIndexChanged :
- Méthode asynchrone déclenchée lorsque l'utilisateur change la langue sélectionnée dans le Picker de langue.
-
Définit la culture courante en fonction de la langue sélectionnée.
- Charge les ressources de localisation pour la langue sélectionnée.
- Met à jour l'interface utilisateur (UI).
-
Méthode UpdateUI :
- Met à jour les éléments de l'interface utilisateur avec les textes traduits :
-
Placeholders des champs de saisie.
- Titres des Picker.
- Textes des boutons.
- Image du drapeau correspondant à la langue sélectionnée.
Dans le cas de confirmation les données seront sauvegardées selon la plateforme: Sauvegarde des fichiers
-
Sur un appareil Android : Le fichier sera enregistré dans le répertoire interne de l'application, ce qui est inaccessible aux autres applications. Vous pouvez le trouver dans un répertoire similaire à /data/data/com.yourapp/files/ (le chemin exact peut varier).
-
Sur un appareil iOS : Le fichier sera enregistré dans le répertoire de documents de l'application. Vous pouvez le trouver dans le répertoire Documents de l'application, qui est accessible via les outils de débogage Xcode ou d'autres moyens d'accès aux fichiers de l'application.
-
Sur un appareil Windows : Le fichier sera enregistré dans le répertoire des données d'application de l'utilisateur. Vous pouvez le trouver dans le chemin de répertoires des applications de l'utilisateur sous AppData\Local ou AppData\Roaming.
Parfait. Tu souhaites maintenant transformer entièrement cette application MAUI localisée — initialement conçue de façon impérative (code-behind) — en une architecture MVVM complète et propre, tout en préservant les éléments suivants :
TranslateExtensiondynamique déjà améliorée.LanguageToFlagConverterpour l’image du drapeau.- Interaction locale + internationalisation (
*.json). - UI multi-plateforme avec champs à remplir et bouton d’action.
Voici un plan clair, complet et structuré pour effectuer la transformation.
Porter tous les x:Name.Text, .SelectedItem, .Placeholder, Clicked, etc. dans un ObservableObject ou INotifyPropertyChanged.
dotnet add package CommunityToolkit.Mvvmusing System.Collections.ObjectModel;
using System.Globalization;
using System.Text.Json;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace MauiApp1.ViewModels;
public partial class MainViewModel : ObservableObject
{
public ObservableCollection<string> Languages { get; } = new()
{
"Langue française", "English language", "Lingua Española", "Deutsche Sprache"
};
public ObservableCollection<string> Countries { get; } = new()
{
"France", "Allemagne", "Espagne"
};
public ObservableCollection<string> ReservationTypes { get; } = new()
{
"VIP", "Standard"
};
[ObservableProperty] private int selectedLanguageIndex;
[ObservableProperty] private int selectedCountryIndex;
[ObservableProperty] private int selectedReservationIndex;
[ObservableProperty] private string nom;
[ObservableProperty] private string prenom;
[ObservableProperty] private string age;
[ObservableProperty] private string dateNaissance;
public MainViewModel()
{
LoadLanguage("en");
}
partial void OnSelectedLanguageIndexChanged(int value)
{
string langCode = value switch
{
0 => "fr",
1 => "en",
2 => "es",
3 => "de",
_ => "en"
};
CultureInfo.CurrentCulture = new CultureInfo($"{langCode}-{langCode.ToUpper()}");
LoadLanguage(langCode);
}
private async void LoadLanguage(string code)
{
await LocalizationService.LoadLocalizationResourcesAsync(code);
}
[RelayCommand]
private void Cancel()
{
Nom = Prenom = Age = DateNaissance = string.Empty;
SelectedCountryIndex = -1;
SelectedReservationIndex = -1;
}
[RelayCommand]
private async Task Apply()
{
var formData = new
{
Nom,
Prenom,
Age,
DateNaissance,
Langue = Languages[SelectedLanguageIndex],
PaysNaissance = SelectedCountryIndex >= 0 ? Countries[SelectedCountryIndex] : null,
Reservation = SelectedReservationIndex >= 0 ? ReservationTypes[SelectedReservationIndex] : null
};
var result = await Shell.Current.DisplayAlert(
LocalizationService.Translate("DisplayAlertTitle"),
LocalizationService.Translate("DisplayAlertBody"),
LocalizationService.Translate("DisplayAlertOK"),
LocalizationService.Translate("DisplayAlertCancel"));
if (result)
{
var filePath = Path.Combine(FileSystem.Current.AppDataDirectory, $"formData{formData.GetHashCode()}.json");
await File.WriteAllTextAsync(filePath, JsonSerializer.Serialize(formData));
}
else
{
Cancel();
}
}
}- Retire tous les
x:Namesauf si nécessaires. - Remplace les
.Text,.SelectedIndex, etc. par desBinding. - L’image du drapeau reste liée au
SelectedLanguageIndex.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:MauiApp1.ViewModels"
xmlns:local="clr-namespace:MauiApp1"
x:Class="MauiApp1.MainPage">
<ContentPage.Resources>
<local:TranslateExtension x:Key="Translate" />
<local:LanguageToFlagConverter x:Key="LanguageToFlagConverter" />
</ContentPage.Resources>
<ContentPage.BindingContext>
<vm:MainViewModel />
</ContentPage.BindingContext>
<ScrollView>
<StackLayout Padding="20">
<Label Text="{local:Translate Key=FormTitle}"
FontSize="36"
HorizontalOptions="Center" />
<Image HeightRequest="200"
WidthRequest="200"
HorizontalOptions="Center"
VerticalOptions="Center"
Source="{Binding SelectedLanguageIndex, Converter={StaticResource LanguageToFlagConverter}}" />
<Picker Title="{local:Translate Key=LanguagePickerTitle}"
ItemsSource="{Binding Languages}"
SelectedIndex="{Binding SelectedLanguageIndex, Mode=TwoWay}" />
<Entry Placeholder="{local:Translate Key=nomEntryPlaceholder}"
Text="{Binding Nom, Mode=TwoWay}" />
<Entry Placeholder="{local:Translate Key=prenomEntryPlaceholder}"
Text="{Binding Prenom, Mode=TwoWay}" />
<Entry Placeholder="{local:Translate Key=ageEntryPlaceholder}"
Text="{Binding Age, Mode=TwoWay}"
Keyboard="Numeric" />
<Entry Placeholder="{local:Translate Key=dateNaissanceEntryPlaceholder}"
Text="{Binding DateNaissance, Mode=TwoWay}" />
<Picker Title="{local:Translate Key=CountryPickerTitle}"
ItemsSource="{Binding Countries}"
SelectedIndex="{Binding SelectedCountryIndex, Mode=TwoWay}" />
<Picker Title="{local:Translate Key=ReservationPickerTitle}"
ItemsSource="{Binding ReservationTypes}"
SelectedIndex="{Binding SelectedReservationIndex, Mode=TwoWay}" />
<TimePicker /> <!-- libre ou à lier -->
<StackLayout Orientation="Horizontal" HorizontalOptions="CenterAndExpand">
<Button Text="{local:Translate Key=CancelButtonText}"
Command="{Binding CancelCommand}"
Margin="5" />
<Button Text="{local:Translate Key=ApplyButtonText}"
Command="{Binding ApplyCommand}"
Margin="5" />
</StackLayout>
</StackLayout>
</ScrollView>
</ContentPage>Supprimer tout le code lié aux événements (Clicked, SelectedIndexChanged), car tout est maintenant dans le ViewModel.