Typeconverter : Les convertisseurs de types

Les convertisseurs de types

Introduction

Les convertisseurs de types permettent de convertir un type vers un ou plusieurs autres et inversement. Il suffit que le convertisseur sache gérer ces types. Plus spécifiquement, en mode Design, on convertit un type depuis et vers une chaîne pour fournir et traiter le texte d'une propriété dans la fenêtre des propriétés. Un convertisseur de type est construit pour gérer un type et le convertir vers et depuis d'autres types.

L'interface IConvertible

L'interface IConvertible permet à un type qui l'implémente de se convertir vers d'autres types. Cette interface possède les membres suivants (que vous devrez implémenter si vous implémentez IConvertible) :

  • ToType où Type peut être Boolean, Byte, Char, DateTime, Decimal, Double, Int16, Int32, Int64, SByte, Single, String, UIn16, UInt32, UInt64 : permet de convertir le type vers le type Type. ToType permet de convertir le type vers un Type spécifié
  • GetTypeCode : renvoie le code de notre type par exemple TypeCode.Object ou TypeCode.Int32

Pour utiliser un IConvertible, il suffit d'appeler la méthode d'une instance du type ou encore d'utiliser la classe Convert avec n'importe laquelle de ses méthodes ToType afin de tenter de convertir le type vers Type.

La classe TypeConverter

La classe TypeConverter est le type de base de tous les convertisseurs de types. Il possède les méthodes suivantes :

  • CanConvertFrom : indique si l'on peut convertir le type spécifié vers le type que le convertisseur gère
  • CanConvertTo : indique si l'on peut convertir le type que le convertisseur gère vers le type spécifié
  • ConvertFrom : convertit une instance d'un type donné en une instance du type que le convertisseur gère
  • ConvertFromInvariantString : convertit une chaîne en culture neutre en une instance du type que le convertisseur gère
  • ConvertFromString : convertit une chaîne en une instance du type que le convertisseur gère
  • ConvertTo : convertit une instance du type que le convertisseur gère en une instance d'un type donné
  • ConvertToInvariantString : convertit une instance du type que le convertisseur gère en une chaîne en culture neutre
  • ConvertToString : convertit une instance du type que le convertisseur gère en une chaîne
  • CreateInstance : crée une instance du type que le convertisseur gère à partir d'une liste des valeurs de ses propriétés. Cette liste est une collection que l'on peut indexer par une chaîne contenant le nom de la propriété dont on veut la valeur.
  • GetCreateInstanceSupported : indique si le convertisseur sait créer une instance du type qu'il gère. Par défaut, renvoie False.
  • GetProperties : renvoie la liste des propriétés (PropertyDescriptorCollection) de l'objet passé en paramètre
  • GetPropertiesSupported : indique si le convertisseur sait renvoyer la liste des propriétés du type qu'il gère. Par défaut, renvoie False.
  • GetStandardValues : renvoie une liste d'instance contenant des valeurs standards pour le type. Par défaut, renvoie null/Nothing.
  • GetStandardValuesExclusive : indique si les instances des valeurs standards sont les seules possibles. Par défaut, renvoie False.
  • GetStandardValuesSupported : indique si le convertisseur fournit des valeurs standards. Par défaut, renvoie False.
  • IsValid : indique si une instance d'un type contient des données valides

On peut utiliser la méthode statique GetConverter de la classe TypeDescriptor afin de récupérer un convertisseur d'un type à partir d'une instance d'un objet ou d'un Type :

//récupère le convertisseur du type Circle
TypeConverter convCircle = TypeDescriptor.GetConverter(typeof(LesDesignersLib.Circle));
//effectue la conversion vers "texte"
MessageBox.Show(convCircle.ConvertToString(new LesDesignersLib.Circle(12, 1, 1)));

LesDesignersLib.Time t = new LesDesignersLib.Time(12, 12, 12);
//récupère le convertisseur de l'instance de l'objet t
TypeConverter convTime = TypeDescriptor.GetConverter(t);
//effectue la conversion vers "texte"
MessageBox.Show(convTime.ConvertToString(t));

Ensuite, ayant récupéré une instance de TypeConverter, vous pouvez appeler l'une de ses méthodes afin d'effectuer la conversion.

La différence entre IConvertible et TypeConverter

L'interface IConvertible :

Peut être utilisée uniquement à l'exécution
N'utilise pas la réflexion
Autorise la conversion vers d'autres types mais pas l'inverse
Est implémenté directement dans le type

Un TypeConverter :

Peut être utilisé en mode Design et à l'exécution
Utilise la réflexion donc plus lent que IConvertible
Autorise la conversion depuis et vers d'autres types
Est implémenté à part du type

Utiliser un TypeConverter

Pour récupérer un convertisseur de type, on pourra utiliser la méthode GetConverter de TypeDescriptor. Ensuite, on pourra utiliser ConvertTo, ConvertFrom. Par exemple avec le type Font :

//récupère le convertisseur du type Font
TypeConverter convFont = TypeDescriptor.GetConverter(typeof(Font));
//effectue la conversion vers "texte"
MessageBox.Show(convFont.ConvertToString(Me.Font));

Les implémentations de base

La convention veut que les classes dérivant de TypeConverter et implémentant un convertisseur de type portent le nom ConvertedTypeConverter où ConvertedType est le nom du type que le convertisseur gère.

On trouvera par exemple :

  • ArrayConverter pour les tableaux
  • DoubleConverter pour le type Double
  • Int32Converter : pour le type int/Integer
  • ComponentConverter : pour les composants
  • CultureInfoConverter : pour les informations de culture
  • GuidConverter : pour les GUID
  • FontConverter : pour les polices
  • PointConverter : pour les points
  • CursorConverter : pour les curseurs
  • TreeNodeConverter : pour les noeuds de treeview
  • ...

La classe ExpandableObjectConverter

La classe ExpandableObjectConverter est une classe dérivée de TypeConverter qui permet d'éditer les propriétés d'un type en déroulant la propriété dans la fenêtre des propriétés. Cette classe redéfinit simplement les méthodes GetProperties et GetPropertiesSupported afin de renvoyer la liste des propriétés éditables. Cela oblige la fenêtre des propriétés à fournir le mécanisme de propriété déroulante.

Si l'on veut avoir une propriété déroulante, on dérivera notre convertisseur d'ExpandableObjectConverter et l'on pourra redéfinir aussi les méthodes de TypeConverter.

Implémenter son propre convertisseur de type

Création de la classe du convertisseur

On pourra dériver notre classe implémentant notre convertisseur de type de :

TypeConverter si l'on NE souhaite PAS avoir (de base) une propriété déroulante (édition des propriétés du type) dans la fenêtre des propriétés ExpandableObjectConverter si l'on souhaite avoir (de base) une propriété déroulante (édition des propriétés du type) dans la fenêtre des propriétés
Dans les exemples suivants, les types convertissables sont :

/// <summary>
/// Une classe avec un convertisseur de type
/// </summary>
[TypeConverter(typeof(TimeConverter))]
public class Time
{
    private int m_Hours;
    private int m_Minutes;
    private int m_Seconds;

    public int Hours
    {
        get { return m_Hours; }
        set { m_Hours = value; }
    }
            
    public int Minutes
    {
        get { return m_Minutes; }
        set { m_Minutes = value; }
    }

    public int Seconds
    {
        get { return m_Seconds; }
        set { m_Seconds = value; }
    }
    public Time(int hour,int minute,int second)
    {
        this.Hours = hour;
        this.Minutes = minute;
        this.Seconds = second;
    }
    public Time() : this(0,0,0)
    {
    }

    /// <summary>
    /// renvoie la description textuelle de cette classe telle
    /// qu'elle sera affichée dans la fenêtre de propriétés
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return string.Format("{0}:{1}:{2}", this.Hours, this.Minutes, this.Seconds) ;
    }
}

/// <summary>
/// Une classe qui a un convertisseur de type qui permet d'éditer chaque propriété
/// de cette classe en déroulant la propriété du contrôle à l'aide du +
/// </summary>
[TypeConverter(typeof(ExpandableObjectConverter))]
public class MyClass
{
    private int myI;
    private string myS;

    public string S
    {
        get { return myS; }
        set { myS = value; }
    }

    public int I
    {
        get { return myI; }
        set { myI = value; }
    }

    public MyClass()
    : this("", 0)
    {

    }
    public MyClass(string s, int i)
    {
        this.I = i;
        this.S = s;
    }

    /// <summary>
    /// renvoie la description textuelle de cette classe telle
    /// qu'elle sera affichée dans la fenêtre de propriétés
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return string.Format("I={0},S={1}",this.I,this.S);
    }
}

/// <summary>
/// Une classe avec un convertisseur de type
/// </summary>
[TypeConverter(typeof(CircleConverter))]
public class Circle
{
    private int m_Radius;
    private Point m_Center;

    public Point Center
    {
        get { return m_Center; }
        set { m_Center = value; }
    }
        
    public int Radius
    {
        get { return m_Radius; }
        set { m_Radius = value; }
    }

    public Circle(int radius,int x,int y)
    {
        this.m_Radius = radius;
        this.m_Center = new Point(x, y);
    }
    public Circle() : this(0,0,0)
    {
    }

    /// <summary>
    /// renvoie la description textuelle de cette classe telle
    /// qu'elle sera affichée dans la fenêtre de propriétés
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return string.Format("({0};{1}),{2}",
        this.Center.X,this.Center.Y, this.Radius);
    }
}

ITypeDescriptorContext

Cette interface permet d'accéder au composant, à son conteneur et au descripteur de la propriété pour laquelle ce convertisseur est appelé. On récupère une instance d'une classe implémentant cette interface en premier paramètre (nommé context) de toutes les méthodes que l'on peut redéfinir pour implémenter notre convertisseur. Cette interface possède les membres suivants (leur description est à prendre dans le contexte du mode Design) :

La propriété Container renvoie l'IContainer du composant contenant le composant qui possède une propriété du type géré par notre convertisseur pour laquelle on va convertir
La propriété Instance renvoie le composant qui possède une propriété du type géré par notre convertisseur pour laquelle on va convertir
La propriété PropertyDescriptor renvoie le descripteur de la propriété du type géré par notre convertisseur pour laquelle on va convertir
Les méthodes OnComponentChanging et OnComponentChanged permettent d'avertir l'environnement de Design que la valeur de la propriété décrite par PropertyDescriptor a changé.

Ainsi, on pourra se servir du contexte pour savoir quelle propriété de quel composant possède la valeur passée éventuellement en paramètre.

Implémentation de la conversion

Pour implémenter la conversion de notre type vers et depuis un autre type, il faudra redéfinir les quatre méthodes suivantes :

CanConvertFrom : renvoie true si le paramètre sourceType est un type depuis lequel on sait convertir vers notre type, false sinon. En général, on renvoie true pour le type String ce qui permet à la fenêtre de propriétés de mettre à jour la valeur saisie dans la case de la propriété.
CanConvertTo : renvoie true si le paramètre destinationType est un type vers lequel on sait convertir notre type, false sinon. En général, on renvoie true pour le type String. Cela permet à la fenêtre de propriétés d'afficher l'équivalent textuel de la valeur de la propriété. On peut aussi traiter le type InstanceDecriptor afin de générer du code de sérialisation plus propre, voir plus loin.
ConvertFrom : renvoie une instance de notre type à partir d'une instance d'un autre type passée en paramètre. En général, il s'agira de parser une chaîne de caractères afin de construire notre type (si CanConvertFrom a renvoyé true pour le type String).
ConvertTo : renvoie une instance d'un autre type à partir d'une instance de notre type. En général, cela revient à renvoyer le résultat de la méthode ToString de l'instance. Cependant, on peut avoir une chane différente entre ce que renvoie ConvertTo et ToString, ce qui est par exemple le cas du type Font. On pourra aussi gérer InstanceDescriptor (voir CanConverTo).

De plus, en général, on redéfinira la méthode ToString dans le type que l'on gère afin de fournir la représentation de notre type telle qu'elle apparaîtra dans la case de la propriété dans la fenêtre des propriétés et dans le même format qui pourra être parsé pour obtenir une instance du type.

Cependant, on peut effectuer la conversion en chaîne directement dans ConvertTo et une autre conversion en chaîne dans ToString.

/// <summary>
/// Indique si on peut convertir une chaîne vers le type Time
/// </summary>
/// <param name="context"></param>
/// <param name="sourceType"></param>
/// <returns></returns>
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
    //on peut convertir depuis une chaîne
    return (sourceType == typeof(string));
}
/// <summary>
/// Indique si on peut convertir le type Time vers une chaîne
/// </summary>
/// <param name="context"></param>
/// <param name="destinationType"></param>
/// <returns></returns>
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
    //on peut convertir un type vers
    return (destinationType == typeof(string) || destinationType == typeof(InstanceDescriptor));
}
/// <summary>
/// Convertit une chaîne vers notre type Time
/// </summary>
/// <param name="context"></param>
/// <param name="culture"></param>
/// <param name="value"></param>
/// <returns></returns>
public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
{
    if (value != null && value is string)
    {
        if (string.IsNullOrEmpty(value.ToString()))
        {
            return null;
        }
        else
        {
            //parse la chaîne pour extraire les données saisies
            string[] hms = value.ToString().Split(':');
            return new Time(int.Parse(hms[0]),int.Parse(hms[1]),int.Parse(hms[2]));
        }
    }
    else
        return null;
}
/// <summary>
/// Convertit notre type Time en chaîne
/// </summary>
/// <param name="context"></param>
/// <param name="culture"></param>
/// <param name="value"></param>
/// <param name="destinationType"></param>
/// <returns></returns>
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
{
    if (destinationType == typeof(string))
        return (value != null ? value.ToString() : "");
    else
        return null;
}

Création d'instances

L'un des problèmes des propriétés déroulantes (ExpandableObjectConverter) est que l'implémentation de base ne permet pas à la fenêtre de propriétés de rafraîchir son contenu quand une sous propriété d'une propriété est modifiée. Un autre problème est que par défaut et en l'absence d'instance créée dans le constructeur par défaut du type de la propriété, on ne pourra pas créer l'objet et la propriété apparaîtra désactivée.

Pour résoudre ces problèmes, on peut définir la manière d'instancier notre type géré. Cela permet au Designer de recréer l'instance de notre type à chaque fois qu'une sous propriété change et ainsi de rafraîchir la propriété elle-même (parce que le changement d'instance est accompagné d'un avertissement du service de changement des composants IComponentChangeService). De plus, cela permet aussi au Designer de créer une instance pour l'affecter à une propriété et ainsi de permettre l'édition des propriétés ayant pour valeur null/Nothing par défaut.

Pour cela, on devra redéfinir les méthodes :

  • GetCreateInstanceSupported : doit renvoyer true pour indiquer que l'on gère la création d'instance du type que l'on gère
  • CreateInstance qui renvoie une nouvelle instance du type que l'on gère dans le convertisseur. On peut récupérer les valeurs des propriétés de ce type par le biais du paramètre propertyValues qui permet de récupérer la valeur d'une propriété en l'indexant par une chaîne contenant son nom. On pourra ainsi créer une instance à partir des valeurs saisies dans les sous propriétés d'une propriété dans la fenêtre des propriétés.
/// <summary>
/// Permet de créer une instance de notre type Time à partir d'une collection
/// des valeurs des propriétés du type Time
/// </summary>
/// <param name="context"></param>
/// <param name="propertyValues"></param>
/// <returns></returns>
public override object CreateInstance(ITypeDescriptorContext context,System.Collections.IDictionary propertyValues)
{
    //crée une instance de notre type
    return new Time((int)propertyValues["Hours"],(int)propertyValues["Minutes"],(int)propertyValues["Seconds"]);
}
/// <summary>
/// Indique si la création d'instance est supportée
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override bool GetCreateInstanceSupported(ITypeDescriptorContext context)
{
    return true;
}

==Valeurs standards/prédéfinies===
On peut renvoyer une liste d'instance contenant des valeurs particulières du type que notre convertisseur gère. Par exemple, cela pourrait être dans le cas d'une classe représentant un nombre complexe, les vecteurs de la base (1,0)=1 et (0,1)=i.

Pour cela, il faudra redéfinir :

  • GetStandardValuesSupported pour renvoyer true afin de prendre en compte la méthode * * GetStandardValues
  • GetStandardValues pour renvoyer une liste d'instances (StandardValuesCollection) contenant les valeurs prédéfinies qui seront disponibles dans la liste déroulante de la propriété

On pourra aussi éventuellement redéfinir la méthode GetStandardValuesExclusive afin de préciser si seules les valeurs standards représentent des instances valides de notre type géré ou en d'autres termes, si la propriété sera éditable et sélectionnable dans une liste (false) ou si l'on pourra uniquement choisir une valeur prédéfinie (true).

/// <summary>
/// Renvoie une liste de valeurs standards de ce type Time
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
{
    //renvoie une liste contenant les valeurs de standards possibles
    List<Object> ret = new List<object>();
    ret.Add(new Time(0, 0, 0));
    ret.Add(new Time(12, 0, 0));
    ret.Add(new Time(23, 59, 59));
    //renvoie en tant que liste de valeurs standards
    return new StandardValuesCollection(ret);
}
/// <summary>
/// Indique si les valeurs standards sont les seules valeurs possibles
/// pour le type Time
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override bool GetStandardValuesExclusive(ITypeDescriptorContext context)
{
    return false;
}
/// <summary>
/// Indique si le type possède des valeurs standards
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
{
    return true;
}

Test de la validité d'une instance

On peut redéfinir la méthode IsValid afin de renvoyer un booléen indiquant si l'instance passée en paramètre contient des données valides pour le type que notre convertisseur gère.

/// <summary>
/// Indique si la valeur du type est valide
/// </summary>
/// <param name="context"></param>
/// <param name="value"></param>
/// <returns></returns>
public override bool IsValid(ITypeDescriptorContext context, object value)
{
    if (value != null)
    {
        //vérifie que l'heure est bien une heure valide
        Time t = (Time)value;
        return (t.Hours >= 0 && t.Hours < 24) && (t.Minutes >= 0 && t.Minutes < 60) && (t.Seconds >= 0 && t.Seconds < 60);
    }
    else
        return false;
}

Marquer le type à convertir

On pourra marquer avec l'attribut TypeConverter, au choix :

  • Certaines (ou toutes les) propriétés d'un composant qui ont le type géré par notre convertisseur. Cela permet de n'appliquer le convertisseur qu'à certaines propriétés et pas à d'autres.
  • Au type que gère le convertisseur. Cela permet d'appliquer à toutes les propriétés ayant ce type de tous les composants sans avoir à marquer chacune d'elles sur chaque composant.

Cet attribut se construit avec le Type de la classe du convertisseur :

/// <summary>
/// Une classe avec un convertisseur de type
/// </summary>
[TypeConverter(typeof(TimeConverter))]
public class Time
{
}

/// <summary>
/// Définit un convertisseur de type avec valeurs standards
/// et qui fournit une édition déroulante dans la fenêtre de propriétés
/// (ExpandableObjectConverter) : indique l'édition des propriétés du type
/// en déroulant la propriété
/// </summary>
public class TimeConverter : ExpandableObjectConverter
{
}

Contrôler la manière dont le code de sérialisation est généré

Si l'on veut que la génération du code dans InitializeComponent se fasse en une seule ligne du type :

this.typeConverterUserControl1.MyTime = new LesDesignersLib.Time(12, 12, 0);

Au lieu de créer une instance du type, d'affecter ses propriétés puis d'affecter l'instance à la propriété du composant :

LesDesignersLib.MyClass myClass1 = new LesDesignersLib.MyClass();
//...
myClass1.I = 12;
myClass1.S = "Test12";
this.typeConverterUserControl1.MyClass = myClass1;

On pourra renvoyer, en plus, true dans CanConvertTo pour le type InstanceDescriptor (ainsi que pour le type String comme précédemment).

Dans ConvertTo, on prévoira un deuxième cas pour destinationType valant InstanceDescriptor. On renverra alors une instance d'InstanceDescriptor. Ce type se construit avec :

Un ConstructorInfo indiquant le constructeur à utiliser pour construire une instance de notre type.
On récupère le Type de l'objet value passé en paramètre de ConvertTo
On appelle la méthode GetConstructor sur ce Type en passant un tableau des Type de paramètres du constructeur recherché. Cela permet de récupérer le ConstructorInfo
Un tableau d'Object contenant les valeurs nécessaires à la construction de l'instance
On cast vers notre type l'objet value passé en paramètre de ConvertTo
On construit un tableau d'objets avec les propriétés de l'objet casté dans l'ordre attendu par le constructeur trouvé précédemment

/// <summary>
/// Convertit notre type Time en chaîne
/// </summary>
/// <param name="context"></param>
/// <param name="culture"></param>
/// <param name="value"></param>
/// <param name="destinationType"></param>
/// <returns></returns>
public override object ConvertTo(ITypeDescriptorContext context,System.Globalization.CultureInfo culture,object value, Type destinationType)
{
    //si on convertit vers une chaîne
    if (destinationType == typeof(string))
        return (value != null ? value.ToString() : "");
    //si on convertit pour instanciation dans le code
    //de InitializeComponent (sérialisation CodeDom)
    else if (destinationType == typeof(InstanceDescriptor))
    {
        //on caste la valeur passée en notre type
        Time tmpTime = (Time)value;
        //on crée une nouvelle InstanceDescriptor permettant
        //de décrire le constructeur
        //et les valeurs à lui passer lors de l'instanciation
        //cela permet de générer un appel au bon constructeur
        //dans InitializeComponent
        return new InstanceDescriptor(
            typeof(Time).GetConstructor(new Type[] {
            typeof(int), typeof(int), typeof(int) }),
            new Object[] {
                tmpTime.Hours, tmpTime.Minutes, tmpTime.Seconds }
            );
    }
    else
        return null;
}

Trier les propriétés exposées du type

Bien que cela devrait être normalement assez rare, on peut redéfinir les méthodes GetProperties et GetPropertiesSupported afin de préciser la liste des propriétés que doit exposer l'édition déroulante du type dans la fenêtre de propriétés et surtout leur ordre.

GetPropertiesSupported doit renvoyer true pour que GetProperties puisse être utilisée
GetProperties utilisera
Si l'on dérive de TypeConverter, la méthode GetProperties de TypeDescriptor afin de récupérer la liste des propriétés de l'instance passée dans la propriété Instance du paramètre context de la redéfinition.
Si l'on dérive d'ExpandableObjectConverter, on pourra utiliser l'implémentation de cette classe.
Ensuite, on pourra passer un tableau, contenant les chaînes des noms des propriétés dans l'ordre voulu, à la méthode Sort de la PropertyDescriptorCollection obtenue précédemment.

/// <summary>
/// Indique que l'on fournit une liste des propriétés
/// du type pour édition déroulante
/// </summary>
/// <param name="context"></param>
/// <returns></returns>
public override bool GetPropertiesSupported(ITypeDescriptorContext context)
{
    return true;
}
/// <summary>
/// Renvoie une liste des propriétés du type triée dans un ordre personnalisé
/// </summary>
/// <param name="context"></param>
/// <param name="value"></param>
/// <param name="attributes"></param>
/// <returns></returns>
public override PropertyDescriptorCollection GetProperties(ITypeDescriptorContext context, object value, Attribute[] attributes)
{
    //récupère les propriétés du type
    PropertyDescriptorCollection ret = base.GetProperties(context, value, attributes);
    //trie dans un ordre voulu
    ret = ret.Sort(new string[] { "Seconds", "Minutes", "Hours" });
    return ret;
}

Références

How to: Implement a Type Converter

Implementing a TypeConverter

Generalized Type Conversion

IConvertible Interface

A voir également
Ce document intitulé « Typeconverter : Les convertisseurs de types » issu de CodeS SourceS (codes-sources.commentcamarche.net) est mis à disposition sous les termes de la licence Creative Commons. Vous pouvez copier, modifier des copies de cette page, dans les conditions fixées par la licence, tant que cette note apparaît clairement.
Rejoignez-nous