Blazor Multi File Upload with Progress Bar

Blazor Multi File Upload with Progress Bar

Every website has a file upload feature and if your website is built in Blazor then you can create a very good Multi File Upload feature that also has a Progress Bar in it. This tutorial will teach you this, the link to the download of the source code is given at the bottom. But before that, I recommend you to read this tutorial so that you can understand how it is working. I have explained all it’s parts in detailed manner with video illustrations wherever possible.

Once this feature is created it will work as shown by the below given video:

EditForm and InputFile components

To create this feature, I will need a EditForm component with a InputFile component having the multiple attribute. This is added because I will be creating a file upload element that should support multi-file uploads.

The files which I will be uploading will be.png and .jpg only but you can easily modify the custom validator to accept other extension to. I will explain how to do it when I will take the custom validator code.

I created a Reusable HTML Select Component with a Custom Validator in Blazor, you can use it freely in your projects.

So, create a new razor component and then add the below given code to it. I will explain all it’s parts one by one.

@page "/"
@using System.ComponentModel.DataAnnotations
@using System.IO
@using System.Linq
@using System.Threading
@using Microsoft.AspNetCore.Hosting;
@implements IDisposable

<h1 class="bg-info">Upload Multiple Image files</h1>

<EditForm EditContext="editContext" OnValidSubmit="OnSubmit">
    <DataAnnotationsValidator />

    <div class="form-group">
        Name: <InputText @bind-Value="@person.Name" class="form-control" />
        <ValidationMessage For="() => person.Name" />
    </div>

    <div class="form-group">
        Picture: <InputFile multiple OnChange="OnChange" class="form-control" />
        <ValidationMessage For="() => person.Picture" />

        @{
            var progressCss = "progress " + (displayProgress ? "" : "d-none");
            var progressWidthStyle = progressPercent + "%";
        }

        <div class="@progressCss">
            <div class="progress-bar" role="progressbar" style="width:@progressWidthStyle" area-valuenow="@progressPercent" aria-minvalue="0" aria-maxvalue="100"></div>
        </div>
    </div>

    <button class="btn btn-primary">Submit</button>
</EditForm>

@if (imageDataUrls.Count > 0)
{
    <h4>Images</h4>

    <div class="card" style="width:30rem;">
        <div class="card-body">
            @foreach (var imageDataUrl in imageDataUrls)
            {
                <p><img class="rounded m-1" src="@imageDataUrl" /></p>
            }
        </div>
    </div>
}

@code
{
    private CancellationTokenSource cancelation;
    private bool displayProgress;
    private EditContext editContext;
    private Person person;
    private int progressPercent;

    protected override void OnInitialized()
    {
        cancelation = new CancellationTokenSource();
        person = new Person();
        editContext = new EditContext(person);
    }

    [Inject]
    private IWebHostEnvironment env { get; set; }

    private IList<string> imageDataUrls = new List<string>();
    private int Total;
    private async Task OnChange(InputFileChangeEventArgs e)
    {
        person.Picture = e.GetMultipleFiles().ToArray();

        var format = "image/png";
        Total = e.GetMultipleFiles().Count();
        foreach (var imageFile in e.GetMultipleFiles())
        {
            var resizedImageFile = await imageFile.RequestImageFileAsync(format, 100, 100);
            var buffer = new byte[resizedImageFile.Size];
            await resizedImageFile.OpenReadStream().ReadAsync(buffer);
            var imageDataUrl = $"data:{format};base64,{Convert.ToBase64String(buffer)}";
            imageDataUrls.Add(imageDataUrl);
        }
        editContext.NotifyFieldChanged(FieldIdentifier.Create(() => person.Picture));
    }

    private async Task OnSubmit()
    {
        for (int i = 0; i < Total; i++)
        {
            var path = $"{env.WebRootPath}\\{person.Picture[i].Name}";
            using var file = File.OpenWrite(path);
            using var stream = person.Picture[i].OpenReadStream(968435456);

            var buffer = new byte[4 * 1096];
            int bytesRead;
            double totalRead = 0;

            displayProgress = true;

            while ((bytesRead = await stream.ReadAsync(buffer, cancelation.Token)) != 0)
            {
                totalRead += bytesRead;
                await file.WriteAsync(buffer, cancelation.Token);

                progressPercent = (int)((totalRead / person.Picture[i].Size) * 100);
                StateHasChanged();
            }

            displayProgress = false;
        }
    }

    public void Dispose()
    {
        cancelation.Cancel();
    }

    public class Person
    {
        [Required]
        [StringLength(20, MinimumLength = 2)]
        public string Name { get; set; }

        [Required]
        [FileValidation(new[] { ".png", ".jpg" })]
        public IBrowserFile[] Picture { get; set; }
    }

    private class FileValidationAttribute : ValidationAttribute
    {
        public FileValidationAttribute(string[] allowedExtensions)
        {
            AllowedExtensions = allowedExtensions;
        }

        private string[] AllowedExtensions { get; }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            foreach(IBrowserFile file in (IBrowserFile[])value)
            {
                var extension = System.IO.Path.GetExtension(file.Name);

                if (!AllowedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
                {
                    return new ValidationResult($"File must have one of the following extensions: {string.Join(", ", AllowedExtensions)}.", new[] { validationContext.MemberName });
                }
            }
            return ValidationResult.Success;
        }
    }
}

Person Class and Custom Validator

A person class is defined with 2 properties – Name & Picture. There are also validation attributes applied on them.

public class Person
{
    [Required]
    [StringLength(20, MinimumLength = 2)]
    public string Name { get; set; }

    [Required]
    [FileValidation(new[] { ".png", ".jpg" })]
    public IBrowserFile[] Picture { get; set; }
}

The FileValidationAttribute is a custom validation class which only allows files of .png and .jpg to be accepted for uploads.

private class FileValidationAttribute : ValidationAttribute
{
    ….
}

It receives the file extensions that needs to be allowed in it’s constructor.

public FileValidationAttribute(string[] allowedExtensions)
{
    AllowedExtensions = allowedExtensions;
}

Then in the IsValid() method, the validation logic is performed. I use the foreach loop to loop through every file added to the file upload element and checking their extension. If the extension is not .png or .jpg then the validation is made to fail.

foreach(IBrowserFile file in (IBrowserFile[])value)
{
    var extension = System.IO.Path.GetExtension(file.Name);

    if (!AllowedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))
    {
        return new ValidationResult($"File must have one of the following extensions: {string.Join(", ", AllowedExtensions)}.", new[] { validationContext.MemberName });
    }
}

EditContext

These 2 properties of the Employee class (Name & Picture) are bind to the EditForm component and the controls defined inside it. I have added EditContext attribute to the EditForm and provided it with the EditContext class object.

<EditForm EditContext="editContext" OnValidSubmit="OnSubmit">
    ….
</EditForm>

The EditContext class object holds metadata related to a data editing process. It provides valuable information regarding:

  • 1. The bind properties that are modified.
  • 2. The current set of validation messages.

and many more things. I used it to find when the user adds any file to the InputFile component. In that case I signals the EditForm component that the Picture field is modified.

This is how it is done. First initialize the EditContext object in the OnInitialized lifecycle method.

protected override void OnInitialized()
{
    …
    editContext = new EditContext(person);
}

Then on the onchange event of the InputFile, I performed the notification work by the below code:

editContext.NotifyFieldChanged(FieldIdentifier.Create(() => person.Picture));

Note that the OnChange event of the InputFile calls a handler by the name of OnChange, and in this hander the notification to the EditForm component is done.

Showing preview of the Images

The OnChange also creates the preview system that shows the preview of the images that are added to the InputFile component. The below code does this work:

person.Picture = e.GetMultipleFiles().ToArray();
var format = "image/png";
Total = e.GetMultipleFiles().Count();
foreach (var imageFile in e.GetMultipleFiles())
{
    var resizedImageFile = await imageFile.RequestImageFileAsync(format, 100, 100);
    var buffer = new byte[resizedImageFile.Size];
    await resizedImageFile.OpenReadStream().ReadAsync(buffer);
    var imageDataUrl = $"data:{format};base64,{Convert.ToBase64String(buffer)}";
    imageDataUrls.Add(imageDataUrl);
}

Here I assigned all the files in the input file control to the Picture property of the Employee class. I used the GetMultipleFiles() method.

person.Picture = e.GetMultipleFiles().ToArray();

Next, I loop through all the files one by one and show their previews to the user.

The preview is shown in a div which creates img tags containing the preview of each files. Check the code which does this work:

@if (imageDataUrls.Count > 0)
{
    <h4>Images</h4>

    <div class="card" style="width:30rem;">
        <div class="card-body">
            @foreach (var imageDataUrl in imageDataUrls)
            {
                <p><img class="rounded m-1" src="@imageDataUrl" /></p>
            }
        </div>
    </div>
}

In the below I have shown the preview of images:

blazor file preview
blazor file preview

Uploading Multiple Files in Blazor

When the user clicks the submit button then the multiple files added to the InputFile are uploaded to the server. I first loop through all the files using for loop.

private async Task OnSubmit()
{
    for (int i = 0; i < Total; i++)
    {
        …
    }
} 

Inside the for loop I upload these files to the server. The path variable holds the path where the files will be uploaded.

var path = $"{env.WebRootPath}\\{person.Picture[i].Name}";

For it to work, I also have injected the IWebHostEnvironment that will provide me the WebRootPath value which holds the path to the wwwroot folder.

[Inject]
private IWebHostEnvironment env { get; set; }

The files are writing to the “path” using the File class of the System.IO namespace.

I get a FileStream object in the file variable:

using var file = File.OpenWrite(path);

Then opens a stream to the file by the below code line. 968435456 is the maximum allowed size of the file which is approximately 900 mb. You can change it if you want to.

using var stream = person.Picture[i].OpenReadStream(968435456);

After that I start writing the files inside the While loop:

while ((bytesRead = await stream.ReadAsync(buffer, cancelation.Token)) != 0)
{
…
}

WriteAsync() method writes the file to the server.

await file.WriteAsync(buffer, cancelation.Token); 

You can see that I am also updating the progressPercent that calculates the % of file that is uploaded.

progressPercent = (int)((totalRead / person.Picture[i].Size) * 100);

This helps to update the progress bar and tell the user how much the file has uploaded and how much is left.

<div class="progress-bar" role="progressbar" style="width:@progressWidthStyle" area-valuenow="@progressPercent" aria-minvalue="0" aria-maxvalue="100"></div>

You can download the source code:

Download

Testing

Run the app and upload some image files. I have shown the uploading of files in the below given video:

Conclusion

I hope you enjoyed learning Multi File Upload feature in Blazor. Use it in your project freely. Do share this tutorial on your facebook and twitter profiles as it helps it to get the necessary exposure.

SHARE THIS ARTICLE

  • linkedin
  • reddit
yogihosting

ABOUT THE AUTHOR

I hope you enjoyed reading this tutorial. If it helped you then consider buying a cup of coffee for me. This will help me in writing more such good tutorials for the readers. Thank you. Buy Me A Coffee donate

Leave a Reply

Your email address will not be published. Required fields are marked *