0

My intent is to dynamically create a grouping of EditControlBlock (List Item Menu) custom actions for two different list view pages.

Upon loading the Active page, all custom actions are destroyed, and only those necessary for the Active view of this particular library are recreated. Meaning, only items marked as active appear in this list view, so I want the custom actions available to be only those relating to an active item.

The same would happen on the Inactive page but only custom actions related to Inactive items would appear in this list.

The issue I'm facing is that quite often, executeQueryAsync fails to run either success or failure functions as the method call suggests it should. The query is executed and the request seems to go off into infinity never to return (I know, I know not quite).

I've verified this erratic through a number of page-reloads. Every time the page reloads the code is executed again, with the desired result to see all the Active custom actions added to the ECB.

About 50% of the time I see all of them. The other 50% of the time I see between 3 and 7 of the items I've attempted to add.

This code is in a script linked to the master page:

function createTendersCustomActions(action) {
    var siteUrl = L_Menu_BaseUrl;
    var context = new SP.ClientContext.get_current(); 

    this.listRef = context.get_web().get_lists().getByTitle('Tenders');
    var collUserCustomAction = listRef.get_userCustomActions();

    var oUserCustomAction = collUserCustomAction.add();
    oUserCustomAction.set_location('EditControlBlock');
    oUserCustomAction.set_sequence(action.sequence);
    oUserCustomAction.set_title(action.title);
    oUserCustomAction.set_url('javascript:alert(' + action.workflow +')');
    oUserCustomAction.update();

    context.load(listRef, 'Title' ,'UserCustomActions');    
    context.executeQueryAsync(Function.createDelegate(this, onQuerySucceeded), Function.createDelegate(this, onQueryFailed));

    function onQuerySucceeded() {   
        console.log('Custom action ' + action.title + ' created for ' + this.listRef.get_title());    
    }   
    function onQueryFailed(sender, args) {  
        console.log('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
    }
}

function deleteTendersCustomActions() {
    var siteUrl = L_Menu_BaseUrl;
    var context = new SP.ClientContext.get_current(); 

    this.listRef = context.get_web().get_lists().getByTitle('Tenders');
    this.collUserCustomActions = listRef.get_userCustomActions();

    //delete all Custom Actions
    collUserCustomActions.clear();

    context.load(collUserCustomActions);
    context.executeQueryAsync(Function.createDelegate(this, onQuerySucceeded), Function.createDelegate(this, onQueryFailed));

    function onQuerySucceeded() {
    }

    function onQueryFailed(sender, args) {  
        console.log('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
    }
}

var tendersCustomActions_Active = 
[
    { "workflow" : "Assign To", "title" : "Assign To", "sequence" : "0" }, 
    { "workflow" : "Log Check In", "title" : "Log Check In", "sequence" : "1"  },
    { "workflow" : "Log Check Out", "title" : "Log Check Out", "sequence" : "2"  },
    { "workflow" : "Mark as Issued", "title" : "Issue", "sequence" : "3"  },
    { "workflow" : "Mark as Closed", "title" : "Mark as closed", "sequence" : "4"  }, 
    { "workflow" : "Make Inactive", "title" : "Make Inactive", "sequence" : "5"  },     
    { "workflow" : "Mark as Cancelled", "title" : "Cancel", "sequence" : "6"  },
    { "workflow" : "Mark as On Hold", "title" : "Mark as On Hold", "sequence" : "7"  },
];

var tendersCustomActions_Inactive = 
[
    { "workflow" : "Make Active", "title" : "Assign To", "sequence" : "0" }, 
];

function addTendersCustomItems_Active() {
    for (var i = 0; i < tendersCustomActions_Active.length-1; i = i+1) {
        createTendersCustomActions(tendersCustomActions_Active[i]);
    }
}

function addTendersCustomItems_Inactive() {
    for (var i = 0; i < tendersCustomActions_Inactive.length-1; i = i+1) {
        createTendersCustomActions(tendersCustomActions_Inactive[i]);
    }
}

And in the webpage in a hidden WebPart; executes every time this particular page loads:

<script type="text/javascript">
    ExecuteOrDelayUntilScriptLoaded(deleteTendersCustomActions, "sp.js");
    ExecuteOrDelayUntilScriptLoaded(addTendersCustomItems_Active, "sp.js");
</script>

I have read multiple cases of similar sort of behavior and yet I have also heard of others successfully using the SharePoint JavaScript Object Model to execute multiple asynchronous queries without a hitch.

I should let it be known I'm using IE8, the current standard in the organization.

I've been told, by a SharePoint system admin here, that using JavaScript to work with SharePoint objects is unreliable and is not to be trusted.

This might be so, but I would really like to know: What explains this erratic behavior? Is the server configured to throttle the queries coming through the JavaScript object model? Are the requests getting lost within network traffic?

3

1 Answer 1

1

Think about what you're doing: You are deleting and re-provisioning a list custom action, asynchronously, at the same time you are loading a page that will consume that custom action. I would expect unpredictable things to happen. Put this in a production environment with lots of concurrent users, each deleting and re-provisioning custom actions on the list, and you have a "hot mess".

There's a much easier way to accomplish what you're trying to do, and it will be much more functional.

So you have a list, and I'll assume there is a status field which can have values of 'Active' or "Inactive'. You want to call a certain function on Active items (let's call it ActiveFunc() ) and another function on Inactive items ( InactiveFunc() ).

SharePoint has a JavaScript 'hook' that will enable to you to dynamically inject items into the ECB when it is called. Jan Tielens has written an excellent blog series that goes into great detail on this functionality. The posts were written when SharePoint 2007 was still new, but this code works just fine in 2010 and 2013.

You'll want to call a function called Custom_AddListMenuItems (for a doc lib, it's Custom_AddDocLibMenuItems ) ,check which view you are on, and add the right ECB entry (Here we are checking the URL via window.location for the view name):

function Custom_AddListMenuItems(m, ctx) {

    if (ctx.ListTitle == 'Tenders' && window.location.href.indexOf('ActiveTenders') > 0) {

        CAMOpt(m, "Call Active Func", "javascript:ActiveFunc();", "");
    }

     if (ctx.ListTitle == 'Tenders' && window.location.href.indexOf('InactiveTenders') > 0) {

        CAMOpt(m, "Call Inactive Func", "javascript:InactiveFunc();", "");
    }

}

The exact syntax is a little arcane but Jan's posts explain the nuts and bolts of what's going on. So now we have code checking which view we are on and adding the right ECB entry.

But wait - we can do better than that.

Since we are writing JavaScript to dynamically add these entries into the ECB, we can take it a step further and just check the status field itself rather than the view's URL. Doing this we can have the proper functionality in any view no matter what combination of statuses it may contain - even views that haven't been created yet.

To do this just write a simple query to check the status and adjust your code appropriately:

function Custom_AddListMenuItems(m, ctx) {

    if (ctx.ListTitle == 'Tenders' ) {    
    $.ajax({
        async: false,
        url: L_Menu_BaseUrl + "/_vti_bin/listdata.svc/Tenders(" + currentItemID + ")?$select=StatusValue",
        dataType: "json",
        success: function(data) {                                   
            switch(data.d.StatusValue)
            {
                case "Active":
                CAMOpt(m, "Call Active Func", "javascript:ActiveFunc();", "");
                break;
                case "Inactive":
                CAMOpt(m, "Call Inactive Func", "javascript:InactiveFunc();", "");
                break;
                default:
                //some other status, do nothing
                break;
            }                  

        },
        error: function(data) {
            //error, do something           
        }           
    });
  }
}

A couple of points to clarify here:

  • I'm using jQuery to call the REST interface to query the current item's status. It's important to set async = false because otherwise the function would return and build the default ECB before your code had a chance to return.
  • The REST interface treats Choice columns a little funny. My Status column's value was called StatusValue in the web service results. Check ListData.svc in the browser to verify your values.
  • That currentItemID variable is a SharePoint variable available in ECB code. Jan Tielens goes into more detail on this on his blog.

One last point - NEVER, EVER take JavaScript advice from a system administrator! :)

10
  • Thanks for the insightful and helpful post. I can see that I have more to learn about Asynchronous programming. It wasn't my intention to add jQuery but I'll see what I can do in that regards. The Sys Admins ALSO have a thing about modifying master pages. The CAMOpt method seems very helpful in this case. Perhaps I will get to try this out later today. Thanks again.
    – adnauseam
    Commented Jul 3, 2013 at 13:25
  • If you are not using jQuery, you can make ajax calls with native javascript using the XmlHttpRequest object. Chapter 1 of any good ajax book will have a code sample. Commented Jul 3, 2013 at 13:56
  • url: L_Menu_BaseUrl + "/_vti_bin/listdata.svc/DocumentDefects(" + currentItemID + ")?$select=StatusValue" Is "DocumentDefects" a folder specific to your site? I'm afraid I don't quite understand ListData.svc. I've loaded jQuery and the function triggers, but returns with failure so far. I'll keep plugging. Thanks for the push in the right direction!
    – adnauseam
    Commented Jul 5, 2013 at 13:45
  • As it turns out ListData.svc is unavailable. I'm not in charge of the servers so getting it set up is going to be a head ache. Looking for alternate ways to query for data.
    – adnauseam
    Commented Jul 5, 2013 at 14:10
  • OK, if ListData.svc won't work, you'll have to use the Lists.asmx web service. You can use SPServices (really well documented on this site and elsewhere) or you can use raw jQuery. On part three of the blog I referenced above there is a really god example of that. You CANNOT use CSOM/JSOM because it has no option to query synchronously. Commented Jul 5, 2013 at 17:36

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.