ADF 11g – persisted run time user UI personalization or: Impatient man's MDS

 

One of the rather cool pieces of functionality that did not make it into the JDeveloper 11g Boxer release of early October 2008 is the Meta Data Service or MDS and especially its capability to store and reapply user created personalizations of the User Interface across sessions. Some simple examples of what this means: ADF 11g Rich Client Components allow users to manipulate the state of components – such as the position of the separator in the PanelSplitter, the ordering and width of Table Columns, the initially visible tab or accordion child etc.. Through MDS, these changes are captured and stored for th duration of the session (if so desired), which means that when the user returns to a page thus ‘personalized’,  the component will not assume their default state as specified in the JSF page at design time by the developer, but rather the state that user specified. Eventually MDS will persist these component personalizations across sessions – but not right now. That means that at the present when a user starts a new session, all components are presented in their default state.

In this article I will describe two things: Change Persistency for all attributes – not just the built-in settings that can be manipulated through the components and Persisting the changes across sessions, even with the current release of JDeveloper, ADF and MDS.

First of all, the change persistency mechanism does not only apply to the built in features of ADF 11g Rich Components, such as the table and panelsplitter as discussed above, but can also be used for virtuall all other attributes of the Rich Components. If we provide the user with an interface to specify the value of certain attributes, MDS/Change Persistency will ensure that those changes are retained during the session and reapplied whenever the page is accessed.

Let’s start with a simple page that contains a table that displays data (from a Pojo based Data Control, though that is hardly relevant).

ADF 11g - persisted run time user UI personalization or: Impatient man's MDS adfpoormanmds001

The table component in combination with the panelCollection allows us a number of personalization options: change the column order, change the width of columns, hide columns. After making some changes, the page looks like this:

ADF 11g - persisted run time user UI personalization or: Impatient man's MDS adfpoormanmds002

When we have enabled Change Persistency (at session level), the table will retain some of the changes when we revisit the page (when we stay on the same page, all changes are retained anyway). The table will look like this (note that the column order was retained as well as the ‘visible’ setting, though not the width.

ADF 11g - persisted run time user UI personalization or: Impatient man's MDS adfpoormanmds003

In order to enable Change Persistency across the session – default behavior is no change persistency at all – the following entry must be added to the web.xml:

    <context-param>
        <param-name>org.apache.myfaces.trinidad.CHANGE_PERSISTENCE</param-name>
        <param-value>session</param-value>
    </context-param>

Now when we open a new browser window and access this same page, none of the changes made by the user are visible anymore, as they are persisted during the session, not beyond it.

Programmatically injecting Persistent changes

We can apply changes programmatically to the Change Persistence manager, for example at the start of a session. Without doing so, all pages would render in the session just like they were designed. By injecting changes programmatically into the Change  Persistence framework at the start of a session, we can influence the way the pages or rather the components look in the context of that particular session.

In this example, I have hard coded the changes to apply in the session whenever a page is first displayed in that session (so all session will show the same component settings, that are all different from the way the components were configured at run-time). However, the next step of collecting the changes stored in the Change Persistence framework at the end of a session and storing them persistenly in a database for example combined with reading those changes from the database at start of a new session is fairly straightforward.

When we run the page, this time with the code to apply the changes embedded in the application, the same page – I repeat: the exact same page definition, same JSPX file – looks like this:

ADF 11g - persisted run time user UI personalization or: Impatient man's MDS adfpoormanmds004

The code that made the difference, that applied the change to the Change Persistency framework before the page got rendered for this first time in this session, looks like this:

    private void applyAllPersistentChanges() {

        if (!viewsProcessed.containsKey(FacesContext.getCurrentInstance().getViewRoot().getViewId())) {
            System.out.println("Apply all Persistent Changes to " +
                               FacesContext.getCurrentInstance().getViewRoot().getViewId());
            Map viewMap =
                getComponentToChangesMapForView(FacesContext.getCurrentInstance(),
                                                FacesContext.getCurrentInstance().getViewRoot().getViewId(),
                                                true);

            if ("/EmpTable.jspx".equalsIgnoreCase(FacesContext.getCurrentInstance().getViewRoot().getViewId())) {
                String uid = "empTablePanelCollection:empTable:colJob";
                AttributeComponentChange acc =
                    new AttributeComponentChange("visible", false);
                addChangesToComponent(uid, viewMap,
                                      new ComponentChange[] { acc });

                String uid2 = "empTablePanelCollection:empTable:colHiredate";
                AttributeComponentChange acc2 =
                    new AttributeComponentChange("visible", false);
                addChangesToComponent(uid2, viewMap,
                                      new ComponentChange[] { acc2 });

                String uid3 = "empTablePanelCollection:empTable:colEname";
                AttributeComponentChange acc3 =
                    new AttributeComponentChange("displayIndex",
                                                 new Integer(2));
                AttributeComponentChange acc4 =
                    new AttributeComponentChange("width", 400);
                AttributeComponentChange acc5 =
                    new AttributeComponentChange("headerText",
                                                 "Name of Employee");
                addChangesToComponent(uid3, viewMap,
                                      new ComponentChange[] { acc3, acc4,
                                                              acc5 });

                String uid4 = "empTablePanelCollection:empTable:colmgr";
                AttributeComponentChange acc6 =
                    new AttributeComponentChange("displayIndex",
                                                 new Integer(1));
                AttributeComponentChange acc7 =
                    new AttributeComponentChange("width", 400);
                addChangesToComponent(uid4, viewMap,
                                      new ComponentChange[] { acc6, acc7 });
            }
            if ("/goEmp.jspx".equalsIgnoreCase(FacesContext.getCurrentInstance().getViewRoot().getViewId())) {
                String uid = "richTextEditor";
                AttributeComponentChange acc =
                    new AttributeComponentChange("inlineStyle",
                                                 "background-color:Orange;");
                AttributeComponentChange acc1 =
                    new AttributeComponentChange("columns", 250);
                addChangesToComponent(uid, viewMap,
                                      new ComponentChange[] { acc, acc1 });

            }
            viewsProcessed.put(FacesContext.getCurrentInstance().getViewRoot().getViewId(),
                               Boolean.TRUE);
            reportAllComponentChangesForPage(FacesContext.getCurrentInstance().getViewRoot().getViewId());
        }
    }
    public static void addChangesToComponent(String uid, Map viewMap,
                                       ComponentChange[] cc) {
        List changeListForComponent = (List)viewMap.get(uid);
        if (changeListForComponent == null) {
            changeListForComponent = new CopyOnWriteArrayList();
            viewMap.put(uid, changeListForComponent);
        }
        for (ComponentChange c : cc) {
            changeListForComponent.add(c);
        }
    }
    public static Map getComponentToChangesMapForView(FacesContext facesContext,
                                                String viewId,
                                                boolean createIfNecessary) {
        Map sessMap = facesContext.getExternalContext().getSessionMap();
        Map viewToChangesMap =
            (Map)sessMap.get("org.apache.myfaces.trinidadinternal.Change");
        if (viewToChangesMap == null) {
            if (!createIfNecessary)
                return null;
            viewToChangesMap = new ConcurrentHashMap();
            sessMap.put("org.apache.myfaces.trinidadinternal.Change",
                        viewToChangesMap);
        }
        Map componentToChangesMap = (Map)viewToChangesMap.get(viewId);
        if (componentToChangesMap == null) {
            if (!createIfNecessary)
                return null;
            componentToChangesMap = new ConcurrentHashMap();
            viewToChangesMap.put(viewId, componentToChangesMap);
        }
        return componentToChangesMap;
    }

The HttpSession contains an object called  org.apache.myfaces.trinidadinternal.Change. This is a Map that contains a Map with the changes for a particular page (ViewId). Every component against which changes have been recorded is represented in the page specific Map: its Id is one of the key values. Under the key is a List of ComponentChange objects. These component changes specify the persistent changes that have been applied to and recorded for the Component. In method addChangesToComponent is the code that extends the list with one additional ComponentChange. This method is called from applyAllPersistentChanges(), the method that applies the hard coded Component Changes when a page is loaded for the first time in a session.

To trigger execution of this code I have configured a PhaseListener (in faces-config.xml):

<?xml version="1.0" encoding="windows-1252"?>
<faces-config version="1.2" xmlns="http://java.sun.com/xml/ns/javaee">
  <application>
    <default-render-kit-id>oracle.adf.rich</default-render-kit-id>
  </application>
  <lifecycle>
    <phase-listener>nl.amis.view.ApplyPersistentChanges</phase-listener>
  </lifecycle>
...

Whenever a View is about to be rendered, the PersistentChangeManager.applyPersistentChanges() method is invoked; this method was described above: it checks whether the current ViewId already had its persistent changes applied and if not will do that.

package nl.amis.view;

import javax.el.ELContext;

import javax.faces.application.Application;
import javax.faces.context.FacesContext;
import javax.faces.event.PhaseEvent;
import javax.faces.event.PhaseId;
import javax.faces.event.PhaseListener;


public class ApplyPersistentChanges implements PhaseListener {
    public ApplyPersistentChanges() {
    }

    public void afterPhase(PhaseEvent phaseEvent) {
    }

    public void beforePhase(PhaseEvent phaseEvent) {
        invokePersistentChangeManager();

    }

    private void invokePersistentChangeManager() {
        // get hold of #{SessionWideChangePersistenceManager} and invoke applyPersistentChanges()
        FacesContext ctx = FacesContext.getCurrentInstance();
        ELContext elCtx  = ctx.getELContext();
        Application app  = ctx.getApplication();

        SessionWideChangePersistenceManager mgr = (SessionWideChangePersistenceManager)app.evaluateExpressionGet(ctx, "#{SessionWideChangePersistenceManager}" ,SessionWideChangePersistenceManager.class);
        mgr.applyPersistentChanges();

    }


    public PhaseId getPhaseId() {
        return PhaseId.RENDER_RESPONSE;
    }
}

When the application is run and a page is accessed, the PhaseListener is triggered – before render – and calls SessionWideChangePersistenceManager.applyAllPersistentChanges(). This method checks whether the current View is already processed. If not, it will create a series of hard coded AttributeComponentChange objects that are added to the appropriate components. Note: typically these changes will not be hard coded – such changes are better embedded in the JSPX page anyway, although even these hard coded changes could easily be switched on and off using a context parameter or even user preference – but instead retrieved from a database or other meta-data store (MDS when that facility is fully available later this year).

Applying additional persistent changes at run time

We have seen now how we can apply a series of changes programmatically, in this case hard coded but potentially based on change records retrieved from some persistent store. We will now look at ways to provide additional control to the end user that allows her to influence the appearance of components in ways that are not embedded in the components.

For example, we can offer the user a choice for the table layout – radio group with options small, normal and wide – and upon a certain choice apply a set of component changes that will be persisted through out the session! These changes – a silly example – apply to the background color and width of the Manager column, though of course they could pertain to any number of properties for any number of components.

The table with radio group looks like this:

ADF 11g - persisted run time user UI personalization or: Impatient man's MDS adfpoormanmds005

When the user selects for example Small, here is the effect:

ADF 11g - persisted run time user UI personalization or: Impatient man's MDS adfpoormanmds008

and here is the effect of selecting Wide:

ADF 11g - persisted run time user UI personalization or: Impatient man's MDS adfpoormanmds007

Note: the change applied after selecting a style in the radio group is persisted throughout the session, when this page is visited again, the change is still in effect.

The code for the radio group itself:

       <af:selectOneRadio label="(persisted) Theme Selector" autoSubmit="true" id="themeselector"
                           valueChangeListener="#{TableListener.ThemeChangeListener}">
          <af:selectItem label="Small" value="small"/>
          <af:selectItem label="Normal" value="normal"/>
          <af:selectItem label="Relaxed (Wide)" value="wide"/>
        </af:selectOneRadio>

The valueChangeListener method that applies the component changes:

    public void ThemeChangeListener(ValueChangeEvent valueChangeEvent) {
        applyThemeChanges((String)valueChangeEvent.getNewValue());
    }

    private void applyThemeChanges(String theme) {

        Map viewMap =
            SessionWideChangePersistenceManager.getComponentToChangesMapForView(FacesContext.getCurrentInstance(),
                                                                                FacesContext.getCurrentInstance().getViewRoot().getViewId(),
                                                                                true);

        String uid = "empTablePanelCollection:empTable:colmgr";

        AttributeComponentChange accWidth =

            new AttributeComponentChange("width",
                                         ("small".equalsIgnoreCase(theme) ?
                                          50 :
                                          ("normal".equalsIgnoreCase(theme) ?
                                           130 : 400)));
        AttributeComponentChange accStyle =

            new AttributeComponentChange("inlineStyle",
                                         ("small".equalsIgnoreCase(theme) ?
                                          "background-color:yellow" :
                                          ("normal".equalsIgnoreCase(theme) ?
                                           "background-color:gray" :
                                           "background-color:blue")));

        SessionWideChangePersistenceManager.addChangesToComponent(uid, viewMap,
                                      new ComponentChange[] { accWidth, accStyle});

        UIComponent col = FacesContext.getCurrentInstance().getViewRoot().findComponent(uid);
        if (col != null) {
            accWidth.changeComponent(col);
            accStyle.changeComponent(col);
        }

    }

This code is in the class TableListener that has been configured as managed bean:

  <managed-bean>
    <managed-bean-name>TableListener</managed-bean-name>
    <managed-bean-class>nl.amis.view.TableListener</managed-bean-class>
    <managed-bean-scope>session</managed-bean-scope>
  </managed-bean>

Intercepting Attribute Changes

Using this technique of programmatic change injection, we could also intercept changes – such as repositioning of a column – to complement such a user driven change with a change of our own. For example we could implement functionality that would complement the drag & drop of column Salary to a new location with a repositioning of the Commission column, to make sure that whenever Salary is moved, Commission is right aligned with it.

Intercepting attribute changes is done using an Attribute Change Listener which can be configured on almost all AD Faces components.

Retrieving the persisted changes for every page/component

We may want to retrieve all the persisted changes at the end of a session, store them to a persistent store to have them available when the user returns for a new session. The code necessary to read the contents of the Persistent Changes is fairly straightforward and can be seen here:

From the logging in the console we can see which changes are applied:

Apply all Persistent Changes to /EmpTable.jspx

* Changes for empTablePanelCollection:empTable:colmgr
-------------------------------------
displayIndex = 1
width = 400

* Changes for empTablePanelCollection:empTable:colJob
-------------------------------------
visible = false

* Changes for empTablePanelCollection:empTable:colEname
-------------------------------------
displayIndex = 2
width = 400
headerText = Name of Employee

* Changes for empTablePanelCollection:empTable:colHiredate
-------------------------------------
visible = false

This logging is reported by the method  reportAllComponentChangesForPage:

    public void reportAllComponentChangesForPage(String viewId) {
        SessionChangeManager cm = new SessionChangeManager();
        Map changeMap =
            (Map)FacesContext.getCurrentInstance().getExternalContext().getSessionMap().get("org.apache.myfaces.trinidadinternal.Change");


        Map changesForViewMap = (Map)changeMap.get(viewId);
        Iterator<String> compIds = changesForViewMap.keySet().iterator();

        if (compIds != null)
            while (compIds.hasNext()) {
                String compId = compIds.next();
                List<ComponentChange> changes =
                    (List<ComponentChange>)changesForViewMap.get(compId);

                if (changes != null)
                    System.out.println("");
                System.out.println("* Changes for " + compId);
                System.out.println("-------------------------------------");
                for (ComponentChange cc : changes) {

                    if (cc instanceof AttributeComponentChange) {
                        System.out.println(((AttributeComponentChange)cc).getAttributeName() +
                                           " = " +
                                           ((AttributeComponentChange)cc).getAttributeValue());
                    }
                }
            }
    }

Resources

Download JDeveloper 11g Application: impatientmansmds.zip.

 

 

3 Comments

  1. Venkatesh November 12, 2009
  2. Sunanda August 20, 2009
  3. Ram Subramanian August 11, 2009