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.

image

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 dragdragenter, 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!

                             

No Comments