SharePoint, JavaScript und Promises

Für alle, die noch für On-Premises SharePoint Umgebung Lösungen auf Basis von JavaScript erstellen ist die JSOM (JavaScript Object Model) Bibliothek von SharePoint ein sehr guter Weg um mit SharePoint zu kommunizieren. Auch wenn viele Dinge auch per REST Interface zugreifbar sind, so sind die Möglichkeiten über JSOM doch deutlich umfangreicher.

Da die Kommunikation bei JSOM grundsätzlich asynchron abläuft, muss man an dieser Stelle immer sehr viel mit Callbacks arbeiten. Ein typischer Code um z.B. eine neue Liste anzulegen sieht dann etwa so aus:

var ctx = new SP.ClientContext();
var web = ctx.get_web();
var listCI = new SP.ListCreationInformation();
listCI.set_title("Meine Neue Liste");
list = web.get_lists().add(listCI);
ctx.load(list);
ctx.executeQuery(successCreatedList, errorCreatedList);

function successCreatedList(sender, eventArgs) { }
function errorCreatedList(sender, eventArgs) { }

Wenn nun nach dem erstellen der List in dieser List gleich Elemente erstellt werden müssen/sollen, dann muss diese Logik in der Funktion successCreatedList implementiert werden. Das sähe dann wie folgt aus:

function successCreatedList(sender, eventArgs) {
    var listItemCI = new SP.ListItemCreationInformation();
    var listItem = list.addItem(listItemCI);
    listItem.set_item("Title", "Mein neues Listenelement");
    listItem.Update();
    ctx.load(listItem);
    ctx.executeQuery(successCreatedItem, errorCreatedItem);
}

Man sieht also schon, dass hier schon wieder zwei Funktionen als Callback angegeben werden (müssen). Natürlich kann man die Funktionen auch inline definieren – das macht es aber nicht viel übersichtlicher. Dieses Beispiel komplett sieht dann so aus:

var ctx = new SP.ClientContext();
var web = ctx.get_web();
var listCI = new SP.ListCreationInformation();
listCI.set_title("Meine Neue Liste");
list = web.get_lists().add(listCI);
ctx.load(list);
ctx.executeQuery(
    function (sender, eventArgs) {
        var listItemCI = new SP.ListItemCreationInformation();
        var listItem = list.addItem(listItemCI);
        listItem.set_item("Title", "Mein neues Listenelement");
        listItem.Update();
        ctx.load(listItem);
        ctx.executeQuery(
            function (sender, eventArgs) {
                // do something after the item is created
            }
            , function (sender, eventArgs) {
                // error handling
            });
    }
    , function (sender, eventArgs) {
        // error handling
    });

Wie wäre es, wenn nun executeQuery ein Promise zurückliefern würde, anstatt dass man Callbacks angeben muss?

var ctx = new SP.ClientContext();
var web = ctx.get_web();
var listCI = new SP.ListCreationInformation();
listCI.set_title("Meine Neue Liste");
list = web.get_lists().add(listCI);
ctx.load(list);
ctx.executeQueryDeferred()
    .then(function (eventArgs) {
        var listItemCI = new SP.ListItemCreationInformation();
        var listItem = list.addItem(listItemCI);
        listItem.set_item("Title", "Mein neues Listenelement");
        listItem.Update();
        ctx.load(listItem);
        return ctx.executeQueryDeferred();
    })
    .then(function (eventArgs) {
        // do something after the item is created
    })
    .fail(function (eventArgs) {
        // catch any errors along the way
    });

Das sieht schon deutlich übersichtlicher aus, da sich die einzelnen Promises auch hintereinander hängen lassen.

Um dem SharePoint ClientContext Promises beizubringen ist einfach nur eine kleine Erweiterung notwendig:

SP.ClientContext.prototype.executeQueryDeferred = function () {
    var deferred = Q.defer();
    this.executeQueryAsync(
        function (sender, args) { deferred.resolve(args); }
        , function (sender, args) { deferred.reject(args); });
    return deferred.promise;
}; 

In diesem Fall benutze ich die JavaScript Bibliothek Q für die Bereitstellung von Promises, hier wäre aber auch anderen Promises-Implementierungen denkbar.

Damit das ganze auch in TypeScript nutzbar ist, hier einmal die Erweiterung in TypeScript:

export interface ClientContext {
    executeQueryDeferred(): Q.Promise<SP.ClientRequestSucceededEventArgs>
}

SP.SOD.executeOrDelayUntilScriptLoaded(function () {
    SP.ClientContext.prototype.executeQueryDeferred = function (): Q.Promise<SP.ClientRequestSucceededEventArgs> {
        let deferred = Q.defer<SP.ClientRequestEventArgs>();
        this.executeQueryAsync(
            function (sender: any, args: SP.ClientRequestSucceededEventArgs) { deferred.resolve(args); },
            function (sender: any, args: SP.ClientRequestFailedEventArgs) { deferred.reject(args) }
        );
        return deferred.promise;
    }
}, "sp.js");

Zudem ist die Erweiterung noch in einen Script-OnDemand-Block eingebettet, um Sicherzustellen, dass es das Objekt ClientContext auch bereits gibt.

15. Treffen der SharePoint UserGroup Köln

Hallo Liebe Mitglieder und Interessierte,

ich möchte Sie heute herzlich zum 15. Treffen der SharePoint UserGroup Köln einladen. Die Sommerrpause ist bald vorbei, der Urlaubsteint muss dringend wieder ausbleichen, jetzt geht es endlich mit der SPUG Köln weiter. Wir treffen uns am 5. September im Rheinauhafen in Köln.

Unser Programm steht noch nicht ganz fest, wir sprechen noch mit spannenden Sprechern, mehr Infos gibt es in einer Woche.

 

Über Ihre Teilnahme und interessante Diskussionen würde ich mich sehr freuen.

Hier noch einmal die Daten:

SharePoint UserGroup Köln

Termin: Montag, 05.09.2016 – 18.00 Uhr

Ort: ConVista Consulting AG, Im Zollhafen 15/17, 50678 Köln

Parken: Parkplätze stehen in der Tiefgarage im Rheinauhafen zur Verfügung, bitte in der Nähe des Aufgangs 3.04 parken.

Öffentliche Verkehrsmittel:
– U-Bahn Linie 16 ab Dom-Hbf bis Haltestelle Ubierring (ca. 15 Min.), Fußweg vom Ubierring bis zu ConVista ca. 10 Minuten
– U-Bahn Linie 15 ab Rudolfplatz bis Haltestelle Ubierring (ca. 11 Min.), Fußweg vom Ubierring bis zu ConVista ca. 10 Minuten
– Bus Linie 106 ab Heumarkt bis Haltestelle Rheinauhafen (ca. 7 Min), Fußweg vom Rheinauhafen bis zu ConVista ca. 2 Minuten

Anmeldung:
Um Anmeldung für diese Veranstaltung wird bis zum 30.08.2016 gebeten.
Bitte melden Sie sich zum Treffen der SharePoint UserGroup nur an, wenn Sie auch wirklich teilnehmen möchten.

Die Anmeldung kann erfolgen über:
eMail: info@sharepoint-rhein-ruhr.de oder ug-koeln@mysharepoint.de

XING: https://www.xing.com/net/prib6b3dax/spugcgn

Ansprechpartner:
Andrej Doms (ConVista Consulting)
Tel. 0178 888 6018

Neben den Vorträgen gibt es bei einem Snack hoffentlich reichlich Gelegenheit für Sie, mit den anwesenden Experten ins Gespräch zu kommen.
Ich freue mich, Sie alle am 05. September begrüßen zu dürfen.

Mit freundlichen Grüßen

Andrej Doms

Mit Gulp nach SharePoint deployen

Nachdem man nun also seine Entwicklungsumgebung mit VSCode, Node, Bower und Gulp schon sehr gut im Griff hat, stellt sich noch die Frage: wie bekomme ich meine Dateien nun in den SharePoint?

Mit Hilfe von Gulp habe ich ja schon alles nett in einem Verzeichnis (z.B. dist) zusammen. Also muss ich das ja nur alles in den SharePoint laden. Dank Drag&Drop ist das ja recht einfach und der Browser bietet mir ja auch direkt an vorhandene Dateien zu ersetzen. Aber auf die Dauer ist es ja doch irgendwie nervig.

Eigentlich ist die Lösung recht einfach: WebDAV!

Ich kann ja jede SharePoint Bibliothek über WebDAV erreichen. Wenn ich nun auch weiß, dass ich diese WebDAV-Freigaben über eine UNC-Schreibweise ansprechen kann … so kann ich die Bibliothek http://sharepoint.acme.local/sites/henning/scripts auch über \\sharepoint.acme.local\DavWWWRoot\sites\henning\scripts ansprechen.

Entsprechend kann ich in meinem gulpfile.js einfach folgende Task mit aufnehmen

gulp.task('deploy', [], function () {
    return gulp.src(['dist/**/*'])
        .pipe(gulp.dest(\\sharepoint.acme.local\DavWWWRoot\sites\henning\scripts));
};

Wenn ich nun z.B. nach SharePoint-Online deployen will, dann geht das dort ebenfalls, nur dass ich \\henning.sharepoint.com@SSL\[…] schreiben muss, damit der Zugriff per SSL läuft (ich kann SSL natürlich auch bei On-Premise Installationen verwenden, nur bei Office365 geht halt ausschließlich SSL).

Entwicklung mit Visual Studio Code

Ich habe in vorherigen Beiträgen ja schon gezeigt, wie einfach Frontend-Entwicklung sein kann mit den richtigen Werkzeugen. Dazu bedarf es nicht immer einer riesigen IDE wie Visual Studio. Oftmals reicht einfach nur ein guter Editor wie zum Beispiel Notepad++.

Wenn die Entwicklung aber dann doch mal über ein paar Zeilen Code hinausgeht, dann wünscht man sich doch einen Editor, der einen ein wenig mehr unterstützt.

Also doch Visual Studio? Nicht unbedingt. Wenn ich mal eben ein kleines HTML-Frontend bauen will, dann brauch ich zwar schon ein wenig HTML und JavaScript – und genau hier kommt Visual Studio Code ins spiel.

Syntaxhighlighting in Visual Studio Code

Seit letztem Jahr ist VSCode als kleiner, schlanker Editor verfügbar, mit einer Vielzahl an Erweiterungen. Was mir am besten gefällt: VSCode ist keine 100MB schwer. Den kann man mal eben installieren – selbst auf meinem Surface (mit Atom-Prozessor).

Im Gegensatz zum klassischen Visual Studio kennt VSCode keine Solution-Dateien. Das Verzeichnis ist die Solution. Alles was im Verzeichnis ist gehört zum Projekt.

Über Tastenkombinationen kann man zwischen allen Dateien des Projekts navigieren – mit [STRG]+[P] kann man direkt zu einer Datei springen. Eine der Wichtigsten Tastenkombinationen ist [STRG]+[SHIFT]+[P]. Damit kommt man in einen “Kommandomodus” wo man eigentlich alle Funktionen von Visual Studio Code erreichen kann. Ersetzt man das > im Kommandomodus durch ein ? bekommt man eine einfach Hilfe angezeigt.

Kommandomodus von Visual Studio Code

Einige meiner persönlichen Highlights sind:

  • Syntax-Highlighting & Intellisense für verschiedene Sprachen (HTML, JavaScript, PowerShell …)
  • Git Integration
  • Vielfältige Extensions

Wer mehr Erfahren möchte, kann das über

Gulp: Wie Coffeescript zu JavaScript wird

Nachdem ja nun einfache Dinge mit gulp automatisiert werden können, kann man sich ja so langsam mal an weitere Aufgaben begeben.

Coffeescript kann bei der Arbeit mit JavaScript ja schon mal ganz hilfreich sein, nimmt es einem doch manch lästige Zeremonie von JavaScript ab und fügt gleich noch Best-Practices hinzu. Wenn man nicht immer diese .coffee-Datei nach JavaScript übersetzen (compilieren) müsste.

Mit gulp kann man das sehr gut automatisieren.

npm install gulp-coffee --save-dev

sorgt dafür, dass ich das notwendige gulp-coffee Package habe. Nun also noch eine entsprechen Task im gulpfile.js einfügen.

gulp.task('coffee', function () {
    return gulp.src('app/*.coffee')
        .pipe(coffee())
        .pipe(gulp.dest('app'));
})

In diesem Fall werden also alle von Coffee-Script erzeugten Dateien ebenfalls in dem Verzeichnis app gespeichert.

Insgesamt sieht das Build Script nun so aus:

var gulp = require('gulp'),
    coffee = require('gulp-coffee'),
    uglify = require('gulp-uglify');

gulp.task('html', function () {
    return gulp.src('app/*.html')
        .pipe(gulp.dest('dist'));
})

gulp.task('js', ['coffee'], function () {
    return gulp.src('app/*.js')
        .pipe(uglify())
        .pipe(gulp.dest('dist'));
})

gulp.task('coffee', function () {
    return gulp.src('app/*.coffee')
        .pipe(coffee())
        .pipe(gulp.dest('app'));
})

gulp.task('default', ['html', 'js'], function () {
})

Die Task „js“ ist dabei Abhängig von der Coffee-Task. In der Konsole sieht die Ausführung dann so aus:

D:\projects\html_app>gulp
[14:43:24] Using gulpfile D:\projects\html_app\gulpfile.js
[14:43:24] Starting 'html'...
[14:43:24] Starting 'coffee'...
[14:43:24] Finished 'coffee' after 34 ms
[14:43:24] Starting 'js'...
[14:43:24] Finished 'html' after 47 ms
[14:43:24] Finished 'js' after 43 ms
[14:43:24] Starting 'default'...
[14:43:24] Finished 'default' after 13 μs

Gerade beim Umgang mit Coffeescript ist das Debugging des JavaScript Codes z.B. in der Konsole der Browsers etwas umständlich, denn hier wird ja nicht der eigentliche Coffeescript Code debuged, sondern der von Coffeescript erzeugte JavaScript Code. Moderne Browser können aber mit Hilfe von sogenannte SourceMaps auch den ursprünglichen Quellcode anzeigen und debuggen – hier also Coffeescript. Und auch dabei kann gulp helfen.

Dazu erweitern wir die Task coffee entsprechend.

gulp.task('coffee', function () {
    return gulp.src('app/*.coffee')
        .pipe(sourceMaps.init())
        .pipe(coffee())
        .pipe(sourceMaps.write('../dist'))
        .pipe(gulp.dest('app'));
})

Nun wird für jede Coffeescript-Datei eine entsprechende SourceMap-Datei in dem Verzeichnis dist erstellt.

Gulp: JavaScript-Buildsystem

Nachdem ich ja im letzten Post beschrieben habe, wie man mit Hilfe von node und bower sich JavaScript Bibliotheken für seine Anwendung einbinden kann, will ich nun noch etwas mehr zeigen, was einem Node so zu bieten hat.

Typische Aufgabe während der Entwicklung ist ja, dass die HTML und JavaScript-Dateien vom lokalen Rechner auf einen Web-Server kopiert werden müssen. Dazu müssen zunächst alle Dateien, die auf den Server kopiert werden müssen identifiziert und dann kopiert werden.

In reinen .Net Projekten würde ich für das Deployment vielleicht zu MSBuild greifen – das hilft mir aber in meinem Fall nicht so wirklich weiter.

Mit gulp gibt es ein Buildsystem als Node-Module in JavaScript.

npm install gulp -g

installiert gulp und stellt es global zur Verfügung. Nun kann ich in meinem Projekt ein gulpfile.js erstellen.

Ein einfaches Buildfile kann z.B. so aussehen:

var gulp = require('gulp');
gulp.task('copy', function(){
    return gulp.src('app/*.html')
        .pipe(gulp.dest('dist/'));
})

Dabei werden alle *.html Dateien aus dem Verzeichnis app in das Verzeichnis dist kopiert. Existiert das Verzeichnis dist noch nicht, wird es zuvor erstellt. Dieses Beispiel ist natürlich sehr einfach. Mittels gulp copy kann nun die neue copy-Task von einer Konsole ausgeführt werden:

D:\projects\html_app>gulp copy
[14:25:01] Using gulpfile D:\projects\html_app\gulpfile.js
[14:25:01] Starting 'copy'...
[14:25:01] Finished 'copy' after 17 ms

Ein etwas fortgeschritteneres Buildfile könnte wie folgt aussehen:

var gulp = require('gulp'),
    uglify = require('gulp-uglify');

gulp.task('html', function () {
    return gulp.src('app/*.html')
        .pipe(gulp.dest('dist'));
})

gulp.task('js', function () {
    return gulp.src('app/*.js')
        .pipe(uglify())
        .pipe(gulp.dest('dist'));
})

gulp.task('default', ['html', 'js'], function () {
})

Die Task default wird dabei automatisch aufgerufen, wenn man gulp von der Kommandozeile startet. Als zweiter Parameter der Task werden die abhängigen Tasks angegeben. Diese werden also automatisch zuvor ausgeführt.

D:\projects\html_app>gulp
[14:26:15] Using gulpfile D:\projects\html_app\gulpfile.js
[14:26:15] Starting 'html'...
[14:26:15] Starting 'js'...
[14:26:15] Finished 'js' after 39 ms
[14:26:15] Finished 'html' after 49 ms
[14:26:15] Starting 'default'...
[14:26:15] Finished 'default' after 12 μs

In diesem Fall werden alle *.js Datei aus dem Verzeichnis app mit dem uglify-Package minimiert und dann nach dist geschrieben. Somit kann man also in dem app-Verzeichnis die Anwendung entwickeln und in dist erhält man immer alle Dateien, die man auf den Server kopieren muss.

Frontend-Entwicklung mit Node und bower

Immer häufiger greife ich für Anpassungen nicht mehr zu Visual Studio, sondern zu „einfachen“ Editoren, mit denen ich „mal eben“ ein paar Anpassungen in JavaScript machen – oder doch die eine oder Anwendung komplett in HTML & JavaScript schreibe.

Auch wenn man alles nur mit Notepad machen kann, so ist das doch irgendwie auf die Dauer etwas lästig. Immer wieder man man JavaScript Dateien auf den Server kopieren oder man muss Coffee-Script Dateien auf der Kommandozeile durch den entsprechenden Compiler jagen und dann mit den anderen Scripten zum Server kopieren.

Mit Notepad++ kann man das schon etwas verbessern, indem man Plugins wie NppExec verwendet. Wie man mit NppExec Dateien deployen kann, habe ich ja in einem früheren Post schon mal beschrieben. Aber irgendwann hat man auch dort Grenzen erreicht.

Lange habe ich mich gefragt, ob ich Node wirklich brauche. Ich finde JavaScript toll, aber muss ich das mit Node auch auf dem Server ausführen? Inzwischen habe ich erkannt: Node ist doch irgendwie total cool, gerade um HTML & JavaScript basierte Lösungen zu erstellen.

Im Folgenden will ich einmal einen Eindruck geben, wie so ein Projekt exemplarisch aussehen kann.

Zunächst muss man natürlich Node installiert haben. Das geht am besten via Chocolatey. Hat man Chocolatey installiert kann man mit choco install nodejs Node installieren.

Für unsere neues Projekt muss als erstes ein Arbeitsverzeichnis erstellt werden und das für die Arbeit mit Node vorbereitet werden

mkdir html_app
cd html_app
git init & npm init

Als Ergebnis erhält man eine package.json Datei.

{ 
    "name": "html_app", 
    "version": "1.0.0", 
    "description": "", 
    "main": "index.html", 
    "author": "Henning Eiben", 
} 

Diese Datei beschreibt das aktuelle Projekt/Paket und dient auch dazu um alle verwendeten Pakete zu speichern. Nun kann man mit npm sich Node Pakete installieren. Als erstes installiere ich bower. Das ist ein Paket um JavaScript-Bibliotheken für Anwendungen zu verwalten. Da ich Bower häufiger gebrauche installiere ich das gleich global.

npm install -g bower 

Nun kann ich mir mit Bower ein paar Bibliotheken für meine JavaScript-Anwendung laden. In diesem Fall will ich Bootstrap und Knockout verwenden. Zuvor initialisiere ich mit bower init noch eine bower.json. Sie dient ähnlich wie die packages.json dazu um alle verwendeten Pakate zu speichern. Mit

bower install bootstrap knockout --save 

Kann ich nun die beiden Bibliiotheken installieren. Durch das --save werden die Bibliotheken in der bower.json als Abhängigkeit gespeichert. Die bower.json sieht dann so aus:

{ 
    "name": "html_app", 
    "version": "0.0.0", 
    "authors": [ 
        "Henning Eiben <eiben@busitec.de>" 
    ], 
    "ignore": [ 
        "**/.*", 
        "node_modules", 
        "bower_components", 
        "test", 
        "tests" 
    ], 
    "dependencies": { 
        "bootstrap": "~3.3.5", 
        "knockout": "~3.3.0" 
    } 
} 

Dabei wurde neben bootstrap und knockout auch jquery in der Version 2.1.4 installiert, weil das von Bootstrap benötigt wird – ohne dass ich mich darum kümmern musste.

D:\projects\html_app>bower install bootstrap knockout --save 
bower cached git://github.com/twbs/bootstrap.git#3.3.5 
bower validate 3.3.5 against git://github.com/twbs/bootstrap.git#* 
bower cached git://github.com/SteveSanderson/knockout.git#3.3.0 
bower validate 3.3.0 against git://github.com/SteveSanderson/knockout.git#* 
bower cached git://github.com/jquery/jquery.git#2.1.4 
bower validate 2.1.4 against git://github.com/jquery/jquery.git#>= 1.9.1 
bower install knockout#3.3.0 
bower install bootstrap#3.3.5 
bower install jquery#2.1.4 
knockout#3.3.0 bower_components\knockout 
bootstrap#3.3.5 bower_components\bootstrap 
└── jquery#2.1.4 
jquery#2.1.4 bower_components\jquery 

Nun kann mit dem Editor der Wahl begonnen werden die Anwendung zu erstellen. Die Bibliotheken, die über bower installiert wurden liegen dabei in dem Verzeichnis bower_components, die Node-Pakete liegen in node_modules.

Wenn man nun die Anwendung z.B. in git einchecked oder jemand anderem zur Verfügung stellt, dann muss man diese beiden Verzeichnissen nicht mit weitergeben.

Stattdessen reicht es mit npm install & bower install einfach die in der packages.json und bower.json gespeicherten Pakete und Bibliotheken wieder herzustellen.

Debuggen von SharePoint-Apps

Vor einiger Zeit wurde ich gefragt: “Wie kann ich eigentlich eine SharePoint(-Hosted) App debuggen?”.

Die erste Antwort ist natürlich: Developer Tools! Alle Browser verfügen heute über Developer-Tools, die das Debugging von JavaScript Anwendungen erlauben.

imageimage

Wenn ich die Anwendung im Browser ausführe kann ich in den Quellcode springen und dort – wie in Visual Studio – Breakpoints setzen. Anschließend muss ich die Seite ggf. noch einmal laden, damit der JavaScript-Code noch einmal ausgeführt wird und mein Breakpoint erreicht wird.

Anschließend kann ich den Wert von Variablen ansehen oder den Call-Stack betrachten und natürlich kann ich durch meinen Code-Steppen und genau die Ausführung meines Codes untersuchen.

image

Es gibt aber auch noch einen andere Weg: Visual Studio!

Wenn ich den IE als Browser für meine Anwendung verwende, dann kann ich meine Breakpoints auch direkt in Visual Studio in meinem JavaScript Code setzen. (Wenn man mehrere Browser installiert hat, habe ich in einem früheren Beitrag beschrieben, wie ich in einem SharePoint-App-Projekt den zu verwendenden Browser festlegen kann).

Anschließend kann ich meine Anwendung direkt in Visual Studio starten und sobald ich meinen Breakpoint erreiche kann ich meinen JavaScript Code in Visual Studio debuggen – mit alle Debugging-Funktionen die ich in Visual Studio zur Verfügung habe.

image

Ein Vorteil bei der Verwendung von Visual Studio ist, dass ich nicht zunächst die JavaScript Datei einmal im Browser geladen haben muss um den Breakpoint zu setzten.

Dokumentsets im Eigenbau

Dokumentsets sind ein sehr schicke Funktion von SharePoint Server. Leider gibt es die nur in der Server-Version – also nicht wenn man nur Foundation einsetzt.

Was also tun, wenn der Kunde nur eine Foundation hat, aber Anforderungen hat, die nach den Eigenschaften eines Document-Sets schreien?

Die Anforderungen

Vielleicht zunächst zu den Anforderungen des Kunden. Die Anforderung war, dass es ein Dokument gibt, zu welchem Anlagen existieren. Also eine Klassischer Master-Detail Beziehung zwischen den Dokumenten.

SharePoint kennt ja durchaus Anlagen – aber nur für Listenelemente, nicht bei Dokumenten. und Anlagen habe den Nachteil, dass es keine Versionierung und eigene Meta-Daten gibt. Das ist irgendwie doof. Schöner wäre es, wenn man also zu einem Dokument beliebig viele Anlagen anfügen könnte.

Natürlich hat mir diese Frage keine Ruhe gelassen und ich habe mir gedacht, dass sich das doch recht einfach lösen lassen muss.

Ein Lösungspfad

Fangen wir einmal ganz einfach an. Zuerst einmal brauchen wir zwei Dokument-Bibliotheken, eine für die Dokumente und eine für die Anlagen. In der Bibliothek der Anlagen wird dann eine Nachschlage-Spalte hinzugefügt, die auf die Dokumente verweist. Somit kann man also Anlagen hochladen und die mit einem Dokument verknüpfen.

doc_set_diy_01

Als nächstes sollten die Anlage immer beim jeweiligen Dokument mit angezeigt werden. Dazu wir die Standard-Ansicht des Dokuments bearbeitet und ein App-WebPart für die Anlagen Hinzugefügt.

doc_set_diy_02

Nun muss an das WebPart noch ein Filter übergeben werden. Dazu wird die ID des aktuellen Dokuments übergeben und in der Liste der Anlage in der Spalte QM-Dokumente gefiltert.

doc_set_diy_03

Wenn man nun also ein Dokument öffnet, sieht man nur die Anlage, die mit diesem Dokument verbunden sind.

Allerdings hat diese Lösung noch einen Haken: sie ist unhandlich in der Pflege. Wenn man eine neue Anlage zu eine Dokument hinzufügen will, dann muss man die Anlage in die Liste der Anlagen hochladen und in den Meta-Daten auf das Dokument verweisen. Das ist irgendwie doof. Besser wäre es ja, wenn man z.B. in der Detail-Ansicht des Dokuments sagen könnte “bitte eine neue Anlage für dieses Dokument hochladen”.

Das Customizing

Mit ein wenig JavaScript kann man das Handling der Lösung vereinfachen. Zunächst wird jQuery, ein File-Input Element und ein Button gebraucht.

<script type="text/javascript" src="https://mytenant.sharepoint.com/qm/scripts/jquery-1.8.2.min.js"></script>
<input type="file" id="fileSelectorInput">
<input type="button" onClick="javascript: DoUpload();" value="Go!">

Nun folgt also die Magie. Die liegt hier natürlich in der Funktion DoUpload. Hier wird zunächst die Datei aus dem Dateisystem gelesen und dann als ByteCharacter-Array an eine weiter Funktion übergeben.

function DoUpload() {
    var fileInput = jQuery('#fileSelectorInput');

    var file = fileInput[0].files[0];
    var reader = new FileReader();
    reader.onload = function (result) {
         var fileName = '',
         libraryName = '',
         fileData = '';
 
        var byteArray = new Uint8Array(result.target.result)
        for (var i = 0; i < byteArray.byteLength; i++) {
            fileData += String.fromCharCode(byteArray[i])
        }
        
        DoUploadInternal(file.name, fileData);
    };
    reader.readAsArrayBuffer(file);
}

Soweit ist ja noch alles ganz Simple. Als nächstes wird die Datei mittels JSOM in den SharePoint geladen, um direkt auch die Meta-Daten setzen zu können.

Zunächst wird die ID des aktuellen Dokuments aus der URL gelesen und der Wert des Feldes QMName aus dem Anzeigeformular. Denn wir wollen der Anlage nicht nur einen Verweis auf das aktuelle Dokument geben, sondern auch die Meta-Daten aus dem Feld QMName mit an die Anlage übergeben.

JSRequest.EnsureSetup();
var qmName = jQuery('h3:contains("QM Name")').closest('td').next('td').text();
var qmDokumentId = JSRequest.QueryString["ID"];

Nun brauchen wir einen ClientContext, damit wir eine neue Datei für SharePoint anlegen können. Dort fügen wir dann das Character-Array unserer Datei als Content an.

clientContext = new SP.ClientContext.get_current();
var oWebsite = clientContext.get_web();
var oList = oWebsite.get_lists().getByTitle("QM-Anlage");

var fileCreateInfo = new SP.FileCreationInformation();
fileCreateInfo.set_url(fileName);
fileCreateInfo.set_content(new SP.Base64EncodedByteArray());

for (var i = 0; i < fileContent.length; i++) {
    fileCreateInfo.get_content().append(fileContent.charCodeAt(i));
}

newFile = oList.get_rootFolder().get_files().add(fileCreateInfo);
clientContext.load(newFile);

Nun müssen wir noch die Meta-Daten setzen. Dazu holen wir zunächst alle Feld-Informationen und setzen dann den Wert für QMName und erstellen einen Lookup auf das aktuelle Dokument.

item = newFile.get_listItemAllFields();
clientContext.load(item);

item.set_item("QMName", qmName);
var object = new SP.FieldLookupValue();
object.set_lookupId(qmDokumentId);
item.set_item("QMDokument", object);
item.update();

clientContext.executeQueryAsync(successHandler, errorHandler);

Am Schluss wird das Ganze dann als asynchrone Query an den Server gesendet. Im successHandler wird dann noch die aktuelle Seite aktualisiert, damit das neu hinzufügte Dokument auch in der Liste erscheint.

SP.UI.ModalDialog.RefreshPage(SP.UI.DialogResult.OK); 

Fazit

Mit ein bisschen Creativität und ein wenig JavaScript Anpassungen kann man schon ganz ordentlich etwas aus SharePoint heraus kitzeln.

doc_set_diy_04

Autovervollständigung beim PeoplePicker

Wenn man in einer SharePoint-Liste ein Feld vom Typ “Personenauswahl” hinzufügt, dann bekommt man automatisch in den Formularen einen sogenannten PeoplePicker. Das ist ein Feld, in dem man einen Namen, ein Benutzerkonto oder eine eMail-Adresse eingeben kann. Wird der Benutzer erkannt, wird der Name unterstrichen dargestellt, ansonsten wird er mit einer roten Schlangenlinie versehen.

people_picker_01

Über das Adressbuch kann ich einen Suchdialog öffnen um nach Personen zu suchen, wenn ich z.B. nur einen Teil des Namens kenne.

people_picker_02

Das ist aber irgendwie lästig auf diese Art und Weise zu suchen, weil man immer diesen Dialog öffnen muss, dann auf Suchen klicken muss und den gefundenen Treffer auch noch übernehmen muss. Besser wäre es also, wenn man in dem eigentlichen Feld direkt einen Teil des Namens eingeben könnte und man würde eine kleine Liste mit Vorschlägen bekommen.

people_picker_03

Praktischerweise gibt es da auch schon was … mit JavaScript! Alexander Bautz hat vor ein paar Jahren auf seinem Blog beschrieben, wie man so einen People-Picker um Autovervollständigung ergänzen kann.

Ich will die notwendigen Schritte hier nicht wiederholen, stattdessen sei auf den Blogpost verwiesen. Die Einbindung ist eigentlich ganz einfach, erfordert lediglich einen Kniff – die GUID der Benutzerliste.

Um an die GUID zu kommen ruft man einfach die Benutzerliste auf. Jede Websitesammlung hat eine solche Liste, die man über /path-to-my-sitecollection/_catalogs/users erreichen kann. Anschließend die Quellcode-Ansicht für die Seite öffnen und nach ctx.ListName suchen. Voila, schon hat man die GUID.

Kleiner Hinweis noch: die Autovervollständigung kann nur Benutzer vorschlagen, die sich auch schon mal auf der Site angemeldet hatten, denn nur diese Benutzer sind in der Benutzerliste vorhanden.