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.
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.