• Facile

Ce cours est visible gratuitement en ligne.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 29/04/2014

Notre première application

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Nous avons désormais tous les outils qu’il faut pour commencer à apprendre à réaliser des applications pour Windows Phone.
Nous avons pu voir que ces outils fonctionnent et nous avons pu constater un début de résultat grâce à l’émulateur. Mais pour bien comprendre et assimiler toutes les notions, nous avons besoin de plus de concret, de manipuler des éléments et de voir qu’est-ce qui exactement agit sur quoi. Aussi, il est temps de voir comment réaliser une première application avec le classique « Hello World » ! ;)
Cette première application va nous servir de base pour commencer à découvrir ce qu’il faut pour réaliser des applications pour Windows Phone.

Hello World

Revenons donc sur notre écran où nous avons le designer et le XAML. Positionnons-nous sur le code XAML et ajoutons des éléments sans trop comprendre ce que nous allons faire afin de réaliser notre « Hello World ». Nous reviendrons ensuite sur ce que nous avons fait pour expliquer le tout en détail.

Pour commencer, on va modifier la ligne suivante :

<TextBlock Text="nom de la page" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>

et écrire ceci :

<TextBlock Text="Hello World" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>

Nous changeons donc la valeur de l’attribut Text de la balise <TextBlock>.

Ensuite, juste après :

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">

Donc, à l’intérieur de cette balise <Grid>, rajoutez :

<StackPanel>
    <TextBlock Text="Saisir votre nom" HorizontalAlignment="Center" />
    <TextBox x:Name="Nom"  />
    <Button Content="Valider" Tap="Button_Tap_1" />
    <TextBlock x:Name="Resultat" Foreground="Red" />
</StackPanel>

Remarquez que lorsque vous avez saisi la ligne :

<Button Content="Valider" Tap="Button_Tap_1" />

au moment de taper : Tap="", Visual Studio Express vous a proposé de générer un nouveau gestionnaire d’événement (voir la figure suivante).

Génération du gestionnaire d'événement depuis le XAML
Génération du gestionnaire d'événement depuis le XAML

Vous pouvez le générer en appuyant sur Entrée, cela nous fera gagner du temps plus tard. Sinon, ce n’est pas grave.
Vous pouvez constater que le designer s’est mis à jour pour faire apparaître nos modifications (voir la figure suivante).

Le rendu du XAML dans la fenêtre du concepteur
Le rendu du XAML dans la fenêtre du concepteur

Ouvrez maintenant le fichier de code behind et modifiez la méthode Button_Tap_1 pour avoir :

private void Button_Tap_1(object sender, System.Windows.Input.GestureEventArgs e)
{
    Resultat.Text = "Bonjour " + Nom.Text;
}

Nous pouvons dès à présent démarrer notre application en appuyant sur F5. L’émulateur se lance et nous voyons apparaître les nouvelles informations sur l’écran (voir la figure suivante).

Rendu de l'application dans l'émulateur
Rendu de l'application dans l'émulateur

La souris va ici permettre de simuler le « toucher » avec le doigt lorsque nous cliquons. Cliquons donc dans la zone de texte et nous voyons apparaître un clavier virtuel à l’intérieur de notre application (voir la figure suivante).

Le clavier virtuel dans l'émulateur
Le clavier virtuel dans l'émulateur

Ce clavier virtuel s’appelle le SIP (pour Soft Input Panel) et apparait automatiquement quand on en a besoin, notamment dans les zones de saisie, nous y reviendrons. Saisissons une valeur dans la zone de texte et cliquons sur le bouton Valider.
Nous voyons apparaître le résultat en rouge avec un magnifique message construit (voir la figure suivante).

Affichage de l'Hello World dans l'émulateur
Affichage de l'Hello World dans l'émulateur

Et voilà, notre Hello World est terminé ! Chouette non ?
Pour quitter l’application, le plus simple est d’arrêter le débogueur de Visual Studio en cliquant sur le carré Stop.

L’interface en XAML

Alors, taper des choses sans rien comprendre, ça va un moment… mais là, il est temps de savoir ce que nous avons fait !

Nous avons dans un premier temps fait des choses dans le XAML. Pour rappel, le XAML sert à décrire le contenu de ce que nous allons voir à l’écran. En fait, un fichier XAML correspond à une page. Une application peut être découpée en plusieurs pages, nous y reviendrons plus tard. Ce que nous avons vu sur l’émulateur est l’affichage de la page MainPage.

Donc, nous avons utilisé le XAML pour décrire le contenu de la page. Il est globalement assez explicite mais ce qu’il faut comprendre c’est que nous avons ajouté des contrôles du framework .NET dans la page. Un contrôle est une classe C# complète qui sait s’afficher, se positionner, traiter des événements de l’utilisateur (comme le clic sur le bouton), etc. Ces contrôles ont des propriétés et peuvent être ajoutés dans le XAML.
Par exemple, prenons la ligne suivante :

<TextBlock Text="Saisir votre nom" HorizontalAlignment="Center" />

Nous demandons d’ajouter dans la page le contrôle TextBlock. Le contrôle TextBlock correspond à une zone de texte non modifiable. Nous positionnons sa propriété Text à la chaine de caractères "Saisir votre nom". Ce contrôle sera aligné au centre grâce à la propriété HorizontalAlignment positionnée à Center. En fait, j’ai dit que nous l’ajoutons dans la page, mais pour être plus précis, nous l’ajoutons dans le contrôle StackPanel qui est lui-même contenu dans le contrôle Grid, qui est contenu dans la page. Nous verrons plus loin ce que sont ces contrôles.

Derrière, automatiquement, cette ligne de XAML entraîne la déclaration et l’instanciation d’un objet de type TextBlock avec les affectations de propriétés adéquates. Puis ce contrôle est ajouté dans le contrôle StackPanel. Tout ceci nous est masqué. Grâce au XAML nous avons simplement décrit l’interface de la page et c’est Visual Studio qui s’est occupé de le transformer en C#.
Parfait ! Moins on en fait et mieux on se porte… et surtout il y a moins de risque d’erreurs.

Et c’est pareil pour tous les autres contrôles de la page, le TextBlock qui est une zone de texte non modifiable, le TextBox qui est une zone de texte modifiable déclenchant l’affichage du clavier virtuel, le bouton, etc.

Vous l’aurez peut-être deviné, mais c’est pareil pour la page. Elle est déclarée tout en haut du fichier XAML :

<phone:PhoneApplicationPage 
    x:Class="HelloWorld.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
    xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    SupportedOrientations="Portrait" Orientation="Portrait"
    mc:Ignorable="d" d:DesignHeight="768" d:DesignWidth="480"
    shell:SystemTray.IsVisible="True">

C’est d’ailleurs le conteneur de base du fichier XAML, celui qui contient tous les autres contrôles. La page est en fait représentée par la classe PhoneApplicationPage qui est aussi un objet du framework .NET. Plus précisément, notre page est une classe générée qui dérive de l’objet PhoneApplicationPage. Il s’agit de la class MainPage située dans l’espace de nom HelloWorld, c’est ce que l’on voit dans la propriété :

x:Class="HelloWorld.MainPage"

On peut s’en rendre compte également dans le code behind de la page où Visual Studio a généré une classe partielle du même nom que le fichier XAML et qui dérive de PhoneApplicationPage :

public partial class MainPage : PhoneApplicationPage
{
    // Constructeur
    public MainPage()
    {
        InitializeComponent();
    }

    private void Button_Tap_1(object sender, System.Windows.Input.GestureEventArgs e)
    {
        Resultat.Text = "Bonjour " + Nom.Text;
    }
}

Pourquoi partielle ? Parce qu’il existe un autre fichier dans votre projet. Ce fichier est caché mais on peut l’afficher en cliquant sur le bouton en haut de l’explorateur de solution (voir la figure suivante).

Affichage des fichiers cachés dans l'explorateur de solutions
Affichage des fichiers cachés dans l'explorateur de solutions

Et nous pouvons voir notamment un répertoire obj contenant un répertoire debug contenant le fichier MainPage.g.i.cs. Si vous l’ouvrez, vous pouvez trouver le code suivant :

public partial class MainPage : Microsoft.Phone.Controls.PhoneApplicationPage 
{
    internal System.Windows.Controls.Grid LayoutRoot;
    internal System.Windows.Controls.StackPanel TitlePanel;
    internal System.Windows.Controls.Grid ContentPanel;
    internal System.Windows.Controls.TextBox Nom;
    internal System.Windows.Controls.TextBlock Resultat;
        
    private bool _contentLoaded;
        
    /// <summary>
    /// InitializeComponent
    /// </summary>
    [System.Diagnostics.DebuggerNonUserCodeAttribute()]
    public void InitializeComponent() 
    {
        if (_contentLoaded) 
        {
            return;
        }
        _contentLoaded = true;
        System.Windows.Application.LoadComponent(this, new System.Uri("/HelloWorld;component/MainPage.xaml", System.UriKind.Relative));
        this.LayoutRoot = ((System.Windows.Controls.Grid)(this.FindName("LayoutRoot")));
        this.TitlePanel = ((System.Windows.Controls.StackPanel)(this.FindName("TitlePanel")));
        this.ContentPanel = ((System.Windows.Controls.Grid)(this.FindName("ContentPanel")));
        this.Nom = ((System.Windows.Controls.TextBox)(this.FindName("Nom")));
        this.Resultat = ((System.Windows.Controls.TextBlock)(this.FindName("Resultat")));
    }
}

Il s’agit d’une classe qui est générée lorsqu’on modifie le fichier XAML. Ne modifiez pas ce fichier car il sera re-généré tout le temps. On peut voir qu’il s’agit d’une classe MainPage, du même nom que la propriété x:Class de tout à l’heure, qui s’occupe de charger le fichier XAML et qui crée des variables à partir des contrôles qu’il trouvera dedans.
Nous voyons notamment qu’il a créé les deux variables suivantes :

internal System.Windows.Controls.TextBox Nom;
internal System.Windows.Controls.TextBlock Resultat;

Le nom de ces variables correspond aux propriétés x:Name des deux contrôles que nous avons créé :

<TextBox x:Name="Nom"  />
<TextBlock x:Name="Resultat" Foreground="Red" />

Ces variables sont initialisées après qu’il ait chargé tout le XAML en faisant une recherche à partir du nom du contrôle. Cela veut dire que nous disposons d’une variable qui permet d’accéder au contrôle de la page, par exemple la variable Nom du type TextBox. Je vais y revenir.
Nous avons donc :

  • Un fichier MainPage.xaml qui contient la description des contrôles

  • Un fichier généré qui contient une classe partielle qui dérive de PhoneApplicationPage et qui charge ce XAML et qui rend accessible nos contrôles via des variables

  • Un fichier de code behind qui contient la même classe partielle où nous pourrons écrire la logique de notre code

Remarquez qu’il existe également le fichier MainPage.g.cs qui correspond au fichier généré après la compilation. Nous ne nous occuperons plus de ces fichiers générés, ils ne servent plus à rien. Nous les avons regardés pour comprendre comment cela fonctionne.

Le code-behind en C#

Revenons sur le code behind, donc sur le fichier MainPage.xaml.cs, nous avons :

public partial class MainPage : PhoneApplicationPage
{
    // Constructeur
    public MainPage()
    {
        InitializeComponent();
    }

    private void Button_Tap_1(object sender, System.Windows.Input.GestureEventArgs e)
    {
        Resultat.Text = "Bonjour " + Nom.Text;
    }
}

On retrouve bien notre classe partielle qui hérite des fonctionnalités de la classe PhoneApplicationPage. Regardez à l’intérieur de la méthode Button_Tap_1, nous utilisons les fameuses variables que nous n’avons pas déclaré nous-même mais qui ont été générées… Ce sont ces variables qui nous permettent de manipuler nos contrôles et en l’occurrence ici, qui nous permettent de modifier la valeur de la zone de texte non modifiable en concaténant la chaine « Bonjour » à la valeur de la zone de texte modifiable, accessible via sa propriété Text.
Vous aurez compris ici que ce sont les propriétés Text des TextBlock et TextBox qui nous permettent d’accéder au contenu qui est affiché sur la page. Il existe plein d’autres propriétés pour ces contrôles comme la propriété Foreground qui permet de modifier la couleur du contrôle, sauf qu’ici nous l’avions positionné grâce au XAML :

<TextBlock x:Name="Resultat" Foreground="Red" />

Chose que nous aurions également pu faire depuis le code behind :

private void Button_Tap_1(object sender, System.Windows.Input.GestureEventArgs e)
{
    Resultat.Foreground = new SolidColorBrush(Colors.Red);
    Resultat.Text = "Bonjour " + Nom.Text;
}

Sachez quand même que d’une manière générale, on aura tendance à essayer de mettre le plus de chose possible dans le XAML plutôt que dans le code behind. La propriété Foreground ici a tout intérêt à être déclarée dans le XAML.

Le contrôle Grid

Je vais y revenir plus loin un peu plus loin, mais pour que vous ne soyez pas complètement perdu dans notre Hello World, il faut savoir que la Grid est un conteneur.

Après cet effort de traduction intense, nous pouvons dire que la grille sert à contenir et à agencer d’autres contrôles. Dans notre cas, le code suivant :

<Grid x:Name="LayoutRoot" Background="Transparent">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
        <TextBlock Text="MON APPLICATION" Style="{StaticResource PhoneTextNormalStyle}" Margin="12,0"/>
        <TextBlock Text="Hello World" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
    </StackPanel>

    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
        <StackPanel>
            <TextBlock Text="Saisir votre nom" HorizontalAlignment="Center" />
            <TextBox x:Name="Nom"  />
            <Button Content="Valider" Tap="Button_Tap_1" />
            <TextBlock x:Name="Resultat" Foreground="Red" />
        </StackPanel>
    </Grid>
</Grid>

Défini une grille qui contient deux lignes. La première contient un contrôle StackPanel, nous allons en parler juste après. La seconde ligne contient une nouvelle grille sans ligne ni colonne, qui est également composée d’un StackPanel.
Nous aurons l’occasion d’en parler plus longuement plus tard donc je m’arrête là pour l’instant sur la grille.

Le contrôle StackPanel

Ici c’est pareil, le contrôle StackPanel est également un conteneur. Je vais y revenir un peu plus loin également mais il permet ici d’aligner les contrôles les uns en dessous des autres. Par exemple, celui que nous avons rajouté contient un TextBlock, un TextBox, un bouton et un autre TextBlock :

<StackPanel>
    <TextBlock Text="Saisir votre nom" HorizontalAlignment="Center" />
    <TextBox x:Name="Nom"  />
    <Button Content="Valider" Tap="Button_Tap_1" />
    <TextBlock x:Name="Resultat" Foreground="Red" />
</StackPanel>

Nous pouvons voir sur le designer que les contrôles sont bien les uns en dessous des autres.
Nous avons donc au final, la page qui contient une grille, qui contient un StackPanel et une grille qui contiennent chacun des contrôles.

Le contrôle TextBox

Le contrôle TextBox est une zone de texte modifiable. Nous l’avons utilisée pour saisir le prénom de l’utilisateur. On déclare ce contrôle ainsi :

<TextBox x:Name="Nom" />

Lorsque nous cliquons dans la zone de texte, le clavier virtuel apparait et nous offre la possibilité de saisir une valeur. Nous verrons un peu plus loin qu’il est possible de changer le type du clavier virtuel.
La valeur saisie est récupérée via la propriété Text du contrôle, par exemple ici je récupère la valeur saisie que je concatène à la chaine Bonjour et que je stocke dans la variable resultat :

string resultat = "Bonjour " + Nom.Text;

Inversement, je peux pré-remplir la zone de texte avec une valeur en utilisant la propriété Text, par exemple depuis le XAML :

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <StackPanel>
        <TextBox x:Name="Nom" Text="Nicolas" />
    </StackPanel>
</Grid>

Ce qui donne la figure suivante.

La valeur du TextBox s'affiche dans la fenêtre de rendu
La valeur du TextBox s'affiche dans la fenêtre de rendu

La même chose est faisable en code behind, il suffit d’initialiser la propriété de la variable dans le constructeur de la page :

public partial class MainPage : PhoneApplicationPage
{
    public MainPage()
    {
        InitializeComponent();
        Nom.Text = "Nicolas";
    }
}

Évidemment, il sera toujours possible de modifier la valeur pré-remplie grâce au clavier virtuel.

Le contrôle TextBlock

Le contrôle TextBlock représente une zone de texte non modifiable. Nous l’avons utilisé pour afficher le résultat de notre Hello World. Il suffit d’utiliser sa propriété Text pour afficher un texte. Par exemple, le XAML suivant :

<TextBlock Text="Je suis un texte non modifiable de couleur rouge" Foreground="Red" FontSize="25" />

affiche la fenêtre de prévisualisation présentée dans la figure suivante.

Le texte s'affiche en rouge dans le TextBlock
Le texte s'affiche en rouge dans le TextBlock

Je peux modifier la couleur du texte grâce à la propriété Foreground. C’est la même chose pour la taille du texte, modifiable via la propriété FontSize. Nous pouvons remarquer que le texte que j’ai saisi dépasse de l’écran et que nous ne le voyons pas en entier. Pour corriger ça, j’utilise la propriété TextWrapping que je positionne à Wrap :

<TextBlock Text="Je suis un texte non modifiable de couleur rouge" Foreground="Red" FontSize="25" TextWrapping="Wrap" />

Comme nous l’avons déjà fait, il est possible de modifier la valeur d’un TextBlock en passant par le code-behind :

Resultat.Text = "Bonjour " + Nom.Text;

Les événements

Il s’agit des événements sur les contrôles. Chaque contrôle est capable de lever une série d’événements lorsque cela est opportun. C’est le cas par exemple du contrôle bouton qui est capable de lever un événement lorsque nous tapotons dessus (ou que nous cliquons avec la souris). Nous l’avons vu dans l’exemple du Hello World, il suffit de déclarer une méthode que l’on associe à l’événement, par exemple :

<Button Content="Valider" Tap="Button_Tap_1" />

qui permet de faire en sorte que la méthode Button_Tap_1 soit appelée lors du clic sur le bouton. Rappelez-vous, dans notre Hello World, nous avions la méthode suivante :

private void Button_Tap_1(object sender, System.Windows.Input.GestureEventArgs e)
{
    Resultat.Text = "Bonjour " + Nom.Text;
}

Il est également possible de s’abonner à un événement via le code behind, il suffit d’avoir une variable de type bouton, pour cela donnons un nom à un bouton :

<Button x:Name="UnBouton" Content="Cliquez-moi" />

Et d’associer une méthode à l’événement de clic :

public partial class MainPage : PhoneApplicationPage
{
    public MainPage()
    {
        InitializeComponent();

        UnBouton.Tap += UnBouton_Tap;
    }

    void UnBouton_Tap(object sender, System.Windows.Input.GestureEventArgs e)
    {
        throw new NotImplementedException();
    }
}

Il existe beaucoup d’événements de ce genre, par exemple la case à cocher (CheckBox) permet de s’abonner à l’événement qui est déclenché lorsqu’on coche la case :

<CheckBox Content="Cochez-moi" Checked="CheckBox_Checked_1" />

Avec la méthode :

private void CheckBox_Checked_1(object sender, RoutedEventArgs e)
{

}

Il existe énormément d’événement sur les contrôles, mais aussi sur la page, citons encore par exemple l’événement qui permet d’être notifié lors de la fin du chargement de la page :

public MainPage()
{
    InitializeComponent();
    Loaded += MainPage_Loaded;
}

private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
    throw new NotImplementedException();
}

Nous aurons l’occasion de voir beaucoup d’autres événements tout au long de ce cours.

Remarquez que les événements sont toujours construits de la même façon. Le premier paramètre est du type object et représente le contrôle qui a déclenché l’événement. En l’occurrence, dans l’exemple suivant :

<Button Content="Valider" Tap="Button_Tap_1" />

Nous pouvons accéder au contrôle de cette façon :

private void Button_Tap_1(object sender, System.Windows.Input.GestureEventArgs e)
{
    Button bouton = (Button)sender;
    bouton.Content = "C'est validé !";
}

Le second paramètre est, quant à lui, spécifique au type d’événement et peut fournir des informations complémentaires.

Le bouton

Revenons à présent rapidement sur le bouton, nous l’avons vu il n’est pas très compliqué à utiliser. On utilise la propriété Content pour mettre du texte et il est capable de lever un événement lorsqu’on clique dessus, grâce à l’événement Tap. Le bouton possède également un événement Click qui fait la même chose et qui existe encore pour des raisons de compatibilité avec Silverlight.

Il est également possible de passer des paramètres à un bouton. Pour un bouton tout seul, ce n’est pas toujours utile, mais dans certaines situations cela peut être primordial.
Dans l’exemple qui suit, j’utilise deux boutons qui ont la même méthode pour traiter l’événement de clic sur le bouton :

<StackPanel>
    <Button Content="Afficher" Tap="Button_Tap" CommandParameter="Nicolas" />
    <Button Content="Afficher" Tap="Button_Tap" CommandParameter="Jérémie" />
    <TextBlock x:Name="Resultat" Foreground="Red" />
</StackPanel>

C’est la propriété CommandParameter qui me permet de passer un paramètre. Je pourrais ensuite l’utiliser dans mon code behind :

private void Button_Tap(object sender, System.Windows.Input.GestureEventArgs e)
{
    Button bouton = (Button)sender;
    bouton.IsEnabled = false;
    Resultat.Text = "Bonjour " + bouton.CommandParameter;
}

J’utilise ainsi le paramètre CommandParameter pour récupérer le prénom de la personne à qui dire bonjour (voir la figure suivante).

Passage d'un paramètre au bouton s'affichant dans l'émulateur
Passage d'un paramètre au bouton s'affichant dans l'émulateur

Remarquez au passage l’utilisation de la propriété IsEnabled qui permet d’indiquer si un contrôle est activé ou pas. Si un bouton est désactivé, il ne pourra pas être cliqué.

Et Silverlight dans tout ça ?

Vous avez remarqué que j’ai parlé de Silverlight et de XAML. Quelle différence ?

Pour bien comprendre, Silverlight était utilisé pour développer avec les versions 7 de Windows Phone. On utilise par contre le XAML/C# pour développer pour la version 8. En fait, grosso modo c’est la même chose.

XAML est l’évolution de Silverlight. Si vous avez des connaissances en Silverlight, vous vous êtes bien rendu compte que ce qu’on appelle aujourd’hui XAML/C#, c’est pareil.
Il s’agit juste d’un changement de vocabulaire afin d’unifier les développements utilisant du code XAML pour définir l’interface d’une application, qu’elle soit Windows Phone ou Windows …

Ce qui est valable avec Silverlight l'est aussi avec XAML/C#, et inversement proportionnel.

  • Le XAML permet de décrire l’interface de nos pages.

  • Le code behind permet d’écrire le code C# de la logique de nos pages.

  • On utilise des contrôles dans nos interfaces, comme le bouton ou la zone de texte.

  • Les contrôles sont des classes complètes qui savent s’afficher, se positionner ou réagir à des événements utilisateurs, comme le clic sur un bouton.

Exemple de certificat de réussite
Exemple de certificat de réussite