24.08.08
SAGA 4: sérialisation XML
Cette semaine nous allons rester un peu avec les collections avant d'aborder la sérialisation des énumérations et examiner un dernier cas de figure:
List<Pays> liste = new List<Pays>(); liste.Add(new Pays { Capitale = "Paris", Nom = "France" }); liste.Add(new Pays { Capitale = "Bruxelle", Nom = "Belgique" });
Un premier essai donne ceci après le nettoyage d'usage:
<?xml version="1.0" encoding="utf-8"?> <ArrayOfPays> <Pays capitale="Paris"> <nom>France</nom> </Pays> <Pays capitale="Bruxelles"> <nom>Belgique</nom> </Pays> </ArrayOfPays>
Peut-on remplacer le ArrayOfPays?
Nous l'avions fait précédemment à l'aide des attributs XmlArrayAttribute et XmlEllementAttibute qui ne s'appliquent pas à des déclarations de classe. C'est le rôle de XmlRootAttribute.
Dans notre cas il est nécessaire de définir une nouvelle classe et d'y appliquer l'attribut XmlRoot.
[DebuggerDisplay("{Count}")] [XmlRoot("pays")] public sealed class PaysCollection : List<Pays> { }
Cette fois on obtient:
<pays> <Pays capitale="Paris"> <nom>France</nom> </Pays> <Pays capitale="Bruxelles"> <nom>Belgique</nom> </Pays> </pays>
Notons que les collections, du point de vue de la sérialisation, ne sont pas très différentes des Array.
Terminons sur un dernier cas de figure: la sérialisation des énumérations.
On pourrait modéliser l'appartenance à un continent via une énumération:
public enum NomContinent { Europe, Asie, AmeriqueNord, AmeriqueSud, Oceanie, Afrique, Antartique }
Et un nouveau membre de la classe Pays:
public NomContinent Continent;
Telle quel une sérialisation donne dans le cas de la précédente collection:
<pays> <Pays capitale="Paris"> <Continent>Europe</Continent> <nom>France</nom> </Pays> <Pays capitale="Bruxelle"> <Continent>Europe</Continent> <nom>Belgique</nom> </Pays> </pays>
Tout comme pour un autre membre on peut renommer la sortie standard à l'aide de l'attribut XmlEnumAttribute:
public enum NomContinent { [XmlEnum("europe")] Europe, [XmlEnum("asie")] Asie, [XmlEnum("amérique du nord")] AmeriqueNord, [XmlEnum("amérique du sud")] AmeriqueSud, [XmlEnum("océanie")] Oceanie, [XmlEnum("afrique")] Afrique, [XmlEnum("antartique")] Antartique }
Notes d'ailleurs
Il y a quelques temps Michel Fugain s'est laissé aller envers les chanteurs modernes qui ne connaissent pas leur métier, n'ont aucune culture et s'imaginent tout connaître et terminât par cette diatribe: "de notre temps nous écoutions des merveilles".
Je laisse de côté ce débat qui avait déjà cours du temps de ma jeunesse pour vous proposer d'écouter une chanson de mon adolescence, chantée par un chanteur de celle de mes parents avec une chanteuse des années de l'adolescence des miens.
Je trouve tout de même que le talent ça pourrait ressembler à ça:
17.08.08
SAGA 3: Sérialisation XML
Cette semaine nous allons sérialiser de différentes manières des collections.
On commence par créer la classe Continent suivante:
public sealed class Continent { #region Constructeurs /// <summary> /// Constructeur par défaut /// </summary> public Continent() { Pays = new List<Pays>(); } #endregionpublic List<Pays> Pays; }
La sérialisation faite nous donne quelquechose qui ressemble à:
<?xml version="1.0" encoding="utf-8"?> <Continent> <Pays> <Pays capitale="Paris"> <nom>France</nom> </Pays> <Pays capitale="Berlin"> <nom>Allemagne</nom> </Pays> </Pays> </Continent>
Transformons la balise <Continent> avec des minuscules grâce à l'attribut XmlRoot.
[XmlRoot("continent")] public sealed class Continent { }
Je vous laisse découvrit le (très) prévisible résultat.
Profitons en pour découvrir un très méconnu comportement de XmlElement. Effectuons le remplacement suivant:
[XmlElement("pays")] public List<Pays> Pays;
On obtient alors:
<continent> <pays capitale="Paris"> <nom>France</nom> </pays> <pays capitale="Berlin"> <nom>Allemagne</nom> </pays> </continent>;
Un niveau disparaît. Pour le conserver on doit utiliser d'autres attributs:
[XmlArray("nation")] [XmlArrayItem("pays")] public List<Pays> Pays;
On obtient:
<continent> <nation> <pays capitale="Paris"> <nom>France</nom> </pays> <pays capitale="Berlin"> <nom>Allemagne</nom> </pays> </nation> </continent>;
Pour l'instant nous nous sommes contenté de situations pour laquelle le type des membres de la classe sérialisée est connu. Mais dans la réalité ce n'est pas toujours possible.
Complétons par exemple la classe Pays avec la propriété suivante:
public ResourceEnEau Eau;
ResourceEnEau est la classe abstraite suivante:
public abstract class ResourceEnEau { public string Nom; }
elle admet deux dérivés:
public class Lac : ResourceEnEau { public int Surface; } public class Riviere : ResourceEnEau { public int Longueur; }
Pas les même classes, pas les mêmes membres. On vérifie facilement qu'en l'état la sérialisation ne fonctionne pas. La raison est assez facile à comprendre. Lors de la déclaration et l'instanciation de XmlSerializer, on fournit le type de données qui sera sérialisé:
XmlSerializer serialiseur = new XmlSerializer(typeof(Pays));
En interne le compilateur va créer une classe spécialisée dans la sérialisation de Pays. La classe n'est pas créée à la volée, ce qui assure des performances meilleures.
Seulement dans notre nouvelle situation on rencontre une classe abstraite, on aurait même pu avoir une interface. Le compilateur ignore alors tout de la classe réelle et ne pourra pas terminer son travail... à moins d'être aidé!
Il existe deux approches possibles.
Si la liste des types possible est connue et ne varie pas, les attributs peuvent venir à notre secours:
[XmlElement(Type=typeof(Lac))] [XmlElement(Type=typeof(Riviere))] public ResourceEnEau Eau;
Si on ne peut pas accéder au source des classes ou si la liste des types varie et doit donc être obtenue par exemple par réflexion:
Type[] Types = new Type[] {typeof(Lac),typeof(Riviere) }; XmlSerializer serialiseur = new XmlSerializer(typeof(Continent),Types);
Dans les deux cas on obtient quelque chose de similaire (il y a une légère différence dans le résultat):
<continent> <nation> <pays capitale="Paris"> <Riviere> <Nom>Seine</Nom> <Longueur>776</Longueur> </Riviere> <nom>France</nom> </pays> <pays capitale="Berlin"> <Lac> <Nom>Constance</Nom> <Surface>538</Surface> </Lac> <nom>Allemagne</nom> </pays> </nation> </continent>
Suite la semaine prochaine pour voir d'autres cas de figure.
Notes d'ailleurs
Une des premières photographie prise par Neil Armstrong lors de son arrivée sur la Lune fut le pied du LEM. Ce cliché est devenu une tradition à la NASA et se trouve être une des premières images prises par une sonde qui se pose sur un autre sol. On l'appelle depuis le "cliché Armstrong".
Ce cliché a une utilité pratique bien sur, il permet de vérifier que l'appareil s'est posé sur un terrain stable.
10.08.08
SAGA 2: La sérialisation XML
On va aborder un premier écueil: j'ai fais une sérialisation, mais entre temps le schéma a changé.
Je crois que l'on peut distinguer trois cas:
1. Le besoin en sérialisation est interne à votre application
2. Le fichier XML peut en effet contenir des informations parasites aléatoires qui ne vous intéressent pas
3. Vous écrivez un environnement distribué à des clients qui fournit une fonctionnalité de sérialisation. Que va-t-il se passer du côté de vos clients si vous changez le schéma?
Le premier cas est très simple. Corrigez l'application puis redéployez! Les cas suivants sont plus intéressants.
Sachez que la présence d'une section supplémentaire dans le fichier Xml ou bien son absence ne lèvent pas d'exceptions.
C'est une bonne nouvelle pour votre application qui continuera à fonctionner. Mais on peut souhaiter être alerté lorsque cela se produit.
Une nouveauté très attendue (en tout cas par moi-même) est la présence de trois nouveaux événements associés à la classe XmlSerializer:
1. UnknownAttribute
2. UnknownElement
3. UnknownNode
Ces événements n'appellent pas à grands commentaires, ils fournissent un jeu d'informations vraiment complet sur la cause du problème. On peut toutefois se demander la différence entre les deux derniers. Le nœud inconnu se réfère aux types référencés dans un schéma. L'utilisation d'un schéma est un cas que nous n'avons pas encore abordé. On le fera dans un futur article bien sûr.
Notez pour finir que l'une des surcharges de Deserialize() attend une structure XmlDeserializationEvents qui est un conteneur de délégués vers des gestionnaires d'événements correspondant aux trois événements précités.
Une dernière remarque.
Vous avez pu constater que la sérialisation ajoute par défaut des attributs d'espace de noms tels ceux-ci:
<Pays xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd ="http://www.w3.org/2001/XMLSchema" >
Si vous travaillez sans schéma particulier, vous souhaitez peut être vous débarrasser de ces attributs. C'est possible en complétant le code ainsi:
XmlSerializerNamespaces ns = new XmlSerializerNamespaces(); ns.Add(string.Empty, string.Empty);
Vous ajoutez ensuite ns parmi les arguments possible de la method Serialize() et on obtient:
<?xml version="1.0" encoding="utf-8"?> <Pays> <capitale>Paris</capitale> <nom>France</nom> </Pays>
La semaine prochaine nous nous attaqueront aux différentes subtilités qu'il y a à sérialiser les collections.
Bibliographie
[1] documentation de XmlSerializer
http://msdn.microsoft.com/fr-fr/library/system.xml.serialization.xmlserializer(VS.80).aspx
Notes d'ailleurs
La réponse à la question était l'astrophysicien indien Subrahmanyan Chandrasekhar.
Il fut au cœur de 'invention de l'astrophysique, odyssée racontée avec bonheur dans un livre récent "Sous l'empire des étoiles" d'où la citation a été extraite.
Il obtint le prix Nobel de physique en 1983, soit 53 ans après celui de son oncle découvreur de l'effet Raman.
Je me suis toujours demandé si le dr Chandra, père de HAL dans l'Odyssée de l'espace n'était pas un hommage rendu par Clarke à cet astrophysicien.
03.08.08
SAGA 1: la sérialisation XML (I)
Il y a à peine 6 mois je n'aurai jamais eu l'idée de faire un article sur la sérialisation XML tellement j'étais convaincu que tout le monde sait ce dont il s'agit.
Et bien non. J'ai rencontré pas mal de gens qui en ignoraient tout, y compris des développeurs expérimentés. En creusant un peu je me suis rendu compte que beaucoup de développeurs ne connaissent pas la différence entre la sérialisation XML et la sérialisation binaire (runtime serialization) et ne comprennent pas clairement l'inutilité de l'attribut SerializableAttribute dans ce contexte.
Et puis je me suis souvenu des nombreuses difficultés rencontré sur des tas de projets pour sérialiser correctement les collections ou certains types de données.
Ajoutons à cela que .NET 2.0 a apporté son lot de nouveautés, j'ai fini par me dire qu'une saga de l'été sur ce sujet pourrait bien être intéressante, avec bien sûr les "notes d'ailleurs" dont j'aime bien conclure mes articles en ces semaines d'été.
Le mieux est de commencer par une présentation de la chose. On attaquera les choses sérieuses la semaine prochaine.
La sérialisation (on rencontre parfois le terme de marshalling), c'est une façon d'encoder une information. Dans notre cas l'information est une instance d'un objet.
La sérialisation XML se réfère un encodage XML. Mais on peut imaginer ce que l'on veut comme SOAP, PDF… Le code source d'une application peut être vu comme une façon de sérialiser des données à la limite.
La question est de déterminer quels sont les contours d'un objet afin de savoir jusqu'où poursuivre l'effort de sérialisation.
Par exemple dit t'on sérialiser un champ private? Si une propriété pointe sur une instance d'un objet faut t'il s'arrêter à une référence vers cet objet ou bien sérialiser l'objet lui-même pour en avoir une copie. Pour son fonctionnement interne, .NET est amené à décorer une instance d'attributs et/ou de membres à usage purement interne au framework. Doit-on également les sérialiser pour avoir une représentation exacte de l'objet? L'objet détient une référence vers une ressource non gérée (un handler par exemple), que doit t'on en faire?
La sérialisation XML répond de façon extrêmement simple à ces questions:
On ne sérialise que les classes qui:
• Ont un constructeur public sans paramètre
• On ne s'intéresse qu'aux propriétés et champs en lecture/écriture
• Seuls les types intrinsèques (int, string…) et quelques types de la BCL (ArrayList…) sont sérialisables par défaut.
• Par défaut on sérialise tous les types qi répondent aux critères précédents.
Il y a quelques autres critères, mais moins importants à connaître.
Le point important à comprendre est que la sérialisation XML est surtout une façon rapide de sauvegarder et/ou transférer des données dans un fichier XML. On ne cherche pas une représentation fidèle de ses états.
Les contraintes imposées par la sérialisation XML sont assez peu nombreuses et la plupart des objets y répondent nativement. C'est cela la sérialisation XML: sérialisation rapide d'un objet quelconque.
Presque tous les objets étant sérialisables au sens XML par défaut, pas besoin de déclarer des attributs quelconques.
Si vous n'êtes intéressé que par la sérialisation XML, inutile donc d'utiliser l'attribut SerializableAttibute.
Ceci étant cet attribut est géré en interne sous la forme d'un drapeau dans l'assemblage. Sa déclaration ne consommant pas de ressources supplémentaire, certains développeurs ont l'habitude de l'ajouter systématiquement.
Pour toute la suite sérialisation signifiera exclusivement sérialisation XML. Si vous souhaitez avoir un aperçu des autres modes, le plus simple est de lire un des articles de la bibliographie [1].
La sérialisation XML dispose de son propre jeu d'attributs. Ils commencent tous par XML. Par exemple XmlIgnoreAttribute permet de soustraire un membre du processus de sérialisation.
On en parle, on en parle. Mais peut être un exemple ne serai pas de refus non?
La classe à sérialiser pourrait ressembler à ceci:
public class Pays { #region Constructeurs /// <summary> /// Constructeur par défaut /// </summary> public Pays() {} #endregion
#region Nom [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string _Nom;
/// <summary> /// Nom du pays /// </summary> public string Nom { [DebuggerStepThrough] get { return _Nom; } [DebuggerStepThrough] set { _Nom = value; } } #endregion
public string Capitale; }
Une propriété et un champ.
Le code pour sérialiser est celui-ci:
XmlSerializer serialiseur = new XmlSerializer(typeof(Pays)); using (StreamWriter writer = new StreamWriter(fichier)) { Pays pays=new Pays() {Nom="France", Capitale="Paris"};serialiseur.Serialize(writer,pays); }
Rien de très compliqué si ce n'est l'utilisation du bloc using parfois oublié, ce qui est la source de bien des bugs.
On recueille le fichier suivant:
<??xml version="1.0" encoding="utf-8"?>
<?Pays xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<?Capitale>Paris<?/Capitale>
<?Nom>France<?/Nom>
<?/Pays>
On peut ultérieurement le relire ainsi:
XmlSerializer serialiseur = new XmlSerializer(typeof(Pays)); using (StreamReader reader = new StreamReader(fichier)) { Pays pays = (Pays)serialiseur.Deserialize(reader); }
Cet exemple servira de fil rouge tout au long de la série.
Un dernier détail pour terminer. Il est en général demandé d'utiliser la convention camel pour le nommage des sections et attributs d'un fichier XML. On pourrait alors transformer les deux membres de la façon suivante:
[XmlElement("nom")] public string Nom { [DebuggerStepThrough] get { return _Nom; } [DebuggerStepThrough] set { _Nom = value; } }[XmlAttribute("capitale")] public string Capitale;
Sans modifier le code on obtient le fichier suivant:
<?xml version="1.0" encoding="utf-8"?>
<?Pays xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" capitale="Paris"><?/strong>
<?strong><?nom>France<?/nom>
<?/Pays>
Voilà pour cette semaine, on parlera entre autres des nouveautés de .NET 2.0 la semaine prochaine.
Notes d'ailleurs
Une anecdote que j'aime bien du mathématicien britannique Hardy qui aurait dit à un astrophysicien célèbre:
Si quelqu'un vous demande si vous êtes astronome et à quoi ça sert l'astronomie, alors la manière correcte de répondre à pareil béotien est la suivante:
- L'astronomie n'est peut être pas utile, et, de fait, mon travail n'est peut être pas important à quelque égards que ce soit, mais j'ai la conviction que mon travail d'astronome est au moins la partie la plus importante de ma personnalité. Le fait que je suis marié, que j'ai un salaire, que je suis brun de peau et que je suis apprécié d'autrui n'a pas de sens hormis pour moi-même. Mais mon travail créatif a de la valeur pour les autres (même si ces autres sont peu nombreux), ce qui a un sens en dehors de moi-même et possède donc pour moi la plus grade valeur qui soit.
Saurez-vous retrouver à l'aide des indices dispersés dans ce texte le nom de l'astrophysicien?
Bibliographie
[1] Article de synthèse sur les différents modèles de sérialisation
http://www.odelmotte.fr/net/framework/serialisation
14.07.08
Faire apparaître intellisense avec une dll
Ayant pas mal la tête sous l'eau en ce moment j'ai du mal à trouver le temps de bloguer.
En attendant la nouvelle saga de l'été cette année consacrée à la sérialisation Xml un ou deux petits articles.
Vous savez qu'un des intérêts de documenter systématiquement au moins les membres public et protected est de disposer d'Intellisense partout dans le code.
Le problème est qu'une fois compilé dans une belle dll, Intellisense semble disparaître dès que l'on ajoute celle-ci à un autre projet.
Alors comment faire suivre Intellisense?
Je n'ai pas trouvé de réponse complète au problème, mais une réponse qui fonctionne tout de même.
Première étape, cocher la case Xml Documentation File dans l'onglet Build des propriétés du projet.
Ainsi à chaque compilation VS va générer un fichier XML qui pourra éventuellement être traité par Sandcastle pour construire la documentation en ligne.
Mais on ne va pas aller aussi loin.
Le point intéressant est que ce fichier a une autre propriété intéressante.
Lorsque vous déployez la dll qui lui correspond, vous devrez également déployer le XML au même endroit.
VS va détecter sa présence et la charger automatiquement. Ainsi vous disposez d'Intellisense dans le projet.
Pourquoi cette solution n'est pas complète?
Parce que l'on doit déployer deux fichiers. Idéalement un seul devrait suffire.
J'avoue ne pas avoir eu l'occasion de faire des tests, mais il devrait être possible de fusionner ces deux fichiers avec al.exe (je suppose).
Si quelqu'un a trouvé la solution...
La semaine prochaine on résoudra un problème de réutilisation d'un même type entre deux services Web.
11.04.08
Dany la malice m'a écrit!
Que ce soit sur mon blog ou par mail privé, je reçois régulièrement des questions. Malheureusement je n'y réponds pas toujours faute de temps.
Alors je vais essayer de me rattraper au cours des prochains blogs à commencer par un mail privé reçu cette semaine de la part d'un certain "dany la malice".
Que disait-il?
… je voulais savoir si vous pourriez m aider car je suis débutant en dotnet et je dois réaliser un projet qui gère une base de données qui elle même doit pouvoir comporter une table dont un champ qui reçoit des photos
le pb c est que je ne vois pas comment je peut faire cela
j ai déjà créé la base et la table
je travail sur vs2005 en c#...
Il y a deux façons de stocker des images dans une application:
• Dans une base de données
• Dans le système de fichier
Vous avez apparemment fait le premier choix. Avant de l'analyser je voudrai tout de même dire un mot du deuxième. Les deux choix sont tout aussi valides, mais le contexte de l'application peut parfois en préférer un plutôt que l'autre.
Par exemple si on parle d'une application Web, personnellement je pense que le système de fichier est le meilleurs choix, à condition de l'inclure dans un répertoire virtuel, c'est-à-dire accessible via une url.
C'est très simple à faire, vous créez un site Web qui ne contient que des répertoires dans lequel vous positionnez vos images.
Un autre avantage de ce choix est que s'il s'agit d'un site à fort trafic les images pourront être mises en cache très facilement. Il existe des sociétés spécialisées comme Akamai[1] qui louent de l'espace mémoire sur de gros serveurs pour cela justement.
Par expérience, un répertoire contenant de très nombreux fichiers est difficile à gérer et ralentit les performances. Il est donc important de le structurer en petits sous répertoires.
Avoir une règle de nommage précise peut vous simplifier la vie.
Supposons par exemple que le site affiche un catalogue de vêtements représentés par un identifiant. Vous pouvez nommer les images par l'identifiant et l'arborescence des répertoires selon une norme du genre:
/Hommes/Chemise/12345_red_XXL.gif
Côté code c'est facile de reconstituer dynamiquement l'url qui pointe vers l'image.
A titre d'exercice, essayez de vous renseigner pour savoir quelle est la taille maxi d'une url, s'il y en a une. Déduisez en quelques règles simples pour mettre au point votre architecture de répertoire.
Le choix est donc une base de données. Il s'agit de Sql Server 2005.
Sachez déjà que les champs de type image sont considérés comme dépréciés au profit de varbinary(max). Vous trouverez en bibliographie des informations sur le genre de problèmes résolus par varbinary [2].
Varbinary(max) fonctionne comme un champ normal. On peut donc le lire avec les objets ADO usuels sans grandes difficultés.
Côté Dal, on s'en sort comment?
Personnellement j'ai dans la majorité des cas un faible pour les DataReader, les performances sont en général meilleures. Il va vous renvoyer un tableau d'octet. Vous allez donc avoir du code qui ressemble à ceci:
long imgTaille = dr.GetBytes(0, 0, null, 0, 0); byte[] tabOct = new byte[imgTaille]; dr.GetBytes(0, 0, tabOct, 0, (int)imgTaille); // charge l'image
dr étant l'instance du DataReader.
Ensuite vous pouvez insérer l'image dans un flux http:
HttpContext.Current.Response.OutputStream.Write(tabOct, 0, tabOct.Length);
Où la charger en mémoire, par exemple:
using (MemoryStream reader = new MemoryStream(ImageProperty.Image)) { using (Bitmap bitmap = new Bitmap(reader)) { // traitement ici } }
Voilà de quoi vous occuper. J'ai testé ces deux méthodes dans des applications et elles fonctionnent très bien.
Bibliographie
[1] Pour en savoir plus sur Akamai:
http://www.01net.com/article/283899.html
[2] Information sur les champs de type blobs
http://www.teratrax.com/articles/varchar_max.html
http://blog.sqlauthority.com/2007/06/01/sql-server-2005-constraint-on-varcharmax-field-to-limit-it-certain-length/
05.04.08
Localisation de CategoryAttribute et DescriptionAttribute
Un composant ASP va typiquement exposer des déclarations de ce type:
[Category("Data"), Description("Valeur minimale")] public int Min { [DebuggerStepThrough] get { if (ViewState["Min"] == null) { // valeur par défaut return 0; } return (int)ViewState["Min"]; } [DebuggerStepThrough] set { ViewState["Min"] = value; } }
La question que l'on peut se poser est comment peut-on localiser les deux textes "Data" et "Valeur Minimale"?
Localisation de Category
Première chose à savoir: si Category correspond à une catégorie prédéfinie, alors il suffit d'utiliser son nom anglais dans l'attribut Category et Visual Studio se chargera du reste [1].
Le problème se pose plutôt si vous utilisez une catégorie personnalisée.
CategoryAttribute n'offrant aucun support natif de ce scénario, on devra écrire son propre code:
/// <summary> /// Prise en charge d'une catégorie localisée /// </summary> /// <remarks> /// Le constructeur attend une chaîne repésentant la clef dans le fichier de ressources du texte localisé /// </remarks> [AttributeUsage(AttributeTargets.All)] internal sealed class LocalizedCategoryAttribute : CategoryAttribute { #region Constructeurs /// <summary> /// Constructeur par défaut /// <param name="category">Nom de la catégorie non localisé (Data, Behavior...)</param> /// <remarks> /// Si <b>category</b> correspond à une catégorie prédéfinie, elle sera automatiquement localisée. /// /// Par convention une ressource de catégorie localisée devra utiliser la règle de nommage suivante: /// <c>Category_[Nom de la catégorie]</c> /// </remarks> /// </summary> public LocalizedCategoryAttribute(string category) : base(category) {} #endregion
#region GetLocalizedString (protected) /// <summary> /// Recherche le nom localisé de la catégorie spécifiée /// </summary> /// <param name="value">Identificateur de la catégorie à consulter</param> /// <returns>Nom localisé de la catégorie ou <b>Null</b> (<b>Nothing</b> en Visual Basic) s'il n'existe pas</returns> protected override string GetLocalizedString(string value) { // obtient le nom localisé de la catégorie au cas où elle appartient à une catégorie prédéfinie string LocalizedValue = base.GetLocalizedString(value);
if (string.IsNullOrEmpty(LocalizedValue)) { // elle n'appartient pas à une catégorie prédéfinie, on exécute donc notre propre logique de localisation LocalizedValue = Resources.ResourceManager.GetString(string.Concat("Category_",value)); }
return LocalizedValue; } #endregion }
Notez que la classe est déclarée comme internal. C'est normal, car la ligne de code suivante:
LocalizedValue = Resources.ResourceManager.GetString(string.Concat("Category_",value));
Est très spécifique à votre application et vous devrez certainement la modifier dans votre code.
Cas de DescriptionAttribute
Cette fois encore pas de support natif et donc une surcharge de DescriptionAttribute sera nécessaire. Pour les mêmes raisons qu'invoquées précédemment, la classe sera internal.
/// <summary> /// Prise en charge d'une description localisée /// </summary> /// <remarks> /// Le constructeur attend une chaîne repésentant la clef dans le fichier de ressources du texte localisé /// </remarks> [AttributeUsage(AttributeTargets.All)] internal sealed class LocalizedDescriptionAttribute : DescriptionAttribute { #region Constructeurs /// <summary> /// Constructeur par défaut /// </summary> /// <param name="resourceKey">Clef de la descripion dans le fichier de ressources</param> public LocalizedDescriptionAttribute(string resourceKey) : base(resourceKey) {} #endregion
#region Description /// <summary> /// Obtient la description localisée /// </summary> public override string Description { get { if (!_Localized) { _Localized = true;
try { string LocalizedDescription=Resources.ResourceManager.GetString(base.Description); if (!string.IsNullOrEmpty(LocalizedDescription)) { base.DescriptionValue = LocalizedDescription; } } catch {
throw; } } // if
return base.Description; } } #endregion
private bool _Localized; }
Bibliographie
[1] Liste des catégories prédéfinies:
http://msdn2.microsoft.com/fr-fr/library/system.componentmodel.categoryattribute.aspx
Un peu de tout
Les instances utilisateurs de Sqlserver Express
Vous avez sans doute remarqué qu'il est possible de créer dans un projet .NET une base de données locale. Elle n'a pas besoin d'être attaché à une instance de Sql server qui elle-même ne la voit pas apparaître d'ailleurs. On n'a besoin à vrai dire de ne la rattacher à rien, il suffit de se connecter et ça marche!
Justement, ça marche comment?
Grâce à une des nouveautés de SqlServer Express 2005: les instances utilisateur (user instance) que la plupart du temps on utilise sans s'en rendre compte.
Je ne vais pas faire un blog dessus, mais vous signaler deux articles passionnants à lire:
Un tutoriel en français:
http://msdn2.microsoft.com/fr-fr/library/bb264564.aspx
Un blog en en anglais:
http://blogs.msdn.com/sqlexpress/archive/2006/11/22/connecting-to-sql-express-user-instances-in-management-studio.aspx
Notez aussi que j'ai écris il y a quelques temps un livre blanc sur les projets de base de données où je parle longuement des projets avec des bases locales. On peut le télécharger sur ce site:
http://msdn2.microsoft.com/fr-fr/teamsystem/bb383578.aspx
(Les projets de bases de données avec Visual Studio 2005 Team Edition for Database Professionnals : Le guide)
Puisque l'on parle de SQLEXPRESS, sachez qu'il existe un service pack 2 que je vous recommande VIVEMENT d'installer si vous utilisez une base de données locale dans un projet. J'ai failli devenir fou il n'y a pas très longtemps à cause de cela!
Différence entre @@IDENTITY et SCOPE_IDENTITY()
Restons dans la base de données avec cette question très fréquente et sa réponse:
http://blogs.developpeur.org/guldan/archive/2008/04/04/une-histoire-de-scope.aspx
Recherche sur toutes les colonnes d'une table
Depuis des lustres je m'embête à créer une colonne fourre-tout dans laquelle je recopie le contenu de toutes les autres colonnes ou bien à écrire des WHERE complexes qui impliquent toutes les colonnes, mais au prix d'une certaine lenteur.
Voici une méthode nettement plus simple:
http://www.sqlhacks.com/index.php/Retrieve/SearchEveryField
Syntaxe correcte des événements
Depuis quelques temps j'ai remarqué des syntaxes exotiques pour déclarer des événements. On voit par exemple apparaître ceci dans le code y compris dans le code de développeurs confirmés:
public void MonEvent_Click(object sender, EventArgs e)
{
if (MonEvent != null)
{
MonEvent(this, e);
}
}
Ce n'est pas la syntaxe d'une méthode On, mais d'un gestionnaire d'événement. De plus un développeur un peu attentif devrait s'étonner de voir un paramètre au contenu mal définit et inutilisé dans une méthode.
Je dirais deux choses:
1. Ce n'est pas parce qu'un bout de code se trouve sur Internet qu'il est correct
2. Il existe une syntaxe correcte pour faire les choses correctement.
J'adore les snippets, en voici deux que vous pouvez utiliser pour écrire vos événements:
Cas d'événements non génériques:
<?xml version="1.0" encoding="utf-8" ?> <CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet"> <CodeSnippet Format="1.0.0"> <Header> <Title>Event</Title> <Shortcut>event</Shortcut> <Description>Modèle pour créer un événement</Description> <Author>Amethyste</Author> <SnippetTypes> <SnippetType>Expansion</SnippetType> </SnippetTypes> </Header> <Snippet> <Declarations> <Literal> <ID>nom</ID> <Default>MonEvent</Default> <ToolTip>Nom de l'événement</ToolTip> </Literal> <Literal> <ID>description</ID> <Default>(description)</Default> <ToolTip>Description de l'événement</ToolTip> </Literal> </Declarations> <Code Language="csharp"><![CDATA[ #region $nom$ (event) /// <summary> /// Evénement levé lorsque $description$ /// </summary> public event EventHandler $nom$;/// <summary> /// Méthode pour lancer l'événement $nom$ /// </summary> /// <param name="e">Paramètre de type <see cref="EventArgs"/> de l'événement</param> protected virtual void On$nom$(EventArgs e) { if ($nom$ != null) { $nom$(this, e); } } #endregion ]]> </Code> </Snippet> </CodeSnippet> </CodeSnippets>
Cas d'événement générique:
<?xml version="1.0" encoding="utf-8" ?> <CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet"> <CodeSnippet Format="1.0.0"> <Header> <Title>EventG</Title> <Shortcut>eventg</Shortcut> <Description>Modèle pour créer un événement générique</Description> <Author>Amethyste</Author> <SnippetTypes> <SnippetType>Expansion</SnippetType> </SnippetTypes> </Header> <Snippet> <Declarations> <Literal> <ID>nom</ID> <Default>MonEvent</Default> <ToolTip>Nom de l'événement</ToolTip> </Literal> <Literal> <ID>description</ID> <Default>(description)</Default> <ToolTip>Description de l'événement</ToolTip> </Literal> </Declarations> <Code Language="csharp"> <![CDATA[ #region $nom$ (event) /// <summary> /// Evénement levé lorsque $description$ /// </summary> public event EventHandler<$nom$EventArgs> $nom$;/// <summary> /// Méthode pour lancer l'événement $nom$ /// </summary> /// <param name="e">Paramètre de type <see cref="$nom$EventArgs"/> de l'événement</param> protected virtual void On$nom$($nom$EventArgs e) { if ($nom$ != null) { $nom$(this, e); } } #endregion ]]> </Code> </Snippet> </CodeSnippet> </CodeSnippets>
Je laisse à la sagacité de mes lecteurs le soin de trouver ce que l'on pourrait améliorer si on voulait pinailler!
16.11.07
ID, Name, ClientID et UniqueID c’est quoi au juste ?
D’abord je souhaiterai juste signaler un article de Jean-Marie Thia sur les Web SSO [1]. C’est un truc que je ne connaissais pas et qui entre dans la lignée de la série d’articles que j’ai publié cet été sur l’authentification. L’article est très complet avec une solution clef en main.
L’article qui suit est particulier car il était plutôt destiné au blog que j’anime sur l’intranet. J’ai entamé une longue série sur l’écriture des composants ASP et j’en suis venu naturellement à parler de l’identification d’un contrôle. Mais il me semble que sous des apparences inoffensives, cette question soulève en fait diverses difficultés qui sont au cœur même du fonctionnement d’ASP.NET et de toutes les technologies de pages Web interactives. Ces difficultés sont en général mal comprises et peu documentées.
J’ai donc décidé de le publier également sur mon blog Améthyste. Le prochain article sera la suite d’un des articles de l’intranet, mais uniquement publié ici !
Une des propriétés les plus importantes d’un composant est son ID. L’ID est la valeur unique qui va permettre de l’identifier au niveau du code.
Outre un ID, vous avez probablement remarqué qu’un contrôle dispose d’une valeur Name qui est souvent identique à ID. Sachez tout de suite que cela n’est en rien obligatoire.
Quelle est la différence entre Name et ID au juste ?
Name et ID peuvent parfaitement ne pas être fournis, tout dépend de ce que l’on veut faire avec le contrôle.
Pour donner une visibilité au contrôle depuis le code (client ou serveur), celui-ci doit disposer d’un ID. Sans cet ID le contrôle fonctionne normalement, mais n’est pas accessible.
C’est la principale fonction d’un ID, mais le W3C en propose d’autres :
• Servir de sélecteur de classe (la syntaxe avec le . (point) dont raffolent les infographistes et qui pourri la vie des développeurs)
• Servir de cible pour une ancre
• Nom d’un élément OBJECT
• Toutes autres fonctions définies par un agent comme un parseur…
Il faut également savoir que’ID et Name partagent le même espace de nom. Cela signifie qu’un élément ne peut avoir un ID identique au Name d’un élément distinct.
Le Name est lui indispensable pour indiquer à HTML que celui-ci doit renvoyer son contenu vers le serveur en cas de renvoi (postback).
Prenez le temps de tester l’exemple suivant car vous allez comprendre quelque chose d’important qui est au cœur du fonctionnement d’ASP.NET :
<body> <form id="form1" runat="server"> <input id="T1" type ="text" /> <input name="T3" id="T2" type="text" /><asp:Button ID="Button1" runat ="server" Text="Button" OnClick="Button1_Click" /> </form> </body>
Deux zones de saisies, l’un dispose d’un attribut Name, l’autre pas.
Le code behind doit juste déclarer :
protected void Button1_Click(object sender, EventArgs e) { foreach (string Chaine in this.Page.Request.Form.AllKeys) { Response.Write(Chaine + ": " + this.Page.Request.Form[Chaine] + "<br/>"); } }
Le code se contente de vider les valeurs postées par le navigateur vers le serveur.
Une fois la page affichée, faites une saisie dans chaque composant puis cliquez sur le bouton. Quelque chose de similaire à ceci va s’afficher :
__VIEWSTATE: /wEPDwUKMTQ2OTkzNDNiJUgLECjTMAzZIAcnKHmgvbXo=
T3: salut
Button1: Button
__EVENTVALIDATION: /wEWAgLZ+OznCQKM54rGBq73LPgMkgevvUcn9O4MEN
• On constate bien que seule la saisie du inputBox avec le Name est effectivement retournée vers le serveur.
• Du même coup on touche du doigt le mécanisme qui est à la base du fonctionnement de la persistance des données et qui ne doit rien au viewstate comme on le pense souvent, c’est une propriété intrinsèque des navigateurs.
• Au passage on observe que la clef qui est renvoyée est Name et pas ID.
• Vous noterez également que Name n’a pas spécialement besoin de porter la même valeur que ID.
Notez également qu’ASP.NET ajoute automatiquement un Name à tous les contrôles serveurs de type zone de saisie (TextBox, mais pas Label).
Voyons comment un ID est généré. A priori les choses paraissent simples :
• Je pose un composant sur ma page
• Je lui donne un ID, Compo1 par exemple
• Et c’est terminé, ma page HTML exhibe un joli composant avec ID=Compo1
Oui, mais que se passe t’il si ce composant fait partie du template d’un GridView ? Il va être répété autant de fois qu’il y a de lignes. Est-ce à dire que ma page va disposer de plusieurs composants avec le même ID ? Pas terrible pour un identifiant unique !
En fait pas du tout.
Certains composants sont des composants conteneur. Un conteneur a pour propriété de créer un espace de nom d’ID destiné à ses contrôles enfants. Pour cela il préfixe les ID de ses enfants par son propre ID.
Si j’ajoute que GridViewRow (le composant qui implémente une ligne d’un GridView) est un composant conteneur, vous comprendrez facilement ce qui se passe et pourquoi mon composant garde un ID unique.
Dans le cas où vous souhaitez implémenter un composant conteneur vous disposez de deux méthodes :
1. Le faire hériter de CompositeControl
2. Lui faire implémenter l’interface INamingContainer
INamingContainer est une interface purement déclarative, vous n’avez aucune ligne de code à ajouter.
Notez que Panel n’implémente pas cette interface.
Un ID va typiquement ressembler à ceci vu depuis la page Web :
ctl00_ContentPlaceHolder1_ddlGrpAD
Le ctl initial est codé en dur, on ne peut pas le modifier. Le reste dépend de la hiérarchie de conteneurs rencontrée entre la racine de la page et le composant.
L’attribut Name prend la forme suivante :
ctl00$ContentPlaceHolder1$ddlGrpAD
Par défaut il est identique à l’ID, mais le délimiteur est $ et non pas _.
De tels noms peuvent rapidement devenir volumineux. Il n’est donc pas utile de décorer à torts et à travers vos contrôles avec INamingContainer si celui-ci n’entre pas dans une des catégories suivantes de composant :
• Le contrôle peut être lié à des données
• Le contrôle est un template
• Le contrôle contient des contrôles enfants et des événements doivent être routés vers les enfants
Il est possible d’obtenir la valeur générée pour ID depuis le code serveur en interrogeant la propriété ClientID. C’est cette valeur que vous passez au code client pour qu’il puisse atteindre votre contrôle.
La valeur UniqueID retourne quand à elle la valeur de Name. C’est celle que vous allez utiliser pour explorer la liste Request.Form afin de réhydrater vos composants créés dynamiquement par exemple.
Se pose alors la question : à quel moment UniqueID et ClientID sont calculés ?
Intuitivement c’est évident, puisque ces valeurs dépendent de la hiérarchie de contrôles au dessus de votre composant, ce ne peut être qu’au moment où il est inséré dans cette hiérarchie, c'est-à-dire dans la collection Controls de la Page.
L’insertion d’un contrôle dans cette collection déclenche automatiquement la méthode Control.ControlAdded() qui se charge de créer l’ID et d’autres choses encore.
Si vous avez besoin d’accéder au conteneur le plus proche du contrôle, il suffit d’appeler sa propriété NamingContainer.
Notez pour finir les deux propriétés Control.ClientIDSeparator et Control.IdSeparator qui retournent le délimiteur pour un ClientID et un UniqueID respectivement.
Même si son emploi est assez rare, on dispose de la méthode EnsureID() qui assure que tous les contrôles disposent d’un ID.
Bibliographie
1. Les Web SSO :
http://www.techheadbrothers.com/Articles.aspx/websso-internet-information-server-ja-sig-central-authentication-service
16.10.07
Qautre p’tits bugs… et puis s’en vont
Voici une série de bugs de nos outils favoris avec une solution de contournement ou de résolution. J’espère que cela vous fera gagner du temps.
Un bug sur mon blog
Ceux qui ont suivi la SAGA de l’été sur l’authentification ont sans doute lu l’opus numéro 2 qui présentait une solution pour se reconnecter avec un autre utilisateur dans un contexte d’authentification Windows.
J’ai implémenté cette solution sur un de mes projets et je suis tombé sur un problème assez vite : certaines pages étaient appelées sous le nouveau login, d’autres non.
Je ne connais absolument pas la cause de ce problème (si quelqu’un a une idée je suis très intéressé), mais j’ai trouvé une solution.
Le code se termine par ceci :
// redirection vers la page d'accueil this.Response.Redirect("default.aspx");
Tout rentre dans l’ordre si on remplace Response.Redirect par Server.Transfert().
Un bug de IE 7
Créez cette innocente page :
<html><meta> <script/> </meta>
<body>Hello</body>
</html>
Vous vous attendez à voir Hello, s’afficher. En fait on observe ceci :
Une page blanche !
Le problème vient de la balise <script>. Il faut l’écrire en deux fois :
<script> </script>
Un bug avec TableAdapter
TableAdapter n’est en réalité pas un vrai composant. Mais peut importe, j’aime bien m’en servir et n’hésite pas à écrire des DAL entières avec.
Malheureusement ce composant est souvent assez fragile et a des comportements parfois difficiles à comprendre.
Un de ces bugs récurrents se produit de façon aléatoire. Lorsque vous tentez d’ouvrir le TableAdapter dans le designer en double cliquant sur le fichier .xsd. Vous obtenez ceci :

Le plus curieux est que cela n’empêche pas le composant de fonctionner avec l’existant, mais évidemment impossible de développer !
Si vous éditez le fichier .xsd avec Notepad et observez la section <connections>, vous allez trouver quelque chose de similaire à ceci :
<Connections> <Connection AppSettingsObjectName="Settings" AppSettingsPropertyName="Amethyste" ConnectionStringObject="" IsAppSettingsProperty="True" Modifier="Assembly" Name=" Amethyste (Settings)" ParameterPrefix="@" PropertyReference="ApplicationSettings.Amethyste.Dal.Properties.Settings.GlobalReference.Default. Amethyste" Provider="System.Data.SqlClient"> </Connection><Connection AppSettingsObjectName="Settings" AppSettingsPropertyName="Amethyste" ConnectionStringObject="" IsAppSettingsProperty="True" Modifier="Assembly" Name=" Amethyste (Settings)" ParameterPrefix="@" PropertyReference="ApplicationSettings. Amethyste.Dal.Properties.Settings.GlobalReference.Default.Amethyste" Provider="System.Data.SqlClient"> </Connection> </Connections>
De temps à autre Visual Studio duplique la déclaration de la chaîne de connexion, c’est cela qui provoque le problème. Il suffit de supprimer l’une d’entre elles et tout rentre dans l’ordre.
J’ai trouvé cette solution sur un blog, mais pas moyen de le retrouver. Désolé pour l’auteur.
Bug avec les DataSources
Une des nouveautés de .NET 2.0 sont les DataSource. C'est donc gaiement que je tente de lier un DataGridView à un DataSource sur une WinForm en ce jour pourtant grisâtre.
Je clique donc sur le SmartTag, puis la liste déroulante intitulée Choose datasource.
Immédiatement VS plante et je me fais éjecter! Le plus fou est que cela se produisait sur CE projet et pas sur d'autres.
Je ne vais pas vous révéler l'explication, que j'ignore, mais au moins une solution laborieusement trouvée par hasard.
La première fois que vous ajoutez un DataSource à un projet un sous-répertoire DataSource est créé dans le répertoire Properties du projet. Ce répertoire contient un fichier XML de configuration pour les DataSources du projet.
On peut essayer plusieurs choses:
le menu contextuel Refresh appliqué à chaque fichier ou bien carrément les supprimer.
Pour ma part c'est ce que j'ai fais avant de découvrir Refresh et ça ne semble pas perturber pépère.
Un dernier problème demeure avec mon DataGridView, parfois sa fenêtre de propriétés refuse de s'afficher. Pour l'instant je ne peux que relancer VS pour y accéder de nouveau. Si quelqu'un à une idée...
30.09.07
Lorem Ipsum
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Phasellus lobortis pede vitae nisl. Aliquam in lectus eget dui consectetuer pellentesque. Curabitur sed sapien ac nisl vehicula condimentum. Aliquam a nisi. Suspendisse non purus vitae nisi elementum posuere…
Bien sûr que ce texte vous rappelle quelque chose. On le retrouve un peut partout dans la documentation Microsoft.
Microsoft n'a joué aucun rôle dans la genèse de ce texte et ne fait que reprendre une tradition vieille de 5 siècles en usage chez les imprimeurs: le faux-texte.
Le faux-texte (ou ipsum lorem) est utilisé par les imprimeurs pour meubler une page avec un texte neutre qui ne distrait pas l'attention, mais présente une distribution normale des lettres. Par exemple pour exhiber une fonte.
J'ai longtemps cru que ce texte ne signifiait rien de particulier. Mais ce n'est pas le cas. Richard McClintock, un professeur de latin d'un collège de Virginie a découvert sa trace dans un texte de Cicéron: De Finibus Bonorum et Malorum.
Il circule plusieurs versions d'ipsum lorem. Si vous avez besoin d'un texte de remplissage j'ai même trouvé sur Internet un générateur automatique [1].
Le faux-texte un équivalent dans le monde de la chanson: le yaourt!
C'est moins poétique.
Il s'agit d'une technique utilisée par les compositeurs consistant à produire des sons qui évoquent la langue dans laquelle le texte de la chanson sera écrit utilisée pour se donner une idée de la version finale.
Normalement le yaourt n'apparaît pas dans la chanson finale, mais pas toujours. Nous avons je pense tous en mémoire les somptueuses mélodies New Age du groupe Era fondé par Eric Levi qui ne doivent rien au latin.
Une technique similaire, le scat, ne cherche pas à imiter quoi que ce soit. Il est là uniquement pour accompagner la rythmique.
La chanson Ameno (le clip a été tourné à Montsegur):
http://www.youtube.com/watch?v=6SvxaNQ6d7M
Les paroles pour chanter au bureau:
Prologue -
Dori me
Interimo adapare dori me
Ameno ameno latire
Latire-mo
Dorime
AMENO
Omenare imperavi ameno
Dimere dimere matiro
Matire-mo
Ameno
- music intro -
Omenare imperavi emulari
Ameno
Omenare imperavi emulari
-CHORUS-
AMENO
AMENO Dore
Ameno Dori me ( x 2)
Ameno Dom
Dori me reo
Ameno Dori me ( x 2)
Dori me am
-repeat chorus
-repeat music
Un petit blog sur l'histoire des technologies, il y avait longtemps!
Bibliographie
[1] Générateur d'Ipsum Lorem:
http://www.lipsum.com/
[2] Fiche Wikipedia du faux-texte:
http://fr.wikipedia.org/wiki/Faux-texte
[3] Ce que dit Microsoft de l'ipsum lorem
http://support.microsoft.com/kb/114222/en-us
22.09.07
IE se connecte avec un compte venu d'ailleurs!!!
Cette semaine j'ai sentis le vent du plan galère lors d'une livraison chez un client. Alors mon aventure fera peut être gagner du temps à quelques uns.
Une application ASP .NET qui réclame une authentification Windows. Tout fonctionne bien sauf sur deux postes chez le client. Sur ces deux postes l'utilisateur ouvre une session Windows avec son login, lance l'application et IE le connecte avec un autre compte venu de je ne sais où!!!!
Je fais les vérifications d'usage, écrit même un mini site d'une seule page dotée d'un simple composant LoginName pour être certain que le problème ne vient pas de l'appli elle-même et au bout d'une demi journée n'est pas plus avancé.
Je n'y comprends rien et le client s'inquiète car il doit faire une présentation de l'appli lundi au client final…
Je tiens à vivement remercier XiaoYong Dai du Microsoft Online Community Support pour m'avoir tiré d'affaire avec brio.
Le premier test qu'il me propose consiste à forcer l'affichage de la fenêtre d'authentification pour tous les sites provenant de l'intranet. Si vous ne savez pas comment faire ouvrez simplement l'onglet Securité des Options Internet et sélectionnez la zone de confiance Intranet Local puis Personnaliser le niveau. Une option Authentification utilisateur apparaît quelque part.

Cette fois lorsque la popup s'ouvre je vois apparaître le login parasite dans la zone de saisie.
Test qui paraît certes évident, mais je n'y ai pas pensé!
Au moins j'ai compris d'où viens le problème de connexion. Mais comment m'en débarrasser?
Deuxième réponse de XiaoYong Dai qui me permet de découvrir une fonctionnalité Windows dont je n'avais jamais entendu parler.
Dans le panneau de configuration on ouvre l'outil Compte utilisateur, puis Gérer vos mots de passe réseau (cela porte divers noms selon la version de Windows en fait). Une fenêtre de ce genre apparaît:

Et fait apparaître le coupable!
Il n'y a plus qu'à l'éliminer de la liste et tout rentre dans l'ordre.
Moralité: il faudra bien qu'un jour je me décide à me former un peu plus sérieusement à l'administration Windows, même si les histoires de plomberie et mot de passe ça a du mal à me passionner…
15.09.07
LA SAGA 8: un peu de tout
Probablement le dernier article de la SAGA. Cette semaine sera un peu brocante avec de tout.
Collection de liens
On va commencer par un petit bijou signé de Scott Guttrie [1].
Il s'agit d'une liste de dizaines de liens donnant accès à des centaines d'articles sur quasiment tous les aspects liés à la sécurité en .NET. Un article de référence à placer dans vos favoris, des semaines de lecture.
Utilitaire
Dans la même lignée j'ai trouvé un site avec des utilitaires gratuits pour IIS réalisés par l'équipe de développement [2]. En particulier l'un d'entre eux a attiré mon attention puisqu'il est au cœur de cette SAGA: AuthDiag.
AuthDiag est un utilitaire qui peut vous aider à résoudre les problèmes d'authentification avec IIS.
L'écran principal affiche une série de 4 menus et diverses informations sur votre site.
1. Check authentification
Tente de se connecter à votre site avec tous les modes d'authentification supportés par IIS sauf Passport: Anonyme, Kerberos, NTLM. Dan les deux dernier cas vous entrez bien sûr un login/mot de passe pour le compte à tester
2. Check Permissions
Permet de vérifier les permissions de lecture/écriture d'un compte sur une arborescence de répertoire, un répertoire ou les fichiers dans le répertoire
3. View permissions
On va être franc, je ne comprends rien à ce diagnostique!
4. Monitor Url failure
C'est peut être le diagnostique le plus utile. Dans ce mode AuthDiag agit comme un proxy entre IIS et votre application et audite tous les événements relatifs à l'authentification.
Il va vous indiquer pour chaque ressource réclamée quel est le compte concerné, ainsi que les informations relatives à son authentification (échec, réussite).
Impersonnalisation
Je n'ai pas eu l'occasion de faire un article sur ce sujet, il y a pourtant quelques nouveautés. Voici tout de même un article de synthèse [3].
Bibliographie
[1] La liste de Guttrie:
http://weblogs.asp.net/scottgu/archive/2006/02/24/438953.aspx
[2] Des utilitaires pour IIS:
http://www.iis.net/default.aspx?tabid=2&subtabid=29
[3] Synthèse en anglais sur l'impersonnalisation
http://msdn2.microsoft.com/en-us/library/ms998351.aspx
08.09.07
LA SAGA 7: authentification par formulaire, quelques scénarios
Je vais commencer par un rectificatif sur un extrait de code du blog précédent qui contient des erreurs.
Première erreur dans le fichier de configuration:
<authentication mode="Forms"> <forms loginUrl="~/login/login.aspx"> <credentials> <user name="amethyste" password="amethyste"/> </credentials> </forms> </authentication>
J'ai beau être absolument certain d'avoir testé, la réalité m'oblige à dire que cela ne peut pas fonctionner car par défaut ASP attend un mot de passe haché avec l'algorithme SHA1 [3]. Il faut donc tester le code avec le paramétrage suivant:
<authentication mode="Forms"> <forms loginUrl="~/login/login.aspx"> <credentials passwordFormat="Clear"> <user name="amethyste" password="amethyste"/> </credentials> </forms> </authentication>
Evidemment, dans la vie réelle on n'utilisera jamais un tel paramétrage!
Ce n'est pas tout. Le code qui vient ensuite doit plutôt ressembler à ceci:
FormsAuthentication.RedirectFromLoginPage(this.Identifiant.Text, true);
Au lieu de:
FormsAuthentication.RedirectFromLoginPage("nnn", true);
Afin de créer un cookie d'authentification basé sur le login de la personne connectée.
Nous allons rester dans le petit monde de l'authentification par formulaire. Sous ses aspects bonhommes, ce modèle d'authentification présente lui aussi quelques scénarios particuliers:
• Authentification multi-site
• Authentification entre ASP .NET 1.X et 2.0
• Que faire si le navigateur refuse les cookies?
L'authentification multi-site concerne deux situations:
1. Les fermes de site
2. Création d'un SSO pour n'avoir pas à se ré authentifier chaque fois que l'on change de site
Pour tester les exemples qui suivent il suffit de créer deux sites Web, appelons les WebSite1 et WebSite2.
Ces deux sites seront identiques.

La page de login contient le code vu la semaine dernière et corrigé plus haut.
La page principale contient juste un message qui permet d'identifier le site (mais on peut se contenter de l'url) ainsi qu'un bouton de déconnection qui appelle:
FormsAuthentication.SignOut();
Pour finir, le fichier web.config de ces sites contient la même déclaration suivante:
<authentication mode="Forms"> <forms loginUrl="~/login/login.aspx" timeout="1"> <credentials passwordFormat="Clear"> <user name="amethyste" password="amethyste"/> </credentials> </forms> </authentication> <authorization> <deny users="?"/> </authorization>
On va choisir un des sites comme site principal, par exemple WebSite2 et compléter sa page par défaut avec un lien hypertexte que vous adapterez à votre situation:
<asp:HyperLink ID="HyperLink1" runat="server" NavigateUrl="Ici url de WebSite1"> Vers WebSite1 </asp:HyperLink>
L'idée que l'on va tester est que si on s'authentifie sur WebSite2, puis que l'on se dirige vers WebSite1, celui-ci ne nous demandera pas de s'authentifier puisque les paramètres d'authentification sont identiques.
Si on fait l'expérience on constate la séquence suivante:
• On lance WebSite2
• On s'authentifie sur WebSite2
• La page principale de WebSite2 s'ouvre
• On clique vers le lien pour se diriger vers WebSite1
• On tombe sur la fenêtre d'authentification de WebSite1
Cela ne fonctionne donc pas.
Il y a une raison simple qui réside dans la façon dont le ticket d'authentification est crypté.
ASP a besoin de crypter beaucoup de choses comme les tickets d'authentification ou le viewstate. Pour savoir comment il doit procéder, ASP consulte les informations trouvées dans la section <machineKey>. On lira en bibliographie la documentation de cette section [4], pour notre propos il suffit juste de savoir que les attributs validationKey et decryptionKey qui déterminent la clef utilisées pour chiffrer/déchiffrer ainsi que valider les données a la valeur IsolateApp.
Cela signifie qu'une nouvelle clef sera générée pour chaque site.
On comprend donc mieux pourquoi le ticket d'authentification créé dans WebSite2 n'a pu être lu dans WebSite1.
La solution pour que tout marche est donc de fixer à la main une valeur. Par exemple l'utilitaire trouvé en [7] génère ceci: