This Confluence has been LDAP enabled, if you are an ASF Committer, please use your LDAP Credentials to login. Any problems file an INFRA jira ticket please.

Child pages
  • Composite Behaviors
Skip to end of metadata
Go to start of metadata

Table of contents

Howto: Composite Behaviors

It often occurs that multiple wicket behaviors must be used to obtain a certain effect. An example can be a tooltip that requires both a javascript header contribution and an attribute modifier. Another example is that of a confirmation popup when someone clicks on an certain button. Using a composite behavior, which is an example of the composite design pattern, it is possible to realize things like this by adding just a single composite behavior object to a component (e.g. ToolTipBehavior, ConfirmationBehavior) instead of having to add multiple behaviors just to obtain one effect in the user interface. This enhances readability and maintainability of the code.

Example: Confirmation messages

A straightforward example is a confirmation message that asks a user to confirm a (potentially) destructive action before proceeding. One way of doing this is using an attribute on an HTML element that requires confirmation,

   <a href="..." confirmationMessage="Are you sure?">link text</a>

together with javascript that inspects the confirmationMessage attribute and when it's there intercepts the onClick event


function confirmModifyOnclickImpl(element) {
	if (element.nodeType == 1) {
		var attribute = element.getAttribute("confirmationMessage");
		if (attribute) {
			var oldonclick = element.onclick;
			element.onclick = function () {
				var result = confirm(attribute);
				if (!result) {
					return result;
				}
				if (oldonclick) {
					return oldonclick();
				} else {
					// no special actions
				}
			};
		}
	}
	if ((element.nodeType == 1) || (element.nodeType == 9)) {
		var children = element.childNodes;
		for (var i = 0; i < children.length; i++) {
			confirmModifyOnclickImpl(children[i]);
		}
	}
}
function confirmModifyOnclick() {
	confirmModifyOnclickImpl(document);
}

runOnLoad(confirmModifyOnclick);

In the above example, the runOnLoad function is a function defined elsewhere that makes sure that the confirmModifyOnclick function is run when the document is loaded. This function modifies all onClick event handlers for elements that have the confirmationMessage attribute to display a confirmation message before proceeding, allowing the user to cancel.

The standard approach in wicket

The standard approach in wicket is to implement this with behaviors. In this case, we need to add two behaviors to a component that requires confirmation:

  link.add(HeaderContritor.forJavaScript(MyClass.class, "confirm.js"));
  link.add(new AttributeModifier("confirmationMessage", true, new Model(message));  

Nevertheless, this approach is a bit problematic for the following reasons:

  • You need to do two things (add two behaviors) just to accomplish one task (confirmation).
  • If in the future the implementation of confirmation popups changes code might be changed in
    many places to accomplish this.

It would be better to use one behavior and to write code like this:

  link.add(new ConfirmationBehavior("message");

In this way, any implementation changes are localized in the confirmation behavior and the code is conceptually close to reality: we must do one thing in the code to accomplish one thing in the application.

Composite behavior, first attempt

A solution is to use the composite design pattern and create a composite behavior.
This can be done as follows:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import wicket.Component;
import wicket.Response;
import wicket.behavior.IBehavior;
import wicket.markup.ComponentTag;
import wicket.markup.html.IHeaderContributor;

/**
 * Represents a composite behavior allowing the user to attach multiple behaviors to a 
 * component at once. 
 *
 */
public class CompositeBehavior implements IBehavior, IHeaderContributor {
	
	private List<IBehavior> _behaviors; 
	
	public CompositeBehavior(IBehavior[] aBehaviors) { 
		_behaviors = new ArrayList<IBehavior>(Arrays.asList(aBehaviors));
	}
	
	public void add(IBehavior aBehavior) { 
		_behaviors.add(aBehavior);
	}

	public void bind(Component aComponent) {
		for (IBehavior behavior: _behaviors) { 
			behavior.bind(aComponent);
		}
	}

	public void detachModel(Component aComponent) {
		for (IBehavior behavior: _behaviors) { 
			behavior.detachModel(aComponent);
		}
	}

	public void exception(Component aComponent, RuntimeException aException) {
		for (IBehavior behavior: _behaviors) { 
			behavior.exception(aComponent, aException);
		}
	}

	public void onComponentTag(Component aComponent, ComponentTag aTag) {
		for (IBehavior behavior: _behaviors) { 
			behavior.onComponentTag(aComponent, aTag);
		}
	}

	public void rendered(Component aComponent) {
		for (IBehavior behavior: _behaviors) { 
			behavior.rendered(aComponent);
		}
	}

	public void renderHead(Response aResponse) {
		for (IBehavior behavior: _behaviors) {
			if ( behavior instanceof IHeaderContributor) { 
			    ((IHeaderContributor)behavior).renderHead(aResponse);
			}
		}
	}

}

At construction, the behavior is passed a list of behaviors that it must use. In the bind method of the composite behavior, the composite behavior adds each of its individual behaviors to the component to make sure they are used. Note that the composite behavior also implements IHeaderContributor so that header contributions are also taken into account.

Using this composite behavior, defining a confirmation behavior is just as easy as creating a subclass and passing in the two behaviors that it requires.

Composite behavior, now it works...

Nevertheless, we are not there yet. The composite behavior of the previous example has one flaw and this is that behaviors themselves cannot be internationalized. The problem is that behaviors are constructed in a composite behavior subclass before their parent component is known. As a result, it is impossible to use internationalization. And internationalization is required for the confirmation behavior if we want to internationalize the confirmation messages.

The solution is simple. Instead of creating the behaviors at constructions of the composite behavior we need to delay adding them until their parent component is known, and this is in the bind method. So we modify the composite behavior as follows:

  public class CompositeBehavior implements IBehavior, IHeaderContributor {
	
	private List<IBehavior> _behaviors; 
	
	public CompositeBehavior() { 
		_behaviors = new ArrayList<IBehavior>();
	}

        ....

        public void bind(Component aComponent) {
                for (IBehavior behavior: createLocalizedBehaviors(aComponent) ) {
                        behavior.bind(aComponent);
                        add(behavior);
                }
        }

        /**
         * Callback to create localized behaviors.
         * @return Array of behaviors. The bind() method is called on each behavior
         *   returned from this method.
         */
        protected abstract IBehavior[] createLocalizedBehaviors(Component aComponent);

        ....
} 

Now the subclass of CompositeBehavior should implement createLocalizedBehaviors to create the behaviors it requires. Using the component which is passed in, localization can be done.

  • No labels