Sharepoint Developer Tools

Hi,

in this article will be presented some tools that are essential for every SharePoint developer.

SharePoint Manager 2013

SharePoint Manager is like the toolbox that Microsoft would have forgotten to provide with its product.

It will allow you to have a quick view over all your Web Applications, your sites collections, your associated/activated features on all spaces, lists, document libraries, every element properties.
You will certainly gain some time using it !
SPManager

A quick example of the features described above

You will have ton install and run SP Manager on the server where SharePoint is installed.
It may require Administrator rights, depending on which user you are currently logged on.

If you had the choice of one tool in addition to Visual Studio, download SharePoint Manager 2013 !! (also exists in version 2010)

——————————————————————————-

ULS Viewer

Once the SharePoint developments started, you will soon realize that when SP is not happy with what you did he almost never says it.

The only solution is to go in the log directory and open the latest. First mistake is to open it with Notepad or another text editor, you may get scared and never want to do it again.

Microsoft has developed a tool that will provide a quick and friendly way of reading logs.

ULS Viewer

Run ULS-Viewer, select the logs location (C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\15\LOGS\ for SP 2013, \14 for SP2010 etc) and you will realize that lots of things are happening (event if you don’t do anything in the web interface)

Fortunately, Microsoft is providing filtering capabilities for all the log activity.
ULS Viewer2

You will be able to filter by severity, then on defined text strings .

A real example is filtering on field Correlation when SharePoint will pop an error with an associated Guid, you will only see the errors related to the given problem.

This is, in my opinion, the second must-have tool, on my development machine it is always opened.

——————————————————————————-

SharePoint Feature Administration and Clean Up Tool

This is a tool that I use more often but that is also useful.

More focused on the features management in your farm, it will allow you to find in one click all defective features (after a migration, for example) or to quickly see the features enabled at a given scope (Farm / Site / Web)

FeatureAdmin for SharePoint 2013 - v2.3

——————————————————————————-

ILSpy

Sometimes in a developer’s life you might want to get your hands dirty and see what Microsoft’s guys have been doing in SharePoint code.

ILSpy spy will allow you to load and decompile assemblies that you used without knowing its content.

ILSpy
Above : Microsoft.SharePoint.Portal.CommunityEventReceiver class methods

Lots of DDls used by SharePoint are stored in C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI (\14 for SP2010 etc.), you will probably find what you are looking for there.

——————————————————————————-

CAML Designer for SP 2013

If you are experiencing the need to develop customized views or retrieve specific items in lists you may most likely use CAML queries.

With CAML Designer you can quickly generate the correct syntax to get the desired result without the need to deploy lots of times your solution.

CAML Designer for SharePoint 2013.

Complete information about using it can be found on the developer’s site.

——————————————————————————-

Sharepoint Color Palette Tool

If Microsoft’s blue is not really your favorite color and you want to enjoy the new features of SP2013 branding, then this tool is for you.

It will allow you to generate a color palette file (.Spcolor) in two clicks, you can import it into the gallery themes (http://url/_catalogs/theme/15) and apply it either through the interface (Change the look Menu) or through code.

SharePoint Color Palette Tool
Here is a shimmering green

——————————————————————————-

This is a good overview of what you can find as SharePoint development assistance, this list is not exhaustive and will be completed when I find new tools.

Etendre ou modifier le Portail des communautés

Bonjour,

j’ai récemment rencontré plusieurs problématiques avec le nouveau template de site « Portail des communautés » / « Community Portal ».

Pour rappel, un site communautaire est un template qui fournit de base un liste de discussions (sorte de forum) avec un système assez poussé de gestion des membres (possibilité de s’inscrire, gestion de badges pour les membres les plus actifs etc.)

Voilà ce que Microsoft dit à propos des communautés et du portail associé

Créer un portail des communautés :

comPort1

Le portail des communautés OOTB :

comPort2

L’affichage est plutôt sympa, avec les informations principales (Titre, Description, Nombre de membres, Nombre de topics et de réponses), on a aussi accès à plus d’informations avec une popup

comPort4

De base nous obtenons donc un site qui diffère un peu des autres templates standard par le fait qu’il ne possède pas de Logo, pas de colonne de menu gauche.

Le contenu principal est géré par une webPart de type Content Search, préconfigurée pour afficher toutes les communautés accessibles (et par accessibles j’entend à minima que l’utilisateur courant ait un droit Visitor sur les communautés ou la collection qui les contient)

Les communautés sont triées par popularité (formule barbare incluse ci dessous ;-) )

[formula:((CommunityMembersCount*0.5)+CommunityTopicsCount+(CommunityRepliesCount*0.5))/(log(abs(Created-{0})+1)+log(abs(LastModifiedTime-{0})+1)+1)]

La paramètre {0} étant généré par la méthode suivante

private static ulong NowInTicks()
{
    DateTime utcNow = DateTime.UtcNow;
    checked
    {
        ulong num = (ulong)utcNow.Second * 10000000uL;
        num += (ulong)utcNow.Minute * 600000000uL;
        num += (ulong)utcNow.Hour * 36000000000uL;
        num += (ulong)utcNow.DayOfYear * 864000000000uL;
        num += (ulong)utcNow.Year * 316224000000000uL;
        if (utcNow.Month > 2 && !DateTime.IsLeapYear(utcNow.Year))
        {
            num += 864000000000uL;
        }
        return num;
    }
}

N’ayant pas encore acquis la médaille Fields j’ai encore un peu de mal à décortiquer la totalité, ce que j’en ait retenu c’est que le nombre de membres et le nombre de topics (et réponses) sont importants dans le classement. Les dates de création de dernière modification jouent aussi.


Le portail des communauté est donc un site qui peut être très utile OOTB, et qui se met en place en quelques clics.
Par contre si vous avez besoin de modifier ce site vous allez au devant de quelques problèmes.

    1 – Pas de bandeau ni de possibilité d’insérer des WebParts supplémentaires

comPort3

Concrètement la div qui affiche normalement le ruban est volontairement cachée, ce qui vous en conviendrez ne facilite pas l’ajout de nouvelles WebParts par exemple.

<div id="s4-ribbonrow" style="height: 0px"> &amp; Display à none
    2 – Les possibilités de modification de la WebPart « Communautés populaires » sont limitées
  • Il est impossible de modifier le titre de la WebPart, ce sera toujours « Communautés Populaires » / « Popular communities »
  • Par défaut vous ne disposez que des templates d’affichages définis avec la WebPart
  • Les besoins exprimés par le client n’étant pas satisfait par le template OOTB il a fallu composer avec ces problématiques.


      Problématique 1 : Le titre de la WebPart ne convient pas

    Ok, donc là pas de solution miracle il va falloir aller fouiller dans les entrailles SP, à l’aide d’ILSpy

    Un export de la WebPart nous donne le type Microsoft.SharePoint.Portal.WebControls.ExistingCommunitiesWebPart

    Un rapide aperçu de la classe en question
    comPort5

    On remarque qu’elle est sealed (donc pas possible d’en hériter) et qu’elle est basée sur la ContentBySearchWebPart (ça on s’en doutait au vu de l’interface)

    Allons voir ce qui se cache sous notre problème de titre non modifiable
    comPort6

    Hum… mystère résolu, pas de setter et le getter renvoie une chaîne vide, mais alors comment est généré notre titre par défaut ?

    comPort7

    Ok donc tout est géré une méthode spécifique appelée dans le Render via une ressource multilangues.

    Pour modifier le titre il va donc falloir développer notre propre WebPart en s’inspirant de l’existante.


      Problématique 2 : Je dois afficher les communautés dont je suis membre (et pas uniquement celles auxquelles j’ai accès)

    La requête KQL passée pour récupérer les communautés est la suivante : WebTemplate:Community

    Comme dit plus haut elle va remonter toutes les communautés auxquelles j’ai possibilité d’accéder, mais pas forcément celles dont je suis membre (pour rappel le fait d’être membre d’une communauté est conditionné par la présence d’un objet SPUser dans la liste Community Members

    Après quelques recherches j’ai trouvé la méta donnée qui permet d’obtenir ces informations : MemberOWSUSER:{User.Name}
    Si vous exécutez cette requête dans une WebPart vous constaterez que les résultats remontés ne sont hélas pas des communautés mais items de listes (plus précisément les liens vers les objets SPUser dans chaque communauté où l’utilisateur est membre)

    Le problème est que l’on souhaite remonter des communautés (afin que le template fourni affiche correctement les informations)

    Même constatation que pour la problématique 1, sans développement ça ne sera possible.


      Problématique 3 : Par défaut toutes les communautés sont privées, pourtant je dois pouvoir afficher les communautés populaires pour pouvoir demander à les rejoindre

    Etant donné que le moteur de recherche est par défaut « bridé » sur les objets auxquels l’utilisateur a accès, obtenir le résultat en standard ne semble pas possible à priori.

  • Hypothèse 1 : Créer un Result Source retournant les communautés avec un « Credential Informations » ayant des droits élevés

  • Sur papier la solution semble idéale, le Result Source renverrait toutes les communautés et donc on pourrait tout simplement les afficher à partir de là.
    Bon il s’avère que dans les faits je n’ai jamais réussi à obtenir le résultat attendu, le compte utilisé pour le Result Source a pourtant les privilèges qu’il faut (si je lance la requête via ce user j’obtiens bien des résultats corrects)

  • Hypothèse 2 : Créer une WebPart à partir de la WebPart ExistingCommunitiesWebPart dans laquelle j’exécute la requête de récupération des communautés dans un bloc de privilèges élevés (afin de retourner toutes les communautés)
  • Cette solution fonctionne et c’est ce que je vais présenter ci dessous.


    Création de la WebPart vide
    comPort8

    Créer un élément de type WebPart, dans le fichier .cs collez le code ci dessous (code d’ExistingCommunitiesWebPart)

    using Microsoft.Office.Server.Search.Query;
    using Microsoft.Office.Server.Search.WebControls;
    using Microsoft.SharePoint.Security;
    using Microsoft.SharePoint.Utilities;
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Security.Permissions;
    using System.Web;
    using System.Web.UI;
    using System.Web.UI.WebControls.WebParts;
    namespace Microsoft.SharePoint.Portal.WebControls
    {
        [SharePointPermission(SecurityAction.LinkDemand, ObjectModel = true), AspNetHostingPermission(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
        public sealed class ExistingCommunitiesWebPart : ContentBySearchWebPart
        {
            private bool _includeSubSites;
            public override string Title
            {
                get
                {
                    return string.Empty;
                }
                set
                {
                }
            }
            public override PartChromeType ChromeType
            {
                get
                {
                    return PartChromeType.None;
                }
                set
                {
                }
            }
            public bool IncludeSubSites
            {
                get
                {
                    return this._includeSubSites;
                }
                set
                {
                    this._includeSubSites = value;
                }
            }
            public ExistingCommunitiesWebPart()
            {
                base.BypassCBSFeatureCheck = true;
                this._includeSubSites = true;
            }
            protected override bool RequiresWebPartClientScript()
            {
                return false;
            }
            private static ulong NowInTicks()
            {
                DateTime utcNow = DateTime.UtcNow;
                checked
                {
                    ulong num = (ulong)utcNow.Second * 10000000uL;
                    num += (ulong)utcNow.Minute * 600000000uL;
                    num += (ulong)utcNow.Hour * 36000000000uL;
                    num += (ulong)utcNow.DayOfYear * 864000000000uL;
                    num += (ulong)utcNow.Year * 316224000000000uL;
                    if (utcNow.Month &gt; 2 &amp;&amp; !DateTime.IsLeapYear(utcNow.Year))
                    {
                        num += 864000000000uL;
                    }
                    return num;
                }
            }
            protected override void OnLoad(EventArgs e)
            {
                if (base.DataProvider != null)
                {
                    string propertyName = string.Format(CultureInfo.InvariantCulture, "[formula:((CommunityMembersCount*0.5)+CommunityTopicsCount+(CommunityRepliesCount*0.5))/(log(abs(Created-{0})+1)+log(abs(LastModifiedTime-{0})+1)+1)]", new object[]
                    {
                        ExistingCommunitiesWebPart.NowInTicks()
                    });
                    if (this._includeSubSites)
                    {
                        base.DataProvider.QueryTemplate = "WebTemplate:Community";
                    }
                    else
                    {
                        base.DataProvider.QueryTemplate = "WebTemplate:Community AND contentclass:STS_Site";
                    }
                    base.DataProvider.SourceID = "8413CD39-2156-4E00-B54D-11EFD9ABDB89";
                    base.DataProvider.FallbackSort = new List();
                    base.DataProvider.FallbackSort.Add(new ResultSort(propertyName, SortDirection.Descending));
                }
                base.ResultsPerPage = 30;
                base.RenderTemplateId = "~sitecollection/_catalogs/masterpage/Display Templates/Search/Control_SearchResults.js";
                base.ItemTemplateId = "~sitecollection/_catalogs/masterpage/Display Templates/System/Item_CommunityPortal.js";
                base.ShowAdvancedLink = false;
                base.ShowPreferencesLink = false;
                base.ShowAlertMe = false;
                base.ShouldHideControlWhenEmpty = false;
                base.EmptyMessage = StringResourceManager.GetString(LocStringId.CommunityPortalNoCommunities);
                base.OnLoad(e);
            }
            private static void RenderTitle(HtmlTextWriter writer)
            {
                string localizedString = SPUtility.GetLocalizedString("$Resources:spscore,CommunityPortal_WebPart_Title", null, checked((uint)CultureInfo.CurrentUICulture.LCID));
                writer.AddAttribute(HtmlTextWriterAttribute.Class, "ms-webpart-chrome-title");
                writer.RenderBeginTag(HtmlTextWriterTag.Div);
                writer.AddAttribute(HtmlTextWriterAttribute.Class, "ms-webpart-titleText-withMenu ms-webpart-titleText");
                writer.AddAttribute(HtmlTextWriterAttribute.Title, localizedString);
                writer.RenderBeginTag(HtmlTextWriterTag.H2);
                writer.Write(localizedString);
                writer.RenderEndTag();
                writer.RenderEndTag();
            }
            protected override void Render(HtmlTextWriter writer)
            {
                ExistingCommunitiesWebPart.RenderTitle(writer);
                base.Render(writer);
            }
        }
    }

    Modifiez tout d’abord le nom de la classe ainsi que le namespace afin qu’ils correspondent à votre besoin / votre projet

    La méthode qui va principalement nous intéresser est OnLoad car c’est dans celle ci que sont définis la requête et les templates d’affichage des résultat.

    Par rapport à la problématique 3 (l’exécution de la requête avec des privilèges élevés) le code suivant remontera correctement toutes les communautés (que l’utilisateur y ait accès ou non)

    protected override void OnLoad(EventArgs e)
            {
                SPSecurity.RunWithElevatedPrivileges(delegate()
                {
                    if (base.ActiveDataProvider != null)
                    {
                        string propertyName = string.Format(CultureInfo.InvariantCulture, "[formula:((CommunityMembersCount*0.5)+CommunityTopicsCount+(CommunityRepliesCount*0.5))/(log(abs(Created-{0})+1)+log(abs(LastModifiedTime-{0})+1)+1)]", new object[]
                    {
                        PopularCommunitiesWP.NowInTicks()
                    });

                        base.ActiveDataProvider.QueryTemplate = "WebTemplate:Community";
                        // Local Sharepoint Results
                        base.ActiveDataProvider.SourceID = "8413CD39-2156-4E00-B54D-11EFD9ABDB89";
                        base.ActiveDataProvider.FallbackSort = new List();
                        base.ActiveDataProvider.FallbackSort.Add(new ResultSort(propertyName, Microsoft.Office.Server.Search.Query.SortDirection.Descending));
                    }

                    base.ResultsPerPage = 30;
                    base.RenderTemplateId = "~sitecollection/_catalogs/masterpage/Display Templates/Search/Control_SearchResults.js";
                    base.ItemTemplateId = "~sitecollection/_catalogs/masterpage/Display Templates/System/Item_CommunityPortal.js";
                    base.ShowAdvancedLink = false;
                    base.ShowPreferencesLink = false;
                    base.ShowAlertMe = false;
                    base.ShouldHideControlWhenEmpty = false;
                    base.EmptyMessage = "No communities";
                    base.OnLoad(e);
                });
            }

    Il est bien évidemment possible de raffiner la requêtes suivant vos problématiques (collections spécifiques etc.)

    Le sourceID est le GUID par défaut du scope « Local SharePoint Results », scope par défaut du moteur de recherche FAST interne.

    Le FallbackSort est le mode de tri par défaut (formule détaillée plus haut), vous pouvez le modifier pour afficher par ordre alphabétique ou autre…

    Le RenderTemplateId et le ItemTemplateId sont les templates d’affichages globaux et de chaque item, vous pouvez récupérer les existants et les adapter à vos besoins.

    Par rapport à la problématique 1 (Modification du titre de la WebPart) vous avez plusieurs possibilités.
    Si votre site est multilangue alors vous devez probablement utiliser les fichiers de ressources pour gérer les libellés.
    Dans ce cas modifiez dans la méthode RenderTitle la ligne suivante avec vos informations

    string localizedString = SPUtility.GetLocalizedString("$Resources:NomFichierResource,nomChaineResource", null, checked((uint)CultureInfo.CurrentUICulture.LCID));

    Dans le cas où vous n’auriez qu’une langue à gérer vous pouvez tout simplement définir la variable localizedString.
    Il est aussi possible de supprimer l’override du Title afin de redonner à l’utilisateur la possiblité de définir lui même le titre de sa WebPart. (pensez à supprimer l’appel à RenderTitle dans ce cas)

    Il nous reste à voir la réponse à la problématique 2 (Afficher les communautés dont je suis membre)

    Nous avons vu tout à l’heure que la requête MemberOWSUSER:{User.Name} ne remontait que des items de liste et non des communautés, par contre ce qui peut nous être utile est le chemin url de la communauté.
    Une solution est donc d’exécuter une 1ère requête interne qui récupérera tous les chemins des communautés dont l’utilisateur est membre, puis ensuite une requête qui va récupérer les objets communautés pour affichage.

    private string GetFinalQueryFullCommunitiesPath()
            {
                StringBuilder fullQuery = new StringBuilder();

                string curentUser = SPContext.Current.Web.CurrentUser.Name;

                ResultTableCollection myResultCol = SPSearchUtils.ExecKQLQuery(SPContext.Current.Site, "MemberOWSUSER:\"" + curentUser + "\"");

                if (myResultCol.Count &gt; 0)
                {
                    DataTable resultTables = myResultCol.Filter("TableType", KnownTableTypes.RelevantResults).FirstOrDefault().Table;
                    List lCommunityImMember = resultTables.AsEnumerable().ToList();

                    List lCommPath = new List();
                    foreach (DataRow dr in lCommunityImMember)
                    {
                        // Colonne 19 champ ParentLink
                        string listURL = dr.ItemArray[19].ToString();
                        string webURL = listURL.Substring(0, listURL.IndexOf("Lists") - 1);

                        StringBuilder searchPath = new StringBuilder();

                        searchPath.AppendFormat(" Path:{0}", webURL);
                        lCommPath.Add(searchPath.ToString());
                    }
                    fullQuery.Append("(");
                    foreach (string str in lCommPath)
                    {
                        fullQuery.Append(str);
                    }
                    // Filtrage par communautés
                    fullQuery.Append(") AND WebTemplate:Community");
                }
                return fullQuery.ToString();
            }

    et ensuite de passer la requête générée au moteur de la WebPart (dans OnLoad)

    base.ActiveDataProvider.QueryTemplate = GetFinalQueryFullPopularCommunitiesPath();

    Voilà j’espère que cette note un peu détaillée vous a permis d’appréhender un peu mieux les possibilités du Portail des communautés et de sa WebPart associée.

    N’hésitez pas à revenir vers moi pour plus de détails ou si des points sont peu compréhensibles.