ASP.NET Upload Panel
Introduction
I have been playing recently with HTML5, and one thing that I got to understand really well was the new upload mechanisms available. Specifically, I wanted to understand how SkyOneDrive, Google Drive, Dropbox, etc, all support dropping files from the local machine, and how to use it in an ASP.NET Web Forms (sorry!) project, and I got it!
So, I want to have a panel (a DIV) that allows dropping files from the local machine; I want to be able to filter these files by their content type, maximum length, or by any custom condition. If the dropped files pass all conditions, they are sent asynchronously (read, AJAX) and raise a server-side event. After that, if they are multimedia files, I can preview them on the client-side.
This example will use the XMLHttpRequest object and the new FormData and FileReader JavaScript classes and will work on all modern browsers, from IE9 to Firefox, Chrome and Safari.
Markup
My markup looks like this:
1: <web:UploadPanel runat="server" ID="uploadPanel" MaximumFiles="2" MaximumLength="1000000" ContentTypes="image/gif,image/png,image/jpeg" OnPreviewFile="onPreviewFile" OnBeforeUpload="onBeforeUpload" onUploadCanceled="onUploadCanceled" OnUploadComplete="onUploadComplete" OnUploadFailure="onUploadFailure" OnUploadProgress="onUploadProgress" OnUploadSuccess="onUploadSuccess" OnValidationFailure="onValidationFailure" OnUpload="OnUpload" Style="width: 300px; height: 300px; border: solid 1px;" />
The UploadPanel control inherits from the Panel class, so it can have any of its properties. In this case, I am setting a specific width, height and border, so that it is easier to target.
Validations
Out of the box, it supports the following validations:
-
MaximumFiles: The maximum number of files to drop; if not set, any number is accepted;
-
MaximumLength: The maximum length of any individual file, in bytes; if not set, any file size is accepted;
-
ContentTypes: A comma-delimited list of content types; can take a precise content type, such as “image/gif” or a content type part, such as “image/”; if not specified, any content type is accepted.
There is also an optional JavaScript event, OnBeforeUpload, that can be used to validate the files individually.
Client-Side Events
When a file is dropped into the UploadPanel, the following client-side events are raised:
-
OnValidationFailure: Raised whenever any of the built-in validations (maximum files, maximum length and content types) fails, for any of the dropped files;
-
OnBeforeUpload: Raised before the upload starts, and after all built-in validations (maximum files, maximum length and content types) succeed; this gives developers a chance to analyze the files to upload and to optionally cancel the upload, or to add additional custom parameters that will be posted together with the files;
-
OnUploadFailure: Raised if the upload fails for some reason;
-
OnUploadCanceled: Raised if the upload is canceled;
-
OnUploadProgress: Raised possibly several times as the file is being uploaded, providing an indication of the total upload progress;
-
OnUploadSuccess: Raised when the upload terminates successfully;
-
OnUploadComplete: Raised when the upload completes, either successfully or not;
-
OnPreviewFile: Raised for each multimedia file uploaded (images, videos, sound), to allow previewing it on the client-side; the handler function receives the file as a data URL.
For each you can optionally specify the name of a JavaScript function that handles it. Some examples of all these events:
1: <script>
1:
2:
3: function onPreviewFile(name, url)
4: {
5: document.getElementById('preview').src = url;
6: }
7:
8: function onBeforeUpload(event, props)
9: {
10: //set two custom properties
11: props.a = 1;
12: props.b = 2;
13: return (true);
14: }
15:
16: function onUploadCanceled(event)
17: {
18: //canceled
19: }
20:
21: function onUploadComplete(event)
22: {
23: //complete
24: }
25:
26: function onUploadFailure(event)
27: {
28: //failure
29: }
30:
31: function onUploadProgress(event)
32: {
33: if (event.lengthComputable)
34: {
35: var percentComplete = event.loaded / event.total;
36: }
37: }
38:
39: function onUploadSuccess(event)
40: {
41: //success
42: }
43:
44: function onValidationFailure(event, error)
45: {
46: //error=0: maximum files reached
47: //error=1: maximum file length reached
48: //error=2: invalid content type
49: }
50:
</script>
Besides these custom events, you can use any of the regular HTML5 events, such as drag, dragenter, dragend, dragover, dragstart, dragleave or drop.
Server-Side Events
The Upload event takes a parameter of type UploadEventArgs which looks like this:
1: [Serializable]
2: public sealed class UploadEventArgs : EventArgs
3: {
4: public UploadEventArgs(HttpFileCollection files, NameValueCollection form)
5: {
6: this.Files = files;
7: this.Response = String.Empty;
8: this.Form = form;
9: }
10:
11: public String Response
12: {
13: get;
14: set;
15: }
16:
17: public NameValueCollection Form
18: {
19: get;
20: private set;
21: }
22:
23: public HttpFileCollection Files
24: {
25: get;
26: private set;
27: }
28: }
This class receives a list of all the files uploaded and also any custom properties assigned on the OnBeforeUpload event. It also allows the return of a response string, that will be received by the OnUploadSuccess or OnUploadComplete client-side events:
1: protected void OnUpload(Object sender, UploadEventArgs e)
2: {
3: //do something with e.Files
4:
5: //output the parameters that were received
6: e.Response = String.Format("a={0}\nb={1}", e.Form["a"], e.Form["b"]);
7: }
Code
Finally, the code:
1: public class UploadPanel : Panel, ICallbackEventHandler
2: {
3: private static readonly String [] MultimediaContentTypePrefixes = new String[]{ "image/", "audio/", "video/" };
4:
5: public UploadPanel()
6: {
7: this.ContentTypes = new String[0];
8: this.OnUploadFailure = String.Empty;
9: this.OnUploadSuccess = String.Empty;
10: this.OnValidationFailure = String.Empty;
11: this.OnBeforeUpload = String.Empty;
12: this.OnUploadComplete = String.Empty;
13: this.OnUploadProgress = String.Empty;
14: this.OnUploadCanceled = String.Empty;
15: this.OnPreviewFile = String.Empty;
16: }
17:
18: public event EventHandler<UploadEventArgs> Upload;
19:
20: [DefaultValue("")]
21: public String OnPreviewFile
22: {
23: get;
24: set;
25: }
26:
27: [DefaultValue("")]
28: public String OnBeforeUpload
29: {
30: get;
31: set;
32: }
33:
34: [DefaultValue("")]
35: public String OnUploadCanceled
36: {
37: get;
38: set;
39: }
40:
41: [DefaultValue("")]
42: public String OnUploadProgress
43: {
44: get;
45: set;
46: }
47:
48: [DefaultValue("")]
49: public String OnValidationFailure
50: {
51: get;
52: set;
53: }
54:
55: [DefaultValue("")]
56: public String OnUploadComplete
57: {
58: get;
59: set;
60: }
61:
62: [DefaultValue("")]
63: public String OnUploadSuccess
64: {
65: get;
66: set;
67: }
68:
69: [DefaultValue("")]
70: public String OnUploadFailure
71: {
72: get;
73: set;
74: }
75:
76: [DefaultValue(null)]
77: public Int32? MaximumLength
78: {
79: get;
80: set;
81: }
82:
83: [DefaultValue(null)]
84: public Int32? MaximumFiles
85: {
86: get;
87: set;
88: }
89:
90: [TypeConverter(typeof(StringArrayConverter))]
91: public String[] ContentTypes
92: {
93: get;
94: set;
95: }
96:
97: protected override void OnInit(EventArgs e)
98: {
99: var script = new StringBuilder();
100: script.AppendFormat("document.getElementById('{0}').addEventListener('drop', function(event) {{\n", this.ClientID);
101:
102: script.Append("if (event.dataTransfer.files.length == 0)\n");
103: script.Append("{\n");
104: script.Append("event.returnValue = false;\n");
105: script.Append("event.preventDefault();\n");
106: script.Append("return(false);\n");
107: script.Append("}\n");
108:
109: if (this.MaximumFiles != null)
110: {
111: script.AppendFormat("if (event.dataTransfer.files.length > {0})\n", this.MaximumFiles.Value);
112: script.Append("{\n");
113:
114: if (String.IsNullOrWhiteSpace(this.OnValidationFailure) == false)
115: {
116: script.AppendFormat("{0}(event, 0);\n", this.OnValidationFailure);
117: }
118:
119: script.Append("event.returnValue = false;\n");
120: script.Append("event.preventDefault();\n");
121: script.Append("return(false);\n");
122: script.Append("}\n");
123: }
124:
125: if (this.MaximumLength != null)
126: {
127: script.Append("var lengthOk = true;\n");
128: script.Append("for (var i = 0; i < event.dataTransfer.files.length; ++i)\n");
129: script.Append("{\n");
130: script.AppendFormat("if (event.dataTransfer.files[i].size > {0})\n", this.MaximumLength.Value);
131: script.Append("{\n");
132: script.Append("lengthOk = false;\n");
133: script.Append("break;\n");
134: script.Append("}\n");
135: script.Append("}\n");
136: script.Append("if (lengthOk == false)\n");
137: script.Append("{\n");
138:
139: if (String.IsNullOrWhiteSpace(this.OnValidationFailure) == false)
140: {
141: script.AppendFormat("{0}(event, 1);\n", this.OnValidationFailure);
142: }
143:
144: script.Append("event.returnValue = false;\n");
145: script.Append("event.preventDefault();\n");
146: script.Append("return(false);\n");
147: script.Append("}\n");
148: }
149:
150: if (this.ContentTypes.Any() == true)
151: {
152: script.Append("for (var i = 0; i < event.dataTransfer.files.length; ++i)\n");
153: script.Append("{\n");
154: script.Append("var contentTypeOk = false;\n");
155:
156: script.AppendFormat("if ({0})", String.Join(" || ", this.ContentTypes.Select(x => String.Format("(event.dataTransfer.files[i].type.toLowerCase().indexOf('{0}') == 0)", x.ToLower()))));
157: script.Append("{\n");
158: script.Append("contentTypeOk = true;\n");
159: script.Append("}\n");
160:
161: script.Append("}\n");
162: script.Append("if (contentTypeOk == false)\n");
163: script.Append("{\n");
164:
165: if (String.IsNullOrWhiteSpace(this.OnValidationFailure) == false)
166: {
167: script.AppendFormat("{0}(event, 2);\n", this.OnValidationFailure);
168: }
169:
170: script.Append("event.returnValue = false;\n");
171: script.Append("event.preventDefault();\n");
172: script.Append("return(false);\n");
173: script.Append("}\n");
174: }
175:
176: if (String.IsNullOrWhiteSpace(this.OnBeforeUpload) == false)
177: {
178: script.Append("var props = new Object();\n");
179: script.AppendFormat("if ({0}(event, props) === false)\n", this.OnBeforeUpload);
180: script.Append("{\n");
181: script.Append("event.returnValue = false;\n");
182: script.Append("event.preventDefault();\n");
183: script.Append("return(false);\n");
184: script.Append("}\n");
185: }
186:
187: script.Append("var data = new FormData();\n");
188: script.Append("for (var i = 0; i < event.dataTransfer.files.length; ++i)\n");
189: script.Append("{\n");
190: script.Append("var file = event.dataTransfer.files[i];\n");
191: script.Append("data.append('file' + i, file);\n");
192:
193: if (String.IsNullOrWhiteSpace(this.OnPreviewFile) == false)
194: {
195: script.AppendFormat("if ({0})", String.Join(" || ", MultimediaContentTypePrefixes.Select(x => String.Format("(file.type.toLowerCase().indexOf('{0}') == 0)", x.ToLower()))));
196: script.Append("{\n");
197: script.Append("var reader = new FileReader();\n");
198: script.Append("reader.onloadend = function(e)\n");
199: script.Append("{\n");
200: script.AppendFormat("{0}(file.name, reader.result);\n", this.OnPreviewFile);
201: script.Append("}\n");
202: script.Append("reader.readAsDataURL(file);\n");
203: script.Append("}\n");
204: }
205:
206: script.Append("}\n");
207: script.AppendFormat("data.append('__CALLBACKID', '{0}');\n", this.UniqueID);
208: script.Append("data.append('__CALLBACKPARAM', '');\n");
209: script.Append("data.append('__EVENTTARGET', '');\n");
210: script.Append("data.append('__EVENTARGUMENT', '');\n");
211: script.Append("for (var key in props)\n");
212: script.Append("{\n");
213: script.Append("data.append(key, props[key]);\n");
214: script.Append("}\n");
215: script.Append("var xhr = new XMLHttpRequest();\n");
216:
217: if (String.IsNullOrWhiteSpace(this.OnUploadProgress) == false)
218: {
219: script.Append("xhr.onprogress = function(e)\n");
220: script.Append("{\n");
221: script.AppendFormat("{0}(e);\n", this.OnUploadProgress);
222: script.Append("}\n");
223: }
224:
225: if (String.IsNullOrWhiteSpace(this.OnUploadCanceled) == false)
226: {
227: script.Append("xhr.oncancel = function(e)\n");
228: script.Append("{\n");
229: script.AppendFormat("{0}(e);\n", this.OnUploadCanceled);
230: script.Append("}\n");
231: }
232:
233: script.Append("xhr.onreadystatechange = function(e)\n");
234: script.Append("{\n");
235: script.Append("if ((xhr.readyState == 4) && (xhr.status == 200))\n");
236: script.Append("{\n");
237: script.AppendFormat("{0}(e);\n", this.OnUploadSuccess);
238: script.Append("}\n");
239: script.Append("else if ((xhr.readyState == 4) && (xhr.status != 200))\n");
240: script.Append("{\n");
241: script.AppendFormat("{0}(e);\n", this.OnUploadFailure);
242: script.Append("}\n");
243: script.Append("if (xhr.readyState == 4)\n");
244: script.Append("{\n");
245: script.AppendFormat("{0}(e);\n", this.OnUploadComplete);
246: script.Append("}\n");
247: script.Append("}\n");
248: script.AppendFormat("xhr.open('POST', '{0}', true);\n", this.Context.Request.Url.PathAndQuery);
249: script.Append("xhr.send(data);\n");
250: script.Append("event.returnValue = false;\n");
251: script.Append("event.preventDefault();\n");
252: script.Append("return (false);\n");
253: script.Append("});\n");
254:
255: if (ScriptManager.GetCurrent(this.Page) == null)
256: {
257: this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat(this.UniqueID, "drop"), script.ToString(), true);
258: this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat(this.UniqueID, "dragenter"), String.Format("document.getElementById('{0}').addEventListener('dragenter', function(event){{ event.returnValue = false; event.preventDefault(); return(false); }});\n", this.ClientID), true);
259: this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat(this.UniqueID, "dragover"), String.Format("document.getElementById('{0}').addEventListener('dragover', function(event){{ event.returnValue = false; event.preventDefault(); return(false); }});\n", this.ClientID), true);
260: }
261: else
262: {
263: this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat(this.UniqueID, "drop"), String.Format("Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(function() {{ {0} }});\n", script), true);
264: this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat(this.UniqueID, "dragenter"), String.Format("Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(function() {{ document.getElementById('{0}').addEventListener('dragenter', function(event){{ event.returnValue = false; event.preventDefault(); return(false); }}); }});\n", this.ClientID), true);
265: this.Page.ClientScript.RegisterStartupScript(this.GetType(), String.Concat(this.UniqueID, "dragover"), String.Format("Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(function() {{ document.getElementById('{0}').addEventListener('dragover', function(event){{ event.returnValue = false; event.preventDefault(); return(false); }}); }});\n", this.ClientID), true);
266: }
267:
268: base.OnInit(e);
269: }
270:
271: protected virtual void OnUpload(UploadEventArgs e)
272: {
273: var handler = this.Upload;
274:
275: if (handler != null)
276: {
277: handler(this, e);
278: }
279: }
280:
281: #region ICallbackEventHandler Members
282:
283: String ICallbackEventHandler.GetCallbackResult()
284: {
285: var args = new UploadEventArgs(this.Context.Request.Files, this.Context.Request.Form);
286:
287: this.OnUpload(args);
288:
289: return (args.Response);
290: }
291:
292: void ICallbackEventHandler.RaiseCallbackEvent(String eventArgument)
293: {
294: }
295:
296: #endregion
297: }
The UploadPanel class inherits from Panel and implements ICallbackEventHandler, for client callbacks. If you are curious, the __CALLBACKID, __CALLBACKPARAM, __EVENTTARGET and __EVENTARGUMENT are required for ASP.NET to detect a request as a callback, but only __CALLBACKID needs to be set with the unique id of the UploadPanel control.
Conclusion
HTML5 offers a lot of exciting new features. Stay tuned for some more examples of its integration with ASP.NET!