In this article series:
- Customizing the ribbon – creating tabs, groups and controls
- Adding ribbon items into existing tabs/groups
- Ribbon customizations - dropdown controls, Client Object Model and JavaScript Page Components (this post)
- Customize the ribbon programmatically from web parts and field controls
Once you understand how to get your customizations into the right place in the ribbon, you may find you want to go beyond simply adding buttons and make use of other controls such as dropdowns, checkboxes, flyout anchors and so on. This type of ribbon development is fairly involved, but as with so many things in SharePoint development, once you've done it the first time you know the overall "template" for subsequent occasions - hopefully what I'm showing here is a good starting point for quite a few advanced things you might want to do. The key is that a JavaScript "page component" is generally required in addition to the declarative XML we've seen in my earlier posts.
[Beta sidenote] At the time of writing (Feb 2010, still in beta), after some time on this I'm forming the opinion that ribbon development could be one of the more complex development areas in SP2010. Maybe some stellar documentation from Microsoft could change this, but right now there are many dark corners which are quite difficult to understand – currently there is practically no coverage of much of this stuff in the SDK (and very little anywhere really), so unless you have inside information it's mainly blood, sweat and tears all the way. I've mentioned this to the PM in the Product Group (Elisabeth Olson), and it sounds like more MS guidance is on the way soon, so let's hope.
The sample
My sample shows the use of a custom dropdown in the ribbon – I'll go into more detail later, but the concepts I'm showing here can be used for many controls in the ribbon, not just a dropdown. So if what you're wanting to do doesn't specifically involve a dropdown, I'd suggest reading on anyway as this technique is still probably what you will use.
When clicked the first time, it uses the Client Object Model to fetch a collection of lists in the current web, then presents them as options in the dropdown:
When an item is selected, a simple JavaScript alert is raised with the name of the selected list, though a real-life implementation would of course do something more useful with the value. The goal here is to illustrate how to work with ribbon controls other than buttons, and also how to write code behind them – once you can do this, you'll be able to build a wide range of solutions.
One key thing to note – it IS possible to add items to a dropdown or other control entirely in XML. I'm choosing to use a JavaScript page component to illustrate what happens when you need "code-behind" e.g. to iterate all the lists in a web in my case.
What's required – summary
- Declarative XML to provision the ribbon controls
- JavaScript "page component", typically declared in external .js file
- Addition of JavaScript to page (using whatever technique is most appropriate to the scope of your ribbon customization – code in a web part/delegate control which is added to AdditionalPageHead, etc.). This will:
- Link the external .js file
- Ensure core dependent .js files are loaded e.g. SP.js, CUI.js
- Call into the initialization function within our page component – this registers our component with the ribbon framework and ensures our component gets added to the page.
1. Declarative XML
I used the following XML – here I'm actually showing a cut-down extract which is just for the group containing my controls. Really it's just the 'Controls' section which is the most interesting bit, the surroundings would depend on whether you are wanting to create a new tab or add the items into an existing tab/group, see my previous articles for those details.
Key points of note are the Command, PopulateQueryCommand, and QueryCommand attributes on the dropdown – these will link into our JavaScript page component:
<Group
Id="COB.SharePoint.Ribbon.WithPageComponent.PCNotificationGroup"
Description="Contains advanced ribbon controls"
Title="Page component sample"
Sequence="53"
Template="Ribbon.Templates.COB.OneLargeExample">
<Controls Id="COB.SharePoint.Ribbon.WithPageComponent.PCNotificationGroup.Controls">
<Label Id="COB.SharePoint.Ribbon.WithPageComponent.PCNotificationGroup.Label"
ForId="COB.SharePoint.Ribbon.WithPageComponent.PCNotificationGroup.Dropdown"
Command="LabelCommand"
LabelText="Select list:"
Sequence="16"
TemplateAlias="c1"/>
<DropDown
Id="COB.SharePoint.Ribbon.WithPageComponent.PCNotificationGroup.Dropdown"
Sequence="17"
Command="COB.PageComponent.Command.DoAction"
PopulateDynamically="true"
PopulateOnlyOnce="true"
PopulateQueryCommand="COB.PageComponent.Command.PopulateDropDown"
QueryCommand="COB.PageComponent.Command.QueryDoAction"
Width="75px"
TemplateAlias="c2" />
</Controls>
</Group>
2. JavaScript page component
This is the complex bit, the first time at least. We are effectively writing object-oriented JavaScript which contains a class which powers our ribbon control. Consider JavaScript such as this the 'template' to use for page components, where you'll modify the actual implementation bits each time. I've commented some key points, suggest having a scroll through and then we'll walk through the highlights:
Type.registerNamespace('COB.SharePoint.Ribbon.PageComponent');
COB.SharePoint.Ribbon.PageComponent = function () {
COB.SharePoint.Ribbon.PageComponent.initializeBase(this);
}
// the initialize function needs to be called by some script added to the page elsewhere - in the end, it does the important work
// of calling PageManager.addPageComponent()..
COB.SharePoint.Ribbon.PageComponent.initialize = function () {
ExecuteOrDelayUntilScriptLoaded(Function.createDelegate(null, COB.SharePoint.Ribbon.PageComponent.initializePageComponent), 'SP.Ribbon.js');
}
COB.SharePoint.Ribbon.PageComponent.initializePageComponent = function() {
var ribbonPageManager = SP.Ribbon.PageManager.get_instance();
if (null !== ribbonPageManager) {
ribbonPageManager.addPageComponent(COB.SharePoint.Ribbon.PageComponent.instance);
}
}
COB.SharePoint.Ribbon.PageComponent.prototype = {
init: function () { },
getFocusedCommands: function () {
return ['COB.PageComponent.Command.FieldControl.GroupCommand', 'COB.PageComponent.Command.FieldControl.TabCommand', 'COB.PageComponent.Command.FieldControl.ContextualGroupCommand', 'COB.PageComponent.Command.FieldControl.RibbonCommand'];
},
getGlobalCommands: function () {
return ['COB.PageComponent.Command.DoAction', 'COB.PageComponent.Command.PopulateDropDown', 'COB.PageComponent.Command.QueryDoAction'];
},
canHandleCommand: function (commandId) {
if ((commandId === 'COB.PageComponent.Command.DoAction')
(commandId === 'COB.PageComponent.Command.PopulateDropDown') (commandId === 'COB.PageComponent.Command.QueryDoAction')) {
return true;
}
else {
return false;
}
},
handleCommand: function (commandId, properties, sequence) {
if (commandId === 'COB.PageComponent.Command.FieldControl.GroupCommand') {
alert("COB.PageComponent.Command.FieldControl.GroupCommand fired");
}
if (commandId === 'COB.PageComponent.Command.FieldControl.TabCommand') {
alert("COB.PageComponent.Command.FieldControl.TabCommand fired");
}
if (commandId === 'COB.PageComponent.Command.FieldControl.ContextualGroupCommand') {
alert("COB.PageComponent.Command.FieldControl.ContextualGroupCommand fired");
}
if (commandId === 'COB.PageComponent.Command.FieldControl.RibbonCommand') {
alert("COB.PageComponent.Command.FieldControl.RibbonCommand fired");
}
if (commandId === 'COB.PageComponent.Command.QueryDoAction') {
// this command executes as soon as tab is requested, so do initialization here ready for if our dropdown gets requested..
loadCurrentWebLists();
}
if (commandId === 'COB.PageComponent.Command.PopulateDropDown') {
// actually build the dropdown contents by setting the PopulationXML property to a value with the expected format. We have to deal with possible
// timing issues/dependency on core SharePoint JS code with an ExecuteOrDelay..
ExecuteOrDelayUntilScriptLoaded(Function.createDelegate(null, getDropdownItemsXml), 'SP.js');
properties.PopulationXML = getDropdownItemsXml();
}
if (commandId === 'COB.PageComponent.Command.DoAction') {
// here we're using the SourceControlId to detect the selected item, but more normally each item would have a unique commandId (rather than 'DoAction').
// However this isn't possible in this case since each item is a list in the current web, and this can change..
var selectedItem = properties.SourceControlId.toString();
var listName = selectedItem.substring(selectedItem.lastIndexOf('.') + 1);
alert("You selected the list: " + listName);
}
},
isFocusable: function () {
return true;
},
receiveFocus: function () {
return true;
},
yieldFocus: function () {
return true;
}
}
// **** BEGIN: helper code specific to this sample ****
// some global variables which we'll use with the async processing..
var lists = null;
var querySucceeded = false;
// use the Client Object Model to fetch the lists in the current site..
function loadCurrentWebLists() {
var clientContext = new SP.ClientContext.get_current();
var web = clientContext.get_web();
this.lists = web.get_lists();
clientContext.load(lists);
clientContext.executeQueryAsync(
Function.createDelegate(this, this.onQuerySucceeded),
Function.createDelegate(this, this.onQueryFailed));
}
function onQuerySucceeded() {
querySucceeded = true;
}
function onQueryFailed(sender, args) {
querySucceeded = false;
}
function getDropdownItemsXml() {
var sb = new Sys.StringBuilder();
sb.append('<Menu Id=\'COB.SharePoint.Ribbon.WithPageComponent.PCNotificationGroup.Dropdown.Menu\'>');
sb.append('<MenuSection DisplayMode=\'Menu\' Id=\'COB.SharePoint.Ribbon.WithPageComponent.PCNotificationGroup.Dropdown.Menu.Manage\'>');
sb.append('<Controls Id=\'COB.SharePoint.Ribbon.WithPageComponent.PCNotificationGroup.Dropdown.Menu.Manage.Controls\'>');
if (querySucceeded)
{
var listEnumerator = lists.getEnumerator();
while (listEnumerator.moveNext()) {
var oList = listEnumerator.get_current();
sb.append('<Button');
sb.append(' Id=\'COB.SharePoint.Ribbon.WithPageComponent.PCNotificationGroup.Dropdown.Menu.Manage.');
sb.append(oList.get_title());
sb.append('\'');
sb.append(' Command=\'');
sb.append('COB.PageComponent.Command.DoAction');
sb.append('\'');
sb.append(' LabelText=\'');
sb.append(SP.Utilities.HttpUtility.htmlEncode(oList.get_title()));
sb.append('\'');
sb.append('/>');
}
}
sb.append('</Controls>');
sb.append('</MenuSection>');
sb.append('</Menu>');
return sb.toString();
}
// **** END: helper code specific to this sample ****
COB.SharePoint.Ribbon.PageComponent.registerClass('COB.SharePoint.Ribbon.PageComponent', CUI.Page.PageComponent);
COB.SharePoint.Ribbon.PageComponent.instance = new COB.SharePoint.Ribbon.PageComponent();
NotifyScriptLoadedAndExecuteWaitingJobs("COB.SharePoint.Ribbon.PageComponent.js");
- The 'initialize' function is typically responsible for calling 'addPageComponent' on the ribbon PageManager (but not before SP.Ribbon.js has loaded)
- The commands referenced in the JS are those specified in the control XML e.g. for my dropdown
- The 'getFocusedCommands' function returns an array of commands which should execute when my control has focus
- The 'getGlobalCommands' function returns an array of commands which should execute regardless of focus
- We need to list the commands which can be handled in the 'canHandleCommand' function, and provide the actual implementation for each of these in 'handleCommand'
- PopulateQueryCommand – used to build the list of items in the control. This is where I'm using the Client Object Model (ECMAScript version) to fetch the lists for the current web.
- QueryCommand – called when the parent container (e.g. tab) is activated. Remember the ribbon is all about "script on demand" (lazy loading), so I'm choosing this as a better place to do my initialization work of the actual Client OM request – more on this later.
- Command – called when the user actually selects an item
1: <Menu Id="">
2: <MenuSection Id="">
3: <Controls Id="">
4: <Button Command="" Id="" LabelText="" />
5: ..a 'Button' element here for each item in the collection..
6: </Controls>
7: </MenuSection>
8: </Menu>
- There are some interesting facets to combining the Client OM with the ribbon JS framework – effectively the async model used means the result of your method call may not be ready by the time the ribbon framework needs it (it happens on a different request after all). I'll explain how I dealt with this in my example towards the end of this article.
3. Page-level JavaScript
The final element is the JavaScript you need to add to the page to call into the page component. In my example I'm happy for this JavaScript to be added to every page in my site (since that's the scope of my ribbon customization), so I used a delegate control in AdditionalPageHead to add a custom user control, the body of which looks like this:
<SharePoint:ScriptLink Name="sp.js" LoadAfterUI="true" OnDemand="false" Localizable="false" runat="server" ID="ScriptLink1" />
<SharePoint:ScriptLink Name="CUI.js" LoadAfterUI="true" OnDemand="false" Localizable="false" runat="server" ID="ScriptLink3" />
<SharePoint:ScriptLink Name="/_layouts/COB.SharePoint.Demos.Ribbon/COB.SharePoint.Ribbon.PageComponent.js" LoadAfterUI="true" OnDemand="false" Localizable="false" runat="server" ID="ScriptLink2" />
<script type="text/javascript">
//<![CDATA[function initCOBRibbon() {COB.SharePoint.Ribbon.PageComponent.initialize();
}
ExecuteOrDelayUntilScriptLoaded(initCOBRibbon, 'COB.SharePoint.Ribbon.PageComponent.js');////]]></script>
The important things here are that we ensure required system JS files are loaded with the ScriptLink tag, do the same for our JS file, then call the .initialize() function of our page component.
So those the component pieces for complex controls in the ribbon! A wide variety of ribbon customizations should be possible by tailoring this information/sample code as needed (remember 'handleCommand' is the key implementation hook), and I definitely think that starting from such a template is the way to go.
Appendix - considerations for using the Client Object Model in the ribbon
When working with the ribbon it quickly becomes apparent that if the Client Object Model didn't exist, things would be much trickier – they are a natural pairing for many requirements. Despite this, some challenges arise – consider that a control (e.g. dropdown) will have it's items collection populated as late as possible if 'PopulateDynamically' is set to true (generally a good idea) i.e. when the dropdown is actually clicked to select an item! This is because the ribbon is designed around a "script on demand" model (you'll often see "SOD" references in Microsoft's JavaScript) – this ensures only the required JavaScript is downloaded to the client, and no more. This solves the issue where on SharePoint 2007 WCM sites, we would suppress the core.js file for anonymous users because it was big and not required for these users. Anyway, when the dropdown is clicked, at this point the ribbon framework calls 'handleCommand' with the command specified for the 'PopulateQueryCommand' value. If you run your Client OM code here it's no good, since you won't get the result there and then due to the async model – the result will be provided to the callback function, long after 'handleCommand' has completed, so the end result is your control will be empty.
Consequently, you need to do the actual processing before the 'PopulateQueryCommand' is called. You could choose to do as soon as the page loads, but in most cases this could be inefficient – what if the user doesn't come near your ribbon control on this page load? In this case we would have incurred some client-side processing and an extra request to the server which was completely unnecessary – on a high-traffic page, this could be bad news. Without any documentation it's hard to be sure at this stage, but it seems the 'QueryCommand' is a good place to put such Client OM code – this seems to be called when the parent container (e.g. tab) is made visible (at which point there's now a chance the user could use our control). In my code I have the actual Client OM query run here and store the result in page-level variable - this is then picked up and iterated for the 'PopulateQueryCommand'. By the time the script runs this command to populate the control, the query has already executed and has the data ready – happy days. I'll be interested to see what emerges in this area to see whether this is the expected pattern or, well, if I've got things completely wrong.
Summary
Complex ribbon customizations are likely to require a JavaScript page component - in general page components are somewhat complex (partly because of the current lack of documentation perhaps), but once you have a suitable template, subsequent implementations should be easier. If you need to use Client Object Model code, beware of the "async/lifecycle issue" discussed here and ensure your code has data ready for the ribbon framework.