Question

I have a number of Wicket components on a page that use a PropertyModel to reflect properties of some beans. Using AjaxFormComponentUpdatingBehaviors, these components are automatically updated via Ajax when the user changes them.

When properties are changed, the beans I want to edit with my components fire PropertyChangeEvents that should trigger re-renders of certain components that listen to these events (implementing PropertyChangeListener):

Example:

  1. User edits a TextField with a PropertyModel and an AjaxFormComponentUpdatingBehavior
  2. An AJAX request is sent
  3. Wicket dispatches the request to the AjaxFormComponentUpdatingBehavior
  4. The behavior's onEvent updates the PropertyModel (unfortunately, this method is final)
  5. The PropertyModel calls the backing bean's property setter
  6. The backing bean fires and PropertyChangeEvent
  7. Now I want all components listening for changes of the same backing bean to be notified
  8. The behavior calls the abstract onUpdate, but now it's to late, the property change events are already handled.

Since my beans are not serializable, I cannot register the components permanently as event listeners. I either need to register proxy objects that somehow retrieve the component to notify, or register my components temporarily for the scope of the AJAX request.

What I would like to do is to hook into Wickets request cycle after the target page has been loaded but before the Ajax behavior updates the model, that would lead to the PropertyChangeEvent. Here I can register every component as a event listener on their backing beans (addPropertyChangeListener) so that they are notified if they need to be updated.

Then, in onEvent, each component can take measures to update itself using the AjaxRequestTarget if they received a PropertyChangeEvent before.

Finally, in onDetach, the components can unregister from their beans (removePropertyChangeListener).

Unfortunately, I found no built-in way to get a notification "on Ajax request". In my Ajax behavior's onUpdate methods, the model has already been updated and it is too late to register change listeners. I could implement my own behavior, but with the different component options (text fields, choice lists, etc.), this is quite an effort.

Did I miss something?

Was it helpful?

Solution 4

This is what I came up with in the end.

I subclassed IContextProvider<AjaxRequestTarget, Page> to create a custom provider for AjaxRequestTarget objects. When an AjaxRequestTarget is requested, I broadcast it to the component tree using Wicket's event mechanism.

public class BroadcastingAjaxRequestTargetProvider implements IContextProvider<AjaxRequestTarget, Page> {
    private final IContextProvider<AjaxRequestTarget, Page> parent;

    public BroadcastingAjaxRequestTargetProvider(IContextProvider<AjaxRequestTarget, Page> parent) {
        this.parent = parent;
    }

    @Override
    public AjaxRequestTarget get(Page page) {
        AjaxRequestTarget target = parent.get(page);
        page.send(page, Broadcast.BREADTH, new AjaxRequestBegin(target));
        return target;
    }
}

The class AjaxRequestBegin is just a small payload object encapsulating the AjaxRequestTarget.

I register this provider in my Wicket application's init() method:

setAjaxRequestTargetProvider(new BroadcastingAjaxRequestTargetProvider(getAjaxRequestTargetProvider()));

Now each component gets notified when an AJAX request is handled, before Wicket dispatches it to a component or behavior. A component can override onEvent to register a PropertyChangeListener for the request:

public void onEvent(IEvent<?> event) {
    final Object payload = event.getPayload();

    if (payload instanceof AjaxRequestBegin) {
        final AjaxRequestTarget target = ((AjaxRequestBegin) payload).getTarget()
        AjaxPropertyChangeListener listener = new AjaxPropertyChangeListener(target);
        target.addListener(listener);
        getBean().addPropertyChangeListener(listener);
    }
}

private class AjaxPropertyChangeListener implements PropertyChangeListener, AjaxRequestTarget.IListener {
    private final AjaxRequestTarget target;

    public AjaxPropertyChangeListener(AjaxRequestTarget target) {
        this.target = target;
    }

    @Override
    public void propertyChange(PropertyChangeEvent event) {
        target.add(MyComponent.this);
    }

    @Override
    public void onBeforeRespond(Map<String, Component> map, AjaxRequestTarget target) {
    }

    @Override
    public void onAfterRespond(Map<String, Component> map, IJavaScriptResponse response) {
        getBean().removePropertyChangeListener(this);
    }
}

Note that AjaxPropertyChangeListener also implements AjaxRequestTarget.IListener to unregister itself after the AJAX request has been completed.

OTHER TIPS

I don't quite understand exactly what you mean by "components registering as event listeners". Are you talking about registering IRequestCycleListeners?

Either way, perhaps Wicket's inter-component events can help you here. Every component implements the following interface:

public interface IEventSink
{
    /**
     * Called when an event is sent to this sink
     * 
     * @param event
     */
    void onEvent(IEvent<?> event);
}

You could subclass AjaxFormComponentUpdatingBehavior to fire an event after a model is updated like so:

public class AjaxUpdateEvent {
    private final AjaxRequestTarget target;

    public AjaxUpdateEvent(AjaxRequestTarget target) {
        this.target = target;
    }
    public AjaxRequestTarget getAjaxRequestTarget() {
        return target;
    }
}

public class BeanModifiedEvent extends AjaxUpdateEvent {
    private final Bean bean;

    public BeanModifiedEvent(AjaxRequestTarget target, Bean theBean) {
        super(target);
    }
    public Bean getBean() {
        return bean;
    }
}

public class CustomUpdatingBehavior extends AjaxFormComponentUpdatingBehavior {

    protected abstract void onUpdate(AjaxRequestTarget target) {
        Bean bean = getFormComponent().getModelObject();
        getComponent().send(getComponent().getPage(), Broadcast.BREADTH, new BeanModifiedEvent(target, bean));
    }
}

You can then catch the event in the required components and add them to the ajax request:

public class UserDetailsPanel extends Panel {
.....
   @Override
    public void onEvent(IEvent event) {
        if(event.getPayload() instanceof BeanModifiedEvent) {
            // if(whatever) to control whether to add or not
            AjaxRequestTarget target = ((BeanModifiedEvent) event.getPayload()).getAjaxRequestTarget();
            target.add(...);
        }
}

Event doc:

You can override #getUpdateModel() to return false, then in #onUpdate() do whatever you want before calling getFormComponent().updateModel().

You could be overriding onModelChanging of each component you are using and firing your PropertyChangeEvent there. According to the documentation onModelChanging is called before the model is changed.

@Override
protected void onModelChanging() {
   super.onModelChanging();
   oldModelObject =  yourComponent.getModelObject();
   //fire PropertyChangeEvent
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top