1
votes

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?

4
If I understand correctly, in your scheme, the backing beans are responsible for dispatching "update yourself" events to components who register themselves as listeners? Why can't it be the other way around? As in: 1) bean updated; 2) wicket event fired which says "property P of bean B updated"; 3) components interested in that property update themselves. - bernie
My backing beans (business layer) know nothing about Wicket components (UI layer) and I want to keep it that way. The problem would still be the same: How could a bean that does not live in the session notify a Wicket component? I would somehow need to retrieve the page and find the component by Id? - Ferdinand Beyer
I'm not suggesting that the business layer should control the UI either. Maybe I'm the one missing something here... What I'm suggesting is that after the bean is updated, Wicket publishes a Wicket event which is dispatched to every component saying "this Bean's property has been updated". Then the components decide themselves if they should refresh. - bernie
Yes, I understand that. But HOW should Wicket publish that event? I won't change the bean class, the property change could cause to more changes because of other listeners. - Ferdinand Beyer
You're saying that it has to be done through bean change listeners because there is logic in there that can trigger a cascade of bean updates? Is this correct? If so, it is indeed necessary to hook Wicket into that change listener chain to capture derivative bean property updates. - bernie

4 Answers

1
votes

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:

1
votes

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

0
votes

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
}
0
votes

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.