One of the frequently asked questions on Command Framework is 'how to dynamically update a command?' I thought I'll couple the answer with implementing ISourceProvider and explain it with a usecase: Authentication in RCP applications. Most of the RCP applications I've seen, needs authentication. Obviously it would need a Login/Logout menu items and other user related options. This can be accomplished in many ways, and I'm going to show you how to do it with Command Framework.
Lets do it in step by step.
Defining the session state thru ISourceProvider:
First, we need to store the session state in a variable. When we say variable in Command Framework, its not a public final static String. It is something that you can use it in the visibleWhen, activeWhen and enabledWhen expressions. The variable has to be provided thru a ISourceProvider:<extension
point="org.eclipse.ui.services">
<sourceProvider
provider="com.eclipse_tips.rcp.app.SessionSourceProvider">
<variable
name="com.eclipse-tips.rcp.app.sessionState"
priorityLevel="workbench">
</variable>
</sourceProvider>
</extension>
A ISourceProvider can provide the state of multiple variables. However, our source provider will give the value of only one variable - 'com.eclipse-tips.rcp.app.sessionState'. The values of this variable would be either 'loggedIn' or 'loggedOut'. The list of values cannot be defined thru the extension, so this is up to you to define it, publish it and use it.
In the extension, you can see the priorityLevel attribute. This is used by the IHandlerService to determine which handler is active for a given command and its defined in the org.eclipse.ui.ISources interface. See my earlier tip on Command Framework for more explanation on this priority attribute.
The class should either implement ISourceProvider or extend AbstractSourceProvider. The preferred way is extending, so we'll do it that way:
public class SessionSourceProvider extends AbstractSourceProvider {
public final static String SESSION_STATE = "com.eclipse-tips.rcp.app.sessionState";
private final static String LOGGED_IN = "loggedIn";
private final static String LOGGED_OUT = "loggedOut";
boolean loggedIn;
@Override
public String[] getProvidedSourceNames() {
return new String[] {SESSION_STATE};
}
@Override
public Map<String, String> getCurrentState() {
Map<String, String> currentState = new HashMap<String, String>(1);
String currentState = loggedIn?LOGGED_IN:LOGGED_OUT;
currentState.put(SESSION_STATE, currentState);
return currentState;
}
@Override
public void dispose() {}
public void setLoggedIn(boolean loggedIn) {
if(this.loggedIn == loggedIn)
return; // no change
this.loggedIn = loggedIn;
String currentState = loggedIn?LOGGED_IN:LOGGED_OUT;
fireSourceChanged(ISources.WORKBENCH, SESSION_STATE, currentState);
}
}
The first method is simple. As I mentioned earlier, a source provider can provider the state of multiple variables. The list of the variables (source names) is returned in the call. The second method is called to get the current state. The variable:value pairs are put in a map and returned back to the caller. The third method dispose is a no-op method for us. These three methods completes the API contract, but we have added one more method, where the value is updated to the source provider itself. So whenever a Login/Logout happens we need to call this method. This method fires the sourceChanged event, so that all the listeners can update accordingly.
When the sourceChanged event is fired, the status of the Command Handlers which has activeWhen or enabledWhen expressions with this variable re-evaluated with the new value of the variable. This holds good for visibleWhen expressions of the Commands as well. So if you want to show/enable a contribution item only when the user has logged in, you can use this variable like:
<extension
point="org.eclipse.ui.menus">
<menuContribution
locationURI="menu:file?after=additions">
<command
commandId="org.eclipse.ui.window.preferences">
<visibleWhen>
<with
variable="com.eclipse-tips.rcp.app.sessionState">
<equals
value="loggedIn">
</equals>
</with>
</visibleWhen>
</command>
</menuContribution>
</extension>
Now the File->Preferences will be visible only when the user has logged in.
Updating the state:
Now that we have the source provider and the menu items that are dynamically enabled/disabled or shown/hidden according to the sessionState, the problem is how to we update the state?Elsewhere, when a command is executed for Login/Logout, we have to make a call to SessionSourceProvider.setLoggedIn() method. But how do we get hold of the instance of the SessionSourceProvider? This is where the ISourceProviderService comes into picture. This service has a getSourceProvider() method, where if you give the variable name, it will give the corresponding source provider. The next obvious question would be where to get the implementation of this service? Not this service, for any service, you can use IServiceLocator to get the implementation and fortunately the IWorkbenchWindow implements this service. So when we execute the Login/Logout command:
// get the window (which is a IServiceLocator)
IWorkbenchWindow window = HandlerUtil.getActiveWorkbenchWindow(event);
// get the service
ISourceProviderService service = (ISourceProviderService) window.getService(ISourceProviderService.class);
// get our source provider by querying by the variable name
SessionSourceProvider sessionSourceProvider = (SessionSourceProvider) service.getSourceProvider(SessionSourceProvider.SESSION_STATE);
// set the value
sessionSourceProvider.setLoggedIn(isSessionActive);
Dynamically updating the Login/Logout command:
The Login and Logout commands are mutually exclusive. When one appears the other won't. So you can have two different commands and use the visibleWhen with our sessionState variable to show/hide them. The other way is to have one command and update the text of the command according to the state. The first way is very similar to the Preference command that is explained above, so let me explain the second way here:The idea is to define a single command and two handlers:
<extension
point="org.eclipse.ui.commands">
<command
id="com.eclipse-tips.rcp.app.sessionCommand"
name="Session Command">
</command>
</extension>
<extension
point="org.eclipse.ui.handlers">
<handler
class="com.eclipse_tips.rcp.app.handlers.LoginHandler"
commandId="com.eclipse-tips.rcp.app.sessionCommand">
<activeWhen>
<with
variable="com.eclipse-tips.rcp.app.sessionState">
<equals
value="loggedOut">
</equals>
</with>
</activeWhen>
</handler>
<handler
class="com.eclipse_tips.rcp.app.handlers.LogoutHandler"
commandId="com.eclipse-tips.rcp.app.sessionCommand">
<activeWhen>
<with
variable="com.eclipse-tips.rcp.app.sessionState">
<equals
value="loggedIn">
</equals>
</with>
</activeWhen>
</handler>
</extension>
<extension
point="org.eclipse.ui.menus">
<menuContribution
locationURI="menu:file?before=quit">
<command
commandId="com.eclipse-tips.rcp.app.sessionCommand"
style="push">
</command>
</menuContribution>
</extension>
So the first handler, LoginHandler will be active when the user is logged out and the LogoutHandler will be active when the user is logged in. Both the handlers after performing their respective actions will notify the SessionSourceProvider, but how do we change the menu text? This happens thru IElementUpdater. When a handler implements this interface, it can update the associated contributions. When I say update, it means the text, tooltip, image etc. So the LoginHandler would look like:
public class LoginHandler extends AbstractHandler implements IElementUpdater {
@Override
public Object execute(ExecutionEvent event) throws ExecutionException {
// perform login here ...
IWorkbenchWindow window = HandlerUtil.getActiveWorkbenchWindow(event);
ISourceProviderService service = (ISourceProviderService) window.getService(ISourceProviderService.class);
SessionSourceProvider sessionSourceProvider = (SessionSourceProvider) service.getSourceProvider(SessionSourceProvider.SESSION_STATE);
// update the source provider
sessionSourceProvider.setLoggedIn(true);
return null;
}
@Override
public void updateElement(UIElement element, Map parameters) {
element.setText("Login");
}
}
The LogoutHandler also will have a similar code. Remember its not a mandatory thing to have multiple handlers to update the command dynamically. You can do it even with single handler as well.
One last piece, the Command Framework will call the updateElement() method only when the value of the variables in the *when expression is changed. So in other places where you want to update a command when no variable change has occurred, you need to use the ICommandService:
ICommandService commandService = (ICommandService.class)serviceLocator.getService(ICommandService.class);
commandService.refreshElements(commandId, null);
See also:
Part 1: Actions Vs Commands
Part 2: Selection and Enablement of Handlers
Part 3: Parameters for Commands
Part 4: Misc items ...
Part 6: 'toggle' & 'radio' style menu contribution
10 comments:
WoW,This is a great series, too!Because I want to do same things,"How do I implement login and logout menu?" This is very important things to develop Plug-ins. How do you find it?
And I would like to translate this series to Japanese.
Please allow me to translate your great article.
@Hiroki Kondo(kompiro)
You don't have to ask me everytime for translating :-) You can translate any of my posts, just remember to give a link back to the original post from the translated one.
Thanks!
O.K. I'll start to translate your great entries.I must to link your great entries.But I don't use your pictures, because I hope my page visitors to move to your blog my translated entries.If my visitors want to see the pictures, they move to your blog!
Come to think of it,The extension point "org.eclipse.ui.services" is new extension point.(since 3.4 Ganymede release).
I would like to write about target Eclipse version at your entry.How do you think about it?
Yes, the extension point is from 3.4 only. You can add target Eclipse version.
Please send a mail to me (grprakash@gmail.com) after publishing the translated entries. I'll update the original blog entry, so that people who come to my blog know about the translation and visit your page.
Again, thanks for your time in translating this.
I like your example but you cover Authetication only.
Often it is coupled with Authorization for particular functions as well, and the actions for functions not allowed should be hidden.
I did a similar thing for an Eclipse 3.2 based RCP application using an IActionFilter to hide the actions not allowed.
In the getCurrentState() method you have named both the Map and the String variable 'currentState' :)
@Edvin Syse,
Thanks for the pointer. I'm little over confident and many time I write code directly in the blogger. Sometimes these errors happen :-(
Hi,
You are writing "See my earlier tip on Command Framework for more explanation on this priority attribute." Unfortunately I was not able to find the tip, can you direct me?
I am trying to create view-local variable using this attribute (trying to substitute activePartId value), is it possible?
@Sergey Khorev,
You can find it here: http://blog.eclipse-tips.com/2009/01/commands-part-2-selection-and.html
Thanks so much Prakash for such an elaborate and easy-to-understand write-up on this topic. It helped me a lot.
Thanks.
Post a Comment