Eigener Button zum Dateidownload

Folgende Situation: man hat ein selbstentwickeltes Webpart, das über einen Button eine Datei erstellt und dem Benutzer zum Download anbietet. Ein Benutzer klickt den Button und alles funktioniert, d.h. er bekommt die gewünschte Datei. Wenn er jetzt wieder in den Browser wechselt, reagiert kein Button mehr – auch der eigene nicht.

Die Ursache ist eine JavaScript-Variable von SharePoint, die beim Absenden des Formulars auf true gesetzt wird (um zu signalisieren, daß das Formular bereits gesendet wurde). Wenn der Code hinter einem eigenen Button jetzt eine Datei in den ResponseStream schreibt, bleibt dieser Variablenwert erhalten und verhindert weitere Postbacks. Man muß also nur diese Variable wieder auf false setzen und alles funktioniert wie gewünscht.

Getestet habe ich das mit folgendem einfachen Code in einem Webpart, der beim Klick auf einen Button eine einfache Textdatei an den Browser schickt. Der Browser reagiert mit seinem Download-Dialog darauf:

private void Button_Click(object sender, EventArgs e) {

  string text = "Hallo Welt";

  HttpResponse response = this.Page.Response;

  response.Clear();

  response.AddHeader("content-disposition", "attachment;filename=myfile.txt");

  response.ContentType = "application/text";

  response.Output.Write(text);

  response.End();

}

Der Button wurde dabei ebenso einfach erzeugt:

protected override void CreateChildControls() {

  Button btn = new Button();

  this.Controls.Add(btn);

  btn.ID = "myButton";

  btn.Text = "Download";

  btn.Click += Button_Click;

}

Man muß dem Button jetzt einfach noch ein wenig clientseitiges JavaScript mitgeben, das (nach einer kurzen Verzögerung) die SharePoint-Variable zurücksetzt und man kann auf den Button klicken so oft man möchte. Und auf alle anderen auch. Erreicht wird das durch eine weitere Zeile Code:

btn.OnClientClick = "window.setTimeout(function(){_spFormOnSubmitCalled=false;},10);";

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.

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!

Noch eine Anmerkung dazu: wenn es sich um ein Pflichtfeld handelt, wird das Wort „Pflichtfeld“ tatsächlich an den Namen angehängt.

<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>

MachineKeys in der web.config von SharePoint

Wenn man SharePoint mit FBA (Formularbasierter Authentifizierung / Forms Based Authentication) arbeitet, braucht man in vielen Fällen einen sogenannten MachineKey (Computerschlüssel) wenn man Benutzerpasswörter nicht im Klartext ablegen möchte. Sobald man eine Webanwendung für FBA konfiguriert, erzeugt SharePoint einen solchen Schlüssel und trägt ihn in die web.config der Webanwendung ein.

Wenn man mehrere Webanwendungen gegen dieselbe Benutzerdatenbank fahren möchte oder auch nach einer Migration, ist es notwendig hier einzugreifen und eigene Schlüssel zu verwenden. In diesem Fall wird man sich wahrscheinlich sehr schnell wundern, weil sich am nächsten Tag kein Benutzer mehr anmelden kann. Verantwortlich dafür ist ein Timerjob, der regelmäßig jede Nacht ausgeführt wird und der den ursprünglichen Schlüssel wieder in die web.config einträgt.

Man sollte jetzt aber nicht einfach den gesamten Timerjob deaktivieren, weil dieser mehrere Integritätsanalysen (Health Analysis) ausführt. Allerdings kann man die verantwortliche Regel der Integritätsanalyse deaktivieren. Für das Zurücksetzen des Computerschlüssels ist die Regel ViewStateKeysAreOutOfSync zuständig und man kann sie mit folgenden PowerShell-Befehlen deaktivieren:

Get-SPHealthAnalysisRule ViewStateKeysAreOutOfSync | Disable-SPHealthAnalysisRule

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>

Mit XML erzeugtes Personenfeld fehlt in listdata.svc

Heute mal wieder die Lösung für ein kurioses Problem, das mich extrem viele Nerven gekostet hat. Ich hatte eine Liste, die u.a. auch über den zu SharePoint gehörenden REST-Service listdata.svc bearbeitet werden soll. Das hat alles problemlos funktioniert, bis auf die Tatsache, daß ein Personenfeld von dem Service nicht ausgegeben wurde. Die Standard-Personenfelder Erstellt von und Geändert von allerdings schon. Auch ein manuell neu angelegtes Personenfeld machte keine Probleme.

Lösung: die Liste wurde samt Inhaltstyp und allen Feldern deklarativ per XML aus einem eigenen Feature erzeugt. Der Vergleich der SchemaXml-Eigenschaft des problematischen Feldes mit derselben Eigenschaft des manuell angelegten Feldes, brachte dann die Lösung. Man muß dem Personenfeld im XML ein weiteres Attribut List="UserInfo" mitgeben, dann klappt’s auch mit dem REST.

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: