WebApp: Change in the user interface classes

September 26, 2012 by Monomon   Comments (0)

, , , ,

Up until now, with WebApp 1.1 as the last version, most content was put in windows. From a user's perspective, this meant that you could not (or only through workarounds) refer to e.g. a calendar appointment while you were replying to an e-mail. With the recent WebApp 1.2 previews (with 1.2.svn36382 as the first version), this has been changed so that content is now put in tabs instead. It's easy to switch between tabs to refer to other opened e-mails or tasks, etc.

This change is also described in this http://www.zarafa.com/blog/post/2012/09/innovating-power-users-webapp-tabbing post by Milo Oostergo, but from a user perspective. Due to the necessary changes in the WebApp API, this is a deeply technical post about the change and how to take advantage of this in your plugin code. 

So far, Zarafa WebApp only allowed opening content in 'dialogs' (Ext.Window). The Zarafa.core.ui.Dialog was thus always tightly coupled to the Ext.Window class.

With the introduction of a tabbed interface arose the need for more flexibility: being able to insert any content in any container, either an Ext.Window or Tab. Preferably, this should require little effort from a developer to use and extend with new container types.

Time for refactoring, then.


Zarafa.core.ui.Dialog was the main target, as it only handled Ext.Window classes and contained static methods for dialog class registration and creating new dialogs. It needed to be split into parts with clear responsibilities.

Classes that used to be dialogs (Zarafa.core.ui.Dialog and its subclasses) are now called content panels, with base class Zarafa.core.ui.ContentPanelZarafa.core.ui.RecordContentPanel replaces RecordDialog, Zarafa.core.ui.MessageContentPanel replaces MessageDialog, and so on.
Class diagram of ContentPanel and its subclasses

In contrast to the way dialogs worked previously, content panels have no knowledge of their container. A content layer plugin is installed on the content panel, which is specific to the container type and knows how to update it. It provides handlers for events that a ContentPanel might emit, such as updating the title and icon class, closing and focusing. 

The content layer plugin only acts as glue between the component and its container. The base class for content layer plugins is Zarafa.core.plugins.ContentLayerPlugin, with subclasses for each container type - the ones used currently are Zarafa.core.plugins.ContentWindowLayerPlugin and Zarafa.core.plugins.ContentTabLayerPlugin.

Let us take the tab panel as a specific example - a tab has a title and an icon class (applied to the tab strip) which might be changed by the contained panel, and focusing would mean switching to a certain panel's tab. These changes happen inside the content panel (because they depend on the content and/or record), but are reflected in the container by the content layer plugin. If you are writing an e-mail message and type in a subject, the MailCreateContentPanel fires an event for the change, which is captured by the content layer plugin, and applied to the tab title.

Another new class - Zarafa.core.data.UIFactory - makes the decision of which container to use in each case, and then creates a component in that container. It has a cascade of rules that determine the container type. By default the UI factory selects the lowest layer (one with the smallest index). At the moment this would be the tab layer, after which comes the window layer.
There are ways to force a layer each time a content panel is created. To indicate that a content panel must be placed on a specific layer, the 'manager' configuration option should be set to Ext.WindowMgr to use an Ext.Window, for example. Alternatively, the configuration option 'modal' could be used, after which the UI factory will look up the layer which has support for modal content (in the current situation that is only the Window Layer).

The UI factory and content layer plugins are tied together via UI factory layers. These register to the UI factory and are responsible for declaring the availability of a layer and its capabilities (e.g. is modal content supported). UI factory layers follow the same convention as content layer plugins - base class Zarafa.core.data.UIFactoryLayer with subclasses Zarafa.core.data.UIFactoryWindowLayer and Zarafa.core.data.UIFactoryTabLayer.
When a content panel is assigned to the UI factory layer, the latter is responsible for creating the content panel, installing the content layer plugin on it, and inserting the component into the corresponding container. From this point on, the content layer plugin will be responsible for handling the events which should be forwarded to the container. Check the documentation of ContentPanel for a complete list of events that it fires.
Class diagram of UIFactory, UIFactoryLayer, and ContentLayerPlugin

The UIFactory method openLayerComponent is now the general way of opening a component in a container (this succeeds the Zarafa.common.Actions.openRecord function). It accepts as arguments a shared component type, a record or array of records, and a configuration object.
There are other general-purpose methods transferred from Zarafa.common.Actions to the UIFactory, such as openViewRecord, openCreateRecord, and openContextMenu. More specific ones, such as openRecurrenceContent or openCategoriesContent, remain in common.Actions.

Basic usage

Creating a component and inserting it into a container only requires a single call to the UIFactory with the component type.
Let us see how this is done in Zarafa.mail.Actions:
config = Ext.applyIf(config || {}, {
        modal : true

var componentType = Zarafa.core.data.SharedComponentType['mail.dialog.options'];
Zarafa.core.data.UIFactory.openLayerComponent(componentType, records, config);

In the above example modal is set to true in the config, forcing a modal dialog (which would open in an Ext.Window currently).

Other methods do not require a component type to be supplied:
openCreateMailContent : function(model, config)
        var record = model.createRecord();
        Zarafa.core.data.UIFactory.openCreateRecord(record, config);

This method automatically uses componentType = Zarafa.core.data.SharedComponentType['common.create'];

Creating a new layer

Let us create a hypothetical whiteboard layer. It would place components on a 'whiteboard' and update them accordingly with a marker and a sponge.
A layer requires two components - a content layer plugin and a UI factory layer.
The UI factory layer extends Zarafa.core.data.UIFactoryLayer:
Zarafa.core.data.UIFactoryWhiteboardLayer = Ext.extend(Zarafa.core.data.UIFactoryLayer, {

UIFactoryLayer's constructor config specifies its layer type (string) and the ContentLayerPlugin ptype that it would install on a panel:
constructor : function(config)
        config = config || {};

       Ext.applyIf(config, {
                 type : 'whiteboard',
                 index : 2,
                 allowModal : false,
                plugins : [ 'zarafa.contentwhiteboardlayerplugin' ]

       Zarafa.core.data.UIFactoryWhiteboardLayer.superclass.constructor.call(this, config);

Then a create method is required, which instantiates the panel and adds it to the container:
create : function(component, config)
        var panel = new component(config);
        //reference whiteboard from global container object

The layer needs to be registered to the UIFactory, which would make it available for selection:
Zarafa.core.data.UIFactory.registerLayer(new Zarafa.core.data.UIFactoryTabLayer());

Finally, we need a ContentLayerPlugin, derived from Zarafa.core.plugins.ContentLayerPlugin:
Zarafa.core.plugins.ContentWhiteboardLayerPlugin = Ext.extend(Zarafa.core.plugins.ContentLayerPlugin, {

At initialization time, the plugin can obtain a reference to the container, in case it is not globally accessible:
initPlugin : function()
    this.whiteboard = this.field.findParentByType('whiteboard');

The ContentLayerPlugin needs a set of methods to update the container whenever the contained panel changes:
setTitle : function(title)

close : function()

These methods are automatically called when the contained panel fires the events 'titlechange' and 'close'.

Once a developer creates the entire ContentLayerPlugin, they need to register it to Ext.js's plugin registry:
Ext.preg('zarafa.contentwhiteboardlayerplugin', Zarafa.core.plugins.ContentWhiteboardLayerPlugin);

Having all components in place, you might want to ensure your layer is used. This can be done in a number of ways:
* set your UIFactoryLayer to have the lowest index of all layers
* if there is a manager for your layer you could specify it in your configuration:
var config = { manager : WhiteboardManager };
Zarafa.core.data.UIFactory.openLayerComponent(componentType, records, config);
* finally, if your layer allows modal content (and has the lowest index of all layers allowing it) you could specify the 'modal' option:
var config = { modal : true };
Zarafa.core.data.UIFactory.openLayerComponent(componentType, records, config);

And it's done! Content is going to appear in your new layer.