Upload Files Using jQuery Ajax and JavaScript Interop in Blazor
Uploading files from client machine to the server is one of the fundamental
operations in web applications. In ASP.NET Core MVC applications you can
accomplish this task easily using HTML5 file input field and server side logic.
However, Blazor doesn't have any built-in facility (so far) for uploading files.
You either need to use some third-party solution or device your own technique to
accomplish the task. If you want to use third-party feature rich solutions then
take a look at
Syncfusion's implementation and
Steve Sanderson's file upload component.
In this article I am going to illustrate a traditional Ajax based approach to
serve the purpose. In addition to uploading files to the server you will learn
how JavaScript interop of Blazor works. What we will do is:
- We display a file input field to the user that allows one or more files
to be selected.
- In addition to files you might have additional data entry fields.
- Upon submitting the Blazor form, Blazor code invokes a client side
JavaScript function using JS interop.
- The JS function makes jQuery Ajax call to an API controller and sends
the selected files to the server.
- API controller saves the files on the server.
- Blazor is notified of the success or failure of the Ajax call.
The Razor Component that houses the required UI is shown below:
Let's get going.
Begin by creating Blazor server side app using Visual Studio IDE.
Add Employee model class
The data entry form that you create uses a model class called Employee. The
Employee class consists of two properties EmployeeID and FullName. To add this
class, right click on the Data folder and add a new C# class named Employee.
Then modify the class as shown below:
public class Employee
{
[Required]
public string EmployeeID { get; set; }
[Required]
public string FullName{ get; set; }
}
Create Razor Component with data entry form
Now, right click on the Pages folder and add a new Razor Component called
FileUpload.razor.
Then setup the route for the newly added component by adding this code at the
top of the component file.
@page "/fileupload"
@using JQueryFileUploadBlazorServerSide.Data
@inject IJSRuntime JsRuntime;
The first line makes the Razor Component available as if it is a "page". This
is done by associating /fileupload route with the component. The @using
directive uses Data namespace. You need this namespace because you want to use
the Employee model class created earlier. Finally, you also inject JsRuntime
object into the component using @inject directive. This class is required to use
Blazor's JavaScript interop.
Then add a data entry form to the component by writing the following code and
markup.
<h1>Upload Files</h1>
<EditForm Model="emp" OnValidSubmit="OnSubmit">
<DataAnnotationsValidator></DataAnnotationsValidator>
<ValidationSummary></ValidationSummary>
<table cellpadding="11">
<tr>
<td>Employee ID :</td>
<td>
<InputText id="employeeID"
@bind-Value="emp.EmployeeID" />
<ValidationMessage
For="@(() => emp.EmployeeID)" />
</td>
</tr>
<tr>
<td>Full Name :</td>
<td>
<InputText id="fullName"
@bind-Value="emp.FullName" />
<ValidationMessage
For="@(() => emp.FullName)" />
</td>
</tr>
<tr>
<td>Select File(s) To Upload :</td>
<td>
<input type="file"
id="files"
name="files" multiple />
</td>
</tr>
<tr>
<td colspan="2" align="left">
<button type="submit">Upload Files</button>
</td>
</tr>
</table>
</EditForm>
<br />
<h3>@Message</h3>
The <EditForm> component defines Blazor's data entry form. Note that <EditForm>
specifies its Model to be emp object. This object is of type Employee and is
defined later in the code. We require model validations for our form. So, <DataAnnotationsValidator>,
<DataAnnotationsValidator>, and <ValidationMessage> components are used in the
markup. To specify EmployeeID and FullName values you use <InputText> input
component. These input components are validated using <ValidationMessage>
component.
Notice the markup shown in bold letters. It's a file input field that allows
selecting multiple files for uploading. At the bottom of the form there is a
submit button that submits the form. The <EditForm> component's OnValidSubmit
attribute is set to OnSubmit() handler method (discussed later). If all the
input components contain valid values only then OnSubmit() will be invoked.
Otherwise error messages are displayed as shown below:
At the bottom of the data entry form you output the value of Message property
(discussed later). This is basically a success or error message indicating the
status of the file upload operation.
Add @code block and model object
Now that the data entry form is complete, add @code block as shown below:
@code {
Employee emp = new Employee();
public string Message { get; set; }
}
As you can see, the @code block declares an Employee object. Recollect that <EditForm>
component's Model is set to emp object. The code also creates the Message
property. This property holds a message that is rendered on the page so that
user is ware of the status of the file upload operation.
Write OnSubmit() method
Next, add a method called OnSubmit() to handle the form submission. This
method is shown below:
private async void OnSubmit()
{
//do something with Employee properties
Message = $"Uploading files for {emp.FullName}
({emp.EmployeeID})";
var flag = await JsRuntime.InvokeAsync<bool>
("UploadFiles",DotNetObjectReference.Create(this));
StateHasChanged();
}
As you can see, the OnSubmit() method sets the Message property so that the
user is aware of the file upload operation. At this stage file upload has just
began. So, the Message simply informs the user about it. Note that the code uses
EmployeeID and FullName prperties of Employee object just to display the
message. In a more realistic situation you will store these details into
database or process them in a way specific to your application.
The next line of code is important. It calls the InvokeAsync() method of
JsRuntime in an attempt to invoke a JavaScript function. The InvokeAsync() call
specifies that we want to invoke a JavaScript method named UploadFiles. It also
specifies that the UploadFiles method is going to return a Boolean value.
Once file upload operation is complete we want UploadFiles to notify back to
Blazor that the operation is complete. That means we also want to invoke Blazor
code from the UploadFiles JavaScript function. To facilitate this JS to Blazor
communication we pass a reference to the current component instance to the JS
code. This is done using DotNetObjectReference.Create() method.
Write [JSInvokable] method
In the preceding section it was mentioned that the UploadFiles JavaScript
function needs to call Blazor code in order to notify that the file upload
operation is complete. This JS callable code resides as a component method that
is decorated with [JSInvokable] attribute. Have a look at the following code:
[JSInvokable]
public bool SetMessage(string msg)
{
Message = msg;
StateHasChanged();
return true;
}
The SetMessage() method accepts a string message and returns a Boolean value.
This string message will be passed by the JS code. Inside, the string message is
assigned to the Message property so that it gets displayed on to the page.
Write UploadFiles() JavaScript function
Now let's write the UploadFiles() JavaScript function hinted at in the
preceding section. This function makes use of jQuery Ajax to send files to the
server. Therefore, add jQuery library in the wwwroot/Scripts folder. Also add a
new JavaScript file named FileUpload.js
A <script> reference needs to be added to these files from _Host.cshtml file.
Locate _Host.cshtml from Pages folder and add these lines in the head section.
<script src="~/Scripts/jquery.js"></script>
<script src="~/Scripts/FileUpload.js"></script>
Then open FileUpload.js and write the UploadFiles() function in it as shown
below:
function UploadFiles(instance) {
var fileUpload = $("#files").get(0);
var files = fileUpload.files;
var data = new FormData();
for (var i = 0; i < files.length; i++) {
data.append(files[i].name, files[i]);
}
$.ajax({
type: "POST",
url: "/api/FileUpload",
contentType: false,
processData: false,
data: data,
success: function (message) {
instance.invokeMethodAsync("SetMessage",
message)
.then((result) => {
console.log(result);
});
},
error: function () {
instance.invokeMethodAsync("SetMessage",
"Error while uploading files!")
.then((result) => {
console.log(result);
});
}
});
return true;
}
The code shown above grabs the file input field and its files collection. It
then creates a new FormData object and one-by-one files are appended to it. The
resultant FormData object is sent to the server by making an Ajax call. The $.ajax()
call specifies that the FormData is POSTed to /api/FileUpload end-point. This
is actually FileUploadController's UploadFiles() action and you will write it in the
next section.
Notice that the UploadFiles() function receives Blazor component class
instance as its parameter (recollect the DotNetObjectReference.Create() call
earlier). The success and error handler callbacks use this instance object to
call Blazor's SetMessage() method. This is done using invokeMethodAsync()
method. The invokeMethodAsync() method accepts the Blazor method name to invoke
and returns a JavaScript Promise. Here, we use then() method of the
Promise to print the Boolean return value from SetMessage() to the console.
Add UploadFiles() API action to save files on the server
Next, add Controllers folder to the project and add a new API controller
named FileUploadController. Then write UploadFiles() action into the FileUploadController as
shown below:
[HttpPost]
public IActionResult UploadFiles()
{
long size = 0;
var files = Request.Form.Files;
foreach (var file in files)
{
var filename = ContentDispositionHeaderValue
.Parse(file.ContentDisposition)
.FileName
.Trim('"');
filename = hostingEnv.WebRootPath + $@"\{filename}";
size += file.Length;
using (FileStream fs =
System.IO.File.Create(filename))
{
file.CopyTo(fs);
fs.Flush();
}
}
string message = $"{files.Count} file(s) /
{size} bytes uploaded successfully!";
return Json(message);
}
If you ever wrote
file upload
code in ASP.NET Core MVC apps then this code should look familiar to you.
The code iterates through the Request.Form.Files collection and saves the
uploaded files one-by-one on the server. The files are saved under /wwwroot
folder. The physical path of wwwroot folder is obtained using the WebRootPath
property of IWebHostEnvironment object injected into the controller (IWebHostEnvironment
injected into the constructor is not shown for the sake of brevity).
Once all the files are saved on the server a success message containing total
number of files uploaded and their size in bytes is sent back to the caller.
Final step is to configure routing for the API controllers in the Configure()
method of Startup class.
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
This completes the application. Run the application and try uploading one or
more files. Check whether the files get saved under wwwroot folder.
That's it for now! Keep coding!!
"Unless you learn to channelize
three main dimensions of primordial energy - will, knowledge, and action
- to manifest your life goals you aren't utilizing it in its true yogic
sense."
#AjapaYogaByBipinJoshi