Martin Wickman
Martin Wickman

Reputation: 19905

How to handle exceptions thrown in Wicket custom model?

I have a component with a custom model (extending the wicket standard Model class). My model loads the data from a database/web service when Wicket calls getObject().

This lookup can fail for several reasons. I'd like to handle this error by displaying a nice message on the web page with the component. What is the best way to do that?

public class MyCustomModel extends Model {

    @Override
    public String getObject() {
        try {
            return Order.lookupOrderDataFromRemoteService();
        } catch (Exception e) {
            logger.error("Failed silently...");
            // How do I propagate this to the component/page?
        }           
        return null;
}

Note that the error happens inside the Model which is decoupled from the components.

Upvotes: 4

Views: 4660

Answers (5)

Carl-Eric Menzel
Carl-Eric Menzel

Reputation: 1266

Handling an exception that happens in the model's getObject() is tricky, since by this time we are usually deep in the response phase of the whole request cycle, and it is too late to change the component hierarchy. So the only place to handle the exception is very much non-local, not anywhere near your component or model, but in the RequestCycle.

There is a way around that though. We use a combination of a Behavior and an IRequestCycleListener to deal with this:

  • IRequestCycleListener#onException allows you to examine any exception that was thrown during the request. If you return an IRequestHandler from this method, that handler will be run and rendered instead of whatever else was going on beforehand.

    We use this on its own to catch generic stuff like Hibernate's StaleObjectException to redirect the user to a generic "someone else modified your object" page. If you

  • For more specific cases we add a RuntimeExceptionHandler behavior:

    public abstract class RuntimeExceptionHandler extends Behavior {
        public abstract IRequestHandler handleRuntimeException(Component component, Exception ex);
    }
    

    In IRequestCycleListener we walk through the current page's component tree to see whether any component has an instance of RuntimeExceptionHandler. If we find one, we call its handleRuntimeException method, and if it returns an IRequestHandler that's the one we will use. This way you can have the actual handling of the error local to your page.

    Example:

    public MyPage() {
      ...
      this.add(new RuntimeExceptionHandler() {
        @Override public IRequestHandler handleRuntimeException(Component component, Exception ex) {
          if (ex instanceof MySpecialException) {
            // just an example, you really can do anything you want here.
            // show a feedback message...
            MyPage.this.error("something went wrong"); 
            // then hide the affected component(s) so the error doesn't happen again...
            myComponentWithErrorInModel.setVisible(false); // ...
            // ...then finally just re-render this page:
            return new RenderPageRequestHandler(new PageProvider(MyPage.this));
          } else {
            return null;
          }
        }
      });
    }
    

    Note: This is not something shipped with Wicket, we rolled our own. We simply combined the IRequestCycleListener and Behavior features of Wicket to come up with this.

Upvotes: 7

svenmeier
svenmeier

Reputation: 5681

Your model could implement IComponentAssignedModel, thus being able to get hold on the owning component.

But I wonder how often are you able to reuse MyCustomModel? I know that some devs advocate creating standalone model implementations (often in separate packages). While there are general cases where this is useful (e.g. FeedbackMessagesModel), in my experience its easier to just create inner classes which are component specific.

Upvotes: 6

peterp
peterp

Reputation: 3121

You might want to simply return null in getObject, and add logic to the controller class to display a message if getObject returns null.

If you need custom messages for different fail reasons, you could add a property like String errorMessage; to the model which is set when catching the Exception in getObject - so your controller class can do something like this

if(model.getObject == null) {
 add(new Label("label",model.getErrorMessage()));
} else {
 /* display your model object*/
}

Upvotes: 0

Xavi López
Xavi López

Reputation: 27880

Being the main issue here that Models are by design decoupled from the component hierarchy, you could implement a component-aware Model that will report all errors against a specific component.

Remember to make sure it implements Detachable so that the related Component will be detached.

If the Model will perform an expensive operation, you might be interested in using LoadableDetachableModel instead (take into account that Model.getObject() might be called multiple times).

public class MyComponentAwareModel extends LoadableDetachableModel { 
    private Component comp;         
    public MyComponentAwareModel(Component comp) { 
          this.comp = comp;
    }

    protected Object load() {
        try {
            return Order.lookupOrderDataFromRemoteService();
        } catch (Exception e) {
            logger.error("Failed silently...");
            comp.error("This is an error message");
        }           
        return null;
    } 

    protected void onDetach(){
        comp.detach();
    }
}

It might also be worth to take a try at Session.get().error()) instead.

Upvotes: 2

bert
bert

Reputation: 7696

I would add a FeedbackPanel to the page and call error("some description") in the catch clause.

Upvotes: 0

Related Questions