Friday, November 30, 2007

SharePoint 2007 : How To Run STSADM Command From Your Code

Today, I've discovered something very interesting I want to share with you: How to run stsadm command inside my code. It's really easy, if you look at the following code, I specify to search from a membership provider only:
string prgFilePath = System.Environment.GetFolderPath(Environment.SpecialFolder.CommonProgramFiles); string stsadmPath = prgFilePath + @"\Microsoft Shared\web server extensions\12\BIN\stsadm.exe"; System.Diagnostics.Process proc = new System.Diagnostics.Process();
//Wait for the end of the process
proc.EnableRaisingEvents = false; proc.StartInfo.FileName = stsadmPath; proc.StartInfo.Arguments = "-o setproperty -url http://dcmoss:50020/ -pn "peoplepicker-nowindowsaccountsfornonwindowsauthenticationmode" -pv yes"; proc.Start();
//Wait
proc.WaitForExit();
With this method you can dynamically activate or deactivate feature, make some maintenance operation, change properties...

Thursday, November 22, 2007

Sharepoint 2007: SPBuiltInFieldId a Class You Must Know

A short comment today about a class I've just discovered: SPBuiltInFieldId With this class you have access to all built-in fields, even the hidden one's Example:
SPBuiltInFieldId.WorkflowListId SPBuiltInFieldId.WorkflowItemId SPBuiltInFieldId.WorkflowVersion SPBuiltInFieldId._CopySource SPBuiltInFieldId._RightsManagement
Link: http://msdn2.microsoft.com/En-US/library/microsoft.sharepoint.spbuiltinfieldid_members.aspx

Wednesday, November 21, 2007

SharePoint 2007 - Create field lookup and associated list as a feature

Je suis désolé pour les francophones, mais je dois maintenant m'exprimer en anglais car j'ai mis le code sur Codeplex This feature is an extension of the one from Chris O'Brien (SharePoint 2007 - create lookup fields as a feature), but offer you the ability to define one or more fields and the associated lists within one xml file.Moreover you'll can use these fields in one or more content type. When you'll activate the feature, everything will be build ! Let me explain a bit: First, if you want to have a full explanation of managing lookup column dynamically, you can start with a look on the blog of Chris O'Brien : http://sharepointnutsandbolts.blogspot.com/2007/04/creating-list-based-site-columns-as.html For summarizing, a lookup field is like a choice field but the content comes from a list in the site collection. The main difficulty is that the link between the field and the list is a GUID coming from the instance of the list. This means the GUID is only known when the list has been created. So you can't fix the link in your site column definition. ie:
<Field Type="Lookup" List="{A77FA6BC-8915-458a-914A-92BDDC375726}" ID="{E918A3F4-9E34-4bd2-A621-5F353B4529E2}" Name="KeywordLookup" />
is not possible because you don't know the list GUID. Well, of course you can cheat a bit if you define a list in your site and you retrieve the actual GUID of the instance. But what about moving a site from a test environment to a production environement ? Or from a site collection to another...? The GUID change each time ! The solution proposed by Chris is great. But you must still define a list before activating the feature. And because I'm a little bit lazy, I've tried to improve his solution to one more generic. Let's suppose you want to have a lookup field which contain a list of keywords. These keywords come from a SharePoint list, so you can extend it as you want. Create a new XML file and insert the following code inside. Of course you can have more than one field into this XML file, but I'll use only one for the demo.
<?xml version="1.0" encoding="utf-8" ?><Elements xmlns="http://schemas.microsoft.com/sharepoint/"> <Field Type="Lookup" DisplayName="Mots-clés" Mult="TRUE" Required="FALSE" List="Keywords;Mots-Clés" ShowField="Title" UnlimitedLengthInDocumentLibrary="FALSE" Group="TEST" ID="{E918A3F4-9E34-4bd2-A621-5F353B4529E2}" SourceID="http://schemas.microsoft.com/sharepoint/v3" StaticName="KeywordLookup" Name="KeywordLookup" /> </Elements>
As you can, this is a classic field definition. Except for one attribute: List. This attribute has two words, Keyword and Mots-Clés. The first one is the internal name of the list template, the second one is the name of the list we'll see in the site. In our case, we gonna create a list named Mots-Clés based on the template internal name Keyword. For a standard Customlist, the internal name is custlist (have a look on 12\TEMPLATE\FEATURES\CustomList\ListTemplates) Why all of this? Because when the feature will be activated, a program will search for a list named Mots-Clés in the site collection. If this list doesn't exist, then the program creates it, based on the list template and the list name you define in the List attribute. Here is the Feature.xml file:
<?xml version="1.0" encoding="utf-8" ?> <Feature Title="Site Column Lookup Initialization" Id="498CB08B-1786-4e34-AD09-F61009A0C6AB" Description="This feature create site columns lookup related to lists" Version="1.0.0.0" Scope="Site" Hidden="FALSE" DefaultResourceFile="TEST" xmlns="http://schemas.microsoft.com/sharepoint/" ReceiverAssembly="Tls.Moss, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9f4da00116c38ec5" ReceiverClass="MossSolution.Receivers.LookupFeatureReceiver"> <ActivationDependencies> <ActivationDependency FeatureId="bfda08d3-c7a8-4ade-99d5-86de4ce0ac26"/> </ActivationDependencies> <ElementManifests> <ElementFile Location="LookupFields.xml"/> </ElementManifests> <Properties> <Property Key="ColumnDefinitionPath" Value="C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\FEATURES\SiteLookupFields\LookupFields.xml" /> </Properties> </Feature>
In this feature, adapt the event receiver with the name of your assembly. The LookupFields.xml is the file where I put all lookup columns definitions. You can notice one particular property: ColumnDefinitionPath. This is the path where the lookup columns definitions is located. This path will used in the code to retrieve the XML data. I've also added a dependency, because the list template feature must be activated before activating this lookup feature. The tips here is because you defined properly a field, with all the usual behaviors (DisplayName, Show where you want, Readonly...) and a GUID, you can use these fields inside a ContentType! Good news, you have now the opportunity to have a content type with usual field types, PLUS one or more lookup type! The code is available on Codeplex: SharePoint 2007 - Create field lookup and associated list as a feature. There is a folder named Solution which contains the wsp Now, you have to add the solution with stsadm -o addsolution -filename lookupsolution.wsp Then, go to Central Admin / Operation /Solution Management and deploy the solution to the site(s) you want. Then, go to your site settings for activating the collection features, first TEST Keywords then Site Column Lookup Initialization The source code is in _App_code/Receivers. Hope this helps you.

Monday, July 30, 2007

SharePoint 2007 : Workflow et Association Form avec InfoPath 2007

Aujourd'hui je vais m'intéresser à la création d'un formulaire d'association pour un Workflow.
Un formulaire d'association est une page qui apparaît lorsqu'on crée une instance de Workflow : on choisit tout d'abord le modèle de Workflow, puis la Task list qui sera associée, puis l'History List, et enfin le mode d'activation du Workflow (Automatique, manuel...)
En cliquant sur le bouton Next, j'obtiens mon propre formulaire qui me permet de récupérer des informations complémentaires dés l'instanciation du Workflow. En l'occurrence, un flag qui m'indique si un mail doit être envoyé au "Supplier", puis un commentaire à insérer dans la description de tâche. Ainsi ce texte se retrouvera dans chaque tâche. Pour créer un tel formulaire, et l'associer au workflow, il est préférable d'utiliser InfoPath 2007 qui offre beaucoup d'avantages par rapport aux pages ASPX. Voici en quelques étapes comment faire pour construire l'écran précédent et l'intégrer dans le workflow. Principe : Un formulaire InfoPath est est un fichier XML associé à un Schéma XSD. Il y a une partie mise en forme et une partie données. L'astuce principale est de transformer le shéma en une classe C# (.cs) pour disposer facilement des propriétés (Data Source) au sein du Workflow. Les propriétés seront, entre autre, des champs de saisie dans l'écran InfoPath. Préliminaires Il faut avoir créer un projet de type Workflow sous Visual Studio, puis avoir créé une arborescence prête à accueillir les fichier d'installation
Etape 1 : créer l'écran
  1. Ouvrir InfoPath et créer un nouveau Template.
  2. Il faut ensuite créer une "Data Source" (Design Tasks / Data Source), ensemble de variables qui mémoriserons les données entrées. C'est grâce à ces Data Source que l'on va pouvoir dialoguer avec le formulaire. Une bonne pratique pour créer la Data Source est de lui donner un nom explicite. Par ailleurs, ce nom sera plus tard le nom de la classe qui sera générée pour être utilisée dans le Workflow. Dans mon exemple, j'ai pris comme nom de "Data Source" AssocData et j'ai créé deux champs, l'un booleen, l'autre de type texte mutillignes
  3. Mise en forme du formulaire : une fois les champs créés, il faut, dans le menu Design Tasks / Layout faire la mise en forme. Sélectionnez un tableau à deux colonnes, puis retournez dans Design Tasks / Data Source pour faire un Drag & Drop des champs aux bons emplacements. Ajoutez ensuite un bouton (Design Tasks / Controls)
  4. Il faut ensuite paramétrer le bouton pour que le formulaire soit envoyé à SharePoint (à l'instance de workflow en cours), puis que le formulaire soit fermé. Pour cela, il faut faire un click doit sur le bouton et aller dans "Button Properties"... ... et mettre le label (ici "Done"), puis cliquer sur Rules...
  5. A partir de là, il va falloir créer une Data Connexion pour se connecter au Host (SharePoint) et renvoyer les données. Cliquez sur Add Actions puis Add... pour ajouter une Data Connexion Utilisez les mêmes paramètres (on veut faire une soumission de formulaire) et cliquez sur Next... et selectionnez le "Hosting Environment". Puis donnez un nom explicite à cette Connexion.
  6. Il faut ensuite ajouter une action (Add Action) pour fermer le formulaire (choisissez cette option dans la liste)
Etape 2 : Publication
  1. Voilà, le formulaire est prêt. Maintenant il faut (1) le publier, (2) récupérer le schéma. (1) Publication : le mécanisme de publication, dans notre cas, consiste à publier le formulaire dans notre répertoire d'installation de la feature Workflow. Tout d'abord, il faut enregistrer le formulaire quelque part où vous le retrouverez (My Document/Forms par exemple). Puis cliquez sur "Publish Form Template..." et choisir une "Network Location" puis Next Enregistrer le formulaire dans votre répertoire de projet Visual Studio dans le sous-répertoire concerné par la feature Workflow (dans mon cas, GYPOWorkflow) et donnez lui un nom explicite (celui de votre sauvegarde pourra faire l'affaire) Ne mettez rien dans cette form et ignorez l'avertissement. Publiez, c'est fait.
  2. Un fois que c'est publié, il faut encore se faciliter le travail en créant une classe C# à partir du schéma du formulaire. Cette classe aura comme propriété les champs du formulaire. Pour se faire, il faut enregistrer le code source du formulaire dans un répertoire InfoPath va générer un lot de fichier dont seul myschema.xsd nous intéresse Nous allons utiliser le programme XSD.EXE fournit avec VSStudio pour générer une classe C#. Le nom du fichier cs généré portera le nom du fichier xsd (myschema actuellement) mais la classe elle-même portera le nom de notre Data Source (AssocData pour l'exemple). Par conséquent je vous conseille de renommer myschema.xsd en AssocData.xsd pour être cohérent. Ensuite, ouvrez une fenêtre DOS, allez dans le répertoire où est situé votre shéma (My Document\XSD_Sources dans mon cas) puis tapez l'instruction suivante : xsd myschema.xsd /c Vous aurez ensuite un fichier portant le nom myschema.cs (ou AssocData.cs si vous avez renommé le fichier) qui contient une structure particulière et les accesseurs nécessaires pour retrouver les données du formulaire.
  3. Ceci étant fait, retournez sous VS et ajoutez cette classe à votre projet.
  4. Dans l'événement OnWorkflowActivated1_Invoked, ajoutez les lignes de code suivantes: //Get the info about sending email to supplier from the association form //Part 1 XmlSerializer serializer = new XmlSerializer(typeof(AssocData)); XmlTextReader reader = new XmlTextReader(new System.IO.StringReader(workflowProperties.AssociationData)); //Part 2 AssocData AssocForm = (AssocData)serializer.Deserialize(reader); //Internal variables eMailToSupplier = (bool)AssocForm.eMailToSupplier; reviewComments = AssocForm.reviewComment; La première partie du code récupère le fichier xml InfoPath sérialisé qui est stocké dans une propriété (AssociationData) du Workflow. La deuxième partie désérialise le XML en faisant un Cast sur l'objet AssocData. Ceci permet ensuite d'utiliser les propriétés de l'objet pour récupérer les données et les stocker dans des variables locales au Workflow pour un usage ultérieur.
Etape 3 : Paramétrage de la feature
  1. une fois que le code est écrit et compilé, jeté dans le GAC, il faut créer une feature qui va se charger d'assembler tout ceci en quelque chose de cohérent. Dans un répertoire, il vous faudra avoir un fichier feature.xml, un fichier workflow.xml et votre formulaire, GYWFPOAssocForm.xsn dans mon cas. (ne vous occupez pas pour l'instant du quatrième fichier, qui fera l'objet d'un autre article) Le fichier feature.xml se présente de la façon suivante : <?xml version="1.0" encoding="utf-8"?> <Feature Id="B72AA389-51B8-4d7b-8730-F5A2885F9E93" Title="Purchase Order Workflow" Description="Purchase Order workflow, automated review process" Version="12.0.0.0" Scope="Site" ReceiverAssembly="Microsoft.Office.Workflow.Feature, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" ReceiverClass="Microsoft.Office.Workflow.Feature.WorkflowFeatureReceiver" xmlns="http://schemas.microsoft.com/sharepoint/"> <ElementManifests> <ElementManifest Location="workflow.xml" /> </ElementManifests> <Properties> <Property Key="GloballyAvailable" Value="true" /> <Property Key="RegisterForms" Value="*.xsn" /> </Properties> </Feature> Puis le fichier workflow.xml : <?xml version="1.0" encoding="utf-8" ?> <Elements xmlns="http://schemas.microsoft.com/sharepoint/"> <Workflow Name="Purchase Order Workflow" Description="Puchase Order Workflow, automated review process" Id="869774CD-6710-4583-938B-5B691903CCBF" CodeBesideClass="MyWorkflow.WFPO" CodeBesideAssembly="MyWorkflow, Version=1.0.0.0, Culture=neutral, PublicKeyToken=53941405af02689c" TaskListContentTypeId="0x01080100C9C9515DE4E24001905074F980F93160" AssociationUrl="_layouts/CstWrkflIP.aspx" ModificationUrl="_layouts/ModWrkflIP.aspx" StatusUrl="_layouts/WrkStat.aspx"> <Categories/> <!-- Tags to specify InfoPath forms for the workflow; delete tags for forms that you do not have --> <MetaData> <Association_FormURN>urn:schemas-microsoft-com:office:infopath:GYWFPOAssocForm:-myXSD-2007-05-14T07-58-42</Association_FormURN> <AssociateOnActivation>false</AssociateOnActivation> </MetaData> </Workflow> </Elements> Pour obtenir l'URN du formulaire InfoPath, il faut charger le formulaire dans InfoPath (design Template), puis aller dans File / Properties et copier/coller l'urn entre les tags CodeBesideClass et CodeBesideAssembly correspondent à la DLL et la classe de votre workflow.
  2. Ceci fait, il faut copier le répertoire d'installation (GYPOWorkflow) dans le répertoire "12\Template\Feature" et il vous faut installer (stsadm -o install feature GYPOWorkflow\feature.xml) et activer (stsadm -o activatefeature -filename "GYPOWorkflow\feature.xml" -url http://dcmoss:50010/) la feature.
Voilà, c'est tout, normalement cela doit marcher. C'est un peu laborieux, mais une fois que le formulaire est enregistré, il est très simple de le modifier. Et c'est très joli !

Wednesday, July 18, 2007

Champs type Lookup et type People (update) et type Link

Je reviens rapidemment sur les deux champs à saisie simple et multiple, les champs Lookup et People Comme je le précise dans mon article précédent, ils renvoient une chaîne de caractères bizarres composés d'un couple ID/Libellé ( "#1;#data1#;#2#data2#;") Pour pouvoir accéder facilement aux données, il faut réussir à utiliser au final un type SPFieldLookupValue qui a deux propriétés utiles: - LookupId : pour récupérer l'Id de l'item de la liste liée dans le lookup - LookupValue : pour récupérer le libellé (ce qu'on voit dans la liste) Dans le cas d'une sélection multiple, on doit utiliser le constructeur de SPFieldLookupValueCollection avec en paramètre la chaîne de caractère à traiter : SPFieldLookupValueCollection spcol = new SPFieldLookupValueCollection(string_from_field) Ce qui retournera une collection de SPFieldLookupValue Par contre, si c'est une sélection unique, deux cas se présentent : Cas 1 : c'est un champ de type Lookup, alors on peut utiliser le contructeur de SPFieldLookupValue : SPFieldLookupValue spfield = new SPFieldLookupValue(string_from_field) Cas 2 : c'est un champ de type People, alors ce champs renvoit un Integer qui est l'Id du SPUser : SPUser user = web.AllUsers.GetByID(userId); Le champs de type Link Le champ de type Link a besoin de deux informations : - l'URL - La description Pour adresser un tel champs, il faut utiliser l'objet SPFieldUrlValue et renseigner les deux propriétés Description et Url : SPFieldUrlValue linkURL = new SPFieldUrlValue(); linkURL.Description = description; linkURL.Url = link; La description représente le libellé qui apparaîtra dans la colonne. Ce libellé sera un lien vers Url Voilà trois champs que l'on rencontre très souvent et qui sont un petit peu pénible à utiliser.

Tuesday, June 5, 2007

SharePoint 2007 : Champs Lookup et People

Je me suis arraché les cheveux pour répondre à ce qui me semble être une incohérence : Cas 1: si on utilise un champ lookup ou People dans une liste, les données sont renvoyées sous forme d'une chaîne de caractères "#1#;data1#;". Cas 2: Si on utilise un champ lookup ou people dans une liste avec sélection multiple, les données sont renvoyées sous forme d'une chaîne de caractères "#1#;data1#;#2#data2#;". Il existe un objet SPFieldLookupValueCollection qui contient des SPFieldLookupValue qui a deux propriétés intéressantes : - LookupId : pour récupérer l'Id de l'item de la liste liée dans le lookup - LookupValue : pour récupérer le libellé (ce qu'on voit dans la liste) Dans le Cas 2, on peut "caster" cette chaîne à l'aide de : SPListitem relation; SPFieldLookupValueCollection temp = (SPFieldLookupValueCollection)relation["columnName"]; columnName est une colonne de type lookup ou people à sélection multiple et relation est un élément de la liste Mais cela ne marche pas dans le Cas 1 : SharePoint génère une erreur. Une première approche serait de caster avec SPFieldLookupValue : SPFieldLookupValue tempValue = (SPFieldLookupValue)relation["columnName"]; Mais non, ça ne marche toujours pas ! Ce qu'il faut faire, avant de chercher un super algorithme de parsing de la chaîne de caractères, c'est d'utiliser un paramètre du constructeur : SPFieldLookupValue tempValue = new SPFieldLookupValue(relation["columnName"].ToString()); Dans ce cas on obtient bien un objet SPFieldLookupValue avec les propriétés LookupId et LookupValue qui sont vraiment bien pratiques. Pourquoi ne pas avoir une seule façon de "caster" ??

Tuesday, May 29, 2007

Sharepoint 2007: upload d'un document avec ses meta-data

J'ai dû récemment faire l'application suivante: Des documents étaient générés automatiquement par une application externe. Le but du programme était de récupérer ces documents pour les mettre dans une document list, avec leurs meta-data. Puis de lancer un workflow pour approbation avec différents paramètres. Dans cet article, je vais m'attacher à l'importation des fichiers dans la liste. Je ferai un autre article pour le workflow. Le premier réflexe est bien entendu de télécharger le document dans la liste puis de faire une correspondance entre les colonnes de la liste et des meta-data. Mais étant donné que je gère les versions dans cette liste, cette façon de faire risque d'incrémenter un numéro de version pour rien, suite à deux updates (un pour le document, un pour les meta-données) Mais en cherchant un peu, j'ai trouvé un overload bien pratique... La première chose est de considérer la liste comme un folder, et de récupérer dans ce folder la collection des fichiers existants. Le folder se récupère en utilisant le nom interne de la liste. Je spécifie bien le nom interne, c'est à dire le nom qui a été utilisé la première fois lors de la création de la liste (si elle a été créée via l'UI). On trouve ce nom en regardant dans l'url de la liste. Par exemple : http://dcmoss:50010/testgy/PurchaseOrder PurchaseOder est bien le nom du folder, même si la liste s'appelle "Purchase Order". SPFolder folder = _web.GetFolder("nom interne de la liste"); SPFileCollection files = folder.Files; Puis je récupère le fichier lui-même, en le stockant dans un stream mémoire FileStream fStream = File.OpenRead("fileName"); Pour terminer je crée l'URL du fichier, avec son nom: string url = "url Site web" + "nom interne de la liste" + "/" + Path.GetFileName("fileName"); Maintenant, les meta-data doivent être stockée dans une HashTable: HashTable MetaDataTable = new HashTable(); MetaDataTable.Add("nom de colonne", "valeur"); En réalité j'ai une classe qui parse le fichier texte et qui me renvoit une hashtable. Enfin, je l'ajoute le tout à la collection de fichier de la liste: SPFile currentFile = files.Add(url, fStream, MetaDataTable, true); Le flag True permet d'écraser un fichier s'il est déjà présent. En une ligne, on ajoute un fichier avec ses meta-données. Pas mal, non ? Voici un exemple de code pour uploader un document avec les meta-données: //Using required, especially System.IO; using System.Collections.Generic; using System.Collections; using System.Text; using Microsoft.SharePoint; using System.IO; namespace DocImport { public class DocManagement { //Some variables private string _DestUrlPath = string.Empty; private string _DestFolder = string.Empty; private SPSite _site = null; private SPWeb _web = null; public DocManagment(string DestUrl, string DestFolder) { _DestUrlPath = DestUrl; _DestFolder = DestFolder; _site = new SPSite(_DestUrlPath); _web = _site.OpenWeb(); } private bool _uploadDocument(string pdfFile, string txtFile) { try { //This is mandatory for avoiding an error _web.AllowUnsafeUpdates = true; //Use the list as a folder SPFolder folder = _web.GetFolder(_DestFolder); SPFileCollection files = folder.Files; //Get the file FileStream fStream = File.OpenRead(pdfFile); HashTable MetaDataTable = new HashTable(); MetaDataTable.Add("nom de colonne", "valeur"); //Set the destination url for SharePoint string url = _DestUrlPath + _DestFolder + "/" + Path.GetFileName(pdfFile); //Add document to the list with metadata, and overwrite an existing document with the same name SPFile currentFile = files.Add(url, fStream, MetaDataTable, true); fStream.Close(); return true; } catch (Exception x) { throw new Exception(x.Message); } } }

Wednesday, May 23, 2007

Sharepoint 2007 Workflow Update Item

Dans un workflow déclenché par une modification de l'item d'une liste, il est souvent nécessaire de mettre à jour une meta-donnée de cet item. Le principe est simple en utilisant par exemple : workflowProperties.Item["Status"] = "Archived"; Mais que ce passe-t-il lorsqu'on met à jour ? Le fait de faire workflowProperties.Item.update(); déclenche l'événement de mise à jour, et donc redéclenche le workflow, donc une erreur car une instance du même workflow est déjà active sur cet item. La réponse est dans l'instruction suivante : workflowProperties.Item.SystemUpdate(); Cette méthode permet d'écrire directement dans la base, sans déclencher les événements sur l'item. D'autre part, il est possible de spécifier un flag (true/false) pour incrémenter ou non la version.