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:UploadPanelrunat="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 properties11: props.a = 1;12: props.b = 2;13:return (true);14: }15:16:function onUploadCanceled(event)17: {18://canceled19: }20:21:function onUploadComplete(event)22: {23://complete24: }25:26:function onUploadFailure(event)27: {28://failure29: }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://success42: }43:44:function onValidationFailure(event, error)45: {46://error=0: maximum files reached47://error=1: maximum file length reached48://error=2: invalid content type49: }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:publicsealedclass 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:protectedvoid 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!