Bug 112215 - [CommonNavigator] Allow extensions to participate in the save lifecycle
Summary: [CommonNavigator] Allow extensions to participate in the save lifecycle
Status: VERIFIED FIXED
Alias: None
Product: Platform
Classification: Eclipse Project
Component: UI (show other bugs)
Version: 3.2   Edit
Hardware: PC Windows XP
: P3 enhancement (vote)
Target Milestone: 3.2 M6   Edit
Assignee: Boris Bokowski CLA
QA Contact:
URL:
Whiteboard:
Keywords: api
Depends on: 112225
Blocks:
  Show dependency tree
 
Reported: 2005-10-11 10:29 EDT by Dusko CLA
Modified: 2006-06-02 15:38 EDT (History)
8 users (show)

See Also:


Attachments
work in progress (10.75 KB, patch)
2006-03-15 13:56 EST, Boris Bokowski CLA
no flags Details | Diff
work in progress (19.83 KB, patch)
2006-03-27 00:15 EST, Boris Bokowski CLA
no flags Details | Diff
work in progress (42.36 KB, patch)
2006-04-11 01:05 EDT, Boris Bokowski CLA
no flags Details | Diff
patch against org.eclipse.ui.navigator (44.36 KB, patch)
2006-04-11 16:18 EDT, Boris Bokowski CLA
no flags Details | Diff

Note You need to log in before you can comment on or make changes to this bug.
Description Dusko CLA 2005-10-11 10:29:58 EDT
The Project Explorer (Common Navigator) currently does not support the 
ISaveablePart2 interface. It means that its content is stuck with the saving 
pattern tied to the active editor. Since PE is a common navigator for logical 
concepts in some cases (like ours) the content providers populate it with much 
more stuff than just the top level resources. In the combination with context 
menu actions and the Properties View the entries in the PE are actually 
modified.

We did usability testing centered on modifications of our resources through PE 
and the users constantly are trying to use CTRL + S (Save), Save As and Save 
All based on what is selected in Project Explorer, not what is the active 
editor.

I have tried to bypass this by registering ISaveablePart2 adapter on Project 
Explorer but that does not really work since the PE does not expose any way to 
send the 'part dirtied' events used to enable various Save related actions. 
Not to mention that my adapter could easily be in collision with similar 
adapters from other PE clients.

The real solution would be for PE to implement this interface itself and then 
provide extension point for clients to take on handling for the selected node. 
If no client handles the selected node, it would resort to the default 
behavior which is go with the active editor.

I marked this as ‘major’ issue for us because it seriously hampers our 
usability.
Comment 1 Michael D. Elder CLA 2005-10-25 09:23:41 EDT
This requirement is documented (thanks to this bugzilla) and will be considered
a high priority item for the port of the Common Navigator for Platform 3.2. 

I am tentatively targetting this for 1.0M9, but please be advised that the work
may be done in the 3.2 stream, and thus WTP would not pick up the change until
WTP 1.5 (on 3.2). 
Comment 2 Nick Edgar CLA 2005-12-06 12:16:44 EST
Note that simply implementing ISaveablePart2 is not really the right answer.
See bug 112225 comment 11 for more details.
Comment 3 Nick Edgar CLA 2006-01-23 14:29:03 EST
Changing the summary to more accurately reflect the problem.

Just to clarify the requirements, it's not really the support for custom save prompts (as provided by ISaveablePart2) that Dusko is after.
Dusko, please correct me if I'm mis-stating anything here.

What is really needed is to allow content extensions to participate in the save lifecycle if they represent a model that may have unsaved changes.

The main requirement here is to allow the Save action (File>Save or Ctrl+S) to save the model(s) containing the selected element(s).

Also important is to somehow indicate that the model has unsaved changes, e.g. using a '*' dirty flag on relevant elements.  This would typically only be shown on the outermost element representing the "editing domain" (I don't necessarily mean the EMF concept here -- I mean the boundary of what gets saved when you hit Ctrl+S).  For example, in RSM, it would be the file representing the model.

The navigator view should also likely show the dirty flag if any content extensions being shown have unsaved changes (but see the subsequent comments in bug 112225).

To allow us to make progress on this, we need, at a minimum:
- a way for a content extension to express that it supports the save lifeycycle (i.e. it may have unsaved changes)
- a way for a content extension to express that is currently has unsaved changes (i.e. is dirty)
- a way to tell the content extension to save its contents, passing a progress monitor

Note that the Save action (class SaveAction) looks for ISaveablePart on the active view or editor using both a direct instanceof check and a getAdapter(ISaveablePart.class) check.  This is updated each time the active part changes, or when the active view or editor fires a property change event with ISaveablePart.PROP_DIRTY (e.g. using WorkbenchPart.firePropertyChange(int)).

An expedient way to address this problem would be to just delegate the lookup of ISaveablePart to the selected content extension(s), but we'd also need to:
1. properly handle the case where the selection crosses multiple content extensions
2. allow some way for the content extension to notify of a change to the dirty state
Comment 4 Nick Edgar CLA 2006-01-23 14:29:48 EST
This is an important item for RSM for 3.2.

Comment 5 Nick Edgar CLA 2006-02-03 12:22:55 EST
I'm starting to prototype this, using the new ISaveableModel support (see bug 112225 for more details).

Comment 6 Nick Edgar CLA 2006-02-13 14:13:49 EST
I was not able to complete this for M5.  Need to reassess in early M6.

Comment 7 Boris Bokowski CLA 2006-03-15 13:56:51 EST
Created attachment 36350 [details]
work in progress

This is just to keep a copy of the current state.
Comment 8 Boris Bokowski CLA 2006-03-27 00:15:36 EST
Created attachment 36968 [details]
work in progress
Comment 9 Boris Bokowski CLA 2006-03-30 01:30:12 EST
Initial version of new API released for I20060330-0010.
Comment 10 John Arthorne CLA 2006-03-30 11:29:54 EST
Minor comment: INavigatorSaveablesService and ISaveablesSourceHelper are missing class comments.  Minimally they should specify if clients can implement them.
Comment 11 Boris Bokowski CLA 2006-03-30 11:45:43 EST
Here is a summary of the new API:

New classes and interfaces:
  SaveablesProvider
  SaveablesProviderFactory
  INavigatorSaveablesService
  ISaveablesSourceHelper

New in CommonNavigator:
  implements ISaveablePart, ISaveablesSource

New in INavigatorContentService:
  getSaveablesService()

New extension point:
  saveablesProviderFactory
Comment 12 Michael D. Elder CLA 2006-03-30 15:04:28 EST
Some initial comments:

The schema of saveablesProviderFactory has no top level description or examples section. This should be corrected.

The schema describes a saveablesProviderFactory which has a sequence of one element "navigatorContentId" that has a value 

attribute. Why not model this element as part of the navigatorContent extension? This would prevent needing to deal with the 

case where two saveable providers are associated with the same content extension. 

I would actually describe the following approach to the extension point:

(1) Either <saveablesProviderFactory> should be a nested element under the org.eclipse.ui.navigator.navigatorContent 

extension point, with its own Core Expression. I don't really think that this should be modeled as a factory though, as there 

are no other cases where a client contributes a factory, which then contributes the actual "extension" piece. This would have 

the advantage that clients could (a) add saveable models to content that they did not contribute. For instance, a file may be 

contributed by the Resource extension which has XML content. Another extension might come along and decide to expose the XML 

document structure in the Navigator as children of a file. The XMLContent extension might then want to be able to indicate 

when that XML document has gone dirty with decorators and allow the user to save the XML document from the viewer. Your 

current design would not allow this (or at least it's not obvious how it would work just by looking at the pieces exposed to 

a client). When using core expressions, there could be more than one client that has a Saveable representation for a given 

element; however under the current design (driven from the content extension id), the same problem can occur. 

(2) Or, a <saveablesProvider> element should be nested under a <navigatorContent> element. The implication here would be that 

the saveableProvider is directly associated with the enclosing navigator content extension, and should be queried whenever 

the element in the tree matches its possible children expression. This approach limits other downstream providers from 

contributing a Saveable for an element in the viewer that they did not contribute, unless they declare it as one of their 

possible children (which I'd consider a hack to get around the limitation). Alternatively, you could drive the selection of 

which saveableProvider gets precedence by using the _actual_ source of the element; but again, you limit downstream 

extensibility. I think the example use case of the XML document above is quite likely, and if you enforce this limitation, 

the only way to implement the use case would be to override the Resource extension for the single element (whatever XML file 

matched the downstream clients model). 

The current implementation just ignores extra Saveable objects if they exist for a given object.

_________________

This implemention is also subtley different from the other secondary frameworks in the Common Navigator in that it seems to 

drive all INavigatorContentService instances from a single factory. The other secondary services are created by a 

INavigatorContentService if necessary, and drive their object lifecycles from the object lifecycle of the 

INavigatorContentService that the secondary service associated with. This prevents leaking objects that are not required if 

the view has been closed. The NavigatorSaveablesService should _not_ have any static methods (such as bundleChanged(), add(), 

or remove()). Under the current implementation, there will be 0..* INavigatorContentServices each with 0..1 

INavigatorSaveablesService which drive their state from a single static List.The NavigatorSaveablesService should be the 

bundleListener (one for each instance) that will add and remove itself as the INavigatorCOntentService creates and disposes() 

of it. The current design has a bundle listener attached to the NavigatorPlugin that could be notified even if there are no 

active INavigatorContentServices. There is also no place where the listener is removed so long as the NavigatorPlugin is 

active. The lifecycle of every other secondary service is driven from the lifecycle of the INavigatorContentService and this service should be as well.

I would also recommend elimenating the SaveablesSourceHelper and merging its behavior into the INavigatorSaveablesService. 

This would be more consistent with the existing architecture. In the current design, a client must create an 

INavigatorSaveablesService from an INavigatorContentService; then the client must create an ISaveablesSourceHelper. Therefore 

if the client required access to a single instance of the helper they created, they must manage passing that around (opposed 

to just sending a handle to the INavigatorContentService, which could return the same INavigatorSaveablesService exposed 

elsewhere). It also appears that this architecture requires each SaveablesSourceHelper to compute the available Saveables on 

each bundle event, which means if a client creates more than one of these, then each Helper will duplicate the computation of 

all other helpers. It's not clear from the implementation why this would be required, and therefore seems unnecessary. 

Basically the helper is doing the work that the service should be doing.

This implementation also fetches the Content Provider from the viewer throughout. Instead it should access the content 

provider from the contentService that it is created with (I'm referring to the SaveablesSourceHelper class, but again this 

should be merged into the NavigatorSaveablesService, which is created with the contentService in its constructor).

The goto loop in SaveablesSourceHelper needs to be removed. These are _not_ used in the Common Navigator architecture and 

_should absolutely not_ be used. You should be able to restructure your loop so that you don't need to use the "break 

<label>" style code.

SaveablesSourceHelper.getSaveablesProviders() is not thread safe; there is no synchronziation. It should ensure that 

saveableProviders is only in the process of being initialized one time. 

The Factory takes the Content Service Content Provider when it receives a call to createXXX(); I would recommend instead 

passing in the INavigatorCOntentExtension of the associated extension. The client should not need to call into the full-scale 

extensibility of the Navigator Content Service Content Provider to determine their local models; the individual content 

providers will have less overhead and should perform better than the content service content provider which must do more work 

to compute the children. Clients could use the extensible content provider and accidentally trip the load of plugins that do 

not need to be loaded; if the client only has access to _their_ content provider, they cannot load more plugins than they 

need.

Why does doSave() and doSaveAs() in Common Navigator not do anything? If nothing is selected in the viewer, shouldn't everything be saved? Should CTRL+SHIFT+S also save all of the contents of the viewer?


So in summary

(1) The extension point should be a part of org.eclipse.ui.navigator.navigatorContent either as a top level element (saveableProvider) with a core expression of its own or a nested element under <navigatorContent /> and drive itself from either the _actual_ source of an element (see the contribution memory methods on NavigatorContentService) or through the <possibleChildren /> expression. I prefer the former approach. This also implies that with either choice here, the client only provides the SaveableProvider, and not a factory --> provider type pattern. I'd have a SaveableProvider.init(ICommonContentExtensionSite) type method (or another ICommonSaveableExtensionSite, if you think it merits one).

(2) SaveableSourceHelper should be merged into NavigatorSaveableService. 
   (a) The goto-like code should be removed.
   (b) The initial computation of the Saveables should be thread safe.

(3) The static bundle listener attached as part of the NavigatorPlugin's start up should be removed. NavigatorSaveableService instances should add themselves as bundle listeners when created and remove themselves when they are disposed. (This implies that they should be one of the disposed services in the NavigatorContentService.dispose() method). This will be required as you refactor NavigatorSaveableService to drive its object lifecycle from that of NavigatorContentService. 

(4) CommonNavigator (the view part) should implement doSave() and doSaveAs() like a "save all" operation, and also these should be invoked on Ctrl+Shift+S (if not already). 
Comment 13 Michael D. Elder CLA 2006-03-30 17:05:48 EST

I also noticed that the bundle listener seems to be notified for every bundle event. And for each bundle event, it recomputes the set of Saveables. This means that for scenarios where something like a GMF based editor starts, the 30+ plugins triggered to load the editor will cause the recomputation of the saveable model for every provider. Only affected providers should be recomputed (e.g. only bundle events for bundles that define common navigator extensions should trigger the recomputation). Ideally, this should be done as a Job that is scheduled with a delay for each bundle STARTED or STOPPED event (and only these event types; there is _no_ filtering in the current implementation). The delay should be greater than the average amount of time between two plugins loading when a batch of plugins is loaded. So perhaps a good delay would be anywhere from 250-1000ms? 

If there was someway to define an ISchedulingRule that would prevent the Job from executing so long as the Bundle management system was loading or unloading bundles. 

For a commercial product of 1000-2000 plugins, with the Navigator Plugin now being nearer to the bottom of the stack than the top, this has the potential to not only impact startup performance, but also the performance of any plugins loaded once the Workbench is opened. This is really really not a good idea.
Comment 14 Boris Bokowski CLA 2006-03-30 17:35:54 EST
Michael, thanks for your feedback, I will respond to it tomorrow.
Comment 15 Boris Bokowski CLA 2006-04-05 13:20:31 EDT
Michael, I agree with most of your points and will come up with a patch to address them.
Comment 16 Boris Bokowski CLA 2006-04-11 01:05:59 EDT
Created attachment 38248 [details]
work in progress

Michael, here is the patch. I would like to discuss it with you before I release it.
Comment 17 Boris Bokowski CLA 2006-04-11 16:18:11 EDT
Created attachment 38325 [details]
patch against org.eclipse.ui.navigator

This patch addresses Michael's comments. It changes the API in a way that does not cause compile errors for early adopters who are already using it, but it forces them to update their implementations to prepare for a smooth transition.

Some methods and interface are marked as deprecated and will be removed after 3.2RC1. RC1 still contains the deprecated methods in order to avoid compile errors downstream.

Please contact me if you need help with porting to the improved API.
Comment 18 Boris Bokowski CLA 2006-04-11 16:22:31 EDT
Comment #11 is now obsolete. The new API looks like this:

New classes and interfaces:
  SaveablesProvider
  INavigatorSaveablesService

New in CommonNavigator:
  implements ISaveablePart, ISaveablesSource

New in INavigatorContentService:
  getSaveablesService()

New attribute for org.eclipse.ui.navigator.navigatorContent extensions:
  providesSaveables (boolean, defaults to false)

Content providers for which providesSaveables is true must adapt to SaveablesProvider.
Comment 19 Mike Wilson CLA 2006-04-11 16:25:45 EDT
+1. ok to proceed.
Comment 20 Michael D. Elder CLA 2006-04-11 16:53:02 EDT
There's one loophole I see that's easy to fix. The Team use cases wanted the ability to programmatically bind or activate extensions, so onExtensionActivation() doeesn't catch the programmatic visiblity cases (since your configuring the content service directly). 

All you need to do is : 

(1) make NavigatorSaveablesService implement VisibilityAssistant.VisibilityListener 
(2) move "
if (navigatorSaveablesService != null) {
navigatorSaveablesService.recomputeSaveablesAndNotify(true, null);
}" to the listener method and 
(3) add yourself as a listener to the assistant (the Visibility Assistant in NavigatorContentService) when created (in getNavigatorSaveablesService()) 
(4) Remove yourself as a listener to the assistant in NavigatorContentService.dispose().
Comment 21 Boris Bokowski CLA 2006-04-11 17:12:40 EDT
Released (with the changes suggested in comment #20) for >20060411.
Comment 22 Boris Bokowski CLA 2006-06-02 15:38:00 EDT
Verified using I20060602-0010.