Drag-n-Drop file upload in Blazor using JS interop and minimal API (Part 1)
Uploading files from client computer to the server is a common task in web
applications. You can either use
jQuery Ajax or Blazor's
InputFile component to
accomplish that task. Files to be uploaded on the server are typically selected
by the end user using file input field. However, most of the browsers also
allow you to select files by dragging them from the client computer to your web
application. To that end this article discusses how drag-n-drop of files can be
done in Blazor Server apps. I am going to assume that you are familiar with
Blazor's JavaScript interop. You may read my
previous article in case you aren't familiar with Blazor's JS interop.
In this first part of the article I will discuss the drag-n-drop
functionality with an example. In the next part I will show how the selected
files can be uploaded to the server using a minimal API or a controller based
API.
Before we write any code that enables drag-n-drop of files, let's quickly see
the sample app that we are going to build. Take a look at the following Razor
Component that makes the main page of the application.
Initially when you start the app, you will be shown a drop target where you
can drag and drop files from the computer. As soon as you drag and drop one or
more files, their names will be listed in the drop target and the uploading
operation will begin.
As you can see, there are three image files selected. A progress message is
displayed in the drop target and also a progress indicator image is displayed to
the user. Once the upload is complete a success message will be displayed as
follows:
Now that you know how the app is going to work, let's start building it.
Begin by creating a new Blazor Server app using Visual Studio.
Then add a new Razor Component named FileUpload.razor to the Pages folder.
Also add a new JavaScript file named JavaScript.js in the wwwroot/Scripts
folder. We will use some JS interop code that will be housed in this JavaScript
file.
Then add jQuery library file to the www/Scripts folder. We will use
jQuery Ajax for the sake of uploading the selected files to the server.
Then add a <script> reference to both of these files in the _Host.cshtml file
as shown below:
<script src="~/scripts/jquery-2.1.1.min.js"></script>
<script src="~/scripts/JavaScript.js"></script>
Then open FileUpload.razor component and write the following directives to
it.
@page "/fileupload"
@inject IJSRuntime JS
The @page directive sets the route for the component to /fileupload so that
it can be accessed as a standalone "page". Then we inject IJSRuntime object
because we want to use JS interop features later. This injected object can be
accessed in our code using JS property.
Then place a <div> element and an <img> element as shown below.
<div id="fileBasket" class="filebasket">
</div>
<br />
<img id="progress" src="/Images/Progress.gif" />
The fileBasket element acts as a drop target where you drag-n-drop files and
the progress element displays an animated GIF during upload operation. You will
need to place Progress.gif in the wwwroot/Images folder.
The filebasket CSS class applied to the fileBasket element is this:
.filebasket{
border:solid 2px black;
padding:10px;
height:200px;
width:250px;
text-align:center;
font-weight:bold;
}
You can place it in a CSS file (or at the top of the component file if you
don't want to add a CSS file).
In order to implement drag-n-drop on the fileBasket <div> element, you need
to handle three events namely dragenter, dragover, and drop. There are two ways
to handle these events. You can handle them from your C# code by writing the
necessary event handler methods OR you can write them in JavaScript and wire
them using Blazor's JS interop. I will show you both the techniques in the
following discussion.
Let's first handle these events using C# event handlers.
Modify the <div> element as shown below:
<div id="fileBasket" class="filebasket"
@ondrop="@OnDrop"
@ondrop:preventDefault="true"
@ondrop:stopPropagation="true"
@ondragenter="@OnDragenter"
@ondragenter:preventDefault="true"
@ondragenter:stopPropagation="true"
@ondragover="@OnDragover"
@ondragover:preventDefault="true"
@ondragover:stopPropagation="true">
</div>
Notice this markup carefully. It wires three event handlers using @ondrop, @ondragenter,
and @ondragover attributes. The respective event handler methods OnDrop(),
OnDragenter(), and OnDragover() will be written in @code block later.
Also notice that we set preventDefault and stopPropagation properties for all
the three events to true. Since we want to use the <div> element as a drop
target, we need to suppress its default behavior and hence setting these
properties is necessary. If you don't set these properties the browser will
typically navigate to the dropped files rather than allowing our app to handle
the drop operation.
Now let's write the three event handler methods - OnDrop(), OnDragenter(),
and OnDragover() in @code block.
@code {
public async void OnDrop(DragEventArgs evt)
{
}
public void OnDragenter(DragEventArgs evt)
{
}
public void OnDragover(DragEventArgs evt)
{
}
}
Out of these three event handlers, we will write code only for OnDrop() but you
can use the others in case you want to perform some processing. All of them
receive a
DragEventArgs object that provides more information about what is being
dragged.
Now add the following properties at the top of the @code block.
public string[] SelectedFiles { get; set; }
public string Message { get; set; } = "Drag-n-Drop files here.";
The SelectedFiles array holds a list of files selected for the upload
operation. This array is used to display a bulleted list of files in the
fileBastet element discussed earlier. The Message property is used to display a
general purpose message (say, a success message or an error message) to the user
and it is outputted below the fileBasket element.
Then add the following lines to the OnDrop() event handler.
public async void OnDrop(DragEventArgs evt)
{
this.SelectedFiles = evt.DataTransfer.Files;
await JS.InvokeVoidAsync("UploadFiles",
DotNetObjectReference.Create(this));
StateHasChanged();
}
Here, we use DragEventArgs object's DataTransfer.Files property to obtain a
list of files selected by the end user. We store this list into the SelectedFile
array. We then invoke a JS function called UploadFiles() using the
InvokeVoidAsync() method. Notice that current component's reference is passed to
the InvokeVoidAsync() method using DotNetObjectReference.Create() method.
Finally, StateHasChanged() method is called so that the component UI gets the
updated values.
Next, open the JavaScript.js file and add the UploadFiles() function.
function UploadFiles(compRef)
{
alert("File upload API call here.");
}
We will add the jQuery Ajax code that invokes the API later. For the time
being just put an alert for the sake of testing.
The SelectedFiles can be displayed in the fileBasket element as shown below.
<div id="fileBasket" class="filebasket"
@ondrop="@OnDrop"
@ondrop:preventDefault="true"
@ondrop:stopPropagation="true"
@ondragenter="@OnDragenter"
@ondragenter:preventDefault="true"
@ondragenter:stopPropagation="true"
@ondragover="@OnDragover"
@ondragover:preventDefault="true"
@ondragover:stopPropagation="true"
>
@if (SelectedFiles != null)
{
<div>Uploading...</div>
<ul>
@foreach (var file in SelectedFiles)
{
<li>@file</li>
}
</ul>
}
else
{
<strong>@Message</strong>
}
</div>
Inside the <div> element a foreach loop iterates through the SelectedFiles
array and displays a bulleted list of files. If SelectedFiles is null, the
Message property is displayed.
When the jQuery Ajax code completes the file upload operation it sets a
message indicating the completion of the operation. This requires that
UploadFiles() calls a C# method that sets the Message property to desired value.
Therefore, we will add a C# method called SetMessage() in the FileUpload.razor
component.
[JSInvokable]
public bool SetMessage(string msg)
{
Message = msg;
SelectedFiles = null;
StateHasChanged();
return true;
}
Notice that the SetMessage() is marked with [JSInvokable] attribute making it
callable from JS code. Inside, we set the Message property to the supplied
message and clear the SelectedFiles array.
In the preceding code we handled the OnDrop event using C#. Since we will be
uploading files using jQuery Ajax the UploadFiles() function needs to know what
files are selected by the user for uploading. This requires that we handle
OnDrop event in JS code also. So, add another function named WireEventHandlers()
in the JavaScript.js file as shown below.
function WireEventHandlers() {
$("#fileBasket").on("drop", function (evt) {
window.selectedFiles =
evt.originalEvent.dataTransfer.files;
});
}
The WireEventHandlers() function wires drop event handler using jQuery on()
method. The drop event handler simply stores a list of files in the global
selectedFiles variable. We will use the selectedFiles variable in the
UploadFiles() function later.
Note that DataTransfer.Files used in the C# code gives just an array of file
names whereas dataTransfer.files in the JS code gives list of files and can be
used to access file content during the upload operation.
We also want to hide the progress indicator image initially when the
component is rendered. So, add a function named HideProgress() that does the
job.
function HideProgress() {
$("#progress").hide();
}
We will call WireEventHandlers() and HideProgress() in the Razor Component's
OnAfterRenderAsync() method.
protected async override Task OnAfterRenderAsync
(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("WireEventHandlers");
await JS.InvokeVoidAsync("HideProgress");
}
}
So far so good. Although we haven't written any code that does the file
upload, you can check the drag-n-drop behavior by running the operation. If you
followed all the steps so far, you will see an alert followed by a list of files
selected for the upload operation like this:
And
Of course, no files will be uploaded at this stage but the skeleton for doing
that is now ready.
In the example discussed so far, we handled the dragenter and dragover events
from Blazor code. If you want you can handle these two events from JS code also.
Let's see that possibility also.
Go to JavaScript.js file and add the following client side event handlers to
WireEventHandlers() function.
$("#fileBasket").on("dragenter", function (evt) {
evt.preventDefault();
evt.stopPropagation();
});
$("#fileBasket").on("dragover", function (evt) {
evt.preventDefault();
evt.stopPropagation();
});
As you can see, we are now calling preventDefault() and stopPropagation()
from the JS event handlers. Since we are cancelling the default behavior and
stopping event bubbling from JS code you can remove the preventDefault and
stopPropagation properties from the fileBasket <div> element (you can also
remove the corresponding C# empty event handler methods).
<div id="fileBasket" class="filebasket"
@ondrop="@OnDrop"
@ondrop:preventDefault="true"
@ondrop:stopPropagation="true">
If you run the application you will get the same result as before.
In the next part of this article we will create a
minimal API endpoint
that is invoked using jQuery Ajax to upload files to the server.
That's it for now! Keep coding!!