Friday, May 6, 2011

PageComponent code example for SharePoint Ribbon

I get a lot of requests lately to share my example for a simple generic PageComponent JavaScript object (class) that can handle ribbon events.

The was it works is simple, I will probably blog about it in more details later on but in a nutshell there are 3 client side objects that are involved with the ribbon work on the page:

1. PageManager

One object that have a collection of all PageComponent objects for the CommandDispatcher to be able to work with them

2. CommandDispatcher

The only object that handles ribbon events. Any action that happens on the ribbon sends the CommandName of the associated event to the CommandDispatcher. In turn, the CommandDispatcher goes to all available PageComponent objects and ask each one of them if the can handle that command, and if they do – it asks them to handle the command, thus invoking their command handlers.

3. Collection of PageComponent objects

PageComponent object are client side script classes that inherit from CUI.Page.PageComponent (yes, JavaScript class that inherits from a base class!). They declare a collection of CommandName strings of commands they know how to handle and are sometimes associated with a control on the page (web part, field control etc), and when ever such a command is invoked the CommandDispatcher asks them if they can currently handle that command, and if yes – it asks them to handle it. There is no limit to the number of PageComponent objects on the page, or to the number of PageComponent that handles a specific commands. You can also handle commands from OOB ribbon controls, and does not have to be the creator of that ribbon control.
Note: If a command has no PageComponent that can handle it, the control associated with it will be disabled (button, group) except for a tab that does not have to have a command associated with it.

Here is an example of a page component that I start from, feel free to take it and use it, it is very similar to the example available on MSDN with a bit more explanations:

//See documentation in page component: http://msdn.microsoft.com/en-us/library/ff407303.aspx
//TODO: choose namespace like you would in .NET applications. Use company name, project name and module name to avoid conflicts
Type.registerNamespace('Company.Project.Ribbon.PageComponent');

//Create the object
Company.Project.Ribbon.PageComponent = function (PageComponentId) {
this._pageComponentId = PageComponentId;//keep record of associated control (web part, field control, etc..) that is active in this current instance of page component.
//Initializes the base type CUI.Page.PageComponent (Base class is associated through Type.registerClass(Type, BaseType)
Company.Project.Ribbon.PageComponent.initializeBase(this);
}

//Declate it's prototype
Company.Project.Ribbon.PageComponent.prototype =
{
//This will be initialized by caller (web part, field control etc)
_pageComponentId: "PageComponentIDHolder",

getId: function () {
return this._pageComponentId;
},
init: function () {
//Create a list of commands that your page component can handle (JSON string array).
//TODO: edit this collection, these are the command names you wish to handle from the ribbon controls you are listening to.
this._myCommandList = ['Company.Project.Ribbon.PageComponent.CMD1',
'Company.Project.Ribbon.PageComponent.CMD2',
'Company.Project.Ribbon.PageComponent.CMD3'];

//Create an array of methods used to handle commands passed to the page component.
//Use Function.createDelegate to keep current class instance (this) when the method is called.
//TODO: add handler per command in this._myCommandList. Later on - you will have to create the actual script handler method.
this._myHandledCommands = {};
this._myHandledCommands['Company.Project.Ribbon.PageComponent.CMD1'] = Function.createDelegate(this, this.CMD1_Handler);
this._myHandledCommands['Company.Project.Ribbon.PageComponent.CMD2'] = Function.createDelegate(this, this.NotImplemented);
this._myHandledCommands['Company.Project.Ribbon.PageComponent.CMD3'] = Function.createDelegate(this, this.NotImplemented);
},
getFocusedCommands: function () {
return this._myCommandList; //return supported commands collection
},
getGlobalCommands: function () {
return this._myCommandList; //return supported commands collection
},
canHandleCommand: function (commandId) {
//TODO: In our logic, if there is a handler we can handle the command.
//But you might have more logic here, like: if commandId = DeleteItem - can handle only if there is 1 item selected.
var canHandle = this._myHandledCommands[commandId];

if (canHandle)
return true;
else
return false
;
},
handleCommand: function (commandId, properties, sequence) {
//Handle the command - simply getting the command handler (delegate) form the hash table and invoking it.
return this._myHandledCommands[commandId](commandId, properties, sequence);
},
isFocusable: function () {
return true;
},

//=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~
//=~=~ CUSTOM PAGE COMPONENT LOGIC STARTS HERE ~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~
//=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~
// This is where you should add your custom script hadler methods.
// TODO: Implement each handler in this._myHandledCommands
//

CMD1_Handler: function (commandId, properties, sequence) {
alert("Handling CMD1_Handler");
},
NotImplemented: function (commandId, properties, sequence) {
alert("This command was not implemented yet");
}

//
//
//
//=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~
//=~=~ CUSTOM PAGE COMPONENT LOGIC ENDS HERE ~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~
//=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~=~
}

//This makes our object inherit of CUI.Page.PageComponent
Company.Project.Ribbon.PageComponent.registerClass('Company.Project.Ribbon.PageComponent', CUI.Page.PageComponent)
//Execute pending operations waiting for this script to be loaded.
NotifyScriptLoadedAndExecuteWaitingJobs("Company.Project.Ribbon.PageComponent.js");


So, now that we implemented our PageComponent, all we need is to create an instance and register is with the PageManager using this code that makes sure all JS files were fully loaded:



function init2() {
//All JS files were loaded - create instance and register it.
var instance = new Company.Project.Ribbon.PageComponent("ComponentID");
SP.Ribbon.PageManager.get_instance().addPageComponent(instance);
}

function init1() {
//Wait for ribbon JS to load
ExecuteOrDelayUntilScriptLoaded(init2, 'sp.ribbon.js');
}
//Wait for our JS to load
ExecuteOrDelayUntilScriptLoaded(init1, 'Company.Project.Ribbon.PageComponent.js');


I hope this helps you when you are implementing your custom ribbon solutions, or if you need to handle an existing ribbon command in addition to its OOB logic.



Note: to replace an OOB logic of a ribbon command there are other steps you need to take, I will try blogging about it soon as well.



Thanks, Shai.

15 comments:

BSukhwal said...

How to activate the custom ribbon tab using the Sandbox solution.

Shai Petel said...

Hi BSukhwal,
I will write a post on that in the near future.

Shai Petel said...

For now, you can have a look at this complete guide with code examples:
http://kwizcom.blogspot.com/2010/07/all-about-sharepoint-ribbon-for.html

BSukhwal said...

Thanks for the reply. But on the 22 slide i found "SPRibbon in not accessible from sandboxed code". So, then how can we customize the ribbon in sandbox and show to publishing and content pages

BHarat Sukhwal

Shai Petel said...

Hi,

ONLY the "SPRibbon" server object is not accessible in sandbox.

Everything else is.

So, this means you have to declare tabs/groups/controls staticly in an XML that goes into a feature.

If you need to load dynamic changing controls - your only option is to use a drop down that populates dynamically and use a page component to generate the list of menu items.
Check out the lazy loading example for that - it should work in sandbox.

Serg said...

When will you write post about OOB logic of a existing ribbon command replacing ?

Shai Petel said...

Hi Serg,

sorry it is taking me so long - other things just keep me busy lately.
All the info should be in this presentation I gave: http://kwizcom.blogspot.com/2010/07/all-about-sharepoint-ribbon-for.html i will try to post it in my blog soon.

If you have questiosn for now, simply leave a comment on my blog I will try to answer.

Thanks!

Saru said...

I am looking for showing/hiding my custom action buttons on the ribbon depending upon which of my custom action buttons the user has clicked. Something like the OOTB Stop Inheriting Permissions and Manage Parent / Inheriting Permissions buttons.

I haven't seen any blog post for this scenario. Have you implemented anything like this?

Have you had any time to post any further articles on ribbon customizations with page components?

Shai Petel said...

Hi Saru,
sure, this shouldn't be too hard.
first off, i'll start by saying MS recommend in most cases not to show/hide buttons, but to enable/disable them. It makes a clearer UI usually.

but, in some cases like the one you mentioned you might want to do that.

In order to do that you must:
1. build the ribbon buttons using a server side control (web part, or page head delegate control).
2. In the server side control use the SPRibbon.Current to inject the buttons you need in runtime, according to the settings you set.
3. The button that show/hide other buttons should be there, and have an action that saves the setting somewhere (like a cookie) and submits the form in order for the server side to rebuild the ribbon.


I hope this makes it clear. Good luck!!!

Anonymous said...

HI Shai Petel,

Is there an event that's fired when the tabs are about to switch contextually? I am trying to override the automatic switching of the ribbon tabs when context changes , but I'm having a tough time, especially since I'm new to Sharepoint and Javascript. I posted a question here: http://sharepoint.stackexchange.com/questions/34024/sharepoint-ribbon-circumventing-context-sensitivity . Perhaps you could take a look at it?

Shai Petel said...

I have to agree with the answer you got on that post,
What you want to do is not in the ribbon designed user experience.

I assume it is possible with some tweaking of the ribbon client side "CommandDispatcher", but didnt try to mess with it.

Sorry. If you do get it to work I'd love to hear about it.

Anonymous said...

Hello Shai,

I was able to solve my problem and have posted my solution here:

http://stackoverflow.com/questions/10175092/sharepoint-ribbon-circumventing-context-sensitivity/10209677#10209677

Cheers.

Shai Petel said...

Thanks for posting,
I have to say - modifying these files is not supported by Microsoft, so I wouldn't do it if I were you.

Instead I would add another JS file to the page, later than the OOB one, that overrides the executeRootCommand with a new fixed function.
This is supported, and would be pretty simple to do.

* set the ribbon instance ".executeRootCommand = function (commandId, properties, commandInfo, root) {
//...new code...
};"

Anonymous said...

Hi Shai,

Thank you very much for the tip. I was also very skeptical about altering the Microsoft files, but with my very little knowledge of Javascript, wasn't sure what alternatives I have at my disposal.

If it is not too much to ask, what do I have to do to ensure that my script gets loaded after the OOB scripts?

If I understand right, after the script is loaded, it has to be executed at least once, for the overriding with my new executeRootCommand function to take effect. So how do I force the script to be executed at least once?

Hopefully that's not too much to ask. ;)

Shai Petel said...

You can either use jQuery.ready to run code on document ready, but in SharePoint 2010 there is a better way, you can fire methods to execute after a specific script file finished loading.

See "ExecuteOrDelayUntilScriptLoaded" usage in my post above.