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 🙂

Dynamische Titel bei benutzerdefinierten Formularen

Wenn man mit SharePoint Designer benutzerdefinierte Formulare anlegt, wird für den Titel, also für den Namen der Spalten, immer der aktuelle Wert fest eingetragen. Für den eigentlichen Feldwert, also für den Teil mit dem die Benutzer später interagieren (z.B. eine Textbox oder Checkbox) wird ein FieldControl eingefügt. Ebenso für den Beschreibungstext einer Spalte. Wenn man dann später etwas ändert, indem man z.B. bei einem Auswahlfeld von DropDown auf Radiobuttons umschaltet, dann sind diese Änderungen auch sofort in den benutzerdefinierten Formularen zu sehen. Wenn man aber den Spaltennamen ändert, wird im benutzerdefinierten Formular immer noch der alte Name angezeigt. Außerdem wird dieser Name immer nur in einer Sprache angezeigt und paßt sich nicht an, wenn ein Benutzer eine andere Sprache auswählt.

Um dieses Problem zu lösen, kann man in der Codeansicht von SharePoint Designer den fest eingefügten Text durch ein in SharePoint immer vorhandenes Control ersetzen:

<SharePoint:FieldLabel ControlMode="Display" runat="server" FieldName="Title" />

Für das FieldName-Attribut gibt man wie immer den internen Namen der Spalte an. Das ControlMode-Attribut muß angegeben werden, aber es macht keinen Unterschied, ob man Display, Edit oder New einträgt.

Das war’s auch schon. Es ist mir (mal wieder) ein Rätsel, warum SharePoint Designer das nicht gleich so macht.

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>

List-Joins und Projected Fields

In einem älteren Beitrag habe ich gezeigt, was mit CAML-Abfragen grundsätzlich möglich ist. Als Ergänzung dazu soll dieser Beitrag zeigen, wie man mit solchen Abfragen zwei oder mehr Listen verbinden kann, analog zum SQL JOIN-Statement. Man erhält dadurch mit einer einzigen Abfrage eine Ergebnisliste, die Daten aus mehreren Listen enthält. Der Mechanismus funktioniert allerdings nur, wenn die Listen durch Nachschlagefelder miteinander verbunden sind.

Motivation

Bei einem Nachschlagefeld kann man zwar weitere Felder der Nachschlageliste auswählen, die dann ebenfalls in Ansichten der Liste dargestellt werden, aber das funktioniert nur mit einer Ebene. Es funktioniert nicht, wenn Daten aus mehr als zwei Listen benötigt werden.

Und es funktioniert nur mit Standard-Nachschlagespalten. Es gibt im Web mehrere Implementierungen eigener Feldtypen, die Nachschlagefelder erweitern, z.B. um ein gefiltertes oder kaskadierendes Nachschlagen zu ermöglichen oder um das Nachschlagefeld mit einem Picker-Dialog auszustatten. Die meisten dieser Implementierungen sind vom Standard-Nachschlagefeld abgeleitet und bieten von Haus aus nicht mehr die Möglichkeit weitere Felder darzustellen. Weil die Daten aber genau wie bei den Standardfeldern gespeichert werden, können diese Felder für CAML List-Joins herangezogen werden.

Umgebung

Für die Demonstration habe ich folgende Umgebung eingerichtet: eine Liste mit Projekten und den Feldern Projektname, Projektnummer und geplantes Projektende. Und eine weitere Liste Projektzeiten, in der die Benutzer ihre Arbeitszeiten eintragen können, die sie jeweils für ein Projekt aufgewendet haben. Diese Liste hat die Felder Datum, Stunden, ein Bemerkungsfeld und natürlich ein Nachschlagefeld auf die Projekte.

Das ist die Projektliste:

Und das die Liste mit den Projektzeiten:

Für eine Übersicht sollen jetzt alle erfaßten Zeiten mit zusätzlichen Informationen aus der Projektliste dargestellt werden.

So geht’s

Man legt für die Liste Projektzeiten eine neue Ansicht an und öffnet die Website in SharePoint Designer. Wenn man über Listen und Bibliotheken auf die Liste navigiert, findet man rechts die Ansichten dieser Liste und kann diese neue Ansicht öffnen. Man markiert in der Entwurfsansicht die Tabelle und schaltet in die Codeansicht um. Jetzt sieht man die CAML-Abfrage, die dieser Ansicht zugrunde liegt. Sie sieht leicht gekürzt ungefähr so aus:

<View>
  <Query>
    <OrderBy>
      <FieldRef Name=“Projekt“/>
    </OrderBy>
  </Query>
  <ViewFields>
    <FieldRef Name=“Projekt“/>
    <FieldRef Name=“Datum“/>
    <FieldRef Name=“Stunden“/>
    <FieldRef Name=“Author“/>
  </ViewFields>
  <RowLimit Paged=“TRUE“>100</RowLimit>
  <Aggregations Value=“Off“/>
  <Toolbar Type=“Standard“/>
</View>

Diese Abfrage wird jetzt editiert. Zunächst verbindet man die Listen durch ein Join-Element, das direkt als Kind des View-Elements notiert wird:

<Joins>
<Join Type=“INNER“ ListAlias=“Projekte“>
    <Eq>
      <FieldRef Name=“Projekt“ RefType=“Id“/>
      <FieldRef List=“Projekte“ Name=“ID“/>
    </Eq>
  </Join>
</Joins>

Als Join-Type kann dabei INNER oder LEFT angegeben werden. Das Ganze funktioniert analog zum bekannten JOIN-Statement in SQL.

Als ListAlias kann ein beliebiger Name gewählt werden. Dieser Name wird später bei den ProjectedFields verwendet (s.u.). Der ListAlias ist außerdem zur Unterscheidung wichtig, falls eine Liste mehrere Nachschlagefelder auf dieselbe Liste hat. Man denke z.B. an eine Liste mit Adressen, aus der sowohl eine Rechnungs- als auch eine Lieferadresse ausgewählt werden soll. Die Liste selbst muß nicht explizit angegeben werden. Sie wird über die Definition der verknüpfenden Felder identifiziert (s.u.).

Als Vergleichsoperator muß immer Eq angegeben werden.

Der Vergleichsoperator Eq enthält immer zwei FieldRef-Elemente, die die Verknüpfung definieren. Das erste stellt dabei immer das Nachschlagefeld der primären Liste dar, das (wie immer in CAML) durch seinen internen Namen referenziert wird. Als RefType wird immer Id angegeben. Falls die primäre Liste der Verknüpfung nicht die eigentliche Liste ist, für die diese Ansicht erstellt wird, z.B. weil es eine weitere Verbindung der Projekte zu einer Kundenliste gibt, muß in einem zusätzlichen List-Attribut auch die Liste angegeben werden. Das zweite FieldRef-Element gibt die Liste an, auf die nachgeschlagen wird. Als Name wird immer ID angegeben.

Damit ist die Verbindung der Listen grundsätzlich definiert. Als nächstes müssen alle Felder der verknüpften Listen deklariert werden, die in irgendeiner Form in der Abfrage verwendet werden sollen, egal ob zur Anzeige, Sortierung oder als Filter. Das geschieht in einem ProjectedFields-Element:

<ProjectedFields>
<Field Name=“Projektname“ Type=“Lookup“ List=“Projekte“ ShowField=“Title“/>
  <Field Name=“Projektnummer“ Type=“Lookup“ List=“Projekte“ ShowField=“Projektnr“/>
  <Field Name=“Projektende“ Type=“Lookup“ List=“Projekte“ ShowField=“GeplEnde“/>
</ProjectedFields>

Jedes Field-Element gibt im Name-Attribut einen Namen an, unter dem das Feld an anderen Stellen der Abfrage identifiziert wird. Der Name wird außerdem bei angezeigten Feldern als Spaltenüberschrift verwendet. Leider muß dieser Name XML-konform sein, so daß das einzige verwendbare Sonderzeichen der Unterstrich ist. Alle anderen nicht-alphanumerischen Zeichen wie Leerzeichen, Punkt oder Komma sind nicht erlaubt.

Das Type-Attribut wird immer mit Lookup belegt.

Das List-Attribut enthält den ListAlias, der oben beim Join (s.o.) angegeben wurde.

Das ShowField-Attribut enthält wiederum den internen Namen des Feldes aus der Nachschlageliste.

Damit stehen die Felder aus der Nachschlageliste in der Abfrage zur Verfügung und können z.B. angezeigt werden. Man kann sie auch problemlos zum Sortieren oder als Filter in der Where-Bedingung verwenden. Sie werden dabei wie gewohnt als FieldRef-Elemente notiert. Als Name wird dabei der oben bei den ProjectedFields angegebene Name verwendet.

Zur Anzeige der Felder werden diese bei den ViewFields notiert:

<ViewFields>
  <FieldRef Name=“Projekt“/>
  <FieldRef Name=“Projektnummer“/>
  <FieldRef Name=“Projektende“/>
  <FieldRef Name=“Datum“/>
  <FieldRef Name=“Stunden“/>
  <FieldRef Name=“Author“/>
</ViewFields>

Die Ansicht der Projektzeiten sieht dann so aus:

Erweiterungen

Wie oben bereits angedeutet, ist dieser Mechanismus nicht auf zwei Listen beschränkt. Es lassen sich damit Daten aus vielen Listen in einem Rutsch abfragen, solange die Listen über Nachschlagefelder verbunden sind.

In unserem Beispiel wäre es denkbar, daß die Projektliste ein Nachschlagefeld auf eine weitere Liste Kunden enthält, mit dem jedes Projekt einem Kunden zugeordnet wird. Man kann dann in einer Ansicht sowohl die erfaßten Projektzeiten als auch die Daten des Projekts und des zugehörigen Kunden (z.B. seine Adresse) anzeigen.

Per Workflow feststellen, ob ein Nachschlagefeld leer ist

Man sollte meinen, daß man in einem SharePoint Designer 2010 Workflow mit Hilfe der Bedingung Feld ist leer sehr einfach feststellen kann, ob ein bestimmtes Feld leer ist oder nicht. Bei Nachschlagefeldern geht das aber leider nicht ganz so einfach.

Die Bedingung Feld ist leer ist nur dann wahr, wenn das Nachschlagefeld noch nie Daten enthalten hatte. Wenn ein Nachschlagefeld zu einem früheren Zeitpunkt Daten enthielt und dann wieder geleert wurde, ist es nicht leer sondern enthält eine Null. Um das zu prüfen, muß man sich den Wert in eine Variable holen und diese dann auf Null prüfen. Das wiederum funktioniert aber nur, wenn das Nachschlagefeld auch eine Null (oder einen echten Wert) enthält. Wenn das Nachschlagefeld leer ist, bekommt man dabei einen Fehler. Für eine vollständige Prüfung müssen also beide Bedingungen kombiniert geprüft werden.

Der Ablauf ist also folgender:

Man legt eine neue Variable an, indem man im Workflowdesigner auf Lokale Variablen klickt und im Dialog dann auf Hinzufügen. Man vergibt einen sinnvollen Namen und wählt als Typ Ganze Zahl.

Wenn man die verwendete Variable weiter oben im Workflow bereits benutzt hat – oder wenn man einfach auf Nummer sicher gehen möchte – setzt man sie zuerst explizit auf Null. Das geht mit der Aktion Workflowvariable festlegen.

Jetzt prüft man mit der Bedingung Wenn das aktuelle Elementfeld gleich Wert ist, ob das Nachschlagefeld leer ist. Wenn man ein Nachschlagefeld einer anderen Liste prüfen möchte, verwendet man die Bedingung Wenn ein beliebiger Wert gleich Wert ist. Für den ersten Wert wählt man das Nachschlagefeld aus. Als Vergleichsoperator ist gleich eingestellt, das man durch Klicken in ist nicht leer ändert. Der zweite Wert, der sonst als Vergleich herangezogen wird, verschwindet dadurch und muß nicht angegeben werden.

Innerhalb der Bedingung verwendet man jetzt wieder die Aktion Workflowvariable festlegen und weist damit der Variablen den Wert des Nachschlagefelds zu (von dem man jetzt annimmt, daß es nicht leer ist). Dabei ist ganz wichtig, daß bei Feld zurückgeben als Als ganze Zahl ausgewählt wird. Man verwendet hier normalerweise Nachschlage-ID (als ganze Zahl), aber das erzeugt einen Fehler, wenn das Nachschlagefeld nur eine Null und keinen gültigen Wert enthält.

Damit hat man jetzt in der Variablen entweder die Zahl Null, falls das Nachschlagefeld leer ist, oder die ID des nachgeschlagenen Elements. Durch eine weitere Bedingung kann man jetzt die Variable prüfen und entsprechend reagieren. Zusammengefaßt sieht das dann so aus:

Eine Ja/Nein-Spalte ausblenden

Normalerweise kann man einzelne Spalten sehr einfach über die Einstellungen beim Inhaltstyp ausblenden. Diese Möglichkeit hat uns Microsoft (warum auch immer) bei Ja/Nein-Spalten leider genommen. In diesem Beitrag wird gezeigt, wie man das mit Hilfe von SharePoint Designer erreichen kann.

Wenn man bei einem Inhaltstyp auf einen Spaltennamen klickt, gelangt man zu einer Seite, auf der man u.a. angeben kann, ob diese Spalte erforderlich, optional oder ausgeblendet sein soll. Z.B. bei einer Standard-Titelspalte sieht das so aus:

Diese Einstellungen sind bei einer Ja/Nein-Spalte ausgegraut, so daß diese praktische Möglichkeit ein Feld zu verstecken hier nicht zur Verfügung steht. Weil ich gerade mal wieder darüber gestolpert bin, habe ich kurz Google bemüht. Eigentlich wollte ich herausfinden, warum das so ist. Das ist mir zwar auf die Schnelle nicht gelungen, aber dafür habe ich hier eine praktische Lösung gefunden. Ich halte das für eines der kuriosesten und am besten versteckten Features von SharePoint Designer.

Um das Gewünschte zu erreichen, hatte ich bisher entweder SharePoint Manager bemüht, mit dem man die Hidden-Eigenschaft einer solchen Spalte problemlos umsetzen kann oder die Spalte wurde ohnehin per Feature erzeugt und auch dort kann man sie problemlos ausblenden.

Lösung mit SharePoint Designer

Man öffnet die Website in SharePoint Designer und geht dann entweder über die Websiteinhaltstypen oder über die Liste und deren Inhaltstyp(en). In den Einstellungen zum Inhaltstyp, der eine auszublendende Spalte enthält, klickt man entweder links oben auf Spalten bearbeiten oder auf den Link in der Mitte Inhaltstypspalten bearbeiten. Man gelangt zu einer Liste mit allen Spalten des Inhaltstyps:

Wie kann man die Einstellung in der dritten Spalte jetzt ändern? Ganz einfach, wenn man es weiß: man klickt genau dreimal auf den Wert in der dritten Spalte und siehe da, sie verwandelt sich in ein DropDown und man kann den Wert ändern.

„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“);
  });