SharePoint 2013 Apps: Lokalisierung im Host Web

In diesem Beitrag geht es jetzt um die Lokalisierung von App-Komponenten, die im Hostweb angezeigt werden. Das ist zum einen der Name der App selbst, aber es sind z.B. auch die Texte, die für eigene Ribbon-Elemente benötigt werden. Oder Name und Beschreibung von Client Webparts (App Parts) und deren benutzerdefinierte Eigenschaften.

Im Beitrag Lokalisierung bei Remote Apps habe ich bereits gezeigt, wie man die Elemente cloud-basierter Apps lokalisieren kann.

Wie immer gehe ich auch hier wieder davon aus, daß die App mit Visual Studio 2012 erstellt wird. Das Hosting-Modell spielt in diesem Fall keine Rolle, weil es die genannten Elemente bei jeder Art App geben kann.

Ressourcendateien hinzufügen

Die zur Lokalisierung der Hostweb-Komponenten notwendigen Ressourcendateien werden über das App-Manifest zum Projekt hinzugefügt. Man öffnet dazu den Manifest-Designer durch einen Doppelklick auf die Datei AppManifest.xml. Im Reiter Supported Locales gibt man jetzt die zu unterstützenden Kulturen an und Visual Studio fügt dann automatisch eine passende resx-Datei ins Projekt ein. Bei der ersten Angabe, z.B. German (Germany), wird nicht nur die Resources.de-DE.resx, sondern auch die neutrale Resources.resx ins Projekt aufgenommen:

Die Dateien werden in einem Ordner Resources abgelegt. Im Solution Explorer sieht das dann so aus:

Achtung: wenn man bei einer App unterstützte Kulturen angibt, kann diese App nicht mehr auf einer Website verwendet werden, die für eine nicht unterstützte Kultur angelegt wurde! Insofern ist die Verwendung der neutralen Ressourcendatei hier ziemlich sinnlos.

Ressourcen verwenden

Die Texte aus den Ressourcendateien können jetzt in der von Farmlösungen gewohnten Weise verwendet werden. Um z.B. den Titel eines App Parts zu lokalisieren, gibt man dort die zu verwendete Ressource an:

Title="$Resources:AppPartTitle;"

Das funktioniert auch beim Titel der App selbst, bei Texten für eigene Ribbon-Elemente und bei eigenen Eigenschaften, die man einem App Part geben möchte.

OPC Beziehungen

Wer jetzt meint, das wäre schon alles gewesen, muß leider enttäuscht werden. Visual Studio fügt leider nicht alle notwendigen Beziehungen in das fertige App-Paket ein. Warum das nicht gemacht wird und ob diese Unterstützung irgendwann nachgeliefert wird, steht derzeit in den Sternen.

Man kann diese Beziehungen jetzt durch manuelles Gefrickel im App-Paket selbst einfügen, aber dann muß die App auch manuell bereitgestellt werden. D.h. man kann nicht mehr einfach in Visual Studio F5 drücken und es wird sich um alles andere gekümmert. Sehr unschön.

Es geht aber sehr viel einfacher mit einem Tool, das Ricardo Loo bereitgestellt hat und das er hier beschreibt: AddRelsToAppPackage.exe

Man kopiert dieses kleine Programm einfach in das Projektverzeichnis, also in das Verzeichnis, in dem Visual Studio die Dateien und Ordner des App-Projekts ablegt. In Visual Studio macht man dann einen Rechtsklick auf das App-Projekt und klickt dann auf Properties. Im Reiter SharePoint fügt man folgendes bei Pre-deployment Command Line ein:

"$(ProjectDir)\AddRelsToAppPackage.exe" "$(TargetDir) "

Achtung: man verwendet wirklich genau diesen Text inklusive der Anführungszeichen und dem Leerzeichen nach $(TargetDir)!

Das gesamte Projekt AddRelsToAppPackage kann hier heruntergeladen werden. Für alle, die sich nur für die fertige exe-Datei interessieren, habe ich hier eine Kopie hinterlegt.

SharePoint 2013 Apps: Lokalisierung bei Remote Apps

In diesem Beitrag soll gezeigt werden, wie cloud-basierte Apps (auto-hosted oder provider-hosted) lokalisiert werden, d.h. wie man die App für verschiedene Sprachen fit machen kann. Bei SharePoint-hosted Apps kann man dazu auf die von Farmlösungen bekannten Möglichkeiten zurückgreifen. Bei cloud-basierten Apps liegt aber die Verantwortung über das AppWeb beim Entwickler und man muß sich deshalb auch um die Lokalisierung selbst kümmern. Achtung: es geht hier wirklich nur um die Lokalisierung der Remote-Komponenten und nicht darum, wie man z.B. per App bereitgestellte Listen lokalisiert. Das werde ich eventuell in einem späteren Beitrag beschreiben.

Update 27.08.2013: hier habe ich in einem Beitrag die Lokalisierung von Hostweb-Komponenten beschrieben.

Ich gehe hier wieder davon aus, daß die App mit Visual Studio 2012 erstellt wird. Damit ist das AppWeb im Grunde eine ganz normale ASP.NET Anwendung und man kann zur Lokalisierung die dafür vorgesehenen Möglichkeiten verwenden. Die grundsätzliche Vorgehensweise ist hier beschrieben: ASP.NET Globalization and Localization. Ich werde hier nur das Wichtigste kurz zusammenfassen.

Ressourcendateien hinzufügen

Ressourcendateien *.resx werden zur Bereitstellung sprachabhängiger Elemente verwendet. Man legt dabei für jede Kultur, die man unterstützen möchte, eine Datei an. Es kann also eine app.de-DE.resx für Deutschland und eine app.de-CH.resx für die Schweiz geben. Man sollte außerdem immer eine neutrale app.resx bereitstellen, die immer dann verwendet wird, wenn es keine spezifische Datei für eine Kultur gibt.

In ASP.NET kann zwischen lokalen und globalen Ressourcen unterschieden werden. Lokale Ressourcen gelten immer nur für eine Seite *.aspx oder ein benutzerdefiniertes Control *.ascx. Diesen weg möchte ich hier nicht weiter verfolgen und empfehle stattdessen die Benutzung von globalen Ressourcen. Damit hat man alle sprachabhängigen Komponenten in einer Datei und man kann gleiche Elemente an unterschiedlichen Stellen wiederverwenden.

Globale Ressourcen werden in einem besonderen Ordner App_GlobalResources abgelegt. In Visual Studio erzeugt man ihn z.B. durch einen Rechtsklick auf das Projekt im Solution Explorerund dann Add -> Add ASP.NET Folder -> App_GloablResources. Dem Ordner kann man dann ebenfalls z.B. durch Rechtsklick und Add -> Resources File Ressourcendateien hinzufügen. Im Solution Explorer kann das dann so aussehen:

Kultur auswählen

Man muß sich entscheiden, wie die App auf verschiedene Kulturen reagieren soll, d.h. in welcher Sprache die App einem bestimmten Benutzer präsentiert wird.

Es gibt die Möglichkeit, daß die App einfach auf die im Browser eingestellte bevorzugte Sprache reagiert. Beim Internet Explorer ist diese Einstellung z.B. über die Internetoptionen und dann den Button Sprachen im Reiter Allgemein erreichbar. Damit diese Einstellung von der ASP.NET Webanwendung verwendet wird, genügt ein einfacher Eintrag in der web.config der Anwendung:

<configuration>
 
<system.web>
    <globalization culture="auto:de-DE" uiCulture="auto:de-DE" />

Die dort angegebene Kultur (hier de-DE) wird als Standard verwendet, falls ein Browser keine bevorzugte Kultur liefert.

Es gibt aber auch die Möglichkeit, sich nach der in SharePoint verwendeten Kultur zu richten. Diese wird beim ersten Aufruf der App aus SharePoint über die {StandardTokens} als QueryString-Parameter SPLanguage übermittelt. Wenn sie verwendet werden soll, kann man den Parameter auslesen und damit die zu verwendende Kultur selbst setzen. Das sollte dann in einer Überschreibung der InitializeCulture-Methode der Startseite geschehen. Der Code dazu kann z.B. so aussehen:

protected override void InitializeCulture() {
 
string selectedLanguage = Request.QueryString["SPLanguage"];
  if (selectedLanguage != null) {
    Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture(selectedLanguage);
    Thread.CurrentThread.CurrentUICulture = new CultureInfo(selectedLanguage);
  }
  base.InitializeCulture();
}

Ressourcen verwenden

Ressourcen können auf zweierlei Art verwendet werden: deklarativ oder per Code. Beim deklarativen Ansatz wird z.B. einem Control direkt beim Erstellen in der aspx ein Wert aus den Ressourcen zugewiesen. Für den Text eines Buttons kann das z.B. so aussehen:

<asp:Button ID="Button1" runat="server" Text="<%$ Resources:AppRes, Button1_Text %>" OnClick="Button1_Click" />

Die Referenz auf ein Ressourcenelement beginnt dabei immer mit dem Schlüsselwort Resources gefolgt von einem Doppelpunkt. Danach kommt der Name der Ressourcendatei ohne Kultur und ohne die resx-Erweiterung. Durch ein Komma getrennt folgt dann der Schlüssel, unter dem das gewünschte Element in den Ressourcen abgelegt ist.

Wenn Ressourcen im Code verwendet werden sollen, z.B. um dynamische Meldungstexte auszugeben, verwendet man dazu die GetGlobalResourceObject-Methode. Zum Festlegen des Seitentitels kann das z.B. so aussehen:

this.Title = GetGlobalResourceObject("AppRes", "PageTitle").ToString();

Ähnlich wie oben steht hier der erste Parameter für den Namen der Ressourcendatei ohne Kultur und ohne die resx-Erweiterung. Der zweite Parameter bezeichnet den Schlüssel, unter dem das gewünschte Element in den Ressourcen abgelegt ist.

SharePoint 2013 Apps: SecurityToken bei Remote Apps

Wenn man in Visual Studio 2012 eine neue cloud-basierte App (auto-hosted oder provider-hosted) anlegt und einfach nur bereitstellt, funktioniert beim ersten Aufruf der App im Browser alles tadellos. Jeder weitere Aufruf z.B. durch eigene Links innerhalb der App oder auch durch einen simplen Button-Klick verursachen jedoch einen Fehler: The parameter ‚token‘ cannot be a null or empty string. Im Browser sieht man die berüchtigte ASP.NET Fehlerseite:

Wie man sieht, entsteht der Fehler beim Versuch ein SecurityToken zu finden, das für den Zugriff von der App aus auf Daten in SharePoint benötigt wird. Visual Studio erzeugt beim Anlegen eines neuen App-Projekts automatisch eine Datei TokenHelper.cs, die eigentlich alles notwendige beinhaltet. Beim Analysieren dieser Datei stellt man dann aber fest, daß dort versucht wird auf Request-Parameter zuzugreifen, die zwar beim ersten Aufruf aus SharePoint gesetzt werden, aber bei jedem weiteren Request dann fehlen.

Man muß sich also selbst darum kümmern diese Werte zwischenzuspeichern. Das kann man mit den bewährten Mitteln von ASP.NET an verschiedenen Stellen machen. Innerhalb einer Seite z.B. im ViewState oder in serverseitigen hidden-Controls. Aber auch seitenübergreifend in der Session. Natürlich sind auch ganz andere Orte denkbar, wie z.B. das Speichern in einer Datenbank.

Eigentlich reicht es das sogenannte Context Token (ein String) zu speichern. Wenn man schon dabei ist, lohnt es sich aber auch gleich die HostUrl mit zu speichern, damit sie in allen weiteren Seitenaufrufen auch wirklich zur Verfügung steht. Dadurch funktioniert auch alles, wenn man vergißt diese Url in einem eigenen Link weiterzugeben.

Ich habe mich dazu entschieden die Werte in der Session abzulegen, weil ich sie dann innerhalb des gesamten AppWebs verwenden kann. Dazu habe ich eine eigene Methode GetContext erstellt, die den passenden ClientContext zurückgibt. Die Methode habe ich als statische Methode einfach zusätzlich in die TokenHelper-Klasse eingebaut. Immer wen man irgendwo einen ClientContext benötigt, kann man dann auf diese Methode zugreifen (anstelle der sonst verwendeten TokenHelper.GetClientContextWithContextToken. Der Methode muß nur der aktuelle HttpContext übergeben werden, der in jeder Seite über HttpContext.Current erreichbar ist. So sieht die Methode aus:

public static ClientContext GetContext(HttpContext httpContext) {
 
string token = httpContext.Session["myToken"] as string;
  string hostUrl = httpContext.Session["myHostUrl"] as string;
  if (token == null || hostUrl == null) {
    token = TokenHelper.GetContextTokenFromRequest(httpContext.Request);
    hostUrl = httpContext.Request["SPHostUrl"];
    httpContext.Session["myToken"] = token;
    httpContext.Session["myHostUrl"] = hostUrl;
  }
  return TokenHelper.GetClientContextWithContextToken(hostUrl, token, httpContext.Request.Url.Authority);
}

SharePoint 2013 Apps: das Chrome Control

In diesem Beitrag soll gezeigt werden, wie man in einer cloud-basierten App (auto-hosted oder provider-hosted) das Chrome Control verwendet. Dieses sorgt dafür, daß die eigene App die von SharePoint 2013 gewohnte Titelleiste verwendet. Außerdem wird dadurch das passende CSS eingebunden, so daß sich die App damit dem Aussehen des Hostwebs anpassen kann. Bei SharePoint-hosted Apps ist das nicht notwendig, weil die Seiten dort als normale Webpartseiten realisiert werden können.

Das wichtigste zu diesem Thema habe ich MSDN entnommen: How to: Use the client chrome control in apps for SharePoint. Ich habe aber das notwendige JavaScript etwas modifiziert und in eine eigene Datei ausgelagert, damit es leichter wiederverwendet werden kann.

Ich gehe hier davon aus, daß die App mit Visual Studio 2012 erstellt wird. Wer die App mit anderen Mitteln baut, kann aber immer noch das notwendige JavaScript gebrauchen.

QueryString erweitern

Wir beginnen zunächst damit die Abfragezeichenfolge (QueryString), die an die App übergeben wird, zu erweitern. Das läßt sich sehr einfach über den AppManifest-Designer erledigen. Visual Studio fügt hier bereits den Platzhalter {StandardTokens} ein, der von SharePoint zur Laufzeit aufgelöst wird und bereits einige wichtige Parameter enthält. Es gibt aber einige mehr und wir wollen hier noch den Titel und das Logo des Hostwebs, damit wir diese ebenfalls im Chrome verwenden können. Das läßt sich erreichen, indem man das hier anhängt: &SPHostTitle={HostTitle}&SPHostLogoUrl={HostLogoUrl}

Weitere Informationen zu den verfügbaren Tokens finden sich hier bei MSDN: URL strings and tokens in apps for SharePoint

Außerdem nutzen wir gleich die Gelegenheit und geben einen weiteren Parameter DisplayType mit, über den wir in der App unterscheiden können, ob eine Seite im FullScreen-Modus oder als AppPart (in einem iframe) aufgerufen wird. Falls eine Seite als AppPart verwendet wird, wollen wir das Chrome Control nicht laden, sondern binden nur die CSS-Datei ein, damit das Styling korrekt funktioniert. Für den DisplayType-Parameter gibt es kein vordefiniertes Token, so daß wir den Wert fest verdrahtet eingeben müssen. Als mögliche Optionen habe ich FullScreen bzw. iframe gewählt.

Insgesamt sieht es dann so aus:

AppWeb erweitern

Unserer App fügen wir jetzt eine zusätzliche JavaScript-Datei hinzu. Ich habe sie appScripts.js genannt und sie kann hier heruntergeladen werden. Wir greifen außerdem auf jQuery zurück, das von Visual Studio aber bereits automatisch zum Projekt hinzugefügt wurde. Der Solution Explorer für das AppWeb sieht dann ungefähr so aus:

JavaScript

Alles was zum Einbinden des Chrome Controls notwendig ist, werden wir hier per JavaScript in der appScripts.js machen. Die fertige Datei kann hier heruntergeladen werden. Ein deklarativer Ansatz ist ebenfalls möglich, aber meiner Meinung nach nicht wirklich wiederverwendbar. Wenn es jemanden interessiert, kann er es im erwähnten Beitrag auf MSDN nachlesen.

Unseren gesamten JavaScript-Code fügen wir in ein eigenes Objekt myApp ein, einfach weil das in JavaScript Best Practices entspricht und den globalen Namensraum nicht "vollmüllt". Wir beginnen also die appScripts.js zu füllen:

"use strict";
var myApp = {
  // code goes here
};

Wir brauchen zum Auslesen der QueryString-Parameter eine möglichst einfache Funktion. Ich habe sie an dieser Stelle schon öfter verwendet und möchte sie deshalb hier nur der Vollständigkeit halber erwähnen. Ich verwende immer das hier:

getQueryStringParameter: function (paramName) {
 
var params = document.URL.split("?")[1].split("&"),
      i,
      singleParam;
  for (i = 0; i < params.length; i++) {
    singleParam = params[i].split("=");
    if (singleParam[0] == paramName) {
      return singleParam[1];
    }
  }
  return "";
}

Wir definieren uns noch eine Variable chromeContainerId, die die ID eines div-Elementes enthält. In dieses div wird dann später das Chrome Control eingesetzt. Weiter unten wird gezeigt, wie man es in die Seite einbaut. Außerdem definieren wir noch ein Array chromeLinks. Diesem können wir einfach weitere Links hinzufügen, die vom Chrome Control gerendert werden. Auch dazu weiter unten mehr. Die Definition als separat zugängliche Variablen habe ich gemacht, damit später immer noch jede Seite die Möglichkeit hat diese Grundeinstellungen zu verändern.

chromeContainerId: "divSPChrome",
chromeLinks: []

Fullscreen oder iframe?

Als nächstes bauen wir eine setupPage-Funktion ein, die von einer Seite aus aufgerufen werden kann und die die ganze Arbeit erledigt. Innerhalb der Funktion holen wir uns zunächst den DisplayType und die SPHostUrl. Außerdem setzen wir uns eine Variable layoutsRoot auf den Layouts-Ordner des Hostwebs, damit wir einfach Elemente von dort referenzieren können:

// Get DisplayType from url parameter
var displayType = decodeURIComponent(myApp.getQueryStringParameter("DisplayType")),
// Get URI decoded SharePoint host site url from the SPHostUrl parameter
spHostUrl = decodeURIComponent(myApp.getQueryStringParameter("SPHostUrl")),
// Build absolute path to the layouts root with the spHostUrl
layoutsRoot = spHostUrl + "/_layouts/15/";

Falls die Seite als AppPart, also in einem iframe, angezeigt werden soll, binden wir kein Chrome Control ein, sondern laden einfach die korrekte CSS-Datei aus dem Hostweb. Das geht über einen dynamisch erzeugten CSS-Link, den wir dem head der Seite hinzufügen:

// Create a Link element for the defaultcss.ashx resource
linkElement = document.createElement("link");
linkElement.setAttribute("rel", "stylesheet");
linkElement.setAttribute("href", layoutsRoot + "defaultcss.ashx");

// Add the linkElement as a child to the head section of the html
headElement = document.getElementsByTagName("head");
headElement[0].appendChild(linkElement);

Wenn die Seite im Fullscreen-Modus angezeigt werden soll, müssen wir zunächst eine SharePoint-Scriptdatei nachladen. Die Datei enthält u.a. das eigentliche Chrome Control. Wir laden die Datei mit der jQuery-Funktion $.getScript und übergeben dabei eine weitere Funktion renderSPChrome. Diese Funktion wird von jQuery aufgerufen, sobald die gewünschte Scriptdatei vollständig geladen ist.

$.getScript(layoutsRoot + "SP.UI.Controls.js", myApp.renderSPChrome);

Jetzt machen wir also mit der renderSPChrome-Funktion weiter. Analog zu oben holen wir uns die SPHostLogoUrl aus dem Querystring:

var hostlogourl = decodeURIComponent(myApp.getQueryStringParameter("SPHostLogoUrl"));

Chrome Control

Als nächstes bauen wir uns ein options-Objekt zusammen. Mit Hilfe dieses Objekts kann man diverse Einstellungen an das Chrome Control übergeben. Die Möglichkeiten sind:

appIconUrl: ein String mit der URL zu einem Bild, das als Anwendungsicon links oben erscheint. Wir verwenden hier einfach das Icon aus dem Hostweb, aber natürlich kann es auch ein eigenes, app-spezifisches Bild sein.

appTitle: ein String mit dem Namen der Anwendung. Er wird rechts neben dem Anwendungsicon und nochmal darüber in der Titelleiste angezeigt. Wir verwenden hier den Titel der Seite, der über document.title erreichbar ist.

appHelpPageUrl: ein String mit der URL zu einer Hilfeseite. Wenn der Parameter angegeben ist, wird ganz rechts oben ein Hilfesymbol mit einem Link zur angegebenen Url eingeblendet. Wie bei allen Urls, die innerhalb der App zur Navigation verwendet werden, sollte man immer die Querystring-Parameter weitergeben, damit sie auf den Folgeseiten ebenfalls zur Verfügung stehen. Das erreicht man mit diesem Code:

"Help.html?" + document.URL.split("?")[1]

onCssLoaded: ein String, der den Namen einer Funktion enthält. Wenn der Parameter angegeben ist, wird diese Funktion aufgerufen, sobald das Chrome Control fertig und alle referenzierten CSS-Dateien geladen sind. Die Funktion kann dann Abschlussarbeiten erledigen, die erst dann ausgeführt werden können oder sollen. Achtung: man muß wirklich einen String angeben. Eine Referenz auf eine Funktion tut es nicht. Der übergebene Parameter wird offenbar per eval() ausgeführt, weshalb man auch die Klammern hinter dem Funktionsnamen () angeben muß. Bei uns sieht das so aus:

"onCssLoaded": "myApp.chromeLoaded()"

settingsLinks: ein Array mit passenden Link-Objekten. Wenn der Parameter angegeben wird, werden diese Links rechts oben als Menü gerendert. Das geschieht an der Stelle, wo sich sonst in SharePoint das Menü befindet, über das z.B. die Websiteeinstellungen zugänglich sind. Die einzelnen Link-Objekte müssen linkUrl und displayName jeweils als Strings enthalten. Die Namen sollten selbsterklärend sein: linkUrl enthält die aufzurufende Url und displayName den angezeigten Text. Das kann z.B. so aussehen:

[
 
{
    "linkUrl": "Account.html?" + document.URL.split("?")[1],
    "displayName": "Account settings"
  },
  {
    "linkUrl": "Contact.html?" + document.URL.split("?")[1],
    "displayName": "Contact us"
  }
]

Über weitere Möglichkeiten der Optionen habe ich noch nichts herausgefunden. Schade, daß Microsoft auch hier wieder auf eine Dokumentation komplett verzichtet 😦

Update 11.09.2013: beim Durchstöbern von SP.UI.Controls.debug.js bin ich auf folgende Möglichkeiten gestossen:

  • siteTitle
  • siteUrl
  • clientTag
  • appWebUrl
  • onCssLoaded
  • assetId
  • appStartPage
  • rightToLeft
  • appTitle
  • appIconUrl
  • appTitleIconUrl
  • appHelpPageUrl
  • appHelpPageOnClick
  • settingsLinks
  • language
  • bottomHeaderVisible
  • topHeaderVisible

Ich habe aber noch nicht alle getestet. Wenn ich mehr über die einzelnen Optionen herausfinde, werde ich obige Aufzählun ergänzen.

Zuletzt müssen wir jetzt noch das eigentliche Chrome Control erzeugen und sichtbar machen:

// Load the Chrome Control in the Chrome Container element of the page
var chromeNavigation = new SP.UI.Controls.Navigation(myApp.chromeContainerId, options);
chromeNavigation.setVisible(true);

Der Vollständigkeit halber hier noch die chromeLoaded-Funktion, die zum Abschluß ausgeführt wird. Wir machen hier nur den body der Seite sichtbar (er ist initial ausgeblendet):

$("body").show();

HTML erweitern

Es folgen noch die Erweiterungen, die im HTML der Seite(n) gemacht werden müssen. Zur Erinnerung: wir verwenden aspx-Seiten, aber grundsätzlich geht es mit jeder anderen Technologie analog. Wir binden zunächst im head jQuery und unsere eigene Scriptdatei ein. Danach rufen wir nur noch eine Funktion auf, sobald die Seite vollständig geladen ist:

<script type="text/javascript" src="../Scripts/jquery-1.8.2.min.js"></script>
<script type="text/javascript" src="../Scripts/appScripts.js"></script>
<script type="text/javascript">
  $(document).ready(
    myApp.setupPage
  );
</script>

Außerdem brauchen wir ein leeres div mit der oben angesprochenen ID. In dieses div wird das Chrome Control später gerendert und deshalb sollte es als allererstes ganz oben im body stehen:

<body style="display: none;">
 
<form id="form1" runat="server">
    <div id="divSPChrome"></div>

Hier sieht man auch gleich, daß der gesamte body anfangs unsichtbar gesetzt wurde. Zu Testzwecken wurde noch etwas Inhalt eingefügt, bei dem SharePoint-eigene CSS-Klassen verwendet wurden. Die Referenz zu den verfügbaren Klassen findet sich hier: Verwenden des CSS der Hostwebsite in Apps für SharePoint. Man kann daran testen, ob angewendete Designs aus dem Hostweb korrekt übernommen werden:

<h1 class="ms-accentText">H1 Header</h1>
<h2 class="ms-accentText">H2 Header</h2>
<div id="MainContent">
  This is the page’s main content.<br/>
  You can use the links in the header to go back to the host web,
  to help, account or contact pages.
</div>

Zum Schluß noch ein Bild, wie das Ganze aussehen kann:

Und hier nochmal der Downloadlink für die JavaScript-Datei.

SharePoint 2013 Workflows Teil 2: Listenelemente iterieren

Im ersten Teil habe ich gezeigt, wie man in einem SharePoint 2013 Workflow mit Hilfe der Aktion Call HTTP Web Service auf Listenelemente zugreifen kann. Dort ging es erstmal nur darum, die Anzahl verbundener Elemente zu ermitteln. In diesem Teil soll jetzt gezeigt werden, wie man mit den in 2013 neuen Mitteln eine Schleife zum Durchlaufen der gefundenen Elemente realisieren kann.

Testumgebung

Als Testumgebung werden die beiden Listen aus dem ersten Teil verwendet. Die Child List wurde um ein Zahlenfeld Value erweitert. Die Parent List bekam ein zusätzliches Feld Item Sum. Per Workflow sollen jetzt die Werte aus Value aller verbundenen Child-Elemente aufsummiert und in Item Sum des Parent-Elementes geschrieben werden.

Es geht hier nur um das Prinzip und natürlich sind auch viele andere Aktionen denkbar. Man könnte innerhalb der Schleife z.B. auch alle verbundenen Child-Elemente aktualisieren.

Schleifenaufbau

Wir erweitern also unseren bestehenden Workflow um eine neue Stufe. Damit wir die Schleife realisieren können, benötigen wir eine Schleifenvariable. Wir legen also eine Integer-Variable LoopCounter an und initialisieren sie mit 0 (Null). Außerdem benötigen wir eine Variable, in der wir die Werte der einzelnen Child-Elemente aufsummieren können. Wir legen also noch eine Number-Variable ItemSum an und initialisieren sie ebenfalls mit 0. Jetzt fügen wir eine Schleife vom Typ Schleife n-mal ein und konfigurieren sie so, daß sie so oft ausgeführt wird, wie unsere Variable ItemCount aus dem ersten Teil vorgibt. Die Variable enthält die Anzahl der gefundenen Elemente. Bis jetzt siehst es also so aus:

Zugriff auf ein Element

Als nächstes holen wir uns die ID eines Listenelementes aus den Ergebnissen der REST-Abfrage. Dazu verwenden wir wieder die Aktion Get an Item from a Dictionary. Für den Parameter item by name or path verwenden wir d/results(<Counter>)/ID, wobei der Platzhalter <Counter> für die nullbasierte Laufvariable steht, in unserem Fall also die Variable LoopCounter. Im Ganzen sieht das so aus:

Wie haben jetzt eine Variable ChildItemID, die pro Schleifendurchlauf immer die Element-ID eines Child-Elementes enthält und wir können damit auf das jeweilige Element zugreifen. Wir verwenden im Beispiel die Aktion Do Calculation, um den Wert des Feldes Value zu unserer Variablen ItemSum zu addieren. Der Zugriff auf das das Feld des Child-Elementes erfolgt dabei so:

Schleifenabschluß

Damit die Schleife auch korrekt funktioniert, müssen wir jetzt noch unsere Laufvariable LoopCounter hochzählen. Dazu verwenden wir wieder die Aktion Do Calculation und addieren einfach 1 (eins). Dabei ist zu beachten, daß Do Calculation immer einen Gleitkommawert zurückliefert, den wir nicht wieder direkt unserer Integer-Variablen zuweisen können. Wir verwenden hier also eine Zwischenvariable vom Typ Number und eine weitere Aktion Set Workflow Variable um diese unserer Laufvariablen zuzuweisen. Hier funktioniert die Konvertierung dann.

Ich habe versucht, als Laufvariable selbst eine Number-Variable zu verwenden, aber das hat bei meinen Versuchen nicht zuverlässig funktioniert. Deshalb der kleine Workaround.

Zum Abschluß benutzen wir noch eine Aktion Set Field in Current Item (nach der Schleife!) und schreiben damit die ermittelte Summe in das Feld Item Sum des aktuellen Elements. Zur Erinnerung: der Workflow läuft auf der Parent List und das aktuelle Element im Workflow ist daher ein Element dieser Liste.

Hier nochmal die gesamte Schleife in der Übersicht:

SharePoint 2013 Workflows Teil 1: Call HTTP Web Service

Nachdem ich mich jetzt endlich etwas mit den neuen SharePoint 2013 Workflows beschäftigen konnte, möchte ich hier meine Erfahrungen festhalten. Auch damit ich selbst etwas zum Nachschlagen habe 😉

Im ersten Teil soll es darum gehen, wie man mit der neuen Aktion Call HTTP Web Service Daten aus SharePoint abfragen kann. Selbstverständlich kann man damit jeden beliebigen Webservice abfragen, aber mir ging es ganz speziell um Daten aus SharePoint. Prinzipiell kommt man damit an die Daten aus jeder beliebigen SharePoint-Website, auch wenn sie sich in einer anderen Farm befinden. Voraussetzung dafür ist, daß der Benutzer dazu berechtigt ist (und natürlich daß der Webservice erreichbar ist).

Testumgebung

Für den Test habe ich ein einfaches Szenario aufgebaut: innerhalb einer Website gibt es eine Parent List und eine Child List, die über ein Nachschlagefeld mit der Parent List verbunden ist. Die Parent List hat ein zusätzliches Zahlenfeld Child Count. Ein Workflow auf der Parent List soll jetzt alle zugehörigen Elemente der Child List finden und deren Anzahl beim Parent in Child Count eintragen. Zur Verdeutlichung hier die Parent List:

Und die Child List:

Workflow erstellen

Wir öffnen jetzt die Website in SharePoint Designer 2013 und erstellen einen neuen Listenworkflow für die Parent List. Die Vorgehensweise dazu ist dieselbe wie in 2010, aber beim Anlegen muß darauf geachtet werden, daß SharePoint 2013 Workflow ausgewählt ist. Nur dann stehen die neuen Funktionen auch zur Verfügung:

Service-URL

Im Workflow brauchen wir zunächst die Adresse des Services, den wir für die Abfrage der verknüpften Elemente verwenden wollen. Wir nehmen dafür den in SharePoint 2013 neuen REST-Service, der unter der Adresse <Website>/_api zur Verfügung steht. Der Platzhalter <Website> steht hierbei für die absolute URL der Website, also ungefähr http://sharepoint.firma.tld/Sites/Site1. Wenn es sich wie in unserem Beispiel um die aktuelle Website handelt, kann man dafür die vordefinierte Variable Workflowkontext: Aktuelle Website-URL verwenden.

Da wir hier die Daten einer ganz bestimmten Liste möchten, hängen wir an die URL noch folgendes an: /_api/lists/GetByTitle(‚Child List‘)/Items. Da wir uns im ersten Schritt nur für die ID und den Titel der verknüpften Elemente interessieren, teilen wir der API das mit: ?$select=Id,Title.

Das wichtigste ist aber der Filter, weil wir nur die Elemente haben möchten, die zu unserem aktuellen Element der Parent List gehören. Prinzipiell geht das so: $filter=<LookupField> eq <ID>. <LookupField> ist der interne Name des Nachschlagefelds, <ID> ist die ID des aktuellen Parent-Elements und eq steht für equals also zu deutsch ist gleich. Achtung: damit der Filter funktioniert, muß man das Feld auch beim $select-Parameter angeben und dort mit dem Suffix Id (also ParentId). Zum Ausprobieren wie die Abfrage genau aussehen muß und wie die Felder heißen, empfehle ich einen Test im Browser. Wie das geht, wird weiter unten unter Exkurs erklärt.

Hier nochmal zusammengefaßt die gesamte aufzurufende URL, die wir einer Variablen REST-URL zuweisen. Da man den Text wegen der Nachschlageinformationen ohnehin nicht kopieren kann hier nur als Screenshot:

Request Header

Damit der REST-Service die Daten in einem Format liefert, das vom Workflow verarbeitet werden kann, müssen für die Abfrage sogenannte Request Header definiert werden. Die Header Accept und Content-Type müssen beide auf den Wert application/json;odata=verbose gesetzt werden. Im Workflow geschieht das durch den ebenfalls neuen Variablentyp Dictionary.

Wir legen also eine neue Dictionary-Variable RequestHeaders an und verwenden die Aktion Build Dictionary um die beiden Header einzutragen:

Exkurs: SharePoint 2013 REST API

Man kann den REST-Service zum Ausprobieren auch jederzeit einfach im Browser aufrufen. Der Service liefert XML zurück, das der Internet Explorer als Feed interpretiert. Wie man ihn dazu bringen kann die rohen XML-Daten anzuzeigen, habe ich hier beschrieben.

Wenn man sich die vom Service gelieferten Json-Daten im Browser anschauen möchte, wird es schon schwieriger, weil man wie oben für den Workflow beschrieben, die an den Server gesendeten Header modifizieren muß. Beim Internet Explorer habe ich keinen Weg gefunden das zu erreichen. Ich habe dann einige Plugins für Firefox getestet, aber leider auch ohne Erfolg. Fündig geworden bin ich dann mit Google Chrome und dem Plugin Change HTTP Request Header. Wenn man damit die Header wie oben gezeigt modifiziert, kann man sich das Ergebnis einer REST-Abfrage auch im Json-Format anzeigen lassen. Wer es etwas schöner formatiert haben möchte, kopiert es in den Json Parser Online.

Ein guter Einstiegspunkt in die offizielle Dokumentation von Microsoft findet sich hier: Get startet with the SharePoint 2013 REST service.

Call http Web Service

Jetzt aber weiter im Workflow. Nachdem wir jetzt die URL des Services und die notwendigen Header haben, fügen wir eine Call HTTP Web Service Aktion ein. Für den ersten Parameter verwenden wir einfach unsere vorher festgelegte Variable und belassen die HTTP-Methode bei GET:

Von den restlichen Parametern der Aktion setzen wir nur den ResponseContent (das Ergebnis der Abfrage) auf eine gleichnamige Dictionary-Variable und den ResponseCode auf eine gleichnamige String-Variable. Der Response Code gibt Auskunft über den Erfolg der Abfrage und kann zur weiteren Steuerung des Workflows verwendet werden. Er lautet OK, wenn die Abfrage syntaktisch in Ordnung war. Natürlich kann die Abfrage trotzdem logische Fehler enthalten, so daß z.B. der Filter nicht wie erwartet funktioniert, aber das muß separat festgestellt werden. Alle anderen Parameter interessieren uns hier nicht.

Achtung: der aufmerksame Leser fragt sich jetzt wahrscheinlich was mit unserer Variablen für die Request Header passiert. Diese muß der Workflowaktion auf anderem Wege zugewiesen werden und zwar über das Eigenschaften-Fenster, das man bei jeder Workflowaktion über ihr Menü erreichen kann. Das Menü findet man, wenn man mit der Maus über die Aktion fährt und dann auf den DropDown-Pfeil ganz rechts klickt. Innerhalb des Fensters kann man jetzt auch die Variable für die Request Header setzen (3. Punkt von oben):

Warum das an dieser Stelle so umständlich zu handhaben ist, kann ich nicht sagen, halte es aber für einen Bug. Oder zumindest für ein Versäumnis.

Ergebnisverarbeitung

Das Ergebnis der Abfrage haben wir jetzt in einer Dictionary-Variablen ResponseContent. Hier soll nur gezeigt werden, wie man die Anzahl der gefundenen Elemente ermitteln und in ein Feld des Parentelements schreiben kann. Zunächst holen wir uns ein Subset des Ergebnisses in eine weitere Dictionary-Variable ResultItems. Dazu verwenden wir die Aktion Get an Item from a Dictionary und geben für den Parameter item by name or path den Wert d/results an. Mit der Aktion Count Items in a Dictionary bekommen wir jetzt die Anzahl der gefundenen Elemente in eine Integer-Variable ItemCount. Mit der Aktion Set Field in Current Item lassen wir uns diesen Wert jetzt noch ins aktuelle Parentelement ausgeben.

Hier nochmal der gesamte Workflow zusammengefaßt:

Im zweiten Teil wird gezeigt, was man mit dem Ergebnis der Abfrage noch anstellen kann. Dort geht es dann um eine Schleife, die alle gefundenen Elemente durchläuft.

SharePoint Designer 2013 stürzt beim Website öffnen ab

Das Problem: SharePoint Designer 2013 stürzt beim Klick auf Website öffnen ab. Dieses Problem hatte ich bereits vor längerer Zeit und konnte es nach einer relativ kurzen Suche im Web auch beseitigen. Jetzt trat es hier erneut auf und ich mußte wieder nach der Lösung suchen. Deshalb dieser Beitrag in der Hoffnung beim nächsten Mal schneller zur Lösung zu gelangen.

Der Fehler tritt nur auf, wenn auf einer Maschine mehrere Versionen von SharePoint Designer installiert sind. Da ich leider immer noch alle möglichen Versionen supporten muß, habe ich hier also SharePoint Designer 2007, 2010 und 2013, was normalerweise auch problemlos funktioniert. Die kürzliche Installation von SP2 für SharePoint Designer 2010 hat dann diesen Fehler wieder zutage gefördert. Die Lösung hatte ich hier bei Marc D Anderson gefunden, aber es gibt inzwischen viele Beiträge dazu.

Die Lösung besteht darin, zwei Werte aus der Windows Registry zu löschen. Es handelt sich dabei um

HKEY_CURRENT_USER\Software\Microsoft\Office\14.0\Common\Open Find\Microsoft SharePoint Designer\Settings\Website öffnen\ClientGUID

und

HKEY_CURRENT_USER\Software\Microsoft\Office\15.0\Common\Open Find\Microsoft SharePoint Designer\Settings\Website öffnen\ClientGUID

Einfach die beiden Werte löschen (Rechtsklick – Löschen), SharePoint Designer neu starten und alles funktioniert wieder 🙂

DateTimeControl und Zugriff verweigert

Wenn man das SharePoint-DateTimeControl in einem eigenen Webpart oder in einer eigenen Anwendungsseite verwendet, kann es passieren, daß man beim Aufklappen des Kalenders eine Zugriff verweigert / Access Denied Meldung bekommt. Das passiert, wenn man sich in einer Unterwebsite befindet und wenn der Benutzer auf die Root-Websitesammlung keine Zugriffsrechte hat.

Man sollte dieses Szenario ohnehin vermeiden und immer dafür sorgen, daß alle Benutzer auf die Root-Websitesammlung zumindest minimale Berechtigungen besitzen. Es kann sonst zu weiteren unerwarteten Fehlern kommen, weil innerhalb von SharePoint immer wieder Dateien Server-relativ von dort referenziert werden.

Beim DateTimeControl wird der aufklappende Kalender ebenfalls Server-relativ als <iframe> gerendert und deshalb bekommen Benutzer ohne Berechtigungen auf die Root-Websitesammlung eben die Zugriff verweigert Meldung. Als Entwickler kann man das umgehen, indem man die Eigenschaft DatePickerFrameUrl mit dem richtigen Pfad innerhalb der aktuellen Website belegt.

Wenn das Control z.B. innerhalb eines Webparts per Code generiert wird, fügt man einfach folgende zusätzliche Codezeile ein:

myDateTimeControl.DatePickerFrameUrl = SPUrlUtility.CombineUrl(SPContext.Current.Web.ServerRelativeUrl, "_layouts/iframe.aspx");

Wenn das Control z.B. innerhalb einer Anwendungseite deklarativ generiert wird, kann man es ebenfalls wie oben im Code-behind der Seite erledigen oder auch gleich deklarativ beim Erzeugen des Controls:

<SharePoint:DateTimeControl DatePickerFrameUrl="<% $SPUrl:~Site/_layouts/iframe.aspx %>" ID="myDateTimeControl" runat="server" />