1

I am trying to get a list named 'Tasks' that is contained within every Subsite in the WebCollection.

I successfully execute the AsyncQuery for the WebCollection with this function:

function getProjectSites() {
    clientContext = new SP.ClientContext.get_current();
    if (clientContext != undefined && clientContext != null) {
        var web = clientContext.get_web();
        this.webCollection = web.getSubwebsForCurrentUser(null);
        clientContext.load(this.webCollection);
        clientContext.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceeded), null);
    }
}

From here I am able to iterate through every Subsite and get their Title and their Relative Server Url with the following:

function onQuerySucceeded() {
    var webEnumerator = this.webCollection.getEnumerator();
    while (webEnumerator.moveNext()) {
        var subWeb = webEnumerator.get_current();
        console.log('Site: ' + subWeb.get_title() + ' URL: ' + subWeb.get_serverRelativeUrl());
    }
}

From this point is where I start getting errors, if I attempt to get the list 'tasks' and modify my function slightly to this:

function onQuerySucceeded() {
    var webEnumerator = this.webCollection.getEnumerator();
    while (webEnumerator.moveNext()) {
        var subWeb = webEnumerator.get_current();
        var listCollection = subWeb.get_lists();            
        taskList = listCollection.getByTitle('Tasks');
        clientContext.load(taskList);
        clientContext.executeQueryAsync(Function.createDelegate(this, this.onSubQuerySucceeded), null);

        console.log('Site: ' + subWeb.get_title() + ' URL: ' + subWeb.get_serverRelativeUrl());
    }
}

It passes the each Tasks list into the SubQuery Function fine, if the contents of the SubQuery are as follows:

function onSubQuerySucceeded(){
    console.log(taskList);
}

The output in Firebug is this, repeated for how many Subsites there are.

Object { $0_0: Object, $2_0: Object }  indexTest.aspx:691:3

But as soon as I attempt to get any variable from the Object for example with

function onSubQuerySucceeded(){
    console.log(taskList.get_title());
}

I start getting this error repeatedly, but not consistently.

Error: The property or field 'Title' has not been initialized. It has not been requested or the request has not been executed. It may need to be explicitly requested.

But the last item will print the actual Title of the list which is 'Tasks'. Any ideas? It seems odd that get_title will work for at least one of the Objects every time.

Update:

As per your suggestion, I have switched to the bind() method, strange that Microsoft use createDelegate() in the MSDN. And on your comment with my use of 'this.' Since ES5 when this is explicitly stated it doesn't send the global object any more, just what was declared for the function, so I'm structuring my parameter passing around that. You mention combining my query for the webcollection as well as the subweb's camlquery, is it ok to iterate through the webcollection before loading it into context?

Here's how I've restructured it so far:

 function getProjectSites() {
        clientContext = new SP.ClientContext.get_current();
        if (clientContext != undefined && clientContext != null) {
            var web = clientContext.get_web();
            this.webCollection = web.getSubwebsForCurrentUser(null);
            clientContext.load(this.webCollection);
            clientContext.executeQueryAsync(onQuerySucceeded.bind(this), null);
        }
    }
function onQuerySucceeded() {
    tableOfContentsHtml = '<h2>Project Site Index</h2>';
    var webEnumerator = this.webCollection.getEnumerator();
    while (webEnumerator.moveNext()) {
        var subWeb = webEnumerator.get_current();
        var listCollection = subWeb.get_lists();            
        var taskList = listCollection.getByTitle('Tasks');
        var camlQuery = new SP.CamlQuery();
        camlQuery.set_viewXml('' +
            '<View>'+
                '<ViewFields>'+
                    '<Query>'+
                        '<OrderBy>'+
                            '<FieldRef Name="DueDate"/>'+
                        '</OrderBy>'+
                    '</Query>'+
                '</ViewFields>'+
            '</View>');
        this.colListItems = taskList.getItems(camlQuery);
        clientContext.load(colListItems);
        clientContext.executeQueryAsync(onSubQuerySucceeded.bind(this), onSubQueryFailed.bind(this));
    }
}

function onSubQuerySucceeded(){
    //var listEnumerator = this.colListItems.getEnumerator();
    console.log(this.colListItems.get_count());
}

This code will produce the real individual item count for each list for each individual subsite. But attempting to even enumerate them, the commented out line in SubQuerySucceeded() it throws the same error as before. Is this to do with the Scope still? The only variable I'm using globally is the context between the two Query functions.

3

1 Answer 1

2

You run into trouble with your scope,

The way you use tasklist is like a global (not in the Global scope but in the functions scope)

But JSOM continues async so tasklist gets all different values

This will get you on the right track: Query multiple lists using javascript/CSOM
(and off that oldskool IE8 use of createDelegate)

Update

As said you problem is the scope

Think of it as your shoppingCart, you dropped items in another

You also do not have to carry that whole this scope with you

You can use JavaScripts .bind() to only carry with you (to other functions) what you need:

Find all Documents Lists in all SubSites (webs):

function getProjectSites() {
    var clientContext = new SP.ClientContext.get_current();
    var webCollection = clientContext.get_web().getSubwebsForCurrentUser(null);
    clientContext.load(webCollection);
    clientContext.executeQueryAsync(inspectWeb.bind(webCollection), null);
}
function inspectWeb() {
    var webCollection = this;//bound by calling definition
    var webEnumerator = webCollection.getEnumerator();
    while (webEnumerator.moveNext()) {
        var subWeb = webEnumerator.get_current();
        var shoppingCart = {
            sitetitle: subWeb.get_title(),
            url: subWeb.get_serverRelativeUrl(),
            list: subWeb.get_lists().getByTitle('Documents')
        }
        // this is from your code!! (abusing) a global clientContext
        // stricty speaking you should get the correct context based on the siteurl again
        // that way the inspectWeb function could be used (with a url PARAMETER) to inspect
        // individual Webs. Your function can now only work in the async chain
        // and only as no other code messes with the ``clientContext`` global var
        clientContext.load(shoppingCart.list);

        console.log('inspecting Site: ' + shoppingCart.sitetitle);
        clientContext.executeQueryAsync(
            inspectList.bind(shoppingCart),//successCallback
            function () {// errorCallback (for site without a Document list)
                console.warn('empty listCollection in', shoppingCart.url);
            }
        );        }    }
function inspectList() {
    var list = this.list;//var shoppingCart = this;
    console.log(list.get_itemCount(), 'items in', list.get_title(), 'from', shoppingCart.url);
    //to get the items itself you have to do more (async) calls: getItems
}
getProjectSites();

Notes

You are now missing the 'Documents' Library in the rootWeb itself

ideally the 2 functions should be merged into one inspectWeb( url ) function, If this===window you know it was NOT called from an async function

Then add a recursive call to dive into all subwebs

Update 2

To get you started with REST and Promises (the fetch API needs a Polyfill in IE!)

This dives into a site and all subwebs/lists/items:

console.clear();
function get(uri,title) {
    if (typeof uri === 'object') uri = uri.__deferred.uri;
    fetch(uri, {
        method: 'GET',
        credentials: 'same-origin',    // or credentials: 'include'         
        headers: new Headers({
            "Accept": "application/json; odata=verbose",
        })
    }).then(response => response.json())
        .then(data => {
             console.info(title,data.d);
            (data.d.results || [data.d]).forEach(item => {
                switch (item.__metadata.type) {
                    case 'SP.Web':
                        get(item.Lists,'Lists in Web: '+item.Title);
                        break;
                    case 'SP.List':
                        if(item.Title==='Tasks'){
                            get(item.Items,'Items in List: '+item.Title);
                        }
                        break;
                    default:
                        console.log('unknown type', item.__metadata.type);
                }
            });
        });
}
get('/sites/[YOURSITENAME]/_api/Web','Root');


iREST - iJS

5
  • Thanks very much, just implementing the suggestions now. I'll report back and mark correct/respond as soon as I work this into my solution.
    – C. Pritch
    Commented May 19, 2017 at 13:14
  • Hmm, it seems that I still get the same error which is odd. However I seem to be able to get the number of items in each list but not their id's, title's or anything else. I'm fairly sure I'm binding the ASyncQueries together correctly but I'll have to elaborate later OOH when I get the chance.
    – C. Pritch
    Commented May 19, 2017 at 14:29
  • It is not something you get your head around in 5 minutes.. or 10 or days . Post you code as update in your question Commented May 19, 2017 at 14:33
  • I've updated my code to reflect some changes made. Still a bit stuck, but I'll keep fiddling around with it this weekend.
    – C. Pritch
    Commented May 20, 2017 at 20:35
  • I've marked this question as correct as it provides all I need to solve the problem, I'll update my answer a final time once I've gotten it working correctly.
    – C. Pritch
    Commented May 23, 2017 at 9:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.