Eigene Ribbon-Controls, EnabledScript und asynchrones JavaScript

In diesem Beitrag soll es nicht darum gehen, wie man dem SharePoint Ribbon (Menüband oder Multifunktionsleiste) eigene Controls mit eigenen Funktionen hinzufügt. Es gibt bereits viele Beiträge dazu, wie z.B. Fabian Moritz: SharePoint 2010 Ribbon anpassen und erweitern oder Chris O’Brien: Customizing the ribbon. Außerdem gibt es hier eine Serie von Wictor Wilén, in der er ausführlich die einzelnen Controls beschreibt (derzeit noch nicht ganz vollständig).

In den einzelnen Artikeln wird zum Teil auch gezeigt, wie man mit Hilfe des EnabledScript-Attributs beim CommandUIHandler dafür sorgen kann, daß ein Control nur unter bestimmten Umständen aktiv ist. Im EnabledScript kann dazu JavaScript hinterlegt werden, das die Randbedingungen prüft und entsprechend true oder false zurückliefert. Die SharePoint-Infrastruktur sorgt dann dafür, daß ein Control aktiv oder nicht (ausgegraut) ist.

Das Problem daran ist, daß dieses Script immer synchron ausgeführt werden muß, d.h. es muß direkt true oder false zurückgeben. In vielen Fällen reicht das aus, z.B. wenn ein Control nur dann aktiv sein soll, wenn in einer Listenansicht genau ein Element ausgewählt ist. Sobald man aber mehr Informationen über dieses ausgewählte Element braucht, muß man diese über die SharePoint Client-API für JavaScript ermitteln und derartige Aufrufe erfolgen immer asynchron.

In diesem Beitrag soll deshalb gezeigt werden, wie man Ribbon-Controls auch über asynchrone JavaScript-Funktionen aktivieren bzw. deaktivieren kann. Ich verwende dabei aus Gewohnheit den "alten" Begriff JavaScript. Der Kern dieser Technologie ist schon länger unter dem Namen ECMAScript standardisiert, weshalb die Begriffe JavaScript und ECMAScript oft synonym behandelt werden.

Im Beispiel soll von einer Aufgabenliste ausgegangen werden, bei der ein Control im Ribbon nur dann aktiv sein soll, wenn genau eine Aufgabe ausgewählt ist, deren Status nicht "Abgeschlossen" ist.

Damit das Ganze etwas übersichtlicher wird, lagert man das Script in eine eigene *.js Datei aus, die per ScriptLink eingebunden wird. Im EnabledScript ruft man dann nur eine Funktion in dieser Datei:

EnabledScript="javascript:myEnabledFunction();"

Innerhalb der Funktion wird zunächst die Anzahl der ausgewählten Elemente geprüft. Ist diese ungleich eins, dann wird sofort false zurückgegeben. Weitere Tests sind in diesem Fall nicht notwendig:

var selectedItems = SP.ListOperation.Selection.getSelectedItems();

if (CountDictionary(selectedItems) != 1) {

  return false;

}

Jetzt haben wir also den Fall, bei dem genau ein Element ausgewählt ist. Für dieses Element wollen wir jetzt über einen asynchronen Aufruf den aktuellen Wert des Statusfeldes holen und erst dann entscheiden, ob das Control aktiviert oder deaktiviert werden soll. Wir brauchen dazu zwei globale Variablen: eine für das eigentliche Listenelement und eine zum Zwischenspeichern des Ergebnisses:

var myListItem = null;

var myButtonEnabled = false;

Jetzt wird geprüft, ob das Listenelement bereits initialisiert wurde und wenn ja, ob dieses Element dieselbe ID besitzt, wie das aktuell ausgewählte Element (durch die Asynchronität könnte sich das in der Zwischenzeit geändert haben). Ist das alles der Fall, dann haben wir bereits ein Ergebnis (wie das ermittelt wurde s.u.) und können dieses direkt zurückgeben:

var itemId = selectedItems[0]["id"];

if (this.myListItem != null && this.myListItem.get_item("ID") == itemId) {

  return this.myButtonEnabled;

}

Andernfalls holen wir uns das Listenelement über einen asynchronen Funktionsaufruf:

else {

  var ctx = new SP.ClientContext();

  var web = ctx.get_web();

  var list = web.get_lists().getById(SP.ListOperation.Selection.getSelectedList());

  this.myListItem = list.getItemById(itemId);

  ctx.load(this.myListItem, "ID", "Status");

  ctx.executeQueryAsync(enabledQuerySuccess, enabledQueryFailed);

}

Beim Aufruf von executeQueryAsync werden zwei Funktionen übergeben. EnabledQueryFailed wird aufgerufen, wenn irgendetwas schiefläuft:

function enabledQueryFailed(s, args) {

  myButtonEnabled = false;

  myListItem = null;

}

Dabei werden einfach die Variablen zurückgesetzt, das Control bleibt deaktiviert und es passiert nichts weiter. Zu Debuggingzwecken kann man dort auch eine Fehlermeldung, z.B. über die alert-Funktion, ausgeben:

alert(‚Fehler: ‚ + args.get_message());

In der Funktion enabledQuerySuccess wird der aktuelle Wert des Statusfeldes geprüft und in Abhängigkeit davon die globale Variable zum Aktivieren bzw. Deaktivieren des Controls gesetzt:

function enabledQuerySuccess() {

  var status = myListItem.get_item("Status");

  if (status == "Abgeschlossen") {

    myButtonEnabled = false;

  } else {

    myButtonEnabled = true;

  }

  RefreshCommandUI();

}

Über einen Aufruf der SharePoint-Funktion RefreshCommandUI wird veranlaßt, daß für das Ribbon erneut geprüft wird, welches Control aktiv sein soll und welches nicht. Dadurch wird unsere Funktion myEnabledFunction erneut aufgerufen. Allerdings wird dort jetzt festgestellt, daß wir alle notwendigen Informationen bereits besitzen und nur noch das Ergebnis zurückgegeben.

Zusammenfassend hier nochmal der gesamte Scriptcode:

var myListItem = null;

var myButtonEnabled = false;

 

function myEnabledFunction() {

  var selectedItems = SP.ListOperation.Selection.getSelectedItems();

  if (CountDictionary(selectedItems) != 1) {

    return false;

  }

  var itemId = selectedItems[0]["id"];

  if (this.myListItem != null && this.myListItem.get_item("ID") == itemId) {

    return this.myButtonEnabled;

  } else {

    var ctx = new SP.ClientContext();

    var web = ctx.get_web();

    var list = web.get_lists().getById(SP.ListOperation.Selection.getSelectedList());

    this.myListItem = list.getItemById(itemId);

    ctx.load(this.myListItem, "ID", "Status");

    ctx.executeQueryAsync(enabledQuerySuccess, enabledQueryFailed);

  }

}

 

function enabledQueryFailed(s, args) {

  myButtonEnabled = false;

  myListItem = null;

}

 

function enabledQuerySuccess() {

  var status = myListItem.get_item("Status");

  if (status == "Abgeschlossen") {

    myButtonEnabled = false;

  } else {

    myButtonEnabled = true;

  }

  RefreshCommandUI();

}

 

WCF in SharePoint – Nachrichtengröße und Stringlängen

Vor einiger Zeit hatte ich in einem Beitrag gezeigt, wie man einen eigenen WCF-Dienst entwickelt und in SharePoint bereitstellt. Das ist alles relativ einfach und durch Nutzung der von SharePoint bereitgestellten Factory-Klasse MultipleBaseAddressBasicHttpBindingServiceHostFactory fällt auch die Konfiguration nicht schwer.

Aber: im produktiven Einsatz wird man wahrscheinlich sehr schnell über eine der folgenden Fehlermeldungen stolpern (ich habe hier nur die englischen Meldungen):

The maximum string content length quota (8192) has been exceeded while reading XML data. This quota may be increased by changing the MaxStringContentLength property on the XmlDictionaryReaderQuotas object used when creating the XML reader.

– oder –

The remote server returned an unexpected response: (400) Bad Request.

– oder –

The maximum message size quota for incoming messages (65536) has been exceeded. To increase the quota, use the MaxReceivedMessageSize property on the appropriate binding element.

Diese Fehlermeldungen bekommt man, wenn entweder ein String-Parameter größer als 8k ist oder die gesamte Nachrichtengröße 64k überschreitet. Dabei handelt es sich um die voreingestellten Standardwerte.

Kein Problem, dachte ich zunächst. Wie man das von WCF kennt, legt man einfach die gewünschten Werte in einer eigenen web.config fest und stellt diese im selben Ordner wie die svc-Datei bereit. Wie im genannten Beitrag gezeigt, gab es in diesem Fall sogar bereits eine web.config für eigene Einstellungen. Nach langem Herumprobieren, Testen und Feststellen, daß es nicht geht, Prüfen der Einstellungen, Testen und Feststellen, daß es immer noch nicht geht und nochmaligem Prüfen, mußte ich dann einsehen, daß die ServiceHostFactory-Klasse von SharePoint keine Einstellungen aus der web.config liest.

Es gab jetzt zwei Möglichkeiten: eine eigene Factory-Klasse oder eine andere Möglichkeit suchen. Nachdem ich mehr Zeit mit der Suche nach einer anderen Lösung verbrachte, als ich zum Entwickeln einer eigenen Factory-Klasse gebraucht hätte, habe ich dann doch noch die Lösung gefunden: einen Blogbeitrag von Doug Ware von Elumenotion Specifying Reader Quotas for WCF Services in SharePoint 2010. Danke dafür!

Doug zeigt in diesem Beitrag, wie man Einstellungen für in SharePoint gehostete WCF-Dienste mit Programmcode setzen kann. Dieser Code läßt sich z.B. im FeatureInstalled- oder FeatureActivated-Ereignis unterbringen.

Man benötigt folgende using-Anweisung:

using Microsoft.SharePoint.Administration;

Der Code sieht so aus:

SPWebService svc = SPWebService.ContentService;

SPWcfServiceSettings settings = new SPWcfServiceSettings();

settings.ReaderQuotasMaxStringContentLength = 10485760;

settings.MaxReceivedMessageSize = 10485760;

settings.MaxBufferSize = 10485760;

// hier kann man weitere Einstellungen vornehmen

svc.WcfServiceSettings["myservice.svc"] = settings;

svc.Update();

Oder mit PowerShell:

$svc = [Microsoft.SharePoint.Administration.SPWebService]::ContentService
$setting = new-object Microsoft.SharePoint.Administration.SPWcfServiceSettings
$setting.ReaderQuotasMaxStringContentLength = 10485760
$setting.MaxReceivedMessageSize = 10485760
$setting.MaxBufferSize = 10485760
$svc.WcfServiceSettings.item("myservice.svc") = $setting
$svc.update()

Als Schlüssel zum Speichern der Einstellungen muß dabei der exakte Name der svc-Datei verwendet werden. Die Werte wurden im Beispiel willkürlich auf 10MB gesetzt. Den "richtigen" Wert muß jeder für seinen Anwendungsfall selbst herausfinden.

Noch ein sehr wichtiger Hinweis zum Schluß: man muß sowohl den Namen der svc-Datei als auch den Schlüssel zum Speichern der Einstellungen in Kleinbuchstaben schreiben, sonst funktioniert es nicht. Den Hinweis darauf habe ich erst nach langem Suchen hier bei MSDN gefunden.

Schnellstartleiste in Webpartseiten

Wenn man in SharePoint eine neue Webpartseite anlegt, dann fehlt immer die Schnellstartleiste (Quicklaunch). Das gilt für alle acht standardmäßig vorhandenen Vorlagen. In vielen Fällen mag das in Ordnung sein, aber manchmal möchte man eben auch auf einer solchen Seite die Schnellstartleiste haben.

Um die Schnellstartleiste einzublenden, öffnet man die Seite im SharePoint Designer und wechselt in die Code-Ansicht. Damit man die Änderungen durchführen kann, muß der erweiterte Modus über das Ribbon aktiviert werden (das kann nur ein Websitesammlungs-Administrator). Jetzt sucht man die folgenden Zeilen und löscht sie:

<SharePoint:UIVersionedContent ID=“WebPartPageHideQLStylesUIVersion=“4runat=“server„>
<ContentTemplate>
<style type=“text/css„>
body #s4-leftpanel {
display:none;
}
.s4-ca {
margin-left:0px;
}
</style>
</ContentTemplate>
</SharePoint:UIVersionedContent>

Diese Zeilen sorgen dafür, daß es links keinen Rand gibt. Den Rand brauchen wir aber, damit die Schnellstartleiste überhaupt Platz hat.

In den Webpartseiten ist der Platzhalter für die Schnellstartleiste immer ohne Inhalt neu definiert, womit der eigentliche Inhalt aus der Gestaltungsvorlage (Masterpage) nicht übernommen wird. Das machen wir rückgängig, indem wir die leere Überschreibung einfach löschen:

<asp:Content ContentPlaceHolderId=“PlaceHolderLeftNavBarrunat=“server„></asp:Content>

Damit die Schnellstartleiste immer dieselbe (Mindest-)Breite hat, löschen wir auch gleich die leere Überschreibung für einen weiteren Platzhalter:

<asp:Content ContentPlaceHolderId=“PlaceHolderNavSpacerrunat=“server„></asp:Content>

Das war es auch schon. Seite speichern und ab sofort hat sie die gewohnte Schnellstartleiste.

Änderungen am XSLT einer Datenansicht werden nicht übernommen

Ich habe mich gerade ziemlich lange mit einem sehr seltsamen Problem in SharePoint Designer 2010 herumgeschlagen: wenn man bei einem XsltListViewWebPart Änderungen am XSLT vornimmt, werden diese zwar in der Entwurfsansicht in SharePoint Designer korrekt angezeigt, aber nach dem Speichern sind die Änderungen nur sporadisch im Browser sichtbar. Seltsamerweise werden diese Änderungen zwar meistens, aber eben nicht immer unterschlagen. Lösung: man entfernt alle ddwrt:ghost-Attribute.

Vorgehensweise:

Damit man überhaupt etwas an der Standardansicht eines XsltListViewWebPart ändern kann, muß man zunächst das XSLT zugänglich machen. Wenn man die Seite im SharePoint Designer geöffnet und das Webpart markiert hat, geht das durch Klick auf XSLT anpassen im Reiter Entwurf:

Man kann dort auswählen, ob man nur das Template für das ausgewählte Element, also z.B. eine Tabellenzelle, oder das gesamte XSLT verändern möchte.

Jetzt kann man die Ansicht nach Belieben verändern. Man kann vieles über die vom Designer gebotenen Möglichkeiten machen, wie z.B. einzelnen Spalten eine andere Hintergrundfarbe geben, man kann bedingte Formatierungen anwenden und Schriftarten und -größen ändern. Wer möchte kann auch durch direktes Editieren im XSLT-Code die gesamte Ansicht umgestalten.

Allen diesen Änderungen ist aber gemein, daß sie zwar im Entwurfsfenster von SharePoint Designer korrekt angezeigt werden, aber wenn man sie nach dem Speichern im Browser betrachten will, wird man meist enttäuscht. Die Änderungen werden fast immer beim Speichern unterschlagen. Nach langem Suchen und verzweifeltem Haareraufen habe ich die Lösung dieses Problems gefunden: im Code findet sich an vielen Stellen ein ddwrt:ghost="hide"-Attribut. Wenn man es leer macht (ddwrt:ghost="") oder gleich ganz entfernt, funktioniert alles wie gewünscht. Weil dieses Attribut mehr als 60 Mal im Code vorkommt, bietet sich dafür die Suchen&Ersetzen-Funktion an (Strg+H).

Falls jemand weiß, wozu dieses Attribut da ist oder ob es überhaupt einen Sinn hat, möge er mir das bitte mitteilen. Ich konnte bisher jedenfalls keinen finden.