Das Intranet wird Social (Part 1)

Business-practices-that-refuse-to-die

Social Media ist bereits seit ein paar Jahren der grosse Begriff in der externen Online-Kommunikation. Man hat über die letzten Jahre beobachten können, wie sich unser Kommunikationsverhalten nach und nach verändert hat. Diese Veränderung sind auch immer stärker in Unternehmen zu beobachten. Betrachtet man die Intranet Trends für das kommende Jahr 2014 so stösst man oft auf die Begriffe „Social Intranet“ und „Social Business“. Was sich hinter diesen Begriffen verbirgt und was ein Social Intranet für Ihr Unternehmen bringen kann, werde ich in dieser zweiteiligen BlogPost-Serie aufzeigen. Weiterlesen

TypeScript SharePoint 2013 App

SharePoint-hosted Apps für SharePoint 2013 schreibt man bekanntlich vermehrt in JavaScript. Da für JavaScript Sprachkonstrukte wie Klasse, Interfaces, Vererbung oder Module nicht existieren, ist es für .NET Entwickler vielfach nicht ganz einfach JavaScript Anwendungen richtig zu schreiben. Um diese Probleme in der Frontend Entwicklung zu eliminieren, hat Microsoft die Programmiersprache TypeScript entwickelt. TypeScript stellt einen Compiler zur Verfügung, welcher TypeScript Code in JavaScript Code umwandelt. Ebenso existiert ein Visual Studio Plugin, welches dem Entwickler IDE Support für TypeScript gibt. Eine Anleitung für die Installation ist auf der offiziellen Website von TypeScript beschrieben.
(mehr …)

RazorEngine in SharePoint 2013 nutzen

Da SharePoint 2013 auf dem .NET Framework 4 (oder 4.5) läuft, kann nun auch die OpenSource Templating Engine Razor in SharePoint Projekte eingebunden werden. SharePoint an sich basiert zwar weiterhin auf den bewährten ASP.NET WebForms, doch können Custom WebParts oder Controls sehr einfach auf die RazorEngine zugreifen.

In diesem Blogpost soll ein einfaches WebPart erstellt werden, welches aus einer SharePoint Liste alle Titel der Elemente ausliest und ausgibt.

Ich erstelle also ein neues SharePoint 2013 Projekt in Visual Studio und füge via NuGet Manager die Razor Engine der Solution hinzu. Anschliessend müssen unbedingt die beiden Assemblies (RazorEngine.dll und System.Web.Razor.dll) im SharePoint Package als additional Assemblies für den GlobalAssemblyCache hinzugefügt werden.

nuget

 

Als nächstes erstelle ich ein neues WebPart in der Solution. Dem WebPart wird ein Property für das Razor Template hinzugefügt. In dieses WebPart Property kann man später sein Model als WebPart Property bearbeiten.

[WebBrowsable(true),
WebDisplayName("TemplateModel"),
WebDescription("TemplatModel Property"),
Personalizable(PersonalizationScope.Shared),
Category("Template")]
public string TemplateModel { get; set; }

Ebenso überschreibe ich die Render Methode, um darin die Parse Methode von der Razor Engine aufzurufen. In meiner Render Methode werden statische Werte geladen, aber auch alle Titel der Seiten in der Pages Library des aktuellen SPWebs geladen.

protected override void Render(HtmlTextWriter writer)
{
// irgendwelche statischen Werte
const string title = "Pages Titles";
var pagesList = SPContext.Current.Web.Lists["Pages"].Items;
var pages = (from SPListItem page in pagesList select page.Title).ToList();
// erstelle ein Model (hier als anonyme Klasse gelöst)
var model = new { Data = title, Pages = pages };
// Parse das Model wenn ein Model vorhanden ist oder geb eine Fehlermeldung aus
if(!string.IsNullOrEmpty(TemplateModel))
{
var parsedHtml = Razor.Parse(TemplateModel, model);
// und der Output wird gerendert
writer.Write(parsedHtml);
}
else
{
writer.Write("Definiere ein Model in den WebPart Properties");
}
}

Nachdem die Solution deployed wurde, kann das WebPart auf einer Page hinzugefügt und editiert werden. Hier wird nun mittels RazorTemplate auf das im Code mitgegebene Model zugegriffen.

editwebpart

 

Wird das WebPart anschliessend gespeichert, rendert es folgenden Output auf die Seite.

webpart

Dieses kleine Beispiel zeigt, wie einfach es ist, mittels RazorEngine WebParts für SharePoint 2013 zu entwickeln.

 

How to Deploy Mobile Channels on SharePoint 2013

SharePoint 2013 bietet neu Mobile Channels an, welche die Device-gerechte Darstellung der Seiten auf verschiedene Mobile-Plattformen gewährleisten. Die Funktionalität sowie die Anwendung der Device Channels kann im Internet beispielsweise unter (http://blogs.msdn.com/b/sharepointdev/archive/2013/05/07/optimizing-sharepoint-2013-websites-for-mobile-devices.aspx) nachgelesen werden.

Dieser Artikel beschreibt, wie Device Channels automatisiert in einem Feature deployed werden können.

Der Ansatz von Microsoft

Als Ausgangslage habe ich das Deployment der Device Channels im SmallBusinessWebsite Feature von Microsoft verwendet. Dieses bietet Deployment für Mobile-Device Channels:

Das Feature.xml im Ordner %ProgramFiles%\Common Files\microsoft shared\Web Server Extensions\15\TEMPLATE\FEATURES\SmallBusinessWebsite verlinkt fürs Deployment der mobilen Master-Pages aufs MobileChannel.xml File.

Das MobileChannel.xml File verlinkt dabei die Mobile-Device Typen (UserAgentSubstrings; z.B. iOS oder $FallbackMobileUserAgents;) mit eigens definierten Namen (ChannelAlias) und füllt diese Informationen in die Device Channels List:

In den SharePoint Settings finden sich diese Informationen unter Device Channels wieder (abweichend zur Xml-Definition oben wird im folgenden Bild der Namics Mobile Channel dargestellt):

Master Pages für Device Channels

Anhand des ChannelAlias werden die hier definierten User Agent Gruppierungen zu Master Pages verknüpft. Die Benutzerschnittstelle dafür sieht wie folgt aus
(abweichend zur Xml-Definition oben wird im folgenden Bild der Namics Mobile Channel dargestellt):

Die Masterpage selbst wird im SmallBusinessWebsite Feature über das gewohnte Module-Deployment in die Masterpage-Gallery eingespielt:

Offen bleibt in der Feature-Definition, wie die Zuweisung der Masterpage auf einen Device Channel vonstattengeht.

Zuweisung der Masterpage auf einen Device Channel

Im SharePoint 2013 geschieht diese Zuweisung der Masterpage auf einen Device Channel über das Mappings-File __DeviceChannelMappings.aspx. Dieses liegt im MasterPage Katalog unter _catalogs/masterpage/ und enthält die Mappings im XML Format:

Beim SmallBusinessWebsite Feature kommt gemäss Feature-Definition der Microsoft.SharePoint.Publishing.SmallBusinessWebsiteFeatureHandler Receiver von Microsoft zum Einsatz, welcher das Mappings-File für die Site anlegt und konfiguriert.

Aufgrund der Komplexität des SmallBusinessWebsiteFeatureHandlers, habe ich hier das PublishingMobile Feature analysiert. Dieses deployed ebenfalls Mobile Channels:

Microsoft verwendet beim PublishingMobile Feature den Microsoft.SharePoint.Publishing.Mobile.FeatureHandler Receiver. Nach einigen Nachforschungen stellte ich fest, dass für die Zuweisung der Masterpage auf einen Device Channel Microsoft-interne (d.h. internal) Klassen wie die Microsoft.SharePoint.Publishing.Mobile.MasterPageMappingsFile zum Einsatz kommen:

Dies erschwert das automatische Deployment der Mobile Channels, da die entsprechenden Features nachgebaut werden müssen. Wesentliche Punkte dabei sind in folgenden Methoden abgedeckt:

  • Microsoft.SharePoint.Publishing.Mobile.MappingsFile<T>.UpdateFile()
    Channel Mappings File in die Master Page Gallery eintragen.
  • Microsoft.SharePoint.Publishing.Mobile.MappingsFile.SaveNoMobileMappingsInfoToRootWeb()
    Root Web Property für die Mobile Channels Deaktivierung (__NoMobileMapping) entfernen.

Putting it all together

Der folgende Code enthält den Receiver als Code-Schnippsel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
/// <summary>
/// Initializes a new instance of the Microsoft.SharePoint.SPFeatureReceiver class.
/// </summary>
public class MobileChannelReceiver : SPFeatureReceiver
{
    private static readonly SPContentTypeId DeviceChannelMappingsContentTypeId = new SPContentTypeId("0x010100FDA260FD09A244B183A666F2AE2475A6");

    private const string mDeviceChannelFile = "__DeviceChannelMappings.aspx";
    private const string mMasterPageLibPath = "_catalogs/masterpage/";
    private const string mSiteCollectionUrlStart = @"href=""";
    private const string mSiteCollectionUrlReplacement = mSiteCollectionUrlStart + "~sitecollection";

    /// <summary>
    /// Deploys the mobile channel file into the Master Page Gallery. The existing
    /// file will be replaced with the file from the current feature.
    /// </summary>
    /// <param name="properties">An SPFeatureReceiverProperties object that represents the properties of the event.</param>
    public override void FeatureActivated(SPFeatureReceiverProperties properties)
    {
        using (SPWeb web = ((SPSite) properties.Feature.Parent).OpenWeb())
        {
            RegisterChannelFile(
                web,
                properties.Definition.RootDirectory + @"\" + mDeviceChannelFile,
                mDeviceChannelFile);
        }
    }


    private void RegisterChannelFile(SPWeb rootWeb, string fullFilePathOfNewFile, string masterPageFileNameToReplace)
    {
        SPFile fileMasterpage = rootWeb.GetFile(mMasterPageLibPath + masterPageFileNameToReplace);

        if (fileMasterpage != null && fileMasterpage.Exists)
        {
            TryCheckedIn(fileMasterpage);
            TryCheckedOut(fileMasterpage);

            // patch file content with the root web url
            string mobileDeploymentFileRaw = File.ReadAllText(fullFilePathOfNewFile);
            string mobileDeploymentFile = mobileDeploymentFileRaw.Replace(
                mSiteCollectionUrlReplacement,
                mSiteCollectionUrlStart + rootWeb.ServerRelativeUrl);

            // Source: Microsoft.SharePoint.Publishing.Mobile.MappingsFile<T>::UpdateFile()
            // save patched content into server-side file library
            fileMasterpage.SaveBinary(Encoding.Unicode.GetBytes(mobileDeploymentFile));

            // update content type if has been set to a wrong one
            SPListItem item = fileMasterpage.Item;
               
            if (!DeviceChannelMappingsContentTypeId.IsParentOf((SPContentTypeId)item[SPBuiltInFieldId.ContentTypeId]))
            {
                item[SPBuiltInFieldId.ContentTypeId] = DeviceChannelMappingsContentTypeId;
                item.Update();
            }
               
            // accept changes and checkin
            fileMasterpage.CheckIn(string.Empty, SPCheckinType.MajorCheckIn);

            // approve if required
            SPModerationInformation moderationInformation = fileMasterpage.Item.ModerationInformation;

            if (moderationInformation != null && moderationInformation.Status == SPModerationStatusType.Pending)
            {
                fileMasterpage.Approve(string.Empty);
            }

            // activate custom master page channel mappings on web level
            // Source: Microsoft.SharePoint.Publishing.Mobile.MappingsFile<T>::SaveNoMobileMappingsInfoToRootWeb()
            string noMobileMappingKey = GetNoMobileMappingKey(rootWeb);
            if (rootWeb.AllProperties.ContainsKey(noMobileMappingKey))
            {
                rootWeb.AllProperties.Remove(noMobileMappingKey);
                rootWeb.Update();
            }
        }
    }

    private static string GetNoMobileMappingKey(SPWeb web)
    {
        return string.Format(CultureInfo.InvariantCulture, "__NoMobileMapping{0}", new object[]
        {
            web.ID.ToString("N")
        });
    }

    private bool TryCheckedOut(SPFile toCheckOut)
    {
        try
        {
            if (toCheckOut.Level != SPFileLevel.Checkout)
            {
                toCheckOut.CheckOut();
            }
        }
        catch (Exception ex)
        {
            LOG.Current.TraceUnexpected(
                CommonLibErrorCodes.EnsureApplicationFeatureReceiver,
                () => ex.Message);
            return false;
        }
        return true;
    }

    private bool TryCheckedIn(SPFile toCheckIn)
    {
        try
        {
            if (toCheckIn.CheckOutType != SPFile.SPCheckOutType.None)
            {
                toCheckIn.CheckIn("checked in by feature activation");
            }
            return false;
        }
        catch (Exception ex)
        {
            LOG.Current.TraceUnexpected(
                CommonLibErrorCodes.EnsureApplicationFeatureReceiver,
                () => ex.Message);
        }
        return true;
    }
}

Dafür wird das __DeviceChannelMappings.aspx Mappings File wie folgt deployed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<%@ Reference VirtualPath="~CustomMasterUrlForMapping0" %><%@ Reference VirtualPath="~CustomMasterUrlForMapping1" %><%@ Page Language="C#" Inherits="Microsoft.SharePoint.Publishing.Internal.WebControls.MappingsFileBasePage" %><html xmlns:mso="urn:schemas-microsoft-com:office:office" xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"><%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<head>
<!--[if gte mso 9]><SharePoint:CTFieldRefs runat=server Prefix="mso:" FieldList="FileLeafRef"><xml>
<mso:CustomDocumentProperties>
<mso:ContentTypeId msdt:dt="string">0x010100FDA260FD09A244B183A666F2AE2475A6</mso:ContentTypeId>
</mso:CustomDocumentProperties>
</xml></SharePoint:CTFieldRefs><![endif]-->
</head><body><mappings>
  <mapping>
    <channelAlias>NamicsMobileChannel</channelAlias>
    <masterUrl href="~sitecollection/_catalogs/masterpage/SP2013Mobile.Master" token="~sitecollection/_catalogs/masterpage/SP2013Mobile.master" />
  </mapping>
  <defaultChannelMapping>
    <siteMasterUrl token="~sitecollection/_catalogs/masterpage/SP2013.Master" href="~sitecollection/_catalogs/masterpage/SP2013.Master" />
    <systemMasterUrl token="~sitecollection/_catalogs/masterpage/seattle.master" href="~sitecollection/_catalogs/masterpage/seattle.master" />
    <alternateCssUrl token="" href="" />
    <themedCssFolderUrl token="" href="" isthemeshared="false" />
  </defaultChannelMapping>
</mappings></body></html>

Fazit

SharePoint 2013 bietet mit den neuen Mobile Channels einen effizienten Weg, um die Seite Mobile-tauglich zu gestalten. Das automatisierte Deployment via Features ist im Moment noch nicht durchgehend gelöst, da die API fast ausschliesslich als internal markiert ist.  Hier muss Hand angelegt und mit einem entsprechenden Feature Receiver die Deployment-Automatisierung nachgerüstet werden.

Display SharePoint 2013 Followed items in a WebPart

SharePoint 2013 bietet die Möglichkeit, verschiedenen Seiten und Dokumenten zu folgen. Hierfür wird die MySite benötigt, welche als Site Collection in der Central Administration angelegt wird. Die Details zum Setup hat Microsoft unter http://technet.microsoft.com/en-us/library/ee624362.aspx beschrieben.

Dieser Artikel beschreibt wie die Followed Sites aus SharePoint in einen eigenen WebPart integriert werden können.

Warten auf die SP.SOD Klasse
Sämtliche Features der MySite werden über JavaScript / AJAX Calls angeboten. Daher ist hier der erste Ansatz, sich mit den Client-Side SharePoint Komponenten auseinander zu setzen. Dabei fällt als erstes die SP.SOD Klasse auf: Diese bietet On-Demand JavaScripting an: Somit ist es möglich, Funktionalitäten im Nachhinein zu laden und auf dessen Bereitstellung zu warten:

// Make sure SP.ClientContext is available
SP.SOD.executeFunc('sp.js', 'SP.ClientContext', function () {
    // Make sure SocialFollowingManager is available
    SP.SOD.executeFunc('userprofile', 'SP.Social.SocialFollowingManager', function() {
        // Do something when SocialFollowingManager is available
    });
});

Die executeFunc wartet, bis die angegebene Funktion (z.B. SP.ClientContext) geladen wurde und ruft anschliessend den bereitgestellten Funktionszeiger auf. Dieses Verhalten kann so verschachtelt werden, bis alle die benötigten Klassen mit Sicherheit geladen wurden.
Im Code-Schnippsel oben wird also gewartet, bis der SP.Social.SocialFollowingManager auf dem Client-Browser verfügbar ist.

Daten mit SP.Social.SocialFollowingManager Abfragen
Zum Abfragen der MySite  Daten wird die SocialFollowingManager Klasse verwendet. Auf dieser ist für unseren Case die getFollowed Methode interessant. Diese gibt als Resultat SocialActors zurück, welche die für uns relevanten Followed Sites enthalten. Aufgrund des SocialActorTypes kann bestimmt werden, welche Resultate (für uns Sites) im Result-Array gelistet werden.

Der folgende Code initialisiert den SocialFollowingManager und ruft anschliessend die getFollowed Methode auf:

var clientContext = SP.ClientContext.get_current();
var socialManager = new SP.Social.SocialFollowingManager(clientContext);
           
var socialActor = new SP.Social.SocialActorInfo();
socialActor.set_actorType(SP.Social.SocialActorTypes.sites);
           
var futureObject =  { result: socialManager.getFollowed(socialActor) };
     
clientContext.executeQueryAsync(
    function() {
        // visualize content in futureObject
    });

Zum Schluss wichtig ist der Aufruf von executeQueryAsync: Erst dieser führt den Call auf den Server aus und lädt die Daten vom Server ins futureObject.

Das WebPart als Ganzes
Anschliessend können die Daten mit beliebigem Client-seitigen Code visualisiert werden. Das Code-Schnippsel enthält ebenfalls den WebPart-Rumpf sowie die für ASP.NET benötigten Komponenten fürs Rendering.

[ToolboxData("<{0}:FavoritesWebPart runat=server></{0}:FavoritesWebPart>")]
public class FavoritesWebPart : System.Web.UI.WebControls.WebParts.WebPart
{
    #region Private Members

    private const string FavoritesContainerId = "fav";

    // Source:
    //  -> MSDN: http://msdn.microsoft.com/en-us/library/jj679862.aspx
    //  -> BLOG: http://techmikael.blogspot.ch/2013/03/following-or-favorite-pages-in.html
    //  -> BLOG: http://anujabhojani.blogspot.ch/2013_03_01_archive.html
    //  -> BLOG: http://blog.lekman.com/2013/06/making-suite-bar-menu-dropdown-in.html (SOLUTION)
    private const string FavoritesScriptStart = @"<script type=""text/javascript"">
<!--
(function($) {

(function(strElementId){

    var namicsFollower = {
        result: null,
        show: function() {
            var element = $("
"#"" + strElementId);
            $.each(this.result, function(idx, site) {
                var followCtnr = $("
"<p />"");
                var imageCtnr = $("
"<span />"",
                    {
                        style: "
"height:16px;width:16px;position:relative;display:inline-block;overflow:hidden;"",
                        'class': "
"s4-clust ms-promotedActionButton-icon""
                    });
                imageCtnr.append($("
"<img />"",
                    {
                        src: "
"/_layouts/15/images/spcommon.png?rev=23"",
                        alt: "
"Follow"",
                        style: "
"position: absolute; left: -218px; top: -48px;""
                    }));

                followCtnr.append(imageCtnr);
                followCtnr.append($("
"<a />"",
                    {
                        href: site.get_uri(),
                        text: site.get_name(),
                        style: "
"vertical-align: middle""
                    }));
                element.append(followCtnr);
            });
        },
        loadData: function() {
            var clientContext = SP.ClientContext.get_current();
            var socialManager = new SP.Social.SocialFollowingManager(clientContext);
       
            var socialActor = new SP.Social.SocialActorInfo();
            socialActor.set_actorType(SP.Social.SocialActorTypes.sites);
       
            this.result = socialManager.getFollowed(socialActor);

            clientContext.executeQueryAsync(Function.createDelegate(this, this.show));
        }
    }

    // Make sure SP.ClientContext is available
    SP.SOD.executeFunc('sp.js', 'SP.ClientContext', function () {
        // Make sure SocialFollowingManager is available
        SP.SOD.executeFunc('userprofile', 'SP.Social.SocialFollowingManager', function() {
            namicsFollower.loadData();
        });
    });
})("
;
    private const string FavoritesScriptInit = @"'{0}'";
    private const string FavoritesScriptEnd = @"
);

})(jQuery);

//-->
</script>"
;
   

    #endregion

    #region Public Members

    public FavoritesWebPart()
    {
    }

    /// <summary>
    /// Lifecycle override method for the CreateChildControls State of the WebPart.
    /// </summary>
    /// <remarks>
    /// The method handles all exceptions.
    /// </remarks>
    protected override void CreateChildControls()
    {
        try
        {
            base.CreateChildControls();

            HtmlControl favContainer = new HtmlGenericControl("div");
            favContainer.ID = FavoritesContainerId;
            Controls.Add(favContainer);

            Controls.Add(new LiteralControl(FavoritesScriptStart));
            Controls.Add(new LiteralControl(string.Format(
                FavoritesScriptInit,
                favContainer.ClientID)));
            Controls.Add(new LiteralControl(FavoritesScriptEnd));
        }
        catch (Exception ex)
        {
            // TODO: Log exception here...
            throw;
        }
    }

    #endregion
}
Seite 1 von 41234