Using RefreshCommandUI with the Server Ribbon (Dallas Tester, Fred Mameri)

Wictor Wilen, one of our MVPs, asked for some illumination around the RefreshCommandUI function in SharePoint 2010. I've been working with that particular function lately, so I have a pretty simple (but useful) example for all of you Server ribbon users out there. I also want to give a shout out to Fred Mameri, a dev on SharePoint here at Microsoft. He was absolutely critical in helping me create this post. Thank you for the inspiration and the code contribution!

In this example, I'll show you how to enable a button based on the file type selected in a list. This can be used to give your users contextual button availability if a certain file type is in a list. This is using purely using the EcmaScript-based object model and declarative customization of the Server ribbon. You can surely find a way to do this on the server-side, but I like the client-side. :) For this post, I'm going to skip over creating the Server ribbon customization. If you need documentation on that, it is well covered in the SDK:

Now, what we're going to do is use a combination of the RefreshCommandUI function, some straight-forward EcmaScript, the EnabledScript attribute on the CommandUIHandler, and magic to make this work. I'm trying to keep this sample as simple as possible, but they will get more complex. As the complexity increases, you should consider putting this into a closure rather than in global code. With that said, let's jump into the code!

Creating the Starter Function

The starter function here is used to start the process of enabling your button. This function is simple in that it tries to retrieve the selected files or, if something is selected, return true to enable the button. This is the function you call in your EnabledScript attribute. 

 function enableButtonBasedOnFileSelection() {
    var aux;
    if (null === window.ribbonPollResponse.fileSelected)
        retrieveSelectedFiles();
    else {
        aux = window.ribbonPollResponse.fileSelected;
        window.ribbonPollResponse.fileSelected = null;
        return aux;
    }
}

Defining the Poll Response and File Selection Status

You set up two variables on the window object that track the ribbon poll response as well as file selection status. These two variables will be used to let the ribbon know to enable the button as well as let the ribbon know a file is selected. Pretty simple.

 if ('undefined' === typeof window.ribbonPollResponse)
    window.ribbonPollResponse = {};
if ('undefined' === typeof window.ribbonPollResponse.fileSelected)
    window.ribbonPollResponse.fileSelected = null;

Retrieving the Selected Files

Here you will use the list selection object model (in the SP.ListOperation namespace) to retrieve the selected files. If there are multiple items selected, you simply do not poll the ribbon. This keeps the button from lighting up if I have a multiple items selected. You could change this to support multiple files by simply iterating through the array, but I wanted to keep this example simple.

After retrieving the files, you will ask the object model to load the file properties as well as the list item asynchronously. The asynchronous call, which was covered above, simply checks that file name and lets you know that a file is selected. At the end you will call repollRibbon, which is detailed below. For now, here's the code for retrieving the files.

 function retrieveSelectedFiles() {
    this.clientContext = SP.ClientContext.get_current();
    var web = this.clientContext.get_web();
    this.selectedItem = SP.ListOperation.Selection.getSelectedItems();
    this.selectedList = SP.ListOperation.Selection.getSelectedList();
    if (CountDictionary(selectedItem) == 1) {
        this.listItem = web.get_lists().getById(this.selectedList).getItemById(this.selectedItem[0].id);
        this.file = this.listItem.get_file();
        this.clientContext.load(this.listItem);
        this.clientContext.load(this.file);
        this.clientContext.executeQueryAsync(Function.createDelegate(this, onQuerySucceeded), Function.createDelegate(this, onQueryFailed));
        repollRibbon();
    }
}

Implementing the Delegate Handlers

When you're working with the EcmaScript object model, you need to execute a query asynchronously and provide two callbacks. One callback handles the successful query case while the other handles the failed query case. In the successful case, you will check the filename of the selected item and set the window variable to let the ribbon know that you have the correct file type selected. You can, obviously, check for multiple file types if your button operation can handle those file types. In the failed query case, you simply display an alert with the error message passed into the arguments.

 function onQuerySucceeded(sender, args) {
    var fileName = this.file.get_name();
    debugger;
    if (fileName.indexOf('.txt') >= 0) {
        window.ribbonPollResponse.fileSelected = true;
    }
    else {
        window.ribbonPollResponse.fileSelected = false;
    }
}

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

Polling the Ribbon

Now, this is the magic and interesting bit of the exercise. The repollRibbon function is where we call RefreshCommandUI for the ribbon. RefreshCommandUI is used to tell the ribbon that it needs to poll again. A poll means that the ribbon reaches out to each of the command handlers to see if a command can be handled or if the state has changed. Well, in this case, the state has, indeed, changed, and the button needs to be enabled. The poll will check the EnabledScript attribute all over again, and this time enableButtonBasedOnFileSelection returns true. Thus, the button is shown as enabled! That's magic, right?!   

 function repollRibbon() {
    if (null === window.ribbonPollResponse.fileSelected)
        this.timeoutId = window.setTimeout(repollRibbon, 1000);
    else {
        window.clearTimeout(this.timeoutId);
        RefreshCommandUI();
    }
}

Putting It All Together

Now that all of the code has been explained, I thought I'd provide a full view of the code. You can throw this into a ribbon button's EnabledScript attribute (with javascript: prepended, of course) and have a conditionally activated button.

 if ('undefined' === typeof window.ribbonPollResponse)
    window.ribbonPollResponse = {};
if ('undefined' === typeof window.ribbonPollResponse.fileSelected)
    window.ribbonPollResponse.fileSelected = null;

function onQuerySucceeded(sender, args) {
    var fileName = this.file.get_name();
    if (fileName.indexOf('.txt') >= 0) {
        window.ribbonPollResponse.fileSelected = true;
    }
    else {
        window.ribbonPollResponse.fileSelected = false;
    }
}

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

function retrieveSelectedFiles() {
    this.clientContext = SP.ClientContext.get_current();
    var web = this.clientContext.get_web();
    this.selectedItem = SP.ListOperation.Selection.getSelectedItems();
    this.selectedList = SP.ListOperation.Selection.getSelectedList();
    if (CountDictionary(selectedItem) == 1) {
        this.listItem = web.get_lists().getById(this.selectedList).getItemById(this.selectedItem[0].id);
        this.file = this.listItem.get_file();
        this.clientContext.load(this.listItem);
        this.clientContext.load(this.file);
        this.clientContext.executeQueryAsync(Function.createDelegate(this, onQuerySucceeded), Function.createDelegate(this, onQueryFailed));
        repollRibbon();
    }
}

function enableButtonBasedOnFileSelection() {
    var aux;
    if (null === window.ribbonPollResponse.fileSelected)
        retrieveSelectedFiles();
    else {
        aux = window.ribbonPollResponse.fileSelected;
        window.ribbonPollResponse.fileSelected = null;
        return aux;
    }
}
function repollRibbon() {
    if (null === window.ribbonPollResponse.fileSelected)
        this.timeoutId = window.setTimeout(repollRibbon, 1000);
    else {
        window.clearTimeout(this.timeoutId);
        RefreshCommandUI();
    }
}

enableButtonBasedOnFileSelection();