ListModelTableViews: selection behavior

Problem definition

The input for a ListModelTableView is always a ListModel which contains ListEntries. So each table row encapsulates a ListEntry as data object. If the user selects one or many rows in a table a selection will be notified. This selection contains all data objects of the selected rows. So in case of ListModelTableViews this data objects are ListEntries. The problem of this behavior is, that there are several selection listeners which receive selection events and extract the containing elements from the selection. Some of these listeners may hold this elements for internal use. If a ListEntry is referenced in this way by any listener, the whole ListModel is also referenced there (because any ListEntry has a reference to the parent ListModel). This behavior may cause performance issues when the user e.g. loads very huge catalogs (with ~ 1 million items). The reason is that a loaded ListModel allocates a lot of memory, especially if the grouping tree is enabled - in this case all grouping columns are loaded completely. So if the user loads another catalog or the same catalog again the "old" ListModel can't be removed from the memory so long as any references to the ListModel (or it's ListEntries) are existing. The new loaded ListModel in turn allocates a lot of memory too. So at this point it can lead to an "OutOfMemory" error. To solve such issues the providing of the selection in all ListModelTableViews has been changed.

Solution

Briefly speaking: a ListModelTableView doesn't fire ListEntries as a selection anymore. Instead of that the corresponding EntityProxies will be extracted and notified as a selection. To ensure that, the selection provider of the ListModelTableView was customized. Each time a selection of the view is requested - the EntityProxies of the currently selected entries will be extracted and provided. In this way no ListEntries anymore will be referenced outside of the view. In most cases the listeners of the selection event need anyway only the proxies of the selected entries. So now they get these proxies directly and should not extract them itself from the ListEntreis.

Adverse impacts

Unfortunately many receivers of a ListModelTableView selection assume that the selection contains ListEntries only and try to cast the selection elements to ListEntries, which can lead to a "ClassCastException" now. In the standard PIM code all such locations were adjusted to be able to handle also selections which contain EntityProxies instead of ListEntries. Luckily both classes - ListEntry and EntityProxy - have a common interface - EntityItem. So to be sure that no "ClassCastException" will be thrown, it is recommended to cast the selection elements to EntityItem which can be both: a ListEntry or an EntityProxy. To get the EntityProxy from an EntityItem just call the method getEntityProxy().

Examples

Command handler

public class CustomCommandHandler extends AbstractHandler
{
@Override
public Object execute( ExecutionEvent event ) throws ExecutionException
{
ISelection selection = HandlerUtil.getCurrentSelection( event );
// if you need only the first selected entry:
EntityProxy selectedElement = StdUiUtils.getFirstSelectedElement( selection, EntityProxy.class );
if ( selectedElement != null )
{
// handle selected proxy
...
}
 
// if you need all selected entries:
EntityProxy[] selectedElements = StdUiUtils.getSelectedElements( selection, EntityProxy.class );
if ( selectedElements.length > 0 )
{
// handle selected proxies
...
}
}
}

Custom views

public class CustomTableView extends StdTableView
{
@Override
public void selectionChanged( IWorkbenchPart selectionPart, ISelection selection )
{
Predicate< ISelection > selectionPredicate = getSelectionPredicate();
if ( selectionPredicate.evaluate( selection ) )
{
EntityProxy selectedElement = StdUiUtils.getFirstSelectedElement( selection, EntityProxy.class );
if ( selectedElement != null )
{
// set/update viewer input for the current selected entry
...
}
this.lastReceivedSelectionPart = selectionPart;
}
else if ( selectionPart.equals( this.lastReceivedSelectionPart ) )
{
if ( selection.isEmpty() )
{
handleEmptySelection();
}
else
{
handleMultiSelection( selection );
}
}
}
}

Don'ts

Never cast the elements of a selection to ListEntries without checking if they are really ListEntries:

public void anyMethodWhichGetsAnySelection ( ISelection selection )
{
IStructuredSelection structuredSelection = ( IStructuredSelection ) selection;
ListEntry listEntry = ( ListEntry ) structuredSelection.getFirstElement(); // <- this is a potential cause of a "ClassCastException"
}

Alternatively use StdUiUtils to extract any objects you expect from the given selection (like in examples above). The corresponding methods of StdUiUtils are proof against NullPointer- or ClassCast-Exceptions.