Tutoriel : Travailler avec les fichiers de configuration en C++/CLI

Nico-pyright(c)

Cet article a pour but de présenter diverses utilisations des fichiers de configuration dans une application Winforms avec le C++/CLI. ♪

- Titre : Tutoriel : Travailler avec les fichiers de configuration en C++/CLI

- Auteur : Nico-pyright(c)

- Parution : 15 octobre 2007

- Mise à jour : 15 octobre 2007

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2007 Nico-pyright(c). Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.

Sommaire

- 1.Introduction

- 2.Avant-propos

- 3.Les fichiers de configuration

- 4.Démarrer avec Visual C++/CLI

- 4.1.Configurer visual C++

- 4.2.Référence

- 4.3.Exemple de lecture simple pour la section prédéfinie AppSettings

- 5.Autre exemple simple avec un type prédéfini : ConnectionStrings

- 5.1.App.config

- 5.2.Code

- 6.Créer sa propre section à partir d'un type prédéfini

- 6.1.Dictionarysectionhandler

- 6.2.NameValueSectionHandler

- 6.3.SingleTagSectionHandler

- 7.Modification

- 8.Section personnalisée avec types

- 8.1.Section simple héritant de ConfigurationSection

- 8.2.Section simple avec nœuds XML, implémentant IConfigurationSectionHandler

- 8.3.Section avec des collections

- 9.Conclusion

- Remerciements

- Contact

1.Introduction

Vous êtes un développeur .Net utilisant le C++/CLI et vous souhaitez utiliser les fichiers de configuration dans votre application Winforms ?
Alors ce tutoriel est pour vous.

Vous apprendrez dans ce cours comment paramétrer et utiliser le fichier app.config.

NB : les développeurs C# peuvent également suivre ce tutoriel, la syntaxe du C# par rapport au C++/CLI étant proche (et si vous avez des problèmes de traduction, je me ferais un plaisir de traduire ce tutoriel en C#).

2.Avant-propos

Ce tutoriel est dédié aux applications Windows Forms et utilisera le fichier de configuration app.config. Beaucoup de ces concepts sont également transposables pour une programmation en ASP.NET, utilisant le fichier web.config. Notez seulement qu'il y a beaucoup plus de possibilités pour ASP.NET et qu'ici, nous ne verrons qu'une partie de l'utilisation des fichiers de configuration.

Dans cet article, nous allons utiliser des propriétés C++/CLI et des opérateurs d'indexations. Si vous êtes un peu rouillé sur ces notions, allez faire un petit tour dans la FAQ C++/CLI :
Comment implémenter une propriété (get/set) ?
Comment implémenter un opérateur d'indexation ?

Nous allons surcharger des méthodes et des propriétés aussi :
Comment surcharger une méthode abstraite ou virtuelle (override) ?
Comment rompre le polymorphisme d'une fonction (new) ?

3.Les fichiers de configuration

Les fichiers de configuration sont des fichiers XML qui contiennent la configuration de notre exécutable. Cela peut être des chaines de connexions, des valeurs paramétrables, une URL de web services, des préférences utilisateurs, des contrôles personnalisés, etc.
Ces fichiers doivent se situer dans le même répertoire que l'exécutable.

Pourquoi utiliser un fichier de configuration ?
- pour éviter de mettre des valeurs en dur dans le code. Imaginons que nous utilisions une URL de web service dans notre application, si l'URL change, on aimerait ne pas avoir à recompiler le code ;
- pour éviter d'utiliser la base de registre et de donner les droits de modification de base de registre.

Ces fichiers permettent de typer les données à sauvegarder et le framework .Net dispose de méthodes pour y accéder facilement.

L'intérêt d'utiliser un fichier XML plutôt qu'un fichier binaire est que ce fichier est lisible et compréhensible facilement. On peut également le modifier à la main sans un système évolué permettant de faire des modifications.

4.Démarrer avec Visual C++/CLI

4.1.Configurer visual C++

Visual C++ ne gère pas automatiquement les fichiers de configuration comme app.config. Tant est si bien que quand on essaie de les utiliser, à chaque chargement de valeur, on obtient une chaine vide.
Ceci est expliqué par le fait que Visual C++ ne copie pas automatiquement le fichier app.config dans le répertoire de l'exécutable (debug par exemple). Il faut donc le faire manuellement ou bien se servir des événements après génération.
Aller dans les propriétés du projet -> configuration properties -> build events -> post build event. Et modifier la ligne de commande par :

copy app.config "$(TargetPath).config"

4.2.Référence

N'oubliez pas d'ajouter la référence à System.Configuration.
bouton droit sur projet -> références -> add new references

4.3.Exemple de lecture simple pour la section prédéfinie AppSettings

Tout d'abord, il faut créer le fichier de configuration app.config. bouton droit sur le projet -> add -> new item -> configuration file

Le plus simple pour commencer est de travailler avec la section appSettings qui est gérée par le ConfigurationManager. C'est une section où on peut mettre, comme son nom le suggère, les propriétés de l'application.
On utilise une paire clé/valeur pour stocker des informations.

Dans le fichier, ajouter des clés et des valeurs comme ceci :

<configuration>
<appSettings>
<add key="nom" value="pyright"/>
<add key="prenom" value="nico"/>
</appSettings>
</configuration>

Pour y accéder, on utilise le code suivant :

String ^nom = Configuration::ConfigurationManager::AppSettings["nom"];
String ^prenom = Configuration::ConfigurationManager::AppSettings["prenom"];
Console::WriteLine("Je m'appelle {0} {1}", prenom, nom);

On utilise l'objet ConfigurationManager::AppSettings qui mappe cette section particulière.
On peut également utiliser des index numériques pour y accéder, mais je trouve que cela manque vraiment de clarté :

String ^nom = Configuration::ConfigurationManager::AppSettings[0];

Il est possible aussi de boucler sur toutes les valeurs contenues dans la section :

for each(String ^aValue in ConfigurationManager::AppSettings)
{
Console::WriteLine("Utilisateur : {0} - {1}", aValue, ConfigurationManager::AppSettings[aValue]);
}

5.Autre exemple simple avec un type prédéfini : ConnectionStrings

ConnectionString est un type prédéfini pour les chaines de connexion. On peut y stocker entre autres le nom du provider, la chaine de connexion, le nom de la base, etc.

5.1.App.config

<configuration>
<connectionStrings>
<add name="MaConnection" providerName="System.Data.SqlClient"
connectionString="Data Source=localhost; Initial Catalog=MonCatalog; Integrated Security=true"/>
<add name="MaConnection2" providerName="System.Data.SqlClient"
connectionString="Data Source=localhost; Initial Catalog=MonCatalog; Integrated Security=true"/>
</connectionStrings>
</configuration>

5.2.Code

De la même façon, on utilise un objet spécialisé ConfigurationManager::ConnectionStrings pour accéder à cette section.

ConnectionStringSettings ^connex = ConfigurationManager::ConnectionStrings["MaConnection"];
Console::WriteLine("{0} ; {1}", connex->Name, connex->ConnectionString);

On peut également boucler sur toutes les chaines de connexion :

for each(ConnectionStringSettings ^aValue in ConfigurationManager::ConnectionStrings)
{
Console::WriteLine("{0} ; {1}", aValue->Name, aValue->ConnectionString);
}

6.Créer sa propre section à partir d'un type prédéfini

Il est possible de créer sa propre section à partir d'un type prédéfini, par exemple pour créer une section du même genre que la section appSettings, qui utilise une paire clé/valeur, on peut utiliser un handler DictionarySectionHandler. Il existe plusieurs types prédéfinis, que nous allons étudier ci-dessous.

6.1.Dictionarysectionhandler

Dictionarysectionhandler est une classe qui fournit les informations de configuration des paires clé/valeur d'une section de configuration. Elle implémente l'interface IConfigSectionHandler.

Pourquoi utiliser une section particulière Dictionarysectionhandler plutôt que d'utiliser appsettings qui utilise le même système de clé/valeur ?
Le but de pouvoir faire des sections particulières est d'organiser sémantiquement son fichier app.config, pour découper logiquement son fichier au lieu d'avoir tout dans la même section.

Pour illustrer le fait qu'on peut avoir plusieurs sections personnalisées dans le même fichier de configuration, ce fichier app.config exemple va contenir l'illustration du handler Dictionarysectionhandler ainsi que le suivant NameValueSectionHandler, qui utilise aussi une paire clé/valeur. Pourquoi deux handler différents me diriez vous si chacun fournit des informations de configuration clé/valeur ? Allez … réponse dans le prochain chapitre…

<configuration>
<configSections>
<sectionGroup name="MonGroupe">
<section name="MaSectionUne" type="System.Configuration.NameValueSectionHandler" />
<section name="MaSectionDeux" type="System.Configuration.DictionarySectionHandler" />
</sectionGroup>
</configSections>
<MonGroupe>
<MaSectionUne>
<add key="cle1" value="valeur1" />
<add key="cle2" value="valeur2" />
<add key="cle3" value="valeur3" />
</MaSectionUne>
<MaSectionDeux>
<add key="id1" value="valeur4" />
<add key="id2" value="valeur5" />
<add key="id3" value="valeur6" />
</MaSectionDeux>
</MonGroupe>
</configuration>

Pour récupérer notre section personnalisée, on va utiliser la méthode GetSection en lui passant le nom de la section en paramètre.
On va dans le cas du Dictionarysectionhandler récupérer un objet Hashtable, et on pourra accéder à la valeur correspondant à une clé comme suit :

Hashtable ^section = (Hashtable^)System::Configuration::ConfigurationManager::GetSection("MonGroupe/MaSectionDeux");
Console::WriteLine(section["id2"]);

On pourra toujours bien sûr boucler sur tous les éléments :

for each(DictionaryEntry ^d in section)
{
Console::WriteLine("{0} ; {1}", d->Key, d->Value);
}

6.2.NameValueSectionHandler

Alors, voici la différence entre le Dictionarysectionhandler et le NameValueSectionHandler. Chacun fournit les informations de configuration des paires clé/valeur d'une section de configuration. La différence consiste en l'objet construit et retourné qui contient les informations de configuration.
Ici en l'occurrence, nous allons récupérer une collection spécialisée NameValueCollection.

On pourra accéder à un élément ainsi :

NameValueCollection ^section = (NameValueCollection^)ConfigurationManager::GetSection("MonGroupe/MaSectionUne");
Console::WriteLine(section["cle1"]);

et boucler sur nos valeurs ainsi :

for each(String ^aKey in section)
{
Console::WriteLine("{0} ; {1}", aKey, section[aKey]);
}

Après, à chacun de décider avec quel objet il préfère travailler ensuite.

N. B. Noter que lorsqu'il y a plusieurs sections, on peut les regrouper dans une section group, on y accèdera par l'URL « MonGroupe/MaSection ».

6.3.SingleTagSectionHandler

Ce troisième handler permet de gérer des sections de configuration qui sont représentées par une balise XML unique dans le fichier .config. L'avantage ici est qu'on n'est pas limité dans le nombre d'attributs de la section et par leurs noms. Le framework saura retrouver les noms que l'on aura renseignés dans le fichier de configuration. L'inconvénient est que la section ne pourra apparaitre qu'une seule fois dans le fichier de configuration.

<configuration>
<configSections>
<section name="sampleSection" type="System.Configuration.SingleTagSectionHandler" />
</configSections>
<sampleSection monAttribut="Value1" unAutreAttribut="deuxieme valeur" cequejeveux="avec mes configs"/>
</configuration>

Le code pour récupérer les valeurs :

Hashtable ^section = (Hashtable^)ConfigurationManager::GetSection("sampleSection");
Console::WriteLine(section["monAttribut"]);

Ainsi que notre boucle habituelle :

for each(DictionaryEntry ^d in section)
{
Console::WriteLine("{0} ; {1}", d->Key, d->Value);
}

N. B. Ici il n'y a pas de groupe de section, on accède à la section directement grâce au nom de la section, en utilisant GetSection.

7.Modification

Il peut être utile de pouvoir modifier le fichier de configuration depuis son application.
Reprenons notre premier exemple :

<configuration>
<appSettings>
<add key="nom" value="pyright"/>
<add key="prenom" value="nico"/>
</appSettings>
</configuration>

Voici comment faire :

String ^nom = ConfigurationSettings::AppSettings["nom"];
String ^prenom = ConfigurationSettings::AppSettings["prenom"];
Console::WriteLine("{0} - {1}", prenom, nom);

System::Configuration::Configuration ^config = ConfigurationManager::OpenExeConfiguration(ConfigurationUserLevel::None);
config->AppSettings->Settings->Remove("prenom");
config->AppSettings->Settings->Add("prenom", "Nouveau prenom");
config->Save(ConfigurationSaveMode::Modified);
ConfigurationManager::RefreshSection("appSettings");

prenom = ConfigurationSettings::AppSettings["prenom"];
Console::WriteLine("{0} - {1}", prenom, nom);

Vous pouvez aller vérifier votre fichier de configuration app.config (attention, celui qui est dans le même répertoire que l'exécutable (debug par défaut)), la nouvelle valeur aura été modifiée. Mais pas celui qui est dans la solution, puisque c'est l'IDE qui va recopier le fichier app.config dans le répertoire de l'exécutable.

8.Section personnalisée avec types

8.1.Section simple héritant de ConfigurationSection

Une des façons de créer des sections personnalisées est d'hériter de la classe ConfigurationSection. On pourra dès lors ajouter des attributs en créant des propriétés et en utilisant l'attribut ConfigurationProperty pour associer notre propriété à un attribut XML.
Ainsi, pour lire un fichier de configuration de ce genre :

<configuration>
<configSections>
<section name="MaSection" type="testAppConfig.MaSection, testAppConfig" />
</configSections>
<MaSection nom="pyright" prenom="nico"/>
</configuration>

on utilisera :

using namespace System;
using namespace System::Configuration;

namespace testAppConfig
{
ref class MaSection : ConfigurationSection
{
public:
[ConfigurationProperty("nom", IsRequired = true)]
property String ^nom
{
String ^ get() { return (String ^)this["nom"]; }
void set(String ^value) { this["nom"] = value; }
}

[ConfigurationProperty("prenom", IsRequired = true)]
property String ^prenom
{
String ^ get() { return (String^)this["prenom"]; }
void set(String ^value) { this["prenom"] = value; }
}
};
}

using namespace testAppConfig;

int main(array<System::String ^> ^args)
{
MaSection ^section = (MaSection^)ConfigurationManager::GetSection("MaSection");
Console::WriteLine("{0} ; {1}", section->nom, section->prenom);

return 0;
}

Dans le fichier app.config, on aura remarqué le type de la section type=« testAppConfig.MaSection, testAppConfig ».
Il s'agit du nom de la classe qui aura pour charge de gérer la section. On la précède de son namespace. Ce qui implique de définir un namespace, ce qui n'est pas toujours le cas dans une application console.

8.2.Section simple avec nœuds XML, implémentant IConfigurationSectionHandler

Une autre façon de créer une section personnalisée est d'implémenter l'interface IConfigurationSectionHandler. Ceci implique de surcharger la méthode Create qui va s'occuper de désérialiser la section.
On aura donc un fichier de configuration de cette forme :

<configuration>
<configSections>
<section name="MaSection" type="testAppConfig.MaSectionHandler, testAppConfig" />
</configSections>
<MaSection type="testAppConfig.MaSection, testAppConfig">
<Nom>Nico</Nom>
<Prenom>pyright</Prenom>
</MaSection>
</configuration>

On constate encore le type de la section qui nous renvoie à notre classe.
Il ne reste plus qu'à définir la classe de section avec ses propriétés qui feront office de champs XML.

using namespace System;
using namespace System::Xml;
using namespace System::Xml::Serialization;
using namespace System::Configuration;

namespace testAppConfig
{
public ref class MaSection
{
public:
property String ^Nom;
property String ^Prenom;
};

ref class MaSectionHandler : IConfigurationSectionHandler
{
public:
virtual Object^ Create(Object^ parent, Object ^configContext, XmlNode ^section)
{
XmlSerializer ^xs = gcnew XmlSerializer(MaSection::typeid);
XmlNodeReader ^xnr = gcnew XmlNodeReader(section);
return xs->Deserialize(xnr);
}
};
}

using namespace testAppConfig;

int main(array<System::String ^> ^args)
{
MaSection^ maSection = (MaSection^)ConfigurationManager::GetSection("MaSection");
Console::WriteLine("{0} ; {1}", maSection->Nom, maSection->Prenom);

return 0;
}

8.3.Section avec des collections

Dans ces deux exemples de sections personnalisées, on constate à chaque fois qu'on peut seulement définir un élément. Il pourrait être intéressant dans certains cas d'avoir une section personnalisée qui puisse contenir plusieurs éléments.
Comme ci-dessous :

<configuration>
<configSections>
<section name="MaSection" type="testAppConfig.MaSection, testAppConfig" />
</configSections>
<MaSection>
<masection nom="nico" prenom="pyright"/>
<masection nom="CLI" prenom="C++"/>
</MaSection>
</configuration>

Alors, j'arrête tout de suite ceux qui pensent qu'ici on pourrait utiliser une section NameValueSectionHandler. Ils ont raison effectivement, mais là le but est de pouvoir faire une section qui possède plus qu'une paire de clé/valeur, c'est un hasard si notre section personnalisée en contient deux (pas tant un hasard que ça, c'est plutôt pour la lisibilité du code). Rien n'empêche de rajouter des propriétés.
C'est la qu'interviennent les ConfigurationPropertyCollection.
Le principe est de créer un élément (la classe qui s'appelle ici ListeElement qui surcharge ConfigurationElement) qui sera contenu dans une collection d'éléments (ici la classe ListesElementCollection, qui hérite de ConfigurationElementCollection) et notre section qui va utiliser ces éléments (ici MaSection qui hérite de ConfigurationSection).
Voilà pour le code :

using namespace System;
using namespace System::Configuration;

namespace testAppConfig
{
ref class ListeElement: ConfigurationElement
{
private:
static ConfigurationPropertyCollection ^_proprietes;
static ConfigurationProperty ^_nom;
static ConfigurationProperty ^_prenom;

public:
static ListeElement()
{
_nom = gcnew ConfigurationProperty("nom", String::typeid, nullptr, ConfigurationPropertyOptions::IsKey);
_prenom = gcnew ConfigurationProperty("prenom", String::typeid, nullptr,

ConfigurationPropertyOptions::IsRequired);
_proprietes = gcnew ConfigurationPropertyCollection();
_proprietes->Add(_nom);
_proprietes->Add(_prenom);
}

property String ^Nom
{
String ^ get() { return (String^)this[_nom]; }
void set(String ^value) { this[_nom] = value; }
}

property String ^Prenom
{
String ^ get() { return (String^)this[_prenom]; }
void set(String ^value) { this[_prenom] = value; }
}

protected:
property virtual ConfigurationPropertyCollection^ Properties
{
ConfigurationPropertyCollection^get() override { return _proprietes; }
}
};

ref class ListesElementCollection : ConfigurationElementCollection
{
protected:
property virtual String^ ElementName
{
String^ get() override { return gcnew String("masection"); }
}
property virtual ConfigurationPropertyCollection^ Properties
{
ConfigurationPropertyCollection ^get() override { return gcnew ConfigurationPropertyCollection(); }
}
virtual ConfigurationElement^ CreateNewElement() override
{
return gcnew ListeElement();
}
virtual Object^ GetElementKey(ConfigurationElement^ element) override
{
if (element != nullptr)
return ((ListeElement^)element)->Nom;
else
return nullptr;
}

public:
property virtual ConfigurationElementCollectionType CollectionType
{
ConfigurationElementCollectionType get() override { return ConfigurationElementCollectionType::BasicMap; }
}

property ListeElement^ default[int]
{
ListeElement^ get(int index) { return (ListeElement^)BaseGet(index); }
void set(int index, ListeElement^ value)
{
if (BaseGet(index) != nullptr)
{
BaseRemoveAt(index);
}
BaseAdd(index, value);
}
}

property ListeElement^ default[String^]
{
ListeElement ^get(String^ nom) new { return (ListeElement^)BaseGet(nom); }
}

void Add(ListeElement^ item)
{
BaseAdd(item);
}

void Remove(ListeElement^ item)
{
BaseRemove(item);
}

void RemoveAt(int index)
{
BaseRemoveAt(index);
}

void Clear()
{
BaseClear();
}
};

ref class MaSection : ConfigurationSection
{
private:
static ConfigurationPropertyCollection ^_proprietes;
static ConfigurationProperty ^_listes;

public:
static MaSection()
{
_listes = gcnew ConfigurationProperty("", ListesElementCollection::typeid, nullptr,

ConfigurationPropertyOptions::IsRequired | ConfigurationPropertyOptions::IsDefaultCollection);
_proprietes = gcnew ConfigurationPropertyCollection();
_proprietes->Add(_listes);
}

property ListesElementCollection^ Listes
{
ListesElementCollection^ get() { return (ListesElementCollection^)this[_listes]; }
}

property virtual ListeElement^ default[String ^]
{
ListeElement^ get (String ^nom) { return Listes[nom]; }
}

protected :
property virtual ConfigurationPropertyCollection ^Properties
{
ConfigurationPropertyCollection^get() override { return _proprietes; }
}

};

}

using namespace testAppConfig;

int main(array<System::String ^> ^args)
{
MaSection ^ section = (MaSection^)ConfigurationManager::GetSection("MaSection");
ListeElement^ element1 = section["nico"];
Console::WriteLine("{0} ; {1}", element1->Nom, element1->Prenom);

ListeElement^ element2 = section["CLI"];
Console::WriteLine("{0} ; {1}", element2->Nom, element2->Prenom);

return 0;
}

On récupère la section de manière classique avec GetSection et ensuite on manipule les objets ListeElement de notre section.

Télécharger le source exemple (23 ko).

9.Conclusion

Voilà pour ce tutoriel sur les fichiers de configuration app.config. J'espère qu'il vous aura aidé à maîtriser un peu plus cette technique de configuration d'application.

NB : j'ai utilisé beaucoup de string, car j'ai utilisé les exemples simples de nom et prénom, mais sachez qu'on peut très bien typer les valeurs autrement et utiliser un int par exemple pour stocker l'âge. C'est un des intérêts des sections par rapport aux vieux fichiers ini.

Remerciements

Je remercie toute l'équipe C++, particulièrement Hiko-seijuro et Farscape, ainsi que l'équipe Dotnet pour leurs relectures attentives du document.

Contact

Si vous constatez une erreur dans le tutoriel, dans le source, dans la programmation ou pour toutes informations, n'hésitez pas à me contacter par mail, ou par le forum.