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


Bipin Joshi is an independent software consultant, trainer, author, and meditation teacher. He has been programming, meditating, and teaching for 25+ years. He conducts instructor-led online training courses in ASP.NET family of technologies for individuals and small groups. He is a published author and has authored or co-authored books for Apress and Wrox press. Having embraced the Yoga way of life he also teaches Ajapa Yoga to interested individuals. To know more about him click here.

Get connected : Facebook  Twitter  LinkedIn  YouTube

Posted On : 18 August 2021


Tags : ASP.NET ASP.NET Core MVC .NET Framework C# Visual Studio