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.

Kommentar verfassen