ASP.NET Core MVC AJAX Drag-n-Drop File Upload

Uploading files using AJAX and ASP.NET Core MVC could not be simpler. This does not rely upon jQuery. This code allows the uploading of multiple image files.

To begin, create an ASP.NET Core web application (MVC) in Visual Studio named "DragAndDrop".

First, the client side. Modify the "Index.cshtml" file located in the "Home" directory to look like this. The whole file will be modified.

@{
    ViewData["Title"] = "Drag and Drop";
}

<form action="/Ajax/UploadImages" method="post" onsubmit="return submitFilesForm(this);">
	@Html.AntiForgeryToken() <!-- Prevent Cross Site Request Forgery -->
	<input type="file" name="files" id="files-field" accept="image/*" onchange="submitFilesForm(this.form);" multiple />
	<label for="files-field" id="files-label"
			ondragover="stopDefault(event);dragOver(this, 'Drop the images to upload them.');"
			ondragenter="stopDefault(event);dragOver(this, 'Drop the images to upload them.');"
			ondragleave="stopDefault(event);dragLeave(this);"
			ondrop="stopDefault(event);dragLeave(this);addFilesAndSubmit(event);">Click to choose images or drag-n-drop them here</label>
</form>
<div style="text-align: left;"><div id="files-progress"></div></div>
<div class="modal-page" id="uploaded-files"></div>
<script type="text/javascript">
	function stopDefault(event) {
		event.preventDefault();
		event.stopPropagation();
	}
	function dragOver(label, text) {
		label.style.animationName = "dropbox";
		label.innerText = text;
	}
	function dragLeave(label) {
		label.style.animationName = "";
		label.innerText = "Click to choose images or drag-n-drop them here";
	}
	function addFilesAndSubmit(event) {
		var files = event.target.files || event.dataTransfer.files;
		var field = document.getElementById("files-field");
		field.files = files;
		submitFilesForm(field.form);
	}
	function submitFilesForm(form) {
		var label = document.getElementById("files-label");
		dragOver(label, "Uploading images..."); // set the drop zone text and styling
		if(!FormData) {
			alert("Function not supported by this browser.")
			return false;
		}
		var fd = new FormData();
		for (var i = 0; i < form.files.files.length; i++) {
			var field = form.files;
			fd.append(field.name, field.files[i], field.files[i].name);
		}
		fd.append(form.elements[0].name, form.elements[0].value); // must append the AntiForgeryToken to the form data
		var progress = document.getElementById("files-progress");
		var x = new XMLHttpRequest();
		if (x.upload) {
			x.upload.addEventListener("progress", function (event) {
				var percentage = Math.round(event.loaded / event.total * 100);
				progress.innerText = progress.style.width = percentage + "%";
			});
		}
		x.onreadystatechange = function () {
			if (x.readyState == 4) {
				progress.innerText = progress.style.width = "";
				form.files.value = "";
				dragLeave(label); // this will reset the text and styling of the drop zone
				if (x.status == 200) {
					var images = x.responseText.split('|');
					for (var i = 0; i < images.length; i++) {
						var img = document.createElement("img");
						img.src = images[i];
						document.getElementById("uploaded-files").appendChild(img);
					}
					location.href = "#uploaded-files";
				}
				else if(x.status == 500) {
					alert(x.responseText); // do something with the server error
				}
				else {
					alert(x.status + ": " + x.statusText);
				}
			}
		};
		x.open(form.method, form.action, true);
		x.send(fd);
		return false; // do not forget this
	}
</script>

Next, the server side. Make a controller named "Ajax" and add the UploadImages() method.

// AjaxController.cs
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using System.IO;                    // added this line
using Microsoft.AspNetCore.Hosting; // added this line

namespace DragAndDrop.Controllers
{
	public class AjaxController : Controller
	{
		private readonly IWebHostEnvironment env;

		public AjaxController(IWebHostEnvironment env) => this.env = env;


		[HttpPost]
		[ValidateAntiForgeryToken]
		public async Task<string> UploadImages() // MUST BE "ASYNC"
		{
			return await Task.Run(() =>
			{
				var dir = env.WebRootPath + "\\uploads";
				try
				{
					if (!Directory.Exists(dir))
						Directory.CreateDirectory(dir); // make sure there are appropriate permissions on the wwwroot folder
				}
				catch (Exception ex)
				{
					Response.StatusCode = 500; // SERVER ERROR
					return ex.Message.ToString();
				}
				var ret = string.Empty; // return value
				for (int i = 0; i < Request.Form.Files.Count; i++)
				{
					if (Request.Form.Files[i].Length > 0)
					{
						if (Request.Form.Files[i].ContentType.ToLower().StartsWith("image/")) // make sure it is an image; can be omitted
						{
							try
							{
								// make sure that this directory has write permissions; use GUID in name to generate unique file names
								var file = '\\' + System.Guid.NewGuid().ToString("N") + '-' + Request.Form.Files[i].FileName;
								using (FileStream fs = new FileStream(dir + file, FileMode.Create, FileAccess.Write))
								{
									const int bufsize = 2048000;
									byte[] buffer = new byte[bufsize];
									using (Stream stream = Request.Form.Files[i].OpenReadStream())
									{
										int b = stream.Read(buffer, 0, bufsize);
										int written = b;
										while (b > 0)
										{
											fs.Write(buffer, 0, b);
											b = stream.Read(buffer, 0, bufsize);
											written += b;
										}
									}
								}
								ret += (i > 0 ? "|" : "") + "\\uploads" + file; // just return a string with the file names separated by a | because it is less code than JSON
							}
							catch (Exception ex)
							{
								Response.StatusCode = 500; // SERVER ERROR
								return ex.Message.ToString();
							}
						}
					}
				}
				return ret;
			});
		}
	}
}

Modify the "site.css" file to include these lines. Without them, the page will be quite ugly.

#files-label {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 50vh;
    width: 100%;
    background-color: lightpink;
    color: black;
    font-weight: bold;
    font-size: 25px;
    border: 10px dashed black;
    margin: 0;
    text-align: center;
    transition: .5s;
    animation-duration: 1s;
    animation-fill-mode: forwards;
    animation-iteration-count: infinite;
}

#files-field { /* hide the files input field; use files-label to access it */
    display: none;
}

#files-progress {
    width: 0;
    background-color: lightslategray;
    color: white;
    font-weight: bold;
    font-size: 14px;
    line-height: 25px;
    padding: 0 5px;
}

@keyframes dropbox {
    0% {
        background-image: repeating-linear-gradient(30deg, green 1%, green 3%, darkgreen 5%, darkgreen 5%);
    }

    50% {
        background-image: repeating-linear-gradient(30deg, darkgreen 1%, darkgreen 3%, green 5%, green 5%);
    }

    100% {
        background-image: repeating-linear-gradient(30deg, green 1%, green 3%, darkgreen 5%, darkgreen 5%);
    }
}

.modal-page {
    visibility: hidden;
    opacity: 0;
    position: fixed;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    transition: .5s ease;
    background-color: black;
    margin: 0;
    padding: 0;
    overflow: auto;
}

.modal-page img {
    display: block;
    margin: 0 auto;
    width: 100%;
    height: auto;
}

.modal-page:target {
    visibility: visible;
    opacity: 1;
}

Tying Up Loose Ends

Finally, make sure that the server will accept large uploads. Create and edit the "web.config" file to have this section (security) inside the system.webServer tag. Note: Visual Studio and ASP.NET Core require creating a web.config file in the root of the project.

<system.webServer>
<security>
	<requestFiltering>
	<requestLimits maxAllowedContentLength="102400000"/>
	</requestFiltering>
</security>
</system.webServer>

Coding Video

https://youtu.be/6ivx8ZLDOM4