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.

18 décembre 2008

Article sur Entity Framework

Developpez.com vient de publier un article que j'ai écrit sur Entity Framework, la solution mapping objet-relationnel de Microsoft : http://pmusso.developpez.com/tutoriels/dotnet/entity-framework/introduction/.

 
 

Dans l'article, vous trouverez une partie théorique sur le framework expliquant entre autre le concept d'entités, ce qu'est le modèle conceptuel et ses 3 schémas XML.

Dans une 2ème partie, vous apprendrez à vous servir de l'assistant de Visual Studio pour générer vos objets métiers à partir d'une base de données déjà établie.

En dernier lieu, l'utilisation des entités et du contexte sont expliqués en détail. Vous trouverez aussi quelques informations sur l'accès concurrentiel et les transactions.

 
 

Bonne lecture ! :)

10 décembre 2008

POCO avec Entity Framework (1/2)

Qu'est-ce que POCO déjà ?

Cela signifie Plain Old Code Object. Le "C" peut vouloir dire aussi CSharp ou CLR Object. Dans le monde Java on retrouve ainsi l'acronyme POJO qui veut dire Plain Old Java Object, mais le sens est le même.

POCO signifie de laisser tel quel ses classes métiers lorsqu'on souhaite les rendre persistantes avec un outil de mapping objet-relationnel. C'est à dire que les classes métiers ne sont pas modifiées pour les faire fonctionner avec l'outil.

Entity Framework fonctionne avec des entités. Ce sont des classes dérivant de la classe System.Data.Objects.DataClasses.EntityObject. Une autre solution est d'implémenter 3 interfaces fournies par le framework .Le problème avec ce design, c'est que les classes métiers doivent être modifiées pour fonctionner avec Entity Framework. Du coup, on en déduie qu'Entity Framework ne supporte pas le concept POCO, en tout cas pas dans sa version 1.

L'équipe d'Entity Framework travaille sur une solution afin d'annuler la dépendance qu'il existe entre les classes métiers et le framework. Vous trouverez une partie de leur reflexion sur un de leur blog.

Une autre solution appellée PostSharp4EF a été proposée par Ruurd Boeke. Celle-ci est plutôt élégante car elle utilise la programmation orientée aspect (avec Postsharp) pour implémenter lors d'une 2ème compilation les interfaces requises pour rendre persistentes nos classes POCO.

PostSharp4EF exécute entre autres les tâches suivantes :
  • implémente les interfaces IPOCO : IEntityWithChangeTracker, IEntityWithKey et IEntityWithRelaships
  • Ajoute l'attribut EdmEntityType sur la classe, l'attribut EdmScallarProperty sur chaque propriété, l'attribut EdmRelashionShip et EdmSchema sur l'assembly

Toutes ces tâches sont exécutées grâce à PostSharp et à l'attribut [assembly: Poco("InheritanceTypeConnection")], celui-ci étant implémenté par PostSharp4EF.

Nous verrons dans un prochain billet un petit exemple d'implémentation.

3 décembre 2008

Exécuter une chaine de caractère avec CodeDom et la réflexion

Grâce à l'API CodeDom, il est assez facile de générer des assemblies dynamiquement. Le framework .Net permet d'exécuter un assembly grâce à la réflexion. A partir de ces 2 constats, il devient possible d'exécuter une chaine de caractère contenant du code, un peu comme on le ferait dans un langage de script.

Dans un langage statique comme le C#, le code doit d'abord être compilé, ce que CodeDom fait très bien. Il ne reste plus qu'à utiliser la réflexion pour exécuter le code compilé. Voici l'exemple d'une fonction montrant cela :

   1:  using System;
   2:  using System.CodeDom;
   3:  using System.CodeDom.Compiler;
   4:  using System.Reflection;
   5:   
   6:  namespace GenererMethode
   7:  {
   8:      public class Generateur
   9:      {
  10:          public static void Executer(string[] references, string[] usings, string strCode)
  11:          {
  12:              CompilerParameters compilerParameters = new CompilerParameters();
  13:              compilerParameters.GenerateExecutable = false;
  14:              compilerParameters.GenerateInMemory = true;
  15:              compilerParameters.IncludeDebugInformation = true;
  16:   
  17:              foreach (string reference in references)
  18:              {
  19:                  compilerParameters.ReferencedAssemblies.Add(reference);
  20:              }
  21:   
  22:              CodeCompileUnit code = new CodeCompileUnit();
  23:              code.UserData.Add("AllowLateBound", false);
  24:              code.UserData.Add("RequireVariableDeclaration", true);
  25:   
  26:              CodeNamespace nameSpace = new CodeNamespace("GenererMethode");
  27:              foreach (string _using in usings)
  28:              {
  29:                  nameSpace.Imports.Add(new CodeNamespaceImport(_using));
  30:              }
  31:              code.Namespaces.Add(nameSpace);
  32:   
  33:              CodeTypeDeclaration classObject = new CodeTypeDeclaration("Test");
  34:              nameSpace.Types.Add(classObject);
  35:              classObject.TypeAttributes = TypeAttributes.Public;
  36:   
  37:              CodeMemberMethod method = new CodeMemberMethod();
  38:              method.Attributes = MemberAttributes.Public  MemberAttributes.Static;
  39:              method.Name = "HelloWorld";
  40:              CodeSnippetExpression csu = new CodeSnippetExpression(strCode);
  41:              method.Statements.Add(csu);
  42:              classObject.Members.Add(method);
  43:   
  44:              CodeGenerator.ValidateIdentifiers(code);
  45:   
  46:              CodeDomProvider codeProvider = CodeDomProvider.CreateProvider("C#");
  47:   
  48:              var result = codeProvider.CompileAssemblyFromDom(compilerParameters, code);
  49:   
  50:              Assembly assembly = result.CompiledAssembly;
  51:              Type type = assembly.GetType("GenererMethode.Test");
  52:              type.InvokeMember("HelloWorld", BindingFlags.InvokeMethod, null, null, null);
  53:          }
  54:      }
  55:  }

La méthode "Executer" crée une classe quelconque et créé aussi une méthode statique contenant la chaine de caractère passée en paramètre. Pour que le code de la chaine de caractères puisse s'exécuter, la méthode prend en paramètre les dll contenant les assemblies et les usings nécessaires. Finalement, la méthode générée est exécutée via la réflexion.


   1:  Generateur.Executer(new string[] { "System.dll" },
   2:                                  new string[] { "System" },
   3:                                  "Console.WriteLine(\"Hello World\")");