Boite à outils Office

Linq pour CommerceServer : merci CSMetal !

Commerce Server, Développement No Comments »

Il y a déjà quelques semaines, Kerry Havas, Architecte Produit Commerce Server, a publié la version 2 de CSMetal.

Qu’est-ce que CSMetal ? C’est un utilitaire pour Commerce Server qui génère des classes fortement typées utilisable à travers un provider LINQ basé sur l’API Commerce Foundation => ie, ça ne remplace pas l’API Commerce Foundation !

Kerry frappe fort avec CSMetal, car le développement sous Commerce Server devient un “jeu d’enfant”!

Avant de pouvoir utiliser le provider LINQ pour Commerce Server, il nous faut auparavant générer les entités. Pour cela, nous devons utiliser l’outil de commande en ligne CSMetal.

Pour que CSMetal entre en action, il vous faut:

  • Le fichier ChannelConfiguration.config correctement configuré
    • Attention de bien mettre la valeur de l’attribut useSharedCommerceContexts à false.
    • Ajouter dans le noeud MessageHandler avec l’attribut name "CommerceQueryOperation_CommerceEntityDefinition" les noeuds enfant suivants:
   1: <!-- Note that RepositoryMetadataLoader is replaced by a slightly customized one -->

   2: <Component name="RepositoryMetadataLoader" type="Microsoft.Commerce.Metal.Generator.OperationSequenceComponents.RepositoryMetadataLoader, Microsoft.Commerce.Metal.Generator, Version=1.0.0.0, Culture=neutral" />

   3: <!-- Note that this is added to add overlay capability to the Metadata repository -->

   4: <Component name="MetadataOverlayProcessor" type="Microsoft.Commerce.Metal.Generator.OperationSequenceComponents.MetadataOverlayProcessor, Microsoft.Commerce.Metal.Generator, Version=1.0.0.0, Culture=neutral" />

      • Notez que le dernier noeud permet en gros de pouvoir générer d’autres entités ou attributs sans toucher au fichier MetadataDefinitions.xml. Nous y revenons plus bas.
  • Le fichier MetadataDefinitions.xml proprement mis à jour
    • Ajouter au noeud RelationshipTypes, un noeud enfant :
      • <RelationshipType name="Inherits"/>
    • Consultez le fichier “CSMetal_Beta2_ReadMe.rtf” pour ajouter à 2 entités:
      • Basket
        • les énumérations des status de commande
      • Ads
        • Les énumérations des tailles d’une publicité
  • Les “traditionnels” OrderObjectMappings.xml et OrderPipelineMappings.xml, eux aussi mis à jour.

Une fois ces ingrédients sous la main, mettez-les par exemple dans un répertoire temporaire. Ajoutez les fichiers nécessaires à la bonne exécution de CSMetal:

  • CSMetal.exe Smile
  • CSMetal.exe.config qui est une copie du web.config de votre application web
  • GeneratorSettings.xml qui vous permet de définir les dossiers d’outputs pour les assemblies générées avec un namespace et un nom de context de votre choix!
  • Microsoft.Commerce.Metal.Generator.dll pour la génération de code

Soit:

image

Afin de bénéficier pleinement du Linq provider pour Commerce Server, nous avons certaines metadatas à faire évoluer ou créer. Pour cela, Kerry a introduit un concept très pratique appellé “MetaDataOverlays”:

  • Cela revient à étendre les metadatas via un fichier xml extérieur sans avoir à toucher à votre fichier MetadataDefinitions.xml. Dans le cas de notre provider Linq pour Commerce Server, nous avons un certains nombres d’entités à mettre à jour mais qui n’ont pas d’intéret à être dans le runtime de l’application web puisqu’utilisé uniquement par notre provider Linq.

Pour cela, dans le répertoire CSMetal ajouter un nouveau dossier enfant appellé MetadataOverlays et ajoutez-y le fichier xml Metadata_CSMetal.xml

   1: <?xml version="1.0" encoding="utf-8" ?>

   2: <!-- 

   3: This file contains the Metadata Repository

   4: -->

   5: <MetadataDefinitions xmlns="urn:schemas-microsoft-multi-channel-commerce-foundation-metadata">

   6:     <DefaultChannel>

   7:         <CommerceEntities>

   8:             <!-- Added for Linq based Search -->

   9:             <CommerceEntity name="CatalogEntity">

  10:                 <DisplayName value="Catalog Entity" />

  11:                 <Properties>

  12:                     <Property name="Phrase"    dataType="String">

  13:                         <DisplayName value="Phrase"/>

  14:                     </Property>

  15:                     <Property name="ReturnTypes"    dataType="Integer">

  16:                         <DisplayName value="Return Types"/>

  17:                     </Property>

  18:                     <Property name="FirstItem"    dataType="Integer">

  19:                         <DisplayName value="First Item"/>

  20:                     </Property>

  21:                     <Property name="WhereClause"    dataType="String">

  22:                         <DisplayName value="Where Clause"/>

  23:                     </Property>

  24:                     <Property name="TotalItemCount"    dataType="Integer">

  25:                         <DisplayName value="Total Item Count"/>

  26:                     </Property>

  27:                     <Property name="ResultsPerPage"    dataType="Integer">

  28:                         <DisplayName value="Results Per Page"/>

  29:                     </Property>

  30:                 </Properties>

  31:             </CommerceEntity>

  32:             <!-- Added for Linq based Search -->

  33:  

  34:             <!-- Catalog Start -->

  35:             <CommerceEntity name="Catalog">

  36:                 <Relationships>

  37:                     <Relationship name="DependantCatalogs" type="Relationship" modelName="Catalog" isMultipleItems="true" >

  38:                         <DisplayName value="Dependant Catalogs" />

  39:                     </Relationship>

  40:                     <Relationship name="SourceCatalogs" type="Relationship" modelName="Catalog" isMultipleItems="true" >

  41:                         <DisplayName value="Source Catalogs" />

  42:                     </Relationship>

  43:                 </Relationships>

  44:             </CommerceEntity>

  45:  

  46:             <!-- CreditCard Start -->

  47:             <CommerceEntity name="CreditCard">

  48:                 <Relationships>

  49:                     <Relationship name="PaymentAccount" type="Inherits" modelName="PaymentAccount" isMultipleItems="false">

  50:                     </Relationship>

  51:                 </Relationships>

  52:             </CommerceEntity>

  53:  

  54:             <!-- Basket Start -->

  55:             <CommerceEntity name="Basket">

  56:                 <Relationships>

  57:                     <Relationship name="LineItems" modelName="LineItem" type="Relationship" isMultipleItems="true" />

  58:                     <Relationship name="Addresses" modelName="Address" type="Relationship" isMultipleItems="true" />

  59:                     <Relationship name="Payments" modelName="Payment" type="Relationship" isMultipleItems="true" />

  60:                     <Relationship name="Shipments" modelName="Shipment" type="Relationship" isMultipleItems="true" />

  61:                     <Relationship name="RequestedPromoCodes" modelName="RequestedPromoCode" type="Relationship" isMultipleItems="true" />

  62:                 </Relationships>

  63:             </CommerceEntity>

  64:  

  65:             <!-- ShopperList Start -->

  66:             <CommerceEntity name="ShopperList">

  67:                 <Relationships>

  68:                     <Relationship name="LineItems" modelName="LineItem" type="Relationship" isMultipleItems="true" />

  69:                 </Relationships>

  70:             </CommerceEntity>

  71:  

  72:             <!-- LineItem Start -->

  73:             <CommerceEntity name="LineItem">

  74:                 <Relationships>

  75:                     <Relationship name="ItemLevelDiscounts" modelName="Discount" type="Relationship" isMultipleItems="true" />

  76:                 </Relationships>

  77:             </CommerceEntity>

  78:  

  79:             <!-- CashCard Start -->

  80:             <CommerceEntity name="CashCard">

  81:                 <Relationships>

  82:                     <Relationship name="PaymentAccount" type="Inherits" modelName="PaymentAccount" isMultipleItems="false">

  83:                     </Relationship>

  84:                 </Relationships>

  85:             </CommerceEntity>

  86:  

  87:             <!-- GiftCertificate Start -->

  88:             <CommerceEntity name="GiftCertificate">

  89:                 <Relationships>

  90:                     <Relationship name="PaymentAccount" type="Inherits" modelName="PaymentAccount" isMultipleItems="false">

  91:                     </Relationship>

  92:                 </Relationships>

  93:             </CommerceEntity>

  94:  

  95:             <!-- Shipment Start -->

  96:             <CommerceEntity name="Shipment">

  97:                 <Relationships>

  98:                     <Relationship name="ShippingMethod" modelName="ShippingMethod" type="Relationship" isMultipleItems="false" />

  99:                 </Relationships>

 100:             </CommerceEntity>

 101:  

 102:             <!-- Payment Start -->

 103:  

 104:             <!-- PaymentAccount Start -->

 105:             <CommerceEntity name="PaymentAccount">

 106:                 <Properties>

 107:                     <Property name="Id" dataType="String">

 108:                         <DisplayName value="Id"/>

 109:                         <Description value="Unique ID of the payment." />

 110:                     </Property>

 111:                     <Property name="CustomerName" dataType="String">

 112:                         <DisplayName value="CustomerName"/>

 113:                         <Description value="Name of Customer." />

 114:                     </Property>

 115:                     <Property name="BillingAddressId" dataType="String">

 116:                         <DisplayName value="BillingAddressId"/>

 117:                         <Description value="Id of billing address" />

 118:                     </Property>

 119:                     <Property name="DisplayName" dataType="String">

 120:                         <DisplayName value="DisplayName"/>

 121:                         <Description value="DisplayName." />

 122:                     </Property>

 123:                 </Properties>

 124:                 <Relationships>

 125:                     <Relationship name="PaymentMethod" modelName="PaymentMethod" type="Relationship" isMultipleItems="false" />

 126:                 </Relationships>

 127:             </CommerceEntity>

 128:             

 129:             <CommerceEntity name="Payment">

 130:                 <Relationships>

 131:                     <Relationship name="PaymentAccount" modelName="PaymentAccount" type="Relationship" isMultipleItems="false" />

 132:                 </Relationships>

 133:             </CommerceEntity>

 134:         </CommerceEntities>

 135:     </DefaultChannel>

 136: </MetadataDefinitions>

Exécutez maintenant CSMetal.exe:

image

Dans le répertoire configuré dans le GeneratorSettings.xml, vous trouverez 3 fichiers .cs correspondant à vos entités Commerce Server :

image

Vous noterez un fichier odata généré qui sera particulièrement utile pour exposer nos données Commerce Server, mais cela fera l’objet de plusieurs autres posts Smile

Copiez les fichiers GeneratedContexts.cs et GeneratedTypes.cs générés par CSMetal dans le repertoire GeneratedTypes du projet CSMetalLinqProvider (ou mettez les dans les paramètres output de CSMetal, ie le fichier GeneratorSettings.xml, pour ne pas avoir à les copier) et relancez une compilation du projet CSMetalLinqProvider.

Vos entités sont maintenant générées et le provider Linq disponible Smile

 

Récuperer une catégorie reviens maintenant à:

var category = new CategoryContext(commerceServiceProxy)
                  .IncludeChildCategories()
                  .Single(o => o.Id=="Departments" && o.CatalogId==DefaultCatalog);
var childCategories = category.ChildCategories;
ou pour un produit :
var product = new ProductContext(commerceServiceProxy)
                 .Single(o=>o.CatalogId==DefaultCatalog && o.Id=="AW200-12")
var productCode = product.ProductCode;

Ce n’est qu’un petit échantillon de ce que l’on peut faire, et d’autres posts viendront bientôt vous montrer la puissance de ce provider Smile => Pour vos développeurs, plus besoin de connaitre en profondeur CS pour pouvoir l’attaquer et donc faciliter l’adoption de Commerce Server.

Filtrer et récupérer des éléments d’une liste grâce à Linq et les méthodes d’extensions

Développement, SharePoint No Comments »

– UPDATE —

J’ai changé ma méthode pour récupérer directement une valeur dans ma liste de configuration. Je faisais un query qui me retournais une collection que je filtrais ensuite pour récupérer ma valeur. Alors qu’il suffisait simplement de le faire directement dans mon query Nerd

– UPDATE —

Dans mes applications SharePoint, j’utilise très souvent une liste ou je stocke l’ensemble de mes éléments de configuration comme par exemple :

  • Email sender pour mes workflows
  • Sujet d’un email
  • Corps d’un email
  • Url d’un WebService
  • Et bien d’autres choses encore…

Le gros avantage est que le webmaster du site peut facilement (à ses risques et périls tout de même Nerd) mettre à jour les informations et ceci sans reset de l’application pool ou redémarrage de l’appli web sous IIS dans le cas par exemple de variables stockées dans le web.config.

La structure de ma liste est toute simple :

  • Key (le champ title de base renommé);
  • Value (la valeur à retourner) de type « Multiline of text » en mode Plain Text;
  • Category, un champ de type « Choice » qui me permet de regrouper mes informations. Par exemple, sur une page, il peut m’arriver d’avoir à récupérer plusieurs valeurs de configuration. Ce regroupement, me permet de les récupérer via un seul CAML Query. Ceci m’évitant donc de faire un query pour chaque élément.

image 

J’avais donc implémenté une classe qui me permettait de récupérer ces valeurs via un CAML query sur ma liste. Mais suite à un article posté par sur les , j’ai décidé de refaire cette classe en utilisant ces et les nombreuses autres du framework .Net.

Pour commencer, j’ai étendu l’objet pour accéder à notre Liste de configuration (dans mon cas, j’ai aussi besoin d’un paramètre « Category » pour me retourner l’ensemble des valeurs car je ne souhaite pas faire une requête pour chaque entrée). Cette méthode, me retourne une interface générique de type Generic Interface . Cette interface, va me permettre ensuite de récupérer très facilement une entrée de ma configuration :

public static ILookup<String, String> ConfigurationList(this SPWeb spWeb, String Category)
{
    SPList spListConfiguration;
    ILookup<String, String> configurationEntries = null;
    // La liste n'est pas accessible aux end users.
    // On doit donc s'y connecter sur le compte admin pour y accéder
    // Optionnel, si vous avez décidé de mettre la liste en lecture pour tout vos utilisateurs
    SPSecurity.RunWithElevatedPrivileges(delegate()
    {
        using (SPSite mySite = new SPSite(spWeb.Site.ID))
        {
            using (SPWeb myWeb = mySite.OpenWeb(spWeb.ID))
            {

Je récupère ma liste « Configuration » grâce à la méthode d’extension de :

if (myWeb.Lists.TryGet("Configuration", out spListConfiguration))
{

J’exécute ensuite mon query pour récupérer mes éléments :

    SPQuery spQuery = new SPQuery();
    spQuery.Query = String.Format(<Where><Eq><FieldRef Name='Category' /><Value Type='Choice'>{0}</Value></Eq></Where>", Category);

Et je stock les résultats dans un document XML Linq () :

XDocument xml = XDocument.Parse(spListConfiguration.GetItems(spQuery).Xml);

Je définie mon pour me simplifier la lecture du code :

XNamespace z = "#RowsetSchema";

Je renvoie tout ce flux dans mon interface générique en lui spécifiant :

  • Ma clé : key => key.Key;
  • Ma valeur à retourner: element => element.Value;
  • Mon comparateur d’égalité : EqualityComparer<String>.Default (ici comme je travaille sur des Strings je n’ai pas besoin d’en définir de particulier, mais si vous souhaitez le personnaliser, il vous suffit d’écrire une classe qui implémente Generic Interface . Je donne un exemple à la fin de ce post pour implémenter une version qui ne prend pas en compte la casse de la clé) ;
configurationEntries = (from xmlItems in xml.Descendants(z + "row").ToArray()
    select new ConfigurationEntry { Key = (String)xmlItems.Attribute("ows_Title"), Value = (String)xmlItems.Attribute("ows_Value") })
    .ToLookup(key => key.Key, element => element.Value, EqualityComparer<String>.Default);
        });
    }
onfigurationEntries;
}

J’utile cette classe pour stocker ma clé ainsi que la valeur associée dans mon query Linq :

public class ConfigurationEntry
{
    public String Key;
    public String Value;
}

Ensuite, il ne me reste plus qu’a écrire la classe qui va me rechercher mon élément dans ma collection (on peut ici choisir de lui donner un objet SPWeb ou d’utiliser le context)

public sealed class Configuration
{
    private ILookup<String, String> configurationValues;

    public Configuration(SPWeb spWeb, String category)
    {
        if (spWeb == null)
            throw new ArgumentNullException("A SPWeb object cannot be null");
        if (String.IsNullOrEmpty(category))
            throw new ArgumentNullException("A category cannot be empty");

        configurationValues = spWeb.ConfigurationList(category);
    }

    public Configuration(String category)
    {
        if (SPContext.Current == null)
            throw new ArgumentNullException("SPContext cannot be null. Please ensure that you're running a SharePoint application");
        if (String.IsNullOrEmpty(category))
            throw new ArgumentNullException("A category cannot be empty");

        configurationValues = SPContext.Current.Web.ConfigurationList(category);
    }

    public String GetEntry(String key)
    {
        return Convert.ToString(configurationValues[key].FirstOrDefault());
    }
}

Notez l’utilisation du générique , qui nous permet ici de nous retourner un String.Empty s’il n’y a pas de valeur à retourner.

Ensuite, pour l’appel, c’est très simple. Instanciez un objet Configuration et utilisez la méthode GetEntry pour récupérer vos valeurs:

Configuration configValues = new Configuration(spWeb,"SSRS");
Console.WriteLine("SSRS URL:" + configValues.GetEntry("SSRSUrl"));
Console.WriteLine("SSRS Path:" + configValues.GetEntry("SSRSPath"));
Console.WriteLine("SSRS WebPartPage:" + configValues.GetEntry("SSRSWebPartPage"));

image

On peut aussi ajouter une variante à notre méthode d’extension ConfigurationList pour récupérer directement la valeur d’une clé dans notre liste :


public static String ConfigurationList(this SPWeb spWeb, String Category, String Key)
{
    SPList spListConfiguration;
    String returnValue = String.Empty;

    SPSecurity.RunWithElevatedPrivileges(delegate()
    {
        using (SPSite mySite = new SPSite(spWeb.Site.ID))
        {
            using (SPWeb myWeb = mySite.OpenWeb(spWeb.ID))
            {
                if (myWeb.Lists.TryGet("Configuration", out spListConfiguration))
                {
                    // Modification du 25 juin 2008                    // J'ai pas fais attention en publiant mon post que cette méthode était stupide                    // En effet, j'utilisais un query qui me retournais une collection que je filtrais ensuite pour retourner ma valeur                    // Alors que je pouvais faire cela dans un seul CAML Query smile_nerd                    SPQuery spQuery = new SPQuery();
                    spQuery.Query = String.Format("<Where><And><Eq><FieldRef Name='Title' /><Value Type='Text'>{0}</Value></Eq><Eq><FieldRef Name='Category' /><Value Type='Choice'>{1}</Value></Eq></And></Where>", Key, Category);

                    SPListItemCollection items = spListConfiguration.GetItems(spQuery);
                    if (items != null
                        && items.Count > 0)
                        returnValue = Convert.ToString(items[0]["Value"]);

                    // SPQuery spQuery = new SPQuery();                    // spQuery.Query = String.Format("<Where><Eq><FieldRef Name='Category' /><Value Type='Choice'>{0}</Value></Eq></Where>", Category);                    // XDocument xml = XDocument.Parse(spListConfiguration.GetItems(spQuery).Xml);                    // XNamespace z = "#RowsetSchema";                    // configurationEntries = (from xmlItems in xml.Descendants(z + "row")                    // select new ConfigurationEntry { Key = (String)xmlItems.Attribute("ows_Title"), Value = (String)xmlItems.Attribute("ows_Value") })                    // .ToLookup(key => key.Key, element => element.Value, EqualityComparer<String>.Default);                    // returnValue Convert.ToString(configurationEntries[Key].FirstOrDefault()); 

                }
            }
        }
    });

    return returnValue;
}

La récupération de votre valeur se faisant comme ceci:

Console.WriteLine("SSRS Url:" + spWeb.ConfigurationList("SSRS", "SSRSURL"));

image

Comme promis, voici ici un exemple de classe qui vous permet de comparer 2 Strings sans distinction de la casse :

public class StringNoCaseSensitive : IEqualityComparer<String>
{
    public Boolean Equals(String val1, String val2)
    {
        return (val1.ToLower() == val2.ToLower());
    }
    public int GetHashCode(string obj)
    {
        return obj.ToLower().GetHashCode();
    }
}

Dans l’appel du ILookup,remplacer EqualityComparer<String>.Default par new StringNoCaseSensitive() et le tour est joué…

Grouper les SPItems renvoyés par SPQuery via une requête linq

Développement, SharePoint No Comments »

Auparavant, lorsque je voulais regrouper mes valeurs issues d’un query sur une liste. Je devais faire tout un tas de manipulations, comme :
-fonction xpath : pour regrouper mes items
-sur le regroupement faire des calculs si nécessaire
En effet, le ou l’utilisation d’une vue () avec un regroupement, renvoi a chaque fois l’ensemble des éléments de la liste. Grâce à , on peut maintenant récupérer et regrouper très facilement  nos résultats. Comme par exemple, ici je souhaitais avoir une liste unique de catégories présentent dans ma liste :

// Ma liste
SPList spList = null;
try { spList = SPContext.Current.Site.RootWeb.Lists["MaListe"]; }
catch { }

// si null, exception
if (spList == null) throw new NullReferenceException("La liste  n'existe pas");

// Recuperation du nom interne de la colonne
if (!spList.Fields.ContainsField("Categorie")) throw new NullReferenceException("La colonne categorie n'existe pas");
String categoryColumnName = spList.Fields["Categorie"].InternalName;

// Recuperation des categories distinctes triees
SPQuery spQuery = new SPQuery();
spQuery.Query = String.Format("<OrderBy><FieldRef Name='{0}' /></OrderBy>", categoryColumnName);

// Chargement du Xml
XDocument xml = XDocument.Parse(spList.GetItems(spQuery).Xml);

// Ajout du namesplace pour recuperer mes valeurs
XNamespace z = "#RowsetSchema";

// IMPORTANT: n'oubliez pas de typer votre regroupement
var groupedCategories = from allcategories in xml.Descendants(z + "row")
     group allcategories by (string)allcategories.Attribute("ows_Categorie") into distinctCategories
     select distinctCategories;

// Ensuite il nous suffit de recuperer le resultat
// Ici en l'ajoutant a un control de type Checkbox list
foreach (var cat in groupedCategories)
{
     this.cblCategories.Items.Add(new ListItem((string)cat.Key));
}

Simple non?

WP Theme & Icons by N.Design Studio
Entries RSS Comments RSS Log in
Creative Commons License