Wicket - Ajax like file upload on a modal window
A lot of people seem to be asking how to do a file upload using AJAX. The short answer to this is that it cannot be done! That’s because it is not supported by today’s HTML / browser specs.
What you can do is just fake it
.
I’ve been using Wicket (Java Web Framework) regularly for web development. Recently I needed to be able to do a file upload inside a modal window. File uploading needs form submission in order to work so I faced a few problems:
- My modal window was implemented as a panel. I could not do a regular form submission as that would close (submit) my modal window. I’ve been suggested to implement the modal window as a page instead of the panel so that I could use Ajax submit but I really needed it the way it was.
- Ajax and multipart don’t cope.
- Whatever I tried, without regular submission, the file uploading field would not return the file name so it was impossible to get.
I searched the web and I finally found and implemented the solution. It involves using an iframe, but it’s pretty simple. Here’s the code if anyone else needs this:
- UploadIFrame.java
public abstract class UploadIFrame extends WebPage {
private boolean uploaded = false;
private FileUploadField uploadField;
private String newFileUrl;
public UploadIFrame() {
add(new UploadForm("form"));
addOnUploadedCallback();
}
/**
* return the callback url when upload is finished
* @return callback url when upload is finished
*/
protected abstract String getOnUploadedCallback();
/**
* Called when the input stream has been uploaded and when it is available
* on server side
* return the url of the uploaded file
* @param upload fileUpload
*/
protected abstract String manageInputSream(FileUpload upload);
private class UploadForm extends Form {
public UploadForm(String id) {
super(id);
uploadField = new FileUploadField("file");
add(uploadField);
add(new AjaxLink("submit"){
@Override
public void onClick(AjaxRequestTarget target) {
target.appendJavascript("showProgressWheel()");
}
});
}
@Override
public void onSubmit() {
FileUpload upload = uploadField.getFileUpload();
newFileUrl = manageInputSream(upload);
//file is now uploaded, and the IFrame will be reloaded, during
//reload we need to run the callback
uploaded = true;
}
}
private void addOnUploadedCallback() {
//a hacked component to run the callback on the parent
add(new WebComponent("onUploaded") {
@Override
protected void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) {
if (uploaded) {
if (uploadField.getFileUpload() != null){
replaceComponentTagBody(markupStream, openTag,
"window.parent." + getOnUploadedCallback() + "('" +
uploadField.getFileUpload().getClientFileName() + "','" +
newFileUrl +"')");
}
uploaded = false;
}
}
});
}
}
- UploadIFrame.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns:wicket>
<head>
<link href = "../counters/css/style.css" type = "text/css" rel = "stylesheet"/>
<!--[if IE]>
<link href = "../counters/css/ie.css" type = "text/css" rel = "stylesheet"/>
<![endif]–>
<script type="text/javascript">
function showProgressWheel() {
document.images[0].style.display = ‘block’;
// delay the wheel a bit so it can locally shine
setTimeout(function() { document.forms[0].submit() }, 800);
return false;
}
</script>
</head>
<body>
<form wicket:id="form">
<table style="width:500px; margin-top:0px;">
<tr>
<td>
<input style="width:auto;" wicket:id="file" type="file"/>
</td>
<td>
<a wicket:id="submit">Change Avatar</a>
</td>
<td>
<img src="images/indicator.gif" style="display:none"/>
</td>
</tr>
</table>
</form>
<script wicket:id="onUploaded" type="text/javascript"></script>
</body>
</html>
- UploadPanel.java
public abstract class UploadPanel extends Panel {
private InlineFrame uploadIFrame = null;
public UploadPanel(String id) {
super(id);
addOnUploadedCallback();
setOutputMarkupId(true);
}
/**
* Called when the upload load is uploaded and ready to be used
* Return the url of the new uploaded resource
* @param upload {@link FileUpload}
*/
public abstract String onFileUploaded(FileUpload upload);
/**
* Called once the upload is finished and the traitment of the
* {@link FileUpload} has been done in {@link UploadPanel#onFileUploaded}
* @param target an {@link AjaxRequestTarget}
* @param fileName name of the file on the client side
* @param newFileUrl Url of the uploaded file
*/
public abstract void onUploadFinished(AjaxRequestTarget target, String filename, String newFileUrl);
@Override
protected void onBeforeRender() {
super.onBeforeRender();
if (uploadIFrame == null) {
// the iframe should be attached to a page to be able to get its pagemap,
// that's why i'm adding it in onBeforRender
addUploadIFrame();
}
}
/**
* Create the iframe containing the upload widget
*
*/
private void addUploadIFrame() {
IPageLink iFrameLink = new IPageLink() {
@Override
public Page getPage() {
return new UploadIFrame() {
@Override
protected String getOnUploadedCallback() {
return "onUpload_" + UploadPanel.this.getMarkupId();
}
@Override
protected String manageInputSream(FileUpload upload) {
return UploadPanel.this.onFileUploaded(upload);
}
};
}
@Override
public Class<UploadIFrame> getPageIdentity() {
return UploadIFrame.class;
}
};
uploadIFrame = new InlineFrame("upload", getPage().getPageMap(), iFrameLink);
add(uploadIFrame);
}
/**
* Hackie method allowing to add a javascript in the page defining the
* callback called by the innerIframe
*
*/
private void addOnUploadedCallback() {
final OnUploadedBehavior onUploadBehavior = new OnUploadedBehavior();
add(onUploadBehavior);
add(new WebComponent("onUploaded") {
@Override
protected void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) {
// calling it through setTimeout we ensure that the callback is called
// in the proper execution context, that is the parent frame
replaceComponentTagBody(markupStream, openTag,
"function onUpload_" + UploadPanel.this.getMarkupId() +
"(clientFileName, newFileUrl) {window.setTimeout(function() { " +
onUploadBehavior.getCallback() + " }, 0 )}");
}
});
}
private class OnUploadedBehavior extends AbstractDefaultAjaxBehavior {
public String getCallback() {
return generateCallbackScript(
"wicketAjaxGet('" + getCallbackUrl(false) +
"&amp;newFileUrl=' + encodeURIComponent(newFileUrl)" +
" + '&amp;clientFileName=' + encodeURIComponent(clientFileName)").toString();
}
@Override
protected void respond(AjaxRequestTarget target) {
UploadPanel.this.onUploadFinished(target, getRequest().getParameter("clientFileName"), getRequest().getParameter("newFileUrl"));
}
};
}
- UploadPanel.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns:wicket> <wicket:panel> <!-- I put the callback snippet at the body so that is rendered for each panel instead of once --> <script wicket:id="onUploaded" type="text/javascript"></script> <iframe wicket:id="upload" frameborder="0" style="height:60px; width:500px; overflow:hidden"></iframe> </wicket:panel> </html>
- This is how to use it:
add(new UploadPanel("myUpload"){
@Override
public String onFileUploaded(FileUpload upload) {
if (upload != null){
if(upload.getSize() > 1000000) {
return "The Picture Is Too Big !";
} else {
try {
jdbcSignUp.changeAvatarFor(((WebSession)getSession()).getUser().getUsername(), upload.getInputStream(), (int)upload.getSize());
//save on server side here
//and return the url of the saved file
return "The Picture Has Been Uploaded";
} catch (IOException e) {
log.error("Exception:", e);
}
}
}
return "";
}
@Override
public void onUploadFinished(AjaxRequestTarget target, String filename, String newFileUrl) {
//when upload is finished, will be called
messageLabel.setModelObject(newFileUrl);
target.addComponent(messageLabel);
avatarImage.setImageResource(new MyBlobImageResource(((WebSession)getSession()).getUser().getUsername()));
target.addComponent(avatarImage);
((BasePage)parent).onAvatarChanged(target);
}
});
Most credit should go to Vincent Demay, although his code contains errors.





By Andrew on May 2, 2008 | Reply
Thanks for the fixed version.
I’m still having a little problem.
The file uploads file and then triggers the onUploadFinished, but I seem to find that the filename passed into the method is always null.
Would you have any idea why this might be?
By admin on May 4, 2008 | Reply
No, I retested and it works for me. I always get the filename, never null.
By the way, if you’re looking for the filepath, you have to play with the wicket sources I think because the client’s path is encapsulated in the FileUpload class.
On a regular use case though, you should not even need it since you have access to the input stream.
By geke on Jun 3, 2008 | Reply
Your solution works very fine, but in relation with an LinkTree there are problems. Maybe you can help me.
I have a LinkTree on my site. But after closing the modal window, the tree models userObject has a reference, but the attributes of the userObject are all null. Hence after the onClicked event on nodes in my tree, there are no Nodes visible. That is even the modal window is closed without a fileupload.
By admin on Jun 4, 2008 | Reply
I haven’t tested it with a tree, but I use it in a lot of projects and it works just fine.
I’m guessing it must be a little bug somewhere in Wicket when using a tree and an iframe.
By geke on Jun 4, 2008 | Reply
Thank you for you answer.
Today I find the solution.
Now it works fine.
It was my mistake with programming the tree.
By admin on Jun 4, 2008 | Reply
I’m glad it worked out for you!
By Mathias Nilsson on Aug 16, 2008 | Reply
I get this error all the time
Exception evaluating javascript: ReferenceError: Can’t find variable: showProgressWheel
By Siddharth on Aug 21, 2008 | Reply
Hi,
First of all thanks for putting this code. This has been extremely helpful. I have few questions though..These might be simple questions but any help is appreciated.
1. I also have implemented Modal Window as Panel (i.e. I add Panel to Modal Window in this case UploadPanel) but, the problem is IFrame looks bad in Modal window as it either verflows from modal window or is always smaller than Modal window. Is there a way to synchronize the size of Modal window and IFrame inside it so that user cannot tell IFrame from Modal Window.
2. Also, in my case File upload field is required and user should be able to close the pop-up by clicking a close link inside the pop-up. No, problem here..I put Link here and did a modal.close(target) from UploadPanel. But this causes form to validate and it shows validation errors when user clicks it for the first time. However, if uses clicks it second time the modal window is closed. Do you know how to get around this ? I also tried AjaxFallbackButton to close Modal window but it causes “Null Pointer Exception”. Then I read on Nabble (wicket forum) that I cannot close enclosing Modal from IFrame as this is considered Cross Browser scripting.
By admin on Aug 21, 2008 | Reply
Hi,
1. I used CSS to make the IFrame as I needed it to be inside the modal. No magic here.
2. I also had a use case where the upload file field was required, so I did the validation myself.
When the user pressed the close button on the “upload panel” or the X of the modal window (I implemented the callback), on the onSubmit I check if the upload field is blank. If not, I called a method in the class that created the modal to close it. Else show an error message.