Well, ever since I 've started working with Atlas (I mean, Ajax extensions :)) I 've been trying to build a cool control which wocould let me upload files like Gmail does. as most of you know by now, reading files from the browser in a way that works in all of them isNotAn easy task. so, about 3 or 4 months ago, I started developing a client behavior which I called fileupload. as you might guess, this will be a long post where I'm going to explain the major design decisions I 've made while building my component. before going on, here are some things which youmust know and completly understand before reading and using my code:
-
- I'm not giving any kind of support for this code. So, there's really no need to send me lots and lots of emails. I 've only built this control to learn new stuff and to show that it's really possible for a mere mortal (like myself) to produce something cool which might help others;
- You can only use this controlIIS 6 and above. In IIS 5.1 there's a bug which the MS team marked as non fixable which prevents me from returning a response. redirect from the server side when the current request exceeds the maximum allowed size. BTW, this control will not work correctly with the internal server that comes with;
-
- Internaly, the client behavior makes use of window. frames [0] in non ie clients. this means that the control will only work properly if you don't use iframes on the page. alternatively, you can change the code so that you can add those elements to the page;
-
- The control has only been tested in Firefox 2.0 RC3 and Internet Explorer. I think it may need some care if anyone decides to port it to opera.
If you 've read it and agreed, then please download the files here.
Presenting the idea
so, what's the problem we're re having here? We can't access the contents of a file from a browser. i'm already seeing several guys saying that I'm wrong and that it can be done easily if we use FSO. well, the problem is that FSO only works in IE (after all, it's a COM object ). OK, so where does this leave us? Yes, that's right: We'll have to do it with the classical control and use the correct ammount of javascripts and iframes
The plan is simple (as always, the dedevil is on the details ): we must add a hidden IFRAME which will host an control inside a form. then, we can perform the PostBack by submitting that form progamatically. since I wanted to support IE and Firefox, I had to take into account that there are several differences between what you can do with Javascript in each browser. for instance, in ie There's no need to show the control since we can show a window that lets the user pick the file he's interested in programaticaly (ie, by writing some JavaScript ). on the other hand, Firefox doesn't like this and will only show the file dialog when the user clicks on the button associated with the input control.
Another conclusion that can be reached from the 1st paragraph of this section is that we need to be able to save the file on the server side and to validate its size (you'll se why soon ). before that, lets start refreshing ing the client size behavior.
The client Behavior
The client behavior is maintained in uploadbehavior. js file which is embedded in the fileuploader DLL. The behavior (which is called uploadbehavior) Exposes the following properties:
- Frameid: You can use this property to set the ID of the hidden frame that is used to upload the file to the server. you only need to worry with this file when you have several uploadbehaviors on the same page (which might not work in the current release-specially if you use updatepanels and Firefox );
- Inputid: It lets you specify the ID of the input <type = "file"> that's created dinamically inside the hidden IFRAME;
- URL: used to specify the URL of the handler that matches es the file bytes (by default, the predefined handler is used );
During the initialization, the behavior starts by creating a handler and setting it to the click event fired by the associated HTML control. It also creates the hidden IFRAME and adds it to current HTML document:
_ Createframe: function (){
VaR frame = Document. createelement ("iframe ");
Frame. Name = This. _ frameid;
Frame. ID = This. _ frameid;
If (SYS. browser. Agent = SYS. browser. internetexplorer ){
Document. Body. appendchild (FRAME );
}
Else {
VaR parent = This. get_element (). parentnode;
Parent. insertbefore (frame, this. get_element ());
}
// Added this here so that if a second behavior exists on the page
// Its IFRAME gets correctly filled
This. _ generateiframecontent ();
Frame. style. Display = "NONE"
},
// Responsible for setting up the behavior
// It starts by creating a hidden IFRAME
// Which will be used for uploading the control
Initialize: function (){
La. uploadbehavior. callbasemethod (this, "initialize ");
This. _ clickhandler = function. createdelegate (this, this. _ click );
$ Addhandler (this. get_element (), "click", this. _ clickhandler );
// Create a frame
This. _ createframe ();
}
As you can see, if the current browser is not IE, then we insert the hidden IFRAME before the associated HTML control. the objective is to create the extension sion that IFRAME is on the same place as the HTML associated control. the _ generateiframecontent is a interesting method that is responsible for generatig the contents of the hidden IFRAME:
_ Generateiframecontent: function (){
VaR framewnd = Window. Frames [This. _ frameid];
VaR frameobj = Document. getelementbyid (this. _ frameid );
VaR querystringdic = {};
Querystringdic ["ID"] = This. _ currentid. tolocalestring ();
Querystringdic ["retmethod"] = "LA. uploadbehavior. uploadcomplete"
VaR querystringencoded = SYS. net. webrequest. _ createquerystring (querystringdic );
VaR content = "<HTML> <body style = 'margin: 0px '> <form name = 'upload' method = 'post' action ='" + this. _ URL +
"? "+ Querystringencoded +" 'enctype = 'multipart/form-data'> <input type = 'file' style = 'width: 100%; Height: 100% 'id = '"+
This. _ inputid + "'name = '" + this. _ inputid + "'\/> <input type = 'den den' Id = 'inputid' name = 'inputid' value = '" + this. _ inputid +
"'\/> <Input type = 'ddd' id = 'currentid' name = 'currentid' value ='" + this. _ currentid. tolocalestring () + "'\/> </form> </body> // Firefox has some problems recreating the IFRAME
// So I'll just go into the position and let's see what happen
If (! Framewnd.doc ument ){
// Just get frame at POS 0;
Framewnd = Window. Frames [0];
}
Framewnd.doc ument. open ();
Framewnd.doc ument. Write (content );
Framewnd.doc ument. Close ();
// Set event handlers
This. _ sethandler (framewnd );
},
as you can see, there's a small problem with Firefox: When you delete the IFRAME (which is done on the dispose method) and try to recreate it, you'll get a null document if you try to access the IFRAME through its name. to solve that, I start by checking the Document Property and when it's null, I'll just get the first IFRAME placed at position 0. it's important to keep in mind that this will only happen when you put the IFRAME inside an updatepanel and that in Firefox you can only use one behavior of this type per page in those scenarios (unless you find another way of reaching into the correct IFRAME ).
As we 've seen, the upload is started when you click on the associated HTML control. the _ click method is responsible for registering the behavior in a global dictionary added to the page. this dictionary is important since it'll let you have several controls on the same page and you'll be able to get the correct answers on all the uploads. currently, the method performs the following tasks:
_ CLICK: function (){
La. uploadbehavior. _ registerondictionary (this. _ currentid. tolocalestring (), this );
VaR framewnd = Window. Frames [This. _ frameid];
VaR frameobj = Document. getelementbyid (this. _ frameid );
// Firefox has some problems recreating the IFRAME
// So I'll just go into the position and let's see what happen
If (! Framewnd.doc ument ){
// Just get frame at POS 0;
Framewnd = Window. Frames [0];
}
This. _ generateiframecontent ();
If (sys. browser. agent = sys. browser. internetexplorer) {
frameobj. style. display = "NONE"
framewnd.doc ument. getelementbyid (this. _ inputid ). click ();
}< br> else {
This. _ updateframesize (true);
This. get_element (). style. display = "NONE"
}< br> return false;
},
one of the things that you shoshould keep in mind is that iframes are special objects. when you need to access the properties of the DOM element that has been appended to the page, you shocould get a reference through the document. getelementbyid method; on the other hand, when you need to access the contents of the inner document of the window loaded inside of the IFRAME, you need to get a reference through the windows. frames collection maintained by our window. if you really must ask, the _ updateframesize is a helper method that tries to set the size of the IFRAME so that it only occupies the space reserverd by the associated HTML control.
I don't know if you 've ve noticed, but the _ generateiframecontent method I also responsible for setting a handler that will handle the propertychanged event that is fired by the <input type = "file"> control that exists inside the IFRAME. again, we must check the browser because IE and Firefox don't agree (again) on the name of the event that is fired when a property of an HTML control changes. the method _ sethandler is presented in the next lines:
_ Sethandler: function (FRAME ){
Var file = frame.doc ument. getelementbyid (this. _ inputid );
This. _ propertychangedhandler = function. createdelegate (this, this. _ onpropertychanged );
If (SYS. browser. Agent = SYS. browser. internetexplorer ){
File. attachevent ("onpropertychange", this. _ propertychangedhandler );
}
Else {
File. addeventlistener ("change", this. _ propertychangedhandler, false );
}
},
WTF? Why am Im using the attachevent and the addeventlistener instead of using the new $ addhandler method? Well, it happens that the method generated an exception when you try to handle the propertychange event of a control placed inside another window. since I didn't had the time to investigate the issue, I just kept going and used those old methods for setting my handlers. as you might failed CT, The _ onpropertychanged method tries to submit the form maintained in the hidden IFRAME.
Since I wanted to use the client behavior from XML-script, I had to add a descriptor:
La. uploadbehavior. descriptor = {
Properties :[
{Name: "frameid", type: string },
{Name: "url", type: string },
{Name: "fileid", type: string },
{Name: "inputid", type: string}
],
Events :[
{Name: "uploadcompleted "}
]
}
Before ending the client portion, it's also important to show the global methods that are responsible for maintaining the global dictionary and for updating the internal state of the behavior:
// Global dictionary used to maintain references
// Behaviors that have started an upload
VaR ___ mydic = new object ();
// Adds a new entry to the dictionary
La. uploadbehavior. _ registerondictionary = function (ID, ref) {___ mydic [ID] = ref ;}
// Removes reference from the dictionary
La. uploadbehavior. _ removefromdictionary = function (ID) {Delete ___ mydic [ID];}
// Global (static) method which is used to handle the uploadcomplete event
La. uploadbehavior. uploadcomplete = function (Info, ID ){
VaR OBJ = ___ mydic [ID];
La. uploadbehavior. _ removefromdictionary (ID );
If (SYS. browser. Agent! = SYS. browser. internetexplorer ){
OBJ. get_element (). style. Display = ""
OBJ. _ updateframesize (false );
}
OBJ. set_fileid (Info );
OBJ. _ raiseevent ("uploadcompleted", SYS. eventargs. Empty );
}
The uploadcomplete is a static method which must be called from the server side handler to signal the end of the upload. it takes es two parameters: the first, points to the name that was given to the file during the save operation completed MED by the server handler; the second, has the ID of the behavior that was saved previusly to the global dictionary. and I think I 've said everything about the client behavior. now, let's check the server handler.
The server Handler
if you don't want to build a customized handler, You can reuse the one that comes with the file upload control DLL. this is a very simple handler which saves the file to the current directory (normally, the root directory of the APP) and is responsible for building and sending a response to the client behavior that started the upload. this response is really important! If you don't send one back, or if it gets lost somewhere or if the client behavior doesn't receive it in a predefined format, you won't be able to use the behavior to upload more files without should Ming a refresh (you'll start getting access denied errors ).
BTW, if you build a Custom Handler, you're expected to use the inputid and ID query string parameters to get the ID of the <input type = "file"> that was responsible for the upload and the ID used by the behavior to register itself on the global client dictionary introduced by the client behavior file. you must also build the Javascript reply and you shoshould use the retmethod query string parameter to get the name of the method that you must call on the client side (So, what you really must do is return a predefined JavaScript method call from the handler ). here's the current handler's processrequest implementation:
Public void processrequest (httpcontext context)
{
String idfileserver = "0"
String fileid = ""
Httppostedfile file = NULL;
Try
{
Fileid = context. Request. Params ["inputid"];
File = context. Request. Files [fileid];
If (file! = NULL)
{
Guid = guid. newguid ();
File. saveas (context. server. mappath (guid. tostring ()));
Idfileserver = guid. tostring ();
}
}
Catch
{
Idfileserver = "0"
}
String method = context. Request. querystring ["retmethod"]. substring (1, context. Request. querystring ["retmethod"]. Length-2 );
String id = context. Request. querystring ["ID"]. substring (1, context. Request. querystring ["ID"]. Length-2 );
String reply = string. Concat ("<SCRIPT type = 'text/JavaScript '> parent. Window .",
Method,
"('", Idfileserver ,"','",
ID,
"'); </SCRIPT> ");
Context. response. Clear ();
Context. response. clearcontent ();
Context. response. Write (reply );
Context. response. Flush ();
Context. response. Close ();
}