SharePoint 2013: „Mehr anzeigen“ Link auf Formularen entfernen

Und hier gleich noch ein Beitrag zum Thema Anpassung von Aufgabenformularen in SharePoint 2013. Hier ging es darum, die Verwandten Elemente auch auf dem EditForm anzuzeigen. Jetzt soll noch gezeigt werden, wie man den meiner Meinung nach völlig überflüssigen „Mehr anzeigen“ Link von den Aufgabenformularen entfernen und gleich alle Felder darstellen kann.

Der Link selbst ruft nur eine JavaScript-Funktion rlfiShowMore auf. Das machen wir uns zunutze, indem wir diese Funktion einfach selbst aufrufen, sobald die Seite geladen ist. Auf dem Formular muß dazu nur diese Zeile Script eingefügt werden:

_spBodyOnLoadFunctionNames.push(„rlfiShowMore“);

SharePoint 2013: Verwandte Elemente auf dem EditForm anzeigen

Bei SharePoint 2013 haben Workflowaufgaben standardmäßig eine Spalte Verwandte Elemente (Related Items). Leider ist diese Spalte ohne weiteres Zutun nur auf dem Formular zur Anzeige (DispForm) vorhanden und auch nur dort pflegbar.

SharePoint 2013 Workflows fügen beim Erstellen einer Aufgabe dort einen Link zum Element ein, auf dem Workflow läuft. Wenn man also einen Workflow hat, der z.B. Aufgaben erstellt, mit deren Hilfe etwas genehmigt werden soll, hat es durchaus Sinn diesen Link auch auf dem Änderungsformular (EditForm) darzustellen. Die genehmigende Person hat dann direkt auf dem Formular, mit dem sie arbeitet, auch einen Link zum Element, das genehmigt werden soll.

Das ist mal wieder ein typischer Fall für JavaScript. Das notwendige Script dazu kann man, wie fast immer, entweder in einem Script Editor Webpart oder per SharePoint Designer direkt im Code der Seite unterbringen.

SharePoint stellt zum Zugriff auf die Verwandten Elemente das Objekt SP.RelatedItemManager zur Verfügung. Es befindet sich in der Datei sp.js und kann erst verwendet werden, wenn diese geladen ist. Wir beginnen das Script also so:

ExecuteOrDelayUntilScriptLoaded(function() {
    // hier der Code
}, "sp.js");

Wir benötigen den ClientContext, den man auf die bekannte Art bekommt. Außerdem die ID der Aufgabenliste, die man aus dem _spPageContextInfo-Objekt bekommt. Und schließlich die ID der Aufgabe, die man aus der URL holen kann. Wir bedienen uns hierzu der altbekannten Methode getQueryStringParameter:

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

Mit diesen Werten liefert uns SP.RelatedItemManager direkt die Verwandten Elemente zur aktuellen Aufgabe:

var ctx = SP.ClientContext.get_current(),
    itemId = getQueryStringParameter("ID"),
    relItems = SP.RelatedItemManager.getRelatedItems(ctx, _spPageContextInfo.pageListId, itemId);

Jetzt braucht es nur ein executeQueryAsync und die Verwandten Elemente können benutzt werden. Es handelt sich dabei um ein Array, das Objekte enthält. Die Objekte haben eine get_url() Methode, die die URL des Elements liefert. Sie kann direkt z.B. in einen Link eingebaut werden. Und es gibt eine get_title() Methode, die den Anzeigenamen liefert, der z.B. als Text für den Link verwendet werden kann.

Hier nur ein kleines Beispiel, wie damit per jQuery ein Link in einem vorher bereitgestellten Container mit der ID myContainer erzeugt werden kann. Dazu nochmal das gesamte Script zum Kopieren und Einfügen:

ExecuteOrDelayUntilScriptLoaded(function() {
    var ctx = SP.ClientContext.get_current(),
        itemId = getQueryStringParameter("ID"),
        relItems = SP.RelatedItemManager.getRelatedItems(ctx, _spPageContextInfo.pageListId, itemId);
    ctx.executeQueryAsync(function() {
        var html;
        if (relItems.length > 0) {
            html = "<a href=’" + relItems[0].get_url() + "‘ target=’_top‘>" + relItems[0].get_title() + "</a>";
            $("#myContainer").append(html);
        }
        }, function() {}
    );

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

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.

Link zum Erstellen eines Detaildatensatzes

Wenn man zwei 1:n-verbundene Listen hat, wird oft ein Link gewünscht, über den man einen Detaildatensatz erstellen kann und bei dem das Nachschlagefeld zum zugehörigen Elterndatensatz bereits vorbelegt ist. Hier wird gezeigt wie man das mit Hilfe von etwas JavaScript umsetzen kann.

Das grundsätzliche Vorgehen dabei ist folgendes: man erstellt einen eigenen Link, mit dem ausgehenden von einem Elternelement ein neues Detailelement erstellt werden kann. Der Link verweist dabei auf das Formular zur Neuanlage der Detailliste und enthält als zusätzlichen Parameter die ID des Elternelements. In das Formular zur Neuanlage wird JavaScript eingebaut, das diesen Parameter wieder aus der URL abholt und das Nachschlagefeld damit vorbelegt. Falls gewünscht kann man das Nachschlagefeld dann auch ausblenden.

Ausgangslage

Es gibt eine Parentliste, die hier zu Demozwecken ganz einfach aufgebaut ist und nur die Titelspalte enthält:

Dazu gibt es eine Detailliste, die ebenso einfach aufgebaut ist und außer dem Titel nur ein Nachschlagefeld auf die Parentliste enthält:

Man hat dadurch, ähnlich wie bei einem klassischen relationalen Datenmodell, eine 1:n-Verbindung zwischen den Listen. Jedem Elterndatensatz können mehrere Kinddatensätze zugeordnet sein. Jeder Kinddatensatz ist genau einem Elterndatensatz zugeordnet. In der Praxis wird so etwas z.B. bei Rechnungen (ein Datensatz Rechnungskopf mit Rechnungsnummer, Anschrift usw. und mehrere Datensätze Rechnungspositionen mit Artikelnummer, Preis usw.) oder bei Kontaktdaten (ein Datensatz Firma mit Kundennummer, Anschrift usw. und mehrere Datensätze Ansprechpartner mit Name, Telefonnummer usw.) verwendet.

Darstellung

Um die Daten jetzt immer schön im Zusammenhang darzustellen, kann man sehr einfach zu jedem Elterndatensatz die zugehörigen Kinddatensätze anzeigen lassen. Man fügt dazu einfach eine Ansicht der Detailliste auf das Anzeige- und das Ändern-Formular (DispForm und EditForm) der Elternliste ein. SharePoint sorgt dafür, daß diese Ansicht korrekt gefiltert wird.

Zum Einfügen geht man so vor: man geht auf die Elternliste, klickt auf Liste, öffnet den Menüpunkt Formularwebparts ändern und wählt das anzupassende Formular aus:

 

Die Menüpunkte der einzelnen Formulare sind meiner Meinung nach etwas unglücklich übersetzt. Neues Standardformular meint das Formular zur Neuanlage (NewForm.aspx), Standardanzeigeformular meint das Formular zur Anzeige (DispForm.aspx) und Standardformular bearbeiten meint das Formular zum Ändern (EditForm.aspx).

Wenn das gewünschte Formular geöffnet ist, klickt man irgendwo in die Seite, damit der Reiter Einfügen sichtbar wird. Dort gibt es dann einen Menüpunkt Verwandte Liste, der beim Aufklappen alle Listen anzeigt, die ein Nachschlagefeld auf die aktuelle Liste enthalten:

 

Man wählt die einzufügende Liste aus und es wird eine automatisch gefilterte Ansicht dieser Liste eingefügt. Die Ansicht kann jetzt wie gewohnt über die Webparteinstellungen angepaßt werden (Menüpunkt Webpart bearbeiten). Für uns ist hier wichtig den standardmäßig unter der Ansicht angezeigten Link Neues Element hinzufügen zu entfernen, damit wir ihn durch einen eigenen Link ersetzen können. Damit der Link nicht mehr angezeigt wird, setzt man in den Webparteinstellungen den Symbolleistentyp auf Keine Symbolleiste:

Der Neues Element erstellen Link

Um den ausgeblendeten Link durch einen eigenen zu ersetzen, gibt es zwei Möglichkeiten: man kann das Formular in SharePoint Designer öffnen und die Änderungen dort direkt in der Codeansicht machen. Oder man fügt im Browser noch ein Inhalts-Editor-Webpart ein und bearbeitet das in der HTML-Ansicht. Dieser Weg wird hier gezeigt.

Das Webpart findet sich in der Kategorie Medien und Inhalt und man fügt es am Sinnvollsten direkt unter der neu eingefügten Ansicht ein. Eventuell muß man es dazu im Browser nach unten verschieben. Nachdem man in das Webpart geklickt hat, wählt man im Reiter Text formatieren aus dem Menü HTML den Punkt HTML-Quelle bearbeiten:

Wir brauchen zunächst eine Funktion, die uns einen bestimmten Parameter aus der URL liefert:

function getQueryStringParameter(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 "";

}

Diese Funktion benutzen wir, um die ID aus der URL zu holen:

var lookupId = getQueryStringParameter("ID");

Damit setzen wir uns die URL zusammen, über die wir das Formular zur Neuanlage eines Kindelements aufrufen:

var newFormUrl = "/site/Lists/ChildList/NewForm.aspx?LookupId=" + lookupId;

Wenn man möchte, daß der Benutzer nach dem Speichern des neuen Elements (oder nach einem Klick auf Abbrechen) wieder auf die ursprüngliche Seite zurückgeleitet wird, kann man die dafür in SharePoint vorgesehene Standardtechnik benutzen und einen zusätzlichen Source-Parameter mitgeben. Der Parameter muß die Adresse der ursprünglichen Seite und zusätzlich wieder die ID als Parameter enthalten. Man kann das durch folgende Zeile erreichen:

newFormUrl += "&Source=" + encodeURI(window.location.pathname + "?ID=" + lookupId);

Diese URL benutzen wir, um einen vorher definierten Anker mit dem korrekten Link zu versorgen. Außerdem packen wir das Ganze in eine Funktion, damit man es nach dem vollständigen Laden der Seite aufrufen kann:

function createNewLink() {

  var lookupId = getQueryStringParameter("ID"),

      newFormUrl = "/site/Lists/ChildList/NewForm.aspx?LookupId=" + lookupId,

      link = document.getElementById("myNewLink");

  newFormUrl += "&Source=" + encodeURI(window.location.pathname + "?ID=" + lookupId);

  link.href = newFormUrl;

}

Achtung: dieses Verfahren funktioniert nur, wenn das aktuelle Formular nicht in einem Dialog geöffnet wurde. Dialoge kann man in den Listeneinstellungen unter Erweiterte Einstellungen abschalten. Eleganter ist es natürlich, wenn es auch von einem Dialog aus funktioniert und wenn das Formular zum Erstellen des neuen Elements ebenfalls in einem Dialog geöffnet wird.

Das zu erreichen ist aber relativ einfach: man gibt dem Anker einfach ein zusätzliches onclick-Attribut und ruft darin die von SharePoint vorgesehene Funktion auf:

link.onclick = function (event) { NewItem2(event, newFormUrl); return false; };

Dabei muß man der NewItem2-Funktion allerdings die absolute URL übergeben, die man so erzeugen kann:

newFormUrl = window.location.protocol + "//" + window.location.hostname + newFormUrl;

Hier nochmal zusammengefaßt der gesamte Code zum Kopieren und Einfügen in die HTML-Quelle des Webparts. Nicht vergessen, die newFormUrl auf die eigene Umgebung anzupassen!

<a href="#" id="myNewLink">Neues Element erstellen</a>

<script type="text/javascript">

_spBodyOnLoadFunctionNames.push("createNewLink");

function createNewLink() {

  var lookupId = getQueryStringParameter("ID"),

      newFormUrl = "/site/Lists/ChildList/NewForm.aspx?LookupId=" + lookupId,

      link = document.getElementById("myNewLink");

  link.href = newFormUrl;

  newFormUrl = window.location.protocol + "//" + window.location.hostname + newFormUrl;

  link.onclick = function (event) { NewItem2(event, newFormUrl); return false; };

}

function getQueryStringParameter(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 "";

}

</script>

NewForm anpassen / Nachschlagefeld vorbelegen

Mit dem oben Gezeigten haben wir jetzt also einen Link auf das Formular, mit dem die neuen Kindelemente erzeugt werden. Die ID des Elternelements wird dabei in der URL übertragen. Kommen wir jetzt also zu den Anpassungen, die man dort noch machen muß, um diese ID auszulesen und das Nachschlagefeld damit vorzubelegen.

Wie man den notwendigen JavaScript-Code auf ein Listenformular bekommt, habe ich ja oben schon beschrieben und werde deshalb hier nicht weiter darauf eingehen.

Um an die ID des Elternelements zu kommen, verwenden wir wieder die oben gezeigte Funktion getQueryStringParameter. Spätestens jetzt sollte man sich überlegen, diese Funktion in eine eigene js-Datei zu packen, damit man sie einfach wiederverwenden kann.

Außerdem brauchen wir eine Hilfsfunktion getTagFromIdentifierAndTitle, die tausendfach durchs Web geistert. Ich verzichte hier auf eine Quellenangabe, da ich ohnehin nicht weiß, wer sie ursprünglich erdacht hat. Sie liefert ein HTML-Element anhand des Tagnamens, einem optionalen Feldtyp und dem Feldnamen:

function getTagFromIdentifierAndTitle(tagName, identifier, title) {

  var idLength = identifier.length,

      tags = document.getElementsByTagName(tagName),

      i,

      tagID;

  for (i = 0; i < tags.length; i++) {

    tagID = tags[i].id;

    if (tags[i].title == title && (identifier == "" || tagID.indexOf(identifier) == tagID.length – idLength)) {

      return tags[i];

    }

  }

  return null;

}

Diese Funktion können wir jetzt benutzen, um an das Nachschlagefeld zu gelangen. Hier kommt aber eine besondere Verhaltensweise von Nachschlagefeldern hinzu, der besondere Beachtung geschenkt werden muß. Wenn die Nachschlageliste weniger als 20 Elemente enthält, wird das Nachschlagefeld als ganz normales <select> gerendert. Man muß dann einfach nur das gesuchte <option> anhand seines Values finden und auswählen. Dazu dient diese Funktion:

function setSelectedOption(select, value) {

  var opts = select.options,

      optLength = opts.length,

      i;

  for (i = 0; i < optLength; i++) {

    if (opts[i].value == value) {

      select.selectedIndex = i;

      return true;

    }

  }

  return false;

}

Wenn die Nachschlageliste mehr Elemente enthält, wird für das Nachschlagefeld ein komplizierteres DHTML-Konstrukt gerendert. Das Haupteingabefeld ist dabei ein <input type="text">. Der gespeicherte Wert befindet sich allerdings in einem <input type="hidden"> und dieses wiederum findet man über seine ID, die beim Eingabefeld in einem optHid-Attribut gespeichert ist. Puh.

Da das Feld später vorbelegt und sinnvollerweise von den Benutzern nicht geändert werden soll, blenden wir es aus. Wir wollen dabei nicht nur das Feld selbst ausblenden, sondern gleich die gesamte Tabellenzeile, in der es sich befindet. Auch dabei müssen die beiden Arten unterschieden werden. Beim <select> geht es drei Ebenen nach oben, bis man die Tabellenzeile erreicht. Beim <input> sind es vier Ebenen.

Achtung: man darf die Felder nicht auf disabled oder ähnliches setzen, weil sonst ihre Werte beim POST nicht übertragen und damit nicht gespeichert werden!

Damit können wir uns jetzt eine wiederverwendbare Funktion bauen, die den Wert eines Nachschlagefeldes unabhängig von der Anzahl der Nachschlageelemente setzt und auch gleich das Feld ausblendet:

function setLookupField(fieldName, value) {

  var theSelect = getTagFromIdentifierAndTitle("select", "", fieldName),

      theInput;

  if (theSelect == null) {

    theInput = getTagFromIdentifierAndTitle("input", "", fieldName);

    document.getElementById(theInput.optHid).value = value;

    theInput.parentNode.parentNode.parentNode.parentNode.style.display = "none";

  } else {

    setSelectedOption(theSelect, value);

    theSelect.parentNode.parentNode.parentNode.style.display = "none";

  }

}

Auch hier nochmal der gesamte JavaScript-Code für die NewForm zum bequemen Kopieren und Einfügen. Nicht vergessen, den Namen des Nachschlagefelds (hier "Parent") an die eigene Umgebung anzupassen! 

<script type="text/javascript">

_spBodyOnLoadFunctionNames.push("setLookupFromQS");

function setLookupFromQS() {

  var lookupId = getQueryStringParameter("LookupId");

  setLookupField("Parent", lookupId);

}

function setLookupField(fieldName, value) {

  var theSelect = getTagFromIdentifierAndTitle("select", "", fieldName),

      theInput;

  if (theSelect == null) {

    theInput = getTagFromIdentifierAndTitle("input", "", fieldName);

    document.getElementById(theInput.optHid).value = value;

    theInput.parentNode.parentNode.parentNode.parentNode.style.display = "none";

  } else {

    setSelectedOption(theSelect, value);

    theSelect.parentNode.parentNode.parentNode.style.display = "none";

 }

}

function getTagFromIdentifierAndTitle(tagName, identifier, title) {

  var idLength = identifier.length,

      tags = document.getElementsByTagName(tagName),

      i,

      tagID;

  for (i = 0; i < tags.length; i++) {

    tagID = tags[i].id;

    if (tags[i].title == title && (identifier == "" || tagID.indexOf(identifier) == tagID.length – idLength)) {

      return tags[i];

    }

  }

  return null;

}

function setSelectedOption(select, value) {

  var opts = select.options,

      optLength = opts.length,

      i;

  for (i = 0; i < optLength; i++) {

    if (opts[i].value == value) {

      select.selectedIndex = i;

      return true;

    }

  }

  return false;

}

function getQueryStringParameter(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 "";

}

</script>

Link zum Erstellen eines neuen Dokuments aus einer Vorlage

Und weil ich gerade dabei bin oft gestellte Fragen zu beantworten, gleich noch ein derartiger Beitrag. Diesmal geht es darum einen Hyperlink darzustellen, mit dem ein neues Dokument aus einer Vorlage erzeugt werden kann.

Ansichten von Dokumentbibliotheken enthalten unterhalb der Dokumente standardmäßig einen Link "Dokument hinzufügen", der allerdings nur auf das Upload-Formular verweist. Wenn man wirklich ein neues Dokument aus einer hinterlegten Vorlage erstellen möchte, muß man zuerst den Reiter Dokumente aktivieren, das Menü Neues Dokument öffnen und dann den gewünschten Inhaltstyp auswählen. Um das zu vereinfachen, wäre es schön, wenn man an beliebiger Stelle einen Link platzieren könnte, der dieselbe Aufgabe erfüllt, und genau das wird hier gezeigt.

Es gibt grundsätzlich viele Möglichkeiten, wie man irgendwo in SharePoint einen Link einfügen kann. Das kann in einer Wikiseite oder in einem Inhalts-Editor-Webpart auf einer Webpartseite sein. Mit Hilfe von SharePoint Designer kann man Links an fast beliebigen Stellen einbauen. In jedem Fall braucht es JavaScript, damit die Office-Integration auch wirklich funktioniert. Der Link selbst sieht so aus:

<a href="#" onclick="XYZ; return false;">Neues Dokument</a>

Das eigentliche Verweisziel (href-Attribut) läßt man leer bzw. es enthält nur eine Raute #. Ins onclick-Attribut kommt an der Stelle von XYZ die eigentliche JavaScript-Funktion (gleich mehr dazu) und ein return false. Neues Dokument ist der angezeigte Text des Links.

Die Funktion, die in den Link statt XYZ eingesetzt wird, sieht grundsätzlich so aus:

CoreInvoke(‚createNewDocumentWithProgIDEx‘, event, ‚Url der Vorlage‘, ‚Url der Bibliothek‘, ‚SharePoint.OpenDocuments‘, false);

Bei Url der Vorlage gibt man die absolute URL der gewünschten Vorlage an. Man findet sie in der Regel im Ordner Forms der Bibliothek und dort in einem Ordner mit dem Namen des Inhaltstyps. Bei Url der Bibliothek gibt man die absolute URL der Zielbibliothek an. Sie wird benötigt, damit Office die Bibliothek als Standardort beim Speichern des neuen Dokuments vorschlägt. Achtung: beide URLs müssen wirklich absolut sein. Relative funktionieren nicht!

Der gesamte Link kann also so aussehen:

<a href="#" onclick="CoreInvoke(‚createNewDocumentWithProgIDEx‘, event, ‚http://sharepoint/website/DocLib/Forms/Word1/WordTemplate1.dotx&#8216;, ‚http://sharepoint/website/DocLib&#8216;, ‚SharePoint.OpenDocuments‘, false); return false;">Neues Dokument</a>

Personenfelder vorbelegen mit jQuery

Vor einigen Tagen hatte ich hier einen Beitrag geschrieben, der zeigt, wie man Personenfelder mit JavaScript vorbelegen kann. Dort wurden die standardmäßig vorhandenen Möglichkeiten verwendet. In diesem Beitrag wird gezeigt, wie man das Ganze mit Hilfe von jQuery und damit deutlich einfacher erledigen kann. Außerdem hat das den Vorteil, daß man die gewünschten Personenfelder über ihren internen Namen ansprechen kann (anstatt über ihren Index wie im anderen Beitrag).

Ich verwende hier jQuery 1.6.1, ganz einfach weil es ohnehin bereits auf dem Server vorhanden war. Jede neuere (und wahrscheinlich auch ältere) Version sollte aber ebenfalls funktionieren. Ich habe die jQuery-Bibliothek hier aus dem Dateisystem, d.h. aus einem Unterordner von 14\TEMPLATE\LAYOUTS eingebunden, aber man kann sie auch innerhalb einer Website z.B. in die Formatbibliothek hochladen und von dort verlinken.

Die grundsätzliche Vorgehensweise ist hier dieselbe, wie beim vorigen Beitrag: man ermittelt den aktuellen Benutzer per Client Object Model und schreibt nach einem executeQueryAsync dessen Namen in das gewünschte Feld. Ich gehe hier also nur darauf ein, wie man mit jQuery auf ein Personenfeld zugreift.

Vor dem eigentlichen Script muß zunächst jQuery eingebunden werden:

<script type="text/javascript" src="/_layouts/Demo/jquery-1.6.1.min.js"></script>

Nachdem der gewünschte Benutzer erfolgreich geladen wurde, definieren wir zunächst einen regulären Ausdruck, der zur Identifizierung des gewünschten Personenfeldes verwendet wird:

var searchRegEx = RegExp("Fieldname=\"PeopleField\"", "gi");

Dann benutzen wir jQuery um damit alle Tabellenzellen der rechten Seite eines Formulars durchzugehen. Wir verwenden dazu die dort verwendete CSS-Klasse:

$("td.ms-formbody").each(function () {
  // $(this) entspricht hier einer Tabellenzelle
  // eines Listenformulars
});

Innerhalb der Tabellenzelle suchen wir jetzt nach dem vorher definierten regulären Ausdruck:

if (searchRegEx.test($(this).html())) {
  // wir haben die richtige Zelle gefunden
}

Jetzt können wir relativ einfach auf die Bestandteile des Personenfeldes zugreifen, also auf das <div> für die Standardeingabe und das <textarea> für ältere Browser, und sie mit dem Namen des Benutzers belegen:

$("div[Title=’Personenauswahl‘]", this).html(currentUser.get_title());
$("textarea[Title=’Personenauswahl‘]", this).val(currentUser.get_title());

Wichtig: die Elemente warden über ihr title-Attribut identifiziert, das bei Personenfeldern immer dieselben Werte enthält. Allerdings unterscheiden sich die Werte je nach Sprache. Bei einer englischen Website steht dort "People Picker" statt "Personenauswahl".

Damit steht der Name schon mal im Personenfeld. Wenn man den Namen auch gleich validieren möchte, so daß er im Feld wie gewohnt unterstrichen dargestellt wird, kann man einfach noch einen Klick auf den entsprechenden Button simulieren:

$("img[Title=’Namen überprüfen‘]", this).trigger("click");

Wichtig: auch hier wird wieder je nach Sprache unterschieden. Englisch steht dort "Check names" statt "Namen überprüfen".

Auch hier nochmal der gesamte JavaScript-Code zum bequemen Kopieren&Einfügen:

<script type="text/javascript" src="/_layouts/Demo/jquery-1.6.1.min.js"></script>
<script type="text/javascript">
ExecuteOrDelayUntilScriptLoaded(setPicker, "sp.js");
function setPicker() {
  var ctx = SP.ClientContext.get_current(),
      web = ctx.get_web(),
      currentUser = web.get_currentUser(),
      fieldName = "PeopleField",
      searchRegEx = RegExp("Fieldname=\"" + fieldName + "\"", "gi");

  ctx.load(currentUser);
  ctx.executeQueryAsync(function () {
      $("td.ms-formbody").each(function () {
        if (searchRegEx.test($(this).html())) {
          $("div[Title=’Personenauswahl‘]", this).html(currentUser.get_title());
          $("textarea[Title=’Personenauswahl‘]", this).val(currentUser.get_title());
          $("img[Title=’Namen überprüfen‘]", this).trigger("click");
          return;
        }
      });
    }, function (sender, args) {
      alert("Fehler: " + args.get_message());
    }
  );
}
</script>

Personenfelder vorbelegen

Eine oft gestellte Frage zu SharePoint ist die, wie man ein Personenfeld mit dem aktuellen Benutzer vorbelegen kann und genau das soll in diesem Beitrag gezeigt werden.

Es gibt grundsätzlich zwei Möglichkeiten: die erste besteht darin, das Feld erst nach der eigentlichen Neuanlage zu setzen. Das kann dann z.B. per EventReceiver oder per Workflow geschehen. Die zweite Möglichkeit besteht darin, das Eingabefeld auf dem Formular zur Neuanlage (NewForm.aspx) per JavaScript vorzubelegen und genau das wird hier gezeigt.

Update: hier ein ergänzender Beitrag, der zeigt wie man dasselbe deutlich einfacher mit jQuery erreichen kann. 

Wir brauchen also zunächst eine Möglichkeit das Formular mit JavaScript zu erweitern. Auch hierzu gibt es wieder zwei Möglichkeiten: die erste ist NewForm.aspx in SharePoint Designer zu öffnen und den Code direkt in die Seite einzufügen. Die zweite Möglichkeit ist das Formular im Browser zu bearbeiten, ein Inhaltseditor-Webpart einzufügen und dort den Code zu platzieren. Ich werde hier die Vorgehensweise mit SharePoint Designer beschreiben. Im Browser muß nur der fertige JavaScript-Code vom Ende des Beitrags in die HTML-Ansicht eines Inhaltseditor-Webparts kopiert werden.

Man öffnet also die Website in SharePoint Designer, klickt dann links auf Listen und Bibliotheken, dann auf die gewünschte Liste und schließlich rechts unter Formulare auf NewForm.aspx. Anschließend muß über das Menüband noch der erweiterte Modus aktiviert werden. In der Codeansicht sucht man sich jetzt eine geeignete Stelle zum Einfügen des Scripts. Geeignet sind z.B. die Platzhalter PlaceHolderAdditionalPageHead und PlaceHolderMain.

Zur Ermittlung des aktuellen Benutzers bedienen wir uns des Client Object Model für ECMAScript. Man könnte damit auch anderen Code ausführen, um einen bestimmten Benutzer zu ermitteln, aber für das Beispiel beschränke ich mich auf den aktuell angemeldeten Benutzer. Wenn man immer nur einen ganz bestimmten Benutzer vorbelegen möchte, kann man auch ganz darauf verzichten und diesen fest einprogrammieren.

Den aktuellen Benutzer bekommt man so:

var ctx = SP.ClientContext.get_current();
var web = ctx.get_web();
var currentUser = web.get_currentUser();
ctx.load(currentUser);

Wie immer, wenn man das Client Object Model verwendet, muß anschließend ein executeQueryAsync ausgeführt werden, damit die gewünschten Daten auch wirklich geladen werden:

ctx.executeQueryAsync(function () {
   
// hier stehen die Daten zur Verfügung
  }, function (sender, args) {
    alert("Fehler: " + args.get_message());
  }
);

Die erste angegebene Funktion wird im Erfolgsfall und die zweite im Fehlerfall aufgerufen. Zum Testen kann man sich wie hier gezeigt eine Fehlermeldung ausgeben lassen. Bei einem Produktivsystem kann man die Funktion aber auch leer lassen und Fehler damit einfach "verschlucken".

Machen wir also an der Stelle weiter, die ausgeführt wird, wenn der Benutzer erfolgreich geladen wurde. Wir brauchen jetzt Zugriff auf das HTML-Element, das bei einem Personenfeld für die Eingabe vorhanden ist. Leider ist das bei Personenfeldern nicht ganz so einfach, wie bei anderen Feldtypen. Und es gibt nicht nur das altbekannte Eingabefeld (ein <div>), sondern auch ein standardmäßig ausgeblendetes <textarea>, das für ältere Browser erzeugt wird.

Da man Personenfelder im HTML nicht einfach über den Feldnamen identifizieren kann, erstellen wir eine wiederverwendbare Funktion, die alle Personenfelder durchgeht und das mit dem gewünschten Index zurückgibt. Wenn es mehrere Personenfelder gibt, können wir diese Funktion also mehrmals aufrufen und so z.B. das erste und das dritte Personenfeld vorbelegen:

function getPickerDiv(pickerNo) {
 
var divs = document.getElementsByTagName("DIV"),
      length = divs.length,
      i,
      j = 0;

  for
(i = 0; i < length; i++) {
    if (divs[i].id.indexOf("UserField_upLevelDiv") > 0) {
      if (j == pickerNo) {
        return divs[i];
      }
      j++;
    }
  }
  return null;
}

Diese Funktion liefert uns die <div>s für die Standardeingabemethode. Wir erstellen gleich noch eine zweite Funktion, die uns die <textarea>s liefert, damit wird die ebenfalls vorbelegen können:

function getPickerTextarea(pickerNo) {
 
var tas = document.getElementsByTagName("TEXTAREA"),
      length = tas.length,
      i,
      j = 0;

  for
(i = 0; i < length; i++) {
    if (tas[i].id.indexOf("UserField_downlevelTextBox") > 0) {
      if (j == pickerNo) {
        return tas[i];
      }
      j++;
    }
  }
  return null;
}

Jetzt brauchen wir diese Funktionen nur noch in den Code von oben einbauen und können dann den Benutzernamen z.B. in das erste Personenfeld einfügen:

var ele = getPickerDiv(0);
if (ele != null) {
  ele.innerHTML = currentUser.get_title();
}
ele = getPickerTextarea(0);
if (ele != null) {
  ele.value = currentUser.get_title();
}

Bitte beachten, daß beim <div> die innerHTML-Eigenschaft und beim <textarea> die value-Eigenschaft gesetzt wird.

Den Code packen wir jetzt noch in eine weitere Funktion setPicker, die dann beim Laden der Seite aufgerufen werden muß. Um dabei sicherzustellen, daß das Client Object Model zur Verfügung steht, verwenden wir die vordefinierte ExecuteOrDelayUntilScriptLoaded-Methode:

ExecuteOrDelayUntilScriptLoaded(setPicker, "sp.js");

Hier nochmal der gesamte JavaScript-Code zum bequemen Kopieren&Einfügen:

<script type="text/javascript">
ExecuteOrDelayUntilScriptLoaded(setPicker, "sp.js");
function setPicker() {
  var ctx = SP.ClientContext.get_current(),
      web = ctx.get_web(),
      currentUser = web.get_currentUser(),
      pickerNo = 0;

  ctx.load(currentUser);
  ctx.executeQueryAsync(function () {
      var ele = getPickerDiv(pickerNo);
      if (ele != null) {
        ele.innerHTML = currentUser.get_title();
      }
      ele = getPickerTextarea(pickerNo);
      if (ele != null) {
        ele.value = currentUser.get_title();
      }
    }, function (sender, args) {
      alert("Fehler: " + args.get_message());
    }
  );
}
function getPickerDiv(pickerNo) {
  var divs = document.getElementsByTagName("DIV"),
      length = divs.length,
      i,
      j = 0;

  for
(i = 0; i < length; i++) {
    if (divs[i].id.indexOf("UserField_upLevelDiv") > 0) {
      if (j == pickerNo) {
        return divs[i];
      }
      j++;
    }
  }
  return null;
}
function getPickerTextarea(pickerNo) {
  var tas = document.getElementsByTagName("TEXTAREA"),
      length = tas.length,
      i,
      j = 0;

  for
(i = 0; i < length; i++) {
    if (tas[i].id.indexOf("UserField_downlevelTextBox") > 0) {
      if (j == pickerNo) {
        return tas[i];
      }
      j++;
    }
  }
  return null;
}
</script>

„Gefällt mir“ im SharePoint 2010 Blog

In diesem Beitrag wird gezeigt, wie man eine SharePoint 2010 Blogwebsite mit Facebook-ähnlicher Gefällt mir Funktionalität ausstatten kann. Die gezeigte Lösung ist mit JavaScript realisiert und kommt deshalb komplett ohne Postbacks aus. Das Ergebnis wird sich später so darstellen:

Wir beginnen, indem wir eine neue Website aus der Vorlage Blog erstellen.

Die Likes-Liste

Welcher Person welcher Beitrag gefällt, wird in einer SharePoint-Liste gespeichert. Wir legen also eine neue Liste an und geben ihr den Namen Likes. Man klickt dazu auf Websiteaktionen Weitere Optionen, markiert dann die Vorlage Benutzerdefinierte Liste und klickt rechts auf Weitere Optionen. Die Markierung bei Liste in der Schnellstartleiste anzeigen setzen wir auf Nein und klicken dann auf Erstellen.

Die Liste muß jetzt noch etwas verändert werden. Dazu klickt man im Reiter Liste auf Listeneinstellungen. Die Standardspalte Titel brauchen wir nicht. Die Spalte ist aber als Pflichtfeld gekennzeichnet und das müssen wir ändern. Dazu klickt man auf den Spaltennamen Titel und setzt die Markierung bei Diese Spalte muss Informationen enthalten auf Nein.

Um die einzelnen Likes einem Blogbeitrag zuzuordnen, brauchen wir jetzt noch eine Nachschlagespalte auf die Beiträge. Wir klicken dazu auf Spalte erstellen, geben der neuen Spalte den Namen Beitrag und wählen als Informationstyp Nachschlagen (in Informationen, die sich bereits auf dieser Website befinden). Nachdem wir geprüft haben, daß auch wirklich in der Liste Beiträge nachgeschlagen wird, klicken wir auf OK.

Weitere Anpassungen wie z.B. spezielle Ansichten sind nicht notwendig, da in dieser Liste niemand direkt Daten einträgt. Man könnte sie sogar komplett aus der Oberfläche ausblenden, indem man ihre Hidden-Eigenschaft auf true setzt (was nicht im Browser geht).

JavaScript

Den benötigten JavaScript-Code legt man am Besten in eine eigene .js-Datei und bindet diese dann ein. Es gibt viele Möglichkeiten, wo man diese Datei ablegen kann. Das kann irgendwo in der Website, z.B. in einer Assets-Bibliothek, aber auch im Dateisystem der Webfrontend-Server sein, z.B. in einem eigenen Unterordner von 14\TEMPLATE\LAYOUTS. Für die Demo hier erstellen wir eine einfache Dokumentbibliothek Assets, die nicht in der Schnellstartleiste angezeigt wird und bei der wir als Vorlage Keine wählen. Unser Script kommt in eine Datei Likes.js, die wir in diese Bibliothek hochladen.

Vorhandene Likes anzeigen

Wir fangen an mit einer Funktion, die alle derzeit vorhandenen Likes anzeigt. Der Funktion übergeben wir die ID des Blogbeitrags, für den die Likes angezeigt werden sollen:

function getAllPostLikes(postId) {
  // hier holen wir uns die Likes und stellen sie dar
}

In dieser Funktion holen wir uns zunächst den Kontext und die erforderlichen Objekte wie die Likes-Liste und den aktuellen Benutzer. Diesen brauchen wir um festzustellen, ob dem aktuellen Benutzer dieser Beitrag gefällt oder nicht:

var ctx = new SP.ClientContext();
var web = ctx.get_web();
var currentUser = web.get_currentUser();
ctx.load(currentUser);
var list = web.get_lists().getByTitle("Likes");

Die vorhandenen Likes zum Beitrag holen wir jetzt über eine kleine CAML-Abfrage aus der Liste:

var query = new SP.CamlQuery();
query.set_viewXml("<View><Query><Where><Eq><FieldRef Name=’Beitrag‘ LookupId=’True’/><Value Type=’Lookup‘>" + postId + "</Value></Eq></Where></Query></View>");
var items = list.getItems(query);
ctx.load(items);

Sämtliche Interaktionen mit dem Server müssen im SharePoint Client Object Model immer asynchron ausgeführt werden. Man gibt dabei jeweils eine Funktion an, die bei Erfolg und im Fehlerfall aufgerufen wird. Fehler ignorieren wir hier einfach und die Funktion für den Erfolg geben wir inline an. Das Gerüst dazu sieht also so aus:

ctx.executeQueryAsync(function () {
  // alle Likes stehen jetzt zur Verfügung
  }, function () { }
);

In dieser Funktion bauen wir uns den HTML-Quelltext zusammen, mit dem wir später die Anzahl der Likes darstellen. Auch den Button, mit dem der Benutzer später einen Beitrag "Liken" kann, bzw. einen "Like" zurücknehmen kann, erstellen wir dort als HTML.

Die Anzahl der Likes erstellen wir als einfachen Text und fügen davor und dahinter Leerraum ein, damit sich die Darstellung später besser in die Seite einpaßt. Man könnte hier z.B. noch eine Fallunterscheidung machen und für die Fälle, daß der Beitrag bisher niemandem oder nur einer Person gefällt, andere Texte anzeigen. Hier soll aber nur so einfach wie möglich das Prinzip gezeigt werden.

var html = "&nbsp;|&nbsp;" + items.get_count() + " Person(en) gef&auml;llt das&nbsp;&nbsp;";

Jetzt ermitteln wir, ob dem aktuellen Benutzer der Beitrag bereits gefallen hat. Dazu vergleichen wir einfach die ID desjenigen, der ein Like erstellt hat mit der ID des aktuellen Benutzers:

var userLikesThis = false;
for (var i = 0; i < items.get_count(); i++) {
  var item = items.get_item(i);
  var author = item.get_item("Author");
  if (author.get_lookupId() == currentUser.get_id()) {
    userLikesThis = true;
    break;
  }
}

Jetzt fehlt noch der Teil zur Darstellung der Like- bzw. Unlike-Buttons. Wir erzeugen hier einfache HTML-Buttons, die beim Klick entsprechende JavaScript-Funktionen aufrufen und dabei den Button selbst, die ID des Beitrags und die ID des aktuellen Benutzers übergeben. Diese Funktionen werden im Anschluß behandelt. Auch hier könnte man die Darstellung sicher noch aufhübschen, aber wie gesagt: es geht nur um das Prinzip:

if (userLikesThis) {
  html += "<input type=’button‘ value=’Gef&auml;llt mir nicht mehr‘ onclick=’unlikePost(this, " + postId + ", " + currentUser.get_id() + "); return false;’/>";
} else {
  html += "<input type=’button‘ value=’Gef&auml;llt mir‘ onclick=’likePost(this, " + postId + ", " + currentUser.get_id() + "); return false;’/>";
}

Wir haben jetzt das HTML zur Darstellung beieinander und fügen es jetzt in die Seite ein. Dazu setzen wir die innerHTML-Eigenschaft eines in der Seite platzierten <span>-Elements. Bei diesem Element wurde die id um die ID des Beitrags ergänzt, so daß wir jetzt gezielt darauf zugreifen können. Wie das Element erzeugt wurde, zeige ich weiter unten.

$get("PostLikesSpan" + postId).innerHTML = html;

Einen Beitrag "Liken"

Wenn ein Benutzer auf den Gefällt mir Button klickt, wird diese Funktion aufgerufen:

function likePost(btn, postId, userId) {
  btn.enabled = false;
}

In der Funktion wird zuerst der geklickte Button selbst deaktiviert, um zu verhindern, daß ein Benutzer für einen Beitrag mehrfach die Gefällt mir Funktion auslösen kann. Ähnlich wie oben holen wir uns jetzt ein paar Objekte und suchen dann nach einemeventuell bereits vorhandenen Like für den aktuellen Benutzer. Die CAML-Abfrage dazu ist ebenfalls ähnlich wie oben, aber weiter eingeschränkt auf den Benutzer:

query.set_viewXml("<View><Query><Where><And><Eq><FieldRef Name=’Beitrag‘ LookupId=’True’/><Value Type=’Lookup‘>" + postId + "</Value></Eq>" +
"<Eq><FieldRef Name=’Author‘ LookupId=’True’/><Value Type=’Lookup‘>" + userId + "</Value></Eq></And></Where></Query></View>");

Nach dem Ausführen der Abfrage prüfen wir, ob es bereits ein Like gibt und nur wenn nicht erzeugen wire in neues Element in der Likes-Liste. Danach rufen wir einfach die getAllPostLikes-Funktion von oben wieder auf, um die Anzeige zu aktualisieren:

if (items.get_count() == 0) {
  var itemCreateInfo = new SP.ListItemCreationInformation();
  var item = list.addItem(itemCreateInfo);
  item.set_item("Beitrag", postId);
  item.update();
  ctx.load(item);
  ctx.executeQueryAsync(function () {
    getAllPostLikes(postId);
  }, function () { }
  );
}

Einen Beitrag "Unliken"

Die Funktion zum Zurücknehmen eines Gefällt mir ist ähnlich aufgebaut. Der Unterschied besteht darin, daß ein gefundenes Element in der Likes-Liste gelöscht wird:

if (items.get_count() > 0) {
  var item = items.get_item(0);
  item.deleteObject();
  ctx.executeQueryAsync(function () {
    getAllPostLikes(postId);
  }, function () { }
  );
}

In die Seiten einbinden

In einer Blogwebsite werden die Beiträge auf verschiedenen Seiten dargestellt. Hier wird gezeigt, wie man die Gefällt mir Funktion in die Startseite default.aspx einbaut. Auf den anderen Seiten wie z.B. Category.aspx oder Date.aspx funktioniert das analog.

Die Darstellung der Blogbeiträge erfolgt immer durch ein XsltListViewWebPart und wir müssen dort eine Erweiterung im XSL einbauen und unsere Likes.js Datei referenzieren. Wir öffnen also die Website in SharePoint Designer, klicken links auf Alle Dateien und öffnen dann default.aspx im erweiterten Modus. Der erweiterte Modus ist notwendig, damit man Änderungen im Kopf der Seite machen kann.

Wir suchen den Platzhalter PlaceHolderAdditionalPageHead und fügen dort die Referenz auf unsere Scriptdatei ein:

<script type="text/javascript" src="/blog/Assets/Likes.js"></script>

Als nächstes müssen wir noch eine Ergänzung in die Ansicht der Beiträge einbauen. Damit man überhaupt an das XSL gelangt, markiert man das WebPart für die Beitragsanzeige, z.B. indem man einfach in der Entwurfsansicht draufklickt. Dann klickt man im Reiter Entwurf auf XSLT anpassen und dann auf Gesamte Ansicht anpassen. Dadurch wird das XSL in die aktuelle Seite kopiert und kann bearbeitet werden.

Man sucht sich jetzt eine geeignete Stelle für die Gefällt mir Funktion und markiert sie in der Entwurfsansicht. Wenn man jetzt in die Codeansicht umschaltet, ist der Teil im XSL markiert, der für die Darstellung des markierten Bereichs zuständig ist. Manchmal muß man das ein paar Mal machen, bis es wirklich klappt.

Wir wählen für die Demo die Stelle rechts neben der Anzahl der Kommentare. Man findet den Code dazu am einfachsten, wenn man das Wort Kommentar(e) markiert. Im Code sieht die Stelle so aus:

<xsl:value-of select="$thisNode/../@resource.wss.num_comments_blg_post"/>
</a>
</span>
</xsl:template>

Wir fügen unsere Erweiterung zwischen dem schließenden </span> und dem schließenden </xsl:template> ein:

<span id="PostLikesSpan{$thisNode/@ID}"> </span>
<script type="text/javascript">
  ExecuteOrDelayUntilScriptLoaded(getMyLikes<xsl:value-of select="$thisNode/@ID" />, &quot;sp.js&quot;);
  function getMyLikes<xsl:value-of select="$thisNode/@ID" /> () {
    getAllPostLikes(<xsl:value-of select="$thisNode/@ID" />);
  }
</script>

Damit wird zuerst das oben bereits erwähnte <span>-Element erzeugt, bei dem die id die eindeutige ID des Beitrags enthält. Anschließend wird eine JavaScript-Funktion erzeugt, die ebenfalls diese ID im Namen hat. Diese Funktion wird über ExecuteOrDelayUntilScriptLoaded aufgerufen. Dadurch wird dafür gesorgt, daß die für das Client Object Model notwendigen Scripte zur Verfügung stehen. Die aufgerufene Funktion ruft wiederum unsere getAllPostLikes-Funktion auf und übergibt die ID des Beitrags. Über ExecuteOrDelayUntilScriptLoaded kann man nur Funktionen ohne Parameter aufrufen, daher der Umweg.

Man muß jetzt noch den Beginn des <xsl:template> suchen und dort das ddwrt:ghost-Attribut entfernen. Zu diesem Thema habe ich hier etwas geschrieben.

Abschluß

Die hier ausschnittsweise gezeigte und erklärte JavaScript-Datei kann hier heruntergeladen werden. Der Download enthält auch eine deutsche Websitevorlage, aus der eine Blogwebsite mit eingebauter Gefällt mit Funktion erzeugt werden kann.

Die gezeigte Lösung bietet noch viel Raum für Erweiterungen. Man könnte z.B. auch die Kommentare mit der Gefällt mir Funktion versehen. Und man könnte nicht nur die Anzahl der Personen anzeigen, denen etwas gefällt, sondern auch wer diese Personen sind. Z.B. als Tooltip oder Popup, wenn man mit dem Mauszeiger darüber fährt. Das alles sollte sich durch erweitern der gezeigten Lösung relativ leicht machen lassen.

Dialog bei Klick auf Lookup-Link abschalten

In SharePoint 2010 kann man bei Listen und Bibliotheken einstellen, ob die Listenformulare als Dialog geöffnet werden sollen oder nicht. Hyperlinks, die von Nachschlagefeldern (Lookups) erzeugt werden, beachten diese Einstellung aber nicht – sie werden immer als Dialog geöffnet. In diesem Beitrag soll gezeigt werden, wie man die Dialoge für diese Links abstellt.

Nachschlagefelder erzeugen bei der Ansicht immer einen Link auf das nachgeschlagene Element. Das gilt sowohl für das Standardanzeigeformular (DispForm.aspx) als auch für jede Listenansicht. Ein Klick auf einen solchen Link öffnet immer das Standardanzeigeformular des nachgeschlagenen Elements in einem Dialog. Dieser Dialog ist nicht immer erwünscht, z.B. wenn das Anzeigeformular selbst stark angepaßt wurde und deshalb möglichst viel Platz erhalten soll. Oder der Link soll sich gleich in einem neuen Fenster oder in einem neuen Reiter öffnen.

Um das zu erreichen, verwenden wir mal wieder jQuery. Wo man jQuery bekommt und wie man es einbindet, habe ich hier beschrieben.

Man geht dabei wie folgt vor: der Link eines Nachschlagefeldes wird bereits als normales <a>-Tag gerendert. Das Tag besitzt ein gültiges href-Attribut, das wir direkt verwenden können. Es besitzt aber auch ein onclick-Attribut, das für den Dialog verantwortlich ist. Wir müssen also nur die richtigen <a>-Elemente per JavaScript ausfindig machen und deren onclick-Attribut entfernen. Optional kann man dabei auch gleich ein target="_blank"-Attribut einfügen, falls sich die Links in einem neuen Fenster öffnen sollen.

Man öffnet dazu die Website in SharePoint Designer und navigiert zu der Liste oder Bibliothek, für die eine Ansicht angepaßt werden soll. Die Ansicht wird zum Bearbeiten geöffnet. In der Codeansicht sucht man das öffnende Element von PlaceHolderMain und fügt sein Script direkt darunter ein:

<asp:Content ID="Content1" ContentPlaceHolderId="PlaceHolderMain" runat="server">

Wenn man sich den Quelltext einer fertigen Seite im Browser anschaut, stellt man fest, daß die gesuchten Links alle mit der CSS-Klasse ms-vb2 versehen sind. Das machen wir uns zunutze, um die Links zu finden:

var allLinks = $(".ms-vb2 a");

Jetzt können wir die Links alle durchgehen und das onclick-Attribut entfernen:

allLinks.each(function() {
  $(this).removeAttr("onclick");
});

Das Ganze packen wir in eine handliche und wiederverwendbare Funktion:

function noDialogLinks() {
  var allLinks = $(".ms-vb2 a");
  allLinks.each(function() {
    $(this).removeAttr("onclick");
  });
}

Diese Funktion muß jetzt nur noch aufgerufen werden, sobald die Seite fertig geladen ist. Da Ansichten in SharePoint per Ajax geladen werden, müssen wir dabei eine kleine Verzögerung einbauen, damit sichergestellt ist, daß beim Ausführen unserer Funktion auch wirklich alle Daten zur Verfügung stehen:

$(document).ready(function() {
  setTimeout(noDialogLinks, 2000);
});

Hier wird eine Pause von 2.000 Millisekunden, also zwei Sekunden gemacht. In der Praxis sollte das ausreichen, damit die Daten geladen werden können und es sollte kurz genug sein, damit niemand vorher auf einen Link klickt.

Gruppierte Ansichten

Aufpassen muß man, wenn gruppierte Ansichten verwendet werden, die beim ersten Laden alle Knoten geschlossen/eingeklappt anzeigen. Hier werden die einzelnen Zeilen erst geladen, wenn ein Knoten geöffnet/ausgeklappt wird.

Damit unser Script in diesem Fall immer noch funktioniert, müssen wir alle Gruppierungsknoten ausfindig machen, an deren onclick-Funktion anhängen und dann ebenfalls wieder unsere noDialogLinks-Funktion aufrufen. Auch dabei müssen wir wieder eine kleine Verzögerung einbauen, weil die Daten erst geladen werden müssen. Der Code dazu sieht so aus:

var groupLinks = $(".ms-gb a");
groupLinks.each(function() {
  $(this).click(function() {setTimeout(noDialogLinks, 1500);} );
});

Hier nochmal zusammengefaßt das gesamte Script:

<script type="text/javascript">
function noDialogLinks() {
  var allLinks = $(".ms-vb2 a");
  allLinks.each(function() {
    $(this).removeAttr("onclick");
  });
}

$(document).ready(function () {
  setTimeout(noDialogLinks, 2000);
  var groupLinks = $(".ms-gb a");
  groupLinks.each(function() {
    $(this).click(function() { setTimeout(noDialogLinks, 1500); });
  });
});
</script>

Schnellstart-Link in neuem Fenster öffnen

In diesem Beitrag wird gezeigt, wie man einen Link in der Schnellstartleiste so manipulieren kann, daß er sich in einem neuen Fenster (oder in einem neuen Reiter) öffnet.

Man geht dabei wie folgt vor: der gewünschte Link muß per JavaScript/ECMAScript ausfindig gemacht und dann ein Attribut target="_blank" angehängt werden. Am Einfachsten geht das, wenn man jQuery verwendet.

Man braucht dazu die jQuery-Library, die hier heruntergeladen werden kann. Die Datei kann irgendwo in der Website, z.B. in der immer vorhandenen Formatbibliothek, abgelegt werden. Besser ist es aber sie direkt in einen passenden Ordner im Dateisystem der Webfrontendserver zu legen, weil man sie von dort in jeder beliebigen Website einbinden kann. Ich verwende dazu normalerweise einen eigenen Unterordner von 14\LAYOUTS.

Das Script muß in die Gestaltungsvorlage (Masterpage) eingebaut werden. Dazu öffnet man die Website in SharePoint Designer. Die Gestaltungsvorlage findet man (wer hätte es gedacht), wenn man links auf "Gestaltungsvorlagen" klickt. Die Standard-Masterpage heißt V4.master. Die Seite öffnet man in der Codeansicht und sucht das Ende des <head>-Abschnitts, d.h. man macht die Ergänzungen vor der schließenden Zeile </head>.

Zunächst wird die jQuery-Library referenziert:

/_layouts/Demo/jquery-1.6.1.min.js

jQuery macht es sehr einfach bestimmte Elemente anzusprechen. In diesem Fall suchen wir einen Link, also ein <a>-Element, der auf eine bestimmte URL verweist:

$("a[href=’/subsite/‘]")

Bei dieser Methode muß immer der gesamte Link angegeben werden (man beachte, daß Links in der Schnellstartleiste meist Server-relativ sind). Es gibt aber noch andere Möglichkeiten, als den direkten "ist gleich"-Vergleich:

= bedeutet "exakt gleich"
!= bedeutet "ungleich"
^= bedeutet "beginnt mit"
$= bedeutet "endet mit"
*= bedeutet "enthält"

Dem Link wird jetzt das Attribut target="_blank" angehängt:

.attr("target", "_blank")

Das gesamte Script sieht dann so aus:

/_layouts/Demo/jquery-1.6.1.min.js

  $(document).ready(function() {
    $(„a[href=’/subsite/‘]“).attr(„target“, „_blank“);
  });