27 janvier 2009

Astuce du jour : Copier un fichier avec la classe Stream

Voici une petite méthode qui permet de copier un fichier dans un nouveau à l'aide de la classe Stream :

public static void CopierFichier(string pNomFichier1, string pNomFichier2)
{
    // Initialisation des flux
    Stream streamFichier1 = new StreamReader(pNomFichier1).BaseStream ;
    Stream streamFichier2 = new FileStream(pNomFichier2, FileMode.Create, FileAccess.Write);

    // Initialisation du buffer à 32 Ko
    int taille = 1024 * 32;
    Byte[] buffer = new Byte[taille];
    int bytesRead = 0;

    // Copie du flux 1 vers le flux 2
    do
    {
        bytesRead = streamFichier1.Read(buffer, 0, taille);
        streamFichier2.Write(buffer, 0, bytesRead);
    }

    while (bytesRead > 0);
 
    // Fermeture des flux
    streamFichier1.Close();
    streamFichier2.Close();
} 

L'argument pNomFichier1 correspond au nom du fichier à copier et le 2ème correspond au nom du fichier créé par la fonction.

26 janvier 2009

Entity Framework et les types complexes

Les types complexes font parti des fonctionnalités manquantes de l'éditeur de modèle EDM fourni avec Visual Studio 2008. Pour pouvoir en bénéficier, il est nécessaire de modifier les fichiers CSDL (schéma conceptuel) et MSL (schéma de liaison) ainsi que de modifier les classes d'entité.

Tout d'abord à quoi sert un type complexe ? Dans une entité, il peut être utile de séparer dans un nouveau type plusieurs variables cohérentes entre elles. Par exemple, la classe Client est composée des variables suivantes :
  • Id

  • Nom

  • Prenom

  • Telephone

  • Pays

  • Rue

  • CodePostal

  • Ville

Les 4 dernières variables sont en relation avec l'adresse du client. Il serait donc logique de les déplacer dans une classe appelée Adresse, d'où l'utilité du type complexe.

Dans Entity Framework, un type complexe ne contient aucun identifiant et il est forcément relié à une entité mère, Client pour notre exemple. Comme avec l'héritage "1 table -> plusieurs entités", c'est une autre manière de définir plusieurs entités pour une seule table.

Si vous souhaitez mettre en place un type complexe, vous serez obligé d'utiliser directement les fichiers CSDL, MSL et SSDL. L'éditeur de modèle EDM cache ses 3 fichiers dans l'assemblage du projet en tant que ressources embarquées. Pour les extraire, j'ai utilisé Reflector et l'addin FileDisassembler permettant d'extraire tous les fichiers composant un assemblage.

Une autre solution est de passer par l'utilitaire "EdmGen.exe" qui permet de générer les fichiers CSDL, MSL, SSDL. Cependant, vous perdez tout le bénéfice de définir graphiquement votre modèle via l'éditeur d'EDM (une erreur de frappe est vite arrivé dans un fichier XML …).

Pour l'exemple, je pars de la base de données utilisée dans mon article publié sur developpez.com. Vous trouverez les scripts SQL Serveur 2005 de la base dans l'archive, à la fin de ce billet.

En 1er lieu, voici le schéma logique (SSDL) :

<Schema Namespace="helloentityfxModel.Store" Alias="Self" Provider="System.Data.SqlClient" ProviderManifestToken="2005" xmlns:store="http://schemas.microsoft.com/ado/2007/12/edm/EntityStoreSchemaGenerator" xmlns="http://schemas.microsoft.com/ado/2006/04/edm/ssdl">
  <EntityContainer Name="CustomModelStoreContainer">
    <EntitySet Name="Client" EntityType="helloentityfxModel.Store.Client" store:Type="Tables" Schema="dbo" />
  </EntityContainer>
  <EntityType Name="Client">
    <Key>
      <PropertyRef Name="Id" />
    </Key>
    <Property Name="Id" Type="int" Nullable="false" StoreGeneratedPattern="Identity" />
    <Property Name="Nom" Type="varchar" MaxLength="50" />
    <Property Name="Prenom" Type="varchar" MaxLength="50" /&gt
    <Property Name="Pays" Type="varchar" MaxLength="50" />
    <Property Name="Rue" Type="varchar" MaxLength="100" />
    <Property Name="CodePostal" Type="char" MaxLength="5" />
    <Property Name="Telephone" Type="char" MaxLength="10" />
    <Property Name="Ville" Type="varchar" MaxLength="50" />
  </EntityType>
</Schema>

Pour ce fichier, rien de spécial à signaler si ce n'est la description de la table et des propriétés (colonnes) qui la composent. Aucune référence au type complexe n'apparait. Ceci est tout à fait normal puisque c'est le schéma logique faisant référence à la structure de la base de données.

Passons maintenant au schéma conceptuel (CSDL) :

<Schema Namespace="CustomModel" Alias="Self" xmlns="http://schemas.microsoft.com/ado/2006/04/edm">
  <EntityContainer Name="Context">
    <EntitySet Name="Client" EntityType="CustomModel.Client" />
  </EntityContainer>
  <EntityType Name="Client">
    <Key>
      <PropertyRef Name="Id" />
    </Key>
    <Property Name="Id" Type="Int32" Nullable="false" />
    <Property Name="Nom" Type="String" MaxLength="50" Unicode="false" FixedLength="false" />
    <Property Name="Prenom" Type="String" MaxLength="50" Unicode="false" FixedLength="false" />
    <Property Name="Telephone" Type="String" MaxLength="10" Unicode="false" FixedLength="true" />
    <Property Name="Adresse" Type="Self.Adresse" Nullable="false" />
  </EntityType>
  <ComplexType Name="Adresse">
    <Property Name="Pays" Type="String" MaxLength="50" Unicode="false" FixedLength="false" />
    <Property Name="Rue" Type="String" MaxLength="100" Unicode="false" FixedLength="false" />
    <Property Name="CodePostal" Type="String" MaxLength="5" Unicode="false" FixedLength="true" />
    <Property Name="Ville" Type="String" MaxLength="50" Unicode="false" FixedLength="false" />
  </ComplexType>
</Schema>

Il comporte la définition de l'entité Client comprenant entre autre une propriété Adresse faisant référence au type complexe CustomModel.Adresse. Celui-ci est composé de 4 propriétés (Pays, Rue, CodePostal, Ville).

Pour finir, voici le schéma de liaison (MSL) :

<Mapping Space="C-S" xmlns="urn:schemas-microsoft-com:windows:storage:mapping:CS">
  <EntityContainerMapping StorageEntityContainer="CustomModelStoreContainer" CdmEntityContainer="Context">
    <EntitySetMapping Name="Client">
      <EntityTypeMapping TypeName="CustomModel.Client">
        <MappingFragment StoreEntitySet="Client">
          <ScalarProperty Name="Id" ColumnName="Id" />
          <ScalarProperty Name="Nom" ColumnName="Nom" />
          <ScalarProperty Name="Prenom" ColumnName="Prenom" />
          <ScalarProperty Name="Telephone" ColumnName="Telephone" />
          <ComplexProperty Name="Adresse" TypeName="CustomModel.Adresse">
            <ScalarProperty Name="Pays" ColumnName="Pays" />
            <ScalarProperty Name="Rue" ColumnName="Rue" />
            <ScalarProperty Name="CodePostal" ColumnName="CodePostal" />
            <ScalarProperty Name="Ville" ColumnName="Ville" />
          </ComplexProperty>
        </MappingFragment>
      </EntityTypeMapping>
    </EntitySetMapping>
  </EntityContainerMapping>
</Mapping>

On y remarque le mapping entre l'entité Client et la table Client, ainsi qu'entre le type complexe CustomModel.Adresse et la table Client.

Passons maintenant à la définition des classes et du contexte. 2 choix sont possibles, soit :
  • Utiliser le custom tool fourni par Visual Studio 2008 ou l'outil "edmgen.exe" pour générer les classes et le contexte à partir du fichier CSDL.

  • Définir/Modifier à la main vos propres classes d'entité. Cette méthode est utile lorsque vous possédez déjà vos classes métier. Cela consiste à implémenter pour chaque entité les interfaces IPOCO d'Entity Framework et d'ajouter quelques attributs.

Pour faire simple, voici la démarche pour utiliser le Custom Tool "EntityModelCodeGenerator" :
  • Sélectionnez le schéma conceptuel (fichier CSDL) dans Visual Studio.

  • Dans la fenêtre de propriétés, définissez la propriété Custom Tool à "EntityModelCodeGenerator".

  • A chaque sauvegarde du fichier CSDL, un fichier contenant les classes d'entité et le contexte est généré.

Et voila, si tout s'est bien passé, Client possède maintenant une variable de type Adresse, qui est notre type complexe.

Vous trouverez dans l'archive ci-dessous le code source de l'article et les scripts SQL Serveur 2005 de la base de données.

22 janvier 2009

Différentes façons de créer dynamiquement une méthode (3/3)

Ce billet es t le 3ème et dernier de la série. Tout comme les 2 précédents, il traite de la création dynamique d'une méthode et de son exécution.

Avec Mono.Cecil :

Tout d'abord, qu'est ce que Mono ? C'est un projet Open-Source supporté par Novell. Celui-ci a pour but de fournir une machine virtuelle, les compilateurs C# 3.0 et VB.Net (il doit y en avoir d'autres) et supporte notamment Silverlight (projet MoonLight). Mono.Cecil est un sous projet de Mono et propose à peu près les mêmes fonctionnalités que les classes des espaces de nom System.Reflection et System.Reflection.Emit.

D'une manière générale, Mono.Cecil est plus performant que son homologue et offre plus de possibilités. Patrick Smacchia a écrit un billet à ce sujet si cela vous intéresse.

Comparé au billet précédent, l'utilisation de Mono.Cecil, pour générer et exécuter dynamiquement une méthode, est sensiblement pareille. Il y a cependant quelques exceptions :
  • Je n'ai pas réussi à utiliser directement la méthode générée. J'ai du créer un assemblage à partir de la définition de l'assemblage modifié. Il doit être possible de faire autrement, mais je n'ai pas trouvé comment …

  • Mono.Cecil fait bien la distinction entre les TypeDef et le TypeRef (c'est un sujet un peu avancé j'avoue, même pour moi …). En gros, un TypeDef (définition de type) contient toutes les métadonnées pour un type donné, comme par exemple sa classe mère, les interfaces implémentées, les méthodes et les propriétés qui le composent, etc … Un typeRef (référence à un type) contient uniquement le nom du type et une variable faisant référence à l'assemblage/module auquel il appartient. Du coup, avec l'espace de nom System.Reflection, il se peut qu'on obtienne parfois des exceptions car on utilise sans le savoir un TypeRef alors qu'il faudrait un TypeDef …

Voici donc le code définissant et exécutant la méthode créée dynamiquement :

var defAssembly = AssemblyFactory.GetAssembly(Assembly.GetExecutingAssembly().Location);
addGeneratedMethod(defAssembly);
var modifiedAssembly = AssemblyFactory.CreateReflectionAssembly(defAssembly);
var modifiedType = modifiedAssembly.GetType("DynamicIL.Program");
modifiedType.GetMethod("helloCecil").Invoke(null, null);


Voilà l'explication de chaque instruction :
  • Premièrement, on récupère une définition de l'assemblage.

  • On fait appel à la méthode addGeneratedMethod qui créé et ajoute une méthode statique au type de la classe contenant la méthode Main. Son implémentation est expliquée juste après.

  • Un assemblage est créé à partir de la définition de l'assemblage modifié.

  • On récupère le type de la classe DynamicIL.Program?.

  • La méthode générée est appelée.

Voyons maintenant le corps de la méthode addGeneratedMethod :

private static void addGeneratedMethod(AssemblyDefinition pAssembly)
{
    var type = pAssembly.MainModule.Types.Cast<TypeDefinition>().Where(typeDef => typeDef.Name == "Program").First();
    var methodAttributes = Mono.Cecil.MethodAttributes.Public  Mono.Cecil.MethodAttributes.Static; 
    TypeReference voidType = pAssembly.MainModule.Import(typeof(void));
    MethodDefinition methodDef = new MethodDefinition("helloCecil", methodAttributes, voidType);
    MethodInfo methodInfo = typeof(System.Console).GetMethod("WriteLine", new Type[] { typeof(string) });
    var methodRef = pAssembly.MainModule.Import(methodInfo);

    CilWorker cilWorker = methodDef.Body.CilWorker;
    cilWorker.Emit(Mono.Cecil.Cil.OpCodes.Ldstr, "Hello World avec Mono.Cecil !");
    cilWorker.Emit(Mono.Cecil.Cil.OpCodes.Call,methodRef);
    cilWorker.Emit(Mono.Cecil.Cil.OpCodes.Ret);
 
    type.Methods.Add(methodDef);
}


Voici le détail de l'implémentation de la méthode:
  • On récupère la définition du type Program.

  • Une énumératoin Mono.Cecil.MethodAttributes est assignée pour que la méthode générée soit publique et statique.

  • Une référence au type void est récupérée. Celle-ci sert aussi à la création de la méthode statique.

  • Via une instance de la classe MethodDefinition, on définie la signature de la méthode. Celle-ci s'appelle helloCecil, elle est publique ainsi que statique et renvoie un type void (c'est-à-dire rien).

  • Les 2 instructions suivantes servent à récupérer une référence à la méthode WriteLine de la classe Console prenant une chaine de caractère en paramètre.

  • Les 4 instructions d'après permettent de définir le corps de la méthode de la méthode avec des instructions MSIL. Celles-ci sont équivalentes à celles utilisées dans l'exemple précédent.

  • Finalement, la définition de la méthode est ajoutée à la définition du type "Program".

A l'exécution du programme, vous devriez voire apparaître à l'écran "Hello World avec Mono.Cecil !".

Pour plus d'informations, l'archive ci-dessous reprend le code des 3 méthodes de génération dynamique présentées.

Différentes façons de créer dynamiquement une méthode (2/3)

Ce billet est le 2ème de la série. Il traite de la création dynamique d'une méthode.

Avec l'espace de nom System.Reflection.Emit :

Alors que l'espace de nom System.Reflection contient les classes nécessaires pour interroger la structure du code en .Net, l'espace de nom System.Reflection.Emit permet de modifier dynamiquement les différents éléments (classes, structures, énumérations, etc …) du langage.

Pour ce qui concerne la génération d'une méthode à l'exécution, cela se fait simplement avec la classe System.Reflection.Emit.DynamicMethod ,tout du moins pour la déclaration de la méthode. Ensuite, et c'est là où ça devient un peu plus compliqué, il faut rajouter les instructions composant le corps de la méthode en injectant des instructions MSIL.

MSIL pour MicroSoft Intermediate Language ou CIL (Common Intermediate Language) est le langage intermédiaire utilisé par la machine virtuelle du Framework .Net. Celui-ci est ensuite compilé pour pouvoir être exécuté sur la machine hôte. En fait, les compilateurs de la plateforme .Net, comme celui du C# ou de VB .Net transforment le code produit par le développeur en langage MSIL.

Vous trouverez un tutorial en anglais sur le MSIL à l'adresse suivante : http://weblogs.asp.net/kennykerr/archive/2004/09/07/introduction-to-msil-part-1-hello-world.aspx. Comparé à un langage comme l'assembleur, le MSIL est orienté objet et adopte la notion de piles.

Je vous rassure, je ne suis pas du tout un expert en MSIL, enfin pas encore ! Pour m'y retrouver, j'utilise Reflector qui permet de décompiler un assemblage .Net et de voir le code MSIL produit. En comparant avec le code C# ou VB.Net, il est possible de s'y retrouver.

Passons maintenant au code, voici les instructions définissant notre méthode, faisant appel à une procédure définissant le corps de la méthode, un délégué est ensuite créé et assigné, et finalement celui-ci est appelé.

DynamicMethod dynMeth = new DynamicMethod("foo", typeof(void), null, typeof(Program));
WriteHello(dynMeth.GetILGenerator());
Method meth1 = (Method)dynMeth.CreateDelegate(typeof(Method));
meth1();


Le constructeur de DynamicMethod utilisé prend en paramètre:
  • Le nom de la méthode : "foo"

  • Le type renvoyé : typeof(void)

  • Un tableau de Type définissant les types des arguments de la méthode : null (notre méthode ne prend aucun argument)

  • Le type qui reçoit cette nouvelle méthode : typeof(Program). Pour l'exemple, c'est le nom de la classe contenant la méthode statique main.

Voici maintenant le corps de la procédure WriteHello(ILGenerator) qui définit le corps de la méthode générée :

static void WriteHello(ILGenerator cg)
{
    cg.Emit(System.Reflection.Emit.OpCodes.Ldstr, "Hello World avec System.Reflection.Emit !");
    cg.Emit(System.Reflection.Emit.OpCodes.Call,
    typeof(System.Console).GetMethod("WriteLine",
    new Type[] { typeof(string) }));
    cg.Emit(System.Reflection.Emit.OpCodes.Ret);
}


Premièrement, on ajoute sur la pile la chaine de caractères "Hello World avec System.Reflection.Emit !". En MSIL, il est obligatoire de faire cela pour utiliser une constante. La 2ème instruction fait un appel à la méthode statique Console.WriteLine prenant en argument une chaine de caractère. La chaine de caractère utilisée est celle qui est en haut de la pile, c'est dire "Hello World avec System.Reflection.Emit !". La dernière instruction signifie la fin du corps de la méthode (ceci correspond à return;).

Après compilation, vous devriez voir apparaître dans la console "Hello World avec System.Reflection.Emit !". C'est magique non ?!

Pour plus d'informations, l'archive ci-dessous reprend le code des 3 méthodes de génération dynamique présentées.

Différentes façons de créer dynamiquement une méthode (1/3)

Dernièrement, j'avais cherché à créer une méthode grâce à l'API CodeDom, de générer/compiler un assemblage, de le charger en mémoire et enfin d'exécuter la méthode générée.

Cette méthode fonctionne, cependant elle a l'inconvénient de passer par une compilation du C# en MSIL avant de pouvoir enfin exécuter la méthode générée.
Je me suis demandé alors si c'était possible de générer et d'injecter dynamiquement une méthode dans l'assemblage utilisé ou ailleurs et de l'exécuter par la suite. En cherchant sur la toile, je suis tombé sur 4 solutions abordables :

J'ai donc mis en œuvre les 3 premières solutions, c'est-à-dire une implémentation avec System.Reflection.Emit, les arbres d'expressions et Mono.Cecil. Concernant PostSharp, je pense que je m'y pencherai un autre jour.

Pour information, chaque méthode générée est un simple Hello World affichée dans la console.

Avec les arbres d'expressions

Commençons par l'implémentation la plus facile, celle utilisant les arbres d'expressions. Succinctement, un arbre d'expressions est une structure de données représentant du code. Il est ensuite possible de générer une méthode dynamiquement. Dans LinQ , les arbres d'expressions sont utilisés avec l'interface IQueryable et le type générique Expression. Ce dernier est toujours associé à des expressions lambdas, qui ne sont rien d'autres que des méthodes anonymes composée d'une seule instruction (comprenez un seul point-virgule ;). Ensuite le compilateur C# ou VB.Net transforme l'expression lambda en un arbre d'expression.

Pour information, LinQ to SQL et LinQ to Entities utilisent les arbres d'expressions pour transformer une expression lambda (condition dans un méthode Where par exemple) en SQL.

L'instruction suivante :

Expression<Action> expr = () => Console.WriteLine("Hello World avec les arbres d'expression !");

Donne après décompilation avec Reflector :

Expression<Action> expr = Expression.Lambda<Action>(
                                    Expression.Call(null, 
                                                    typeof(System.Console).GetMethod("WriteLine", new Type[] { typeof(string) }), 
                                                    new Expression[] { Expression.Constant("Hello World avec les arbres d'expression !", typeof(string)) }), 
                                    new ParameterExpression[0]);


On s'aperçoit que le compilateur C# a transformé notre expression lambda en un arbre d'expressions.
Pour plus de détails sur les arbres d'expressions, voici le lien MSDN.

Voici donc le code pour générer notre HelloWorld de façon dynamique et avec les arbres d'expression :


Expression<Action> expr = Expression.Lambda<Action>(
                                    Expression.Call(null, 
                                                    typeof(System.Console).GetMethod("WriteLine", new Type[] { typeof(string) }), 
                                                    new Expression[] { Expression.Constant("Hello World avec les arbres d'expression !", typeof(string)) }), 
                                    new ParameterExpression[0]);
var meth2 = expr.Compile();
meth2();


A la 1ère ligne, l'expression lambda est définie et assignée au type Expression. Ce code une fois compilé résulte donc en un arbre d'expressions. En d'autres termes, Le compilateur C# a transformé la logique de l'expression lambda en données. Pour transformer ensuite l'arbre d'expressions en logique, il suffit simplement d'appeler la méthode Compile de la classe Expression . Cette méthode retourne un délégué qui représente la méthode générée.

Petite remarque, il est très facile de générer du code via les arbres d'expressions. Cependant, un arbre ne peut contenir que l'équivalent d'une instruction, tout comme les expressions lambdas. Du coup, pas besoin de connaitre le MSIL.

Nous verrons dans le prochain billet comment générer une méthode avec les classes de l'espace de nom System.Reflection.Emit.

Pour plus d'informations, l'archive ci-dessous reprend le code des 3 méthodes de génération dynamique présentées.

19 janvier 2009

Générer un flux RSS avec LinQ To XML

LinQ To XML permet de manipuler les fichiers et les flux XML. C'est la dernière API XML de Microsoft et celle-ci offre l'avantage d'être beaucoup plus facile à utiliser que XmlTextReader/XmlTextWriter, grâce à son style fonctionnel.

Dernièrement, j'ai du générer quelques flux RSS pour un projet Sharepoint. Je me suis donc servi de LinQ To XML pour créer les flux en question.

Voici donc le code nécessaire pour générer un flux RSS avec LinQ To XML :


public class ItemRSS
{
    public string Titre { get; set; }
    public string Lien { get; set; }
    public string Description { get; set; }
    public string Auteur { get; set; }
    public string Date { get; set; }
    public string Source { get; set; }
    public string Catégorie { get; set; }
    public string Guid { get; set; }
}

public static string BuildRSS(string pTitreRSS, 
                              string pLienRSS, 
                              string pDescription, 
                              string pLanguage,
                              string pPubDate,
                              string pDerniereDate,
                              string pNomGenerateur,
                              string pMailResponsable,
                              IEnumerable<ItemRSS> pListeItems)
{
    XDocument xdoc = new XDocument(
                        new XDeclaration("1.0", "UTF-8", "yes"),
                        new XElement("rss",
                            new XElement("channel",
                                new XElement("title", pTitreRSS),
                                new XElement("link", pLienRSS),
                                new XElement("description", pDescription),
                                new XElement("language", pLanguage),
                                new XElement("pubDate",pPubDate),
                                new XElement("lastBuildDate",pDerniereDate),
                                new XElement("generator", pNomGenerateur),
                                new XElement("webMaster",pMailResponsable),
                                from item in pListeItems
                                select new XElement("item",
                                    new XElement("title", item.Titre),
                                    new XElement("link", item.Lien),
                                    new XElement("description", new XCData(item.Description)),
                                    new XElement("author", item.Auteur),
                                    new XElement("pubDate", item.Date),
                                    new XElement("guid", item.Lien),
                                    new XElement("source",item.Source),
                                    new XElement("category",item.Catégorie))),
                            new XAttribute("version", "2.0")));
    return xdoc.ToString();
}

La classe ItemRSS représente un item du flux RSS et chaque propriété publique représente un élément de l'item.

La méthode statique BuildRSS prend en paramètre quelques valeurs définissant les éléments enfants de l'élément channel d'un flux RSS et prend aussi en paramètre la liste des items contenus dans le flux RSS.

En une seule instruction, celle du constructeur de la classe XDocument, la méthode BuildRSS construit l'intégralité du flux RSS. Un élément XML du style <title>titre</title> est représenté par l'instruction suivante : new XElement("title","titre"). Une déclaration XML est représentée par la classe Xdeclaration et un attribut par XAttribute.

Les classes XDocument et XElement peuvent aussi prendre en paramètre dans leur constructeur un tableau d'objets (params object[]), ce qui permet facilement de définir les enfants d'un élément XML.

Vous trouverez ci-dessous un exemple d'application Console utilisant ce code pour générer un flux RSS (nécessite Visual Studio 2008).

Récupérer la valeur d'une propriété d'un objet à partir de son nom

Si vous souhaitez récupérer la valeur d'une propriété ou d'une variable membre publique à partir de son nom, voici 2 méthodes utilitaires permettant de faire cela grâce à la réflexion :


class Objet
{
    public string Nom { get; set; }
    public string Variable;
}

public static class Util
{
    public static T GetValeurProp<T>(this object pObjet, string pNom)
    {
        return (T)pObjet.GetType().GetProperty(pNom).GetValue(pObjet, null);
    }
    public static T GetValeurField<T>(this object pObjet, string pNom)
    {
        return (T)pObjet.GetType().GetField(pNom).GetValue(pObjet);
    }
}

 
class Program
{
    static void Main(string[] args)
    {
        var obj = new Objet() { Nom = "Propriété", Variable = "variable membre publique" };
        Console.WriteLine(obj.GetValeurProp<string>("Nom"));
        Console.WriteLine(obj.GetValeurField<string>("Variable"));
        Console.ReadKey();
    }
}
Grâce aux méthodes GetField(string) et GetProperty(string) de la classe Type, il est possible de récupérer respectivement une variable membre publique et une propriété publique d'une classe à partir d'une chaine de caractères. Après il suffit simplement d'appeler la méthode GetValue des objets FieldInfo et PropertyInfo pour récupérer la valeur de la propriété ou de la variable membre.

30 décembre 2008

Redirection dans un bloc Try Catch

En voulant faire une simple redirection avec Response.Redirect("url") dans une page ASP.Net, je me suis heurté à un problème. Cette fonction appelle en interne la méthode Response.End() qui a pour rôle de terminer le thread en cours. Le problème c'est que cette méthode génère une exception de type System.Threading.ThreadAbortException.

 
 

Pas forcément de problème si je n'avais pas mis un block try {} catch(Exception exp) {} autour de la redirection … Du coup, une fois l'instruction de la redirection passée, je tombais à chaque fois dans le block catch {}. Il est à noter que le comportement est le même avec la méthode Microsoft.SharePoint.Utilities.SPUtility.Redirect("url",flags,Context) effectuant une redirection dans Sharepoint.

 
 

Une bonne solution est d'utiliser une surcharge de la méthode Response.Redirect prenant en plus un booléen à mettre à false. Ce dernier évitera l'appel à la méthode Response.End(). Vous pouvez aussi enlever le bloc try {} catch{} mais cela n'est pas très propre.

Charger une dll dynamiquement

Grâce à la réflexion, il est tout à fait possible de charger une dll dynamiquement. La classe System.Reflection.Assembly contient une méthode statique LoadFrom prenant comme argument dans une méthode surchargée, une chaine de caractère. En passant à la fonction le nom de la dll, on récupère une référence de l'assembly contenu dans la dll.

string dllName = ConfigurationSettings.AppSettings["dll"];
Assembly assembly = Assembly.LoadFrom(dllName);

A partir de cette référence, il est possible d'instancier un type contenu dans l'assembly. Seulement pour manipuler facilement ce type (sans réflexion), il faut connaitre sa classe, ou du moins une interface ou une classe abstraite dont il hérite. Imaginons que ce type hérite d'une interface appelée IInterface contenant une méthode executer() qu'il implémente, voici un bout de code permettant d'invoquer cette fonction :

string typeName = ConfigurationSettings.AppSettings["type"];
Type dllType = assembly.GetType(typeName);
IInterface obj = Activator.CreateInstance(dllType) as IInterface;
obj.executer();

Dans les 2 exemples ci-dessus, je récupère le nom de la dll et du type dans le fichier de configuration de l'application (de type Console pour l'exemple). Ceci est une bonne façon de faire, si vous souhaitez spécifier une autre dll et un autre type sans recompilation. Voici le fichier de configuration de l'exemple :

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<appSettings>
<add key="dll" value="library.dll"/>
<add key="type" value="library.Class1"/>
</appSettings>
</configuration>

Pour plus d'infos, veuillez vous référer à l'archive ci-présente :

29 décembre 2008

Sharepoint : Provisioning de pages dans un site de publication

Dernièrement, j'ai du créer une mise en page (page layout) pour un site de publication. Toutes les pages utilisant cette mise en page contiennent les 2 mêmes webparts ainsi qu'un champ de type HTML remplie toujours de la même manière.

J'ai donc cherché à provisionner la mise en page, pour qu'à la création d'une page, les 2 webparts et le contenu HTML soient ajoutés automatiquement. Sharepoint propose une solution simple de fichier XML et de CAML, dont voici un exemple :

   1:  <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
   2:      <File Url="PressWIBLayout.aspx" Type="GhostableInLibrary" IgnoreIfAlreadyExists="TRUE">
   3:        <Property Name="Title" Value="Press WIB Layout" />
   4:        <Property Name="MasterPageDescription" Value="PressWIBLayout" />
   5:        <Property Name="ContentType" Value="$Resources:cmscore,contenttype_pagelayout_name;" />
   6:        <Property Name="PublishingPreviewImage" Value="~SiteCollection/_catalogs/masterpage/Preview Images/SAMPLELayout.png, ~SiteCollection/_catalogs/masterpage/Preview Images/SAMPLELayout.png" />
   7:        <Property Name="PublishingAssociatedContentType" Value=";#$Resources:xxx,ContentType_Page_Title;;#0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF390064DEA0F50FC8C147B0B6EA0636C4A7D400ABEA1C761C4D4268997A1EBE2A5C8EA6;#" />
   8:        <Property Name="Footer" Value="&lt;div class=&quot;pageFooter topic&quot;&gt;&lt;p&gt;$Resources:xxx,SiteTemplate_SPS_FooterTopic;&lt;/p&gt;&lt;/div&gt;" />
   9:        <AllUsersWebPart WebPartZoneID="ZoneMainRightCol" WebPartOrder="1">
  10:          <![CDATA[
  11:  <webParts>
  12:    <webPart xmlns="http://schemas.microsoft.com/WebPart/v3">
  13:      <metaData>
  14:        <type name="XX.XXX.Runtime.Webparts.FeedBack, XX.XXX.Runtime, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxx" />
  15:        <importErrorMessage>Impossible d'importer ce WebPart.</importErrorMessage>
  16:      </metaData>
  17:      <data>
  18:        <properties>
  19:          <property name="AllowClose" type="bool">True</property>
  20:          <property name="Width" type="string" />
  21:          <property name="AllowMinimize" type="bool">True</property>
  22:          <property name="AllowConnect" type="bool">True</property>
  23:          <property name="ChromeType" type="chrometype">Default</property>
  24:          <property name="TitleIconImageUrl" type="string" />
  25:          <property name="Description" type="string" />
  26:          <property name="Hidden" type="bool">False</property>
  27:          <property name="TitleUrl" type="string" />
  28:          <property name="AllowEdit" type="bool">True</property>
  29:          <property name="Height" type="string" />
  30:          <property name="MissingAssembly" type="string">Impossible d'importer ce composant WebPart.</property>
  31:          <property name="HelpUrl" type="string" />
  32:          <property name="Title" type="string">XXX:: Feed Back</property>
  33:          <property name="CatalogIconImageUrl" type="string" />
  34:          <property name="Direction" type="direction">NotSet</property>
  35:          <property name="ChromeState" type="chromestate">Normal</property>
  36:          <property name="AllowZoneChange" type="bool">True</property>
  37:          <property name="AllowHide" type="bool">True</property>
  38:          <property name="HelpMode" type="helpmode">Modeless</property>
  39:          <property name="ExportMode" type="exportmode">All</property>
  40:        </properties>
  41:      </data>
  42:    </webPart>
  43:  </webParts>          
  44:            ]]>
  45:        </AllUsersWebPart>
  46:        <AllUsersWebPart WebPartZoneID="ZoneRightTopCol" WebPartOrder="1">
  47:          <![CDATA[        
  48:  <webParts>
  49:    <webPart xmlns="http://schemas.microsoft.com/WebPart/v3">
  50:      <metaData>
  51:        <type name="XX.XXX.Runtime.Webparts.LatestWIBs, XX.XXX.Runtime, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxx" />
  52:        <importErrorMessage>Cannot import this Web Part.</importErrorMessage>
  53:      </metaData>
  54:      <data>
  55:        <properties>
  56:          <property name="Title" type="string">XXX:: latest weeks in brief</property>
  57:          <property name="WIBTitle" type="bool">False</property>
  58:          <property name="NumberOfElements" type="int">1</property>
  59:        </properties>
  60:      </data>
  61:    </webPart>
  62:  </webParts>
  63:            ]]>
  64:        </AllUsersWebPart>
  65:      </File>
  66:    </Module>
  67:  </Elements>

L'attribut Url de l'élément File spécifie la mise en page concernée et l'attribut IgnoreIfAlreadyExists=TRUE dit à Sharepoint que si la mise en page existe déjà, ce n'est pas la peine de la rajouter.

Après compilation, déploiement et activation, le résultat est impeccable : une page créée avec la mise en page ci-dessus contient bien mes 2 webparts. Cependant, je souhaitais modifier quelques propriétés dans une des 2 webparts. Je recompile, redéploie et réactive ma feature. Lorsque je recréée une page, je m'aperçois que ma page contient, non pas 2 webparts, mais 4, 2 de chaque en fait.

En faisant quelques tests, je me rends compte qu'a chaque fois que je désactive/réactive la feature, une instance pour chaque webpart spécifiée dans le module XML est ajoutée à chaque page créée avec cette mise en page. Pourtant, j'ai bien spécifié d'ignorer la mise en page si elle existe déjà dans le catalogue de la collection de site, via l'attribut IgnoreIfAlreadyExists défini à true.

En fait, la mise est bien ignorée si elle existe déjà. Malheureusement, ce n'est pas le cas des webparts spécifiées à l'intérieur. Pour moi, ceci est un bug de Sharepoint, peut-être est-il déjà référencé par Microsoft … Une autre solution de provisioning serait d'utiliser un EventReceiver ajoutant les webparts voulues à la création d'une page.