downloadwicketwicket-8

Wicket 8: How to start a download and switch page/replace panel at the same time?


I have a form which, onSubmit, requests some input-dependent data from a server and creates a file and a ResourceStream of it.

The file is rather important, so the download should start immediately. But I can't allow multiple requests/submits to happen, so I want to switch the page or replace the panel at the same time.

Is there a way to do this? Download the file and switch page/panel in a single click?

So far, I create a ResourceStreamRequestHandler handler and use getRequestCycle().scheduleRequestHandlerAfterCurrent(handler) to initiate the download during onSubmit.

protected void onSubmit() {
    // Get the data.
    // ...

    ResourceStreamRequestHandler handler = new ResourceStreamRequestHandler(
            getResourceStream(data...), getFileName(data...));
    getRequestCycle().scheduleRequestHandlerAfterCurrent(handler);
  
    //oldPanel.this.replaceWith(new newPanel(data...));
    //setResponsePage(mainPage.class);
}

The problem is, if I use setResponsePage to return to the main page, the download does not start, and if I try to replaceWith(new newPanel()), the download starts, but the panel is not properly replaced. I even tried to initiate the download from newPanel (e.g. during onAfterRender, etc.), but the panels still weren't swapped correctly.


Solution

  • Thanks to martin-g pointing me in the right direction, I finally figured it out.

    Using org.apache.wicket.extensions.ajax.AjaxDownloadBehavior is the way to go, but martin-g's answer lacks some crucial information:

    1. The AjaxDownloadBehavior cannot be added "on the fly", as it relies on JavaScript being incorporated into the header on page/panel load. So it should be added in the constructor.
    2. Since AjaxDownloadBehavior does not operate on models and neither it nor the IResource/ResourceReference objects it takes as parameters (as far as I could see) allow for dynamic changes to the download resource, one must create a custom IResource or ResourceReference class for that.

    An example to demonstrate the idea:

    // Global instance of the custom IResource class, for easier handling.
    private final DynamicResourceStreamResource downloadResource = new DynamicResourceStreamResource();
    
    // Custom IResource class for dynamic resource generation/swapping.
    private class DynamicResourceStreamResource extends ResourceStreamResource {
        private IResourceStream stream;
    
        public DynamicResourceStreamResource() {
            this.stream = null;
        }
    
        // This right here allows to swap the ResourceStream dynamically later.
        public void setStream(IResourceStream stream) {
            this.stream = stream;
        }
    
        @Override
        protected IResourceStream getResourceStream(
                IResource.Attributes attributes) {
            return this.stream;
        }
    }
    
    // Panel constructor.
    DownloadPanel(String wicketId) {
        super(wicketId);
    
        FeedbackPanel feedback = new FeedbackPanel("feedback");
        add(feedback);
        feedback.setOutputMarkupPlaceholderTag(true);
    
        AjaxDownloadBehavior download = new AjaxDownloadBehavior(downloadResource) {
            @Override
            protected void onDownloadSuccess(AjaxRequestTarget target) {
                // Redirect after successful download.
                setResponsePage(mainPage.class);
            }
        };
        add(download);
    
        Form<Void> form = new Form<Void>("form");
        add(form);
        form.setOutputMarkupPlaceholderTag(true);
            
        form.add(new AjaxButton("formSubmitButton") {
            @Override
            protected void onError(AjaxRequestTarget target) {
                super.onError(target);
                // Reload form; required, if e.g. input fields are cleared on error. 
                target.add(form);
                // Reload feedback panel; required to display validator messages.
                target.add(feedback);
            }
            @Override
            protected void onSubmit(AjaxRequestTarget target) {
                // Get the data.
                // ...
                downloadResource.setStream(getResourceStream(data...));
                downloadResource.setFileName(getFileName(data...));
                /* 
                 * Download could also be initiated onAfterSubmit, leaving the data
                 * collection and processing to the form itself.
                 */
                download.initiate(target);
            }
        }
    }
    

    Still, I'll have to abandon this approach, as the form validation, as I would expect and prefer it (e.g. tooltips popping up, directing the user to required fields), doesn't seem to work for me with Ajax components.