-
Notifications
You must be signed in to change notification settings - Fork 17
Lifecycle and session
This section covers how WComponents UIs are instantiated, how they handle multiple users and how applications should store custom data.
Only one instance of a WComponents tree is created per application in a similar way as for Servlets. This is to reduce the amount of data which needs to be stored for individual user sessions.
The top-level WComponent for the application's UI must be obtained through the "UIRegistry" rather than being directly instantiated so as to ensure that a single instance is created and initialised correctly. The application's Servlet should resemble the code snippet below:
public class MyApplicationServlet extends WServlet {
public WComponent getUI(Object request) {
// "MyApplication" extends WApplication
String className = MyApplication.class.getName();
return UIRegistry.getInstance().getUI(className);
}
}
The UIRegistry will invoke the default public constructor of the top-level component. The constructor should set up the default state of the application by performing the following steps:
- instantiate each of its child WComponents; then
- add each child WComponent to the WComponents tree; then
- set the default state of the application.
Each application WComponent subclass should follow the same pattern: creating its content and setting the default state of its components. No user information is available during construction.
After the constructor completes the WComponents UI tree is "locked" marking it ready for use in a multi-user environment.
WComponents cannot store state information as fields within their classes as component instances are shared between users. All WComponents store their data in ComponentModels, and the framework ensures that the component models are kept separate for different user sessions. The component model provides a type safe data structure which components use to store their specific data.
When the WComponents UI is initially constructed each WComponent only has a single (shared) model which holds the default state of the application. After the WComponents tree is constructed and locked components are able to access a user-specific model to store changes from their default state.
The main benefit of this approach is that for each user the WComponents framework only needs to hold the differences between the default application state and the current state of the application. The current state may include what screen a user is on, which tab they are on, what data they have entered, etc.
User-specific component models are stored in an object called the "UIContext". The UIContext is then stored in the underlying container's session storage mechanism. For example, WComponents served by WServlet will have their UIContext stored in the underlying HttpSession. The WComponents framework sets up the UIContext automatically when a request is received. Redundant ComponentModels are automatically removed from the UIContext at the end of request processing. The removed models include cases where a component has been removed from the UI tree or the user component model is equivalent to the component's default model.
The UIContext stores additional information which relates to the user's session rather than particular component. Most application code should never need to know about the UIContext but can obtain the currently effective UIContext from the UIContextHolder.
Most application code should never need to know about the UIContext and will either use the getters/setters on existing components or access data in custom ComponentModels.
Any WComponents may need to store some user specific data. This data needs to be stored in the user's UIContext instance as mentioned above. Developers using the WComponents framework are discouraged from storing any non-transactional data in the UIContext directly as it will bloat the underlying session on the server. Data which is not user-sourced (e.g. is persisted to a database) should be retrieved as needed or temporarily stored in an application cache instead.
When writing a WComponent subclass it is important to keep in mind that each instance of the component you write will be shared by all users. Storing data in mutable fields within any WComponent subclass will allow data to leak across to other user sessions. The correct way to store application data is detailed below.
The preferred method for storing application data is by defining an extension of the ComponentModel class.
ComponentModel subclasses are required to be public; have an available public default constructor; and be Serializable
for user session serialization to function correctly. ComponentModel subclasses should not override either the
equals(Object)
or hashCode()
methods.
WComponents which define their own ComponentModel must override the newComponentModel()
method to return a new
instance of their custom model. The return type of the method should be narrowed so that subclasses are forced to use
the correct model type and any errors are picked up at compile time. An example custom component and model
implementation is shown below.
public class MyClass extends AbstractWComponent {
public String getCurrentRecordId() {
return getComponentModel().currentRecordId;
}
public void setCurrentRecordId(String currentRecordId) {
getOrCreateComponentModel().currentRecordId = currentRecordId;
}
protected MyComponentModel newComponentModel() {
return new MyComponentModel();
}
protected MyComponentModel getComponentModel() {
return (MyComponentModel) super.getComponentModel();
}
protected MyComponentModel getOrCreateComponentModel() {
return (MyComponentModel) super.getOrCreateComponentModel();
}
/** Holds additional state information. */
public static class MyComponentModel extends ComponentModel {
private String currentRecordId;
}
}
In the example the methods getComponentModel()
and getOrCreateComponentModel()
have also been overridden for
convenience. These methods are used to obtain the relevant instance of the component model for the current context.
The getComponentModel()
method will return the currently effective component model. When the component is being
constructed this will return the shared model. During request processing this will return the user model, if one exists,
otherwise the shared model. The ComponentModel returned by this method should never be modified. This method must only
be called by getters which do not have any side-effects.
The getOrCreateComponentModel()
should be used to obtain an instance of a ComponentModel for updating. This method
will also return the shared model during UI construction, but will create a new ComponentModel during request processing
if a user-specific model does not already exist. This method must be called by all setters and any getters which have
side-effects.
The WComponent class provides un-typed convenience methods for storing and retrieving and object from the UIContext. These methods behave in much a similar way to that of javax.servlet.http.HttpSession. These methods are as follows:
public void setAttribute(String key, Object value)
public Object getAttribute(String key)
public void removeAttribute(String key)
The use of these methods is discouraged as they do not provide any protection against typos in the keys nor compile time type checking.
The following code will break in a multi-user environment as multiple users will be sharing the same MyComponent instance.
public class MyComponent extends AbstractWComponent {
private String currentRecordId;
public void setCurrentRecordId(String currentRecordId) {
this.currentRecordId = currentRecordId;
}
public String getCurrentRecordId() {
return currentRecordId;
}
}
Storing immutable data in final fields set only from the constructor is safe as there is no risk of accidentally modifying the data.
public class MyComponent extends AbstractWComponent {
private final String recordTypeFilter;
public MyComponent(String recordTypeFilter) {
this.recordTypeFilter = recordTypeFilter;
}
public String getRecordTypeFilter() {
return recordTypeFilter;
}
}
Storing mutable data in final fields set only from the constructor is unsafe as external code accidentally modify the
data. In the code below the getRecordTypeFilters()
method will return a reference to the shared list if
there is no model set. External code can modify the returned list which will affect all users.
public class MyComponent extends AbstractWComponent {
private final List<String> recordTypeFilters;
public MyComponent(List<String> recordTypeFilters) {
this.recordTypeFilters = recordTypeFilters;
}
public List<String> getRecordTypeFilters() {
return recordTypeFilters;
}
}
A safe implementation of the previous code is to create a new list containing the data and to ensure that it can not be updated. The code below creates a read-only copy of the list which was passed in so that it can not be accidentally updated.
public class MyComponent extends AbstractWComponent
{
private final List<String> recordTypeFilters;
public MyComponent(List<String> recordTypeFilters) {
this.recordTypeFilters = Collections.unmodifiableList(new ArrayList<String>(recordTypeFilters));
}
public List<String> getRecordTypeFilters() {
return recordTypeFilters;
}
}
Component models must never be cached. The "myModel" field in the code below will always point to the shared model and will cause unpredictable behaviour in a multi-user environment.
public class MyComponent extends AbstractWComponent {
private MyComponentModel myModel = (MyComponentModel) getComponentModel();
public void setSomething(Object something) {
myModel.something = something;
}
public String getSomething() {
return myModel.something;
}
}
The following code incorrectly uses getComponentModel()
to retrieve a model for updating in both the getter and
setter. This means that updates may be placed in the shared model rather than the user model. In both methods the
getOrCreateComponentModel()
method must be used.
public class MyComponent extends AbstractWComponent {
public void setSomething(Object something) {
((MyComponentModel) getComponentModel()).something = something;
}
public Object getSomething() {
MyComponentModel model = (MyComponentModel) getComponentModel();
model.accessCount++;
return model.something;
}
}