HxInputFile – Blazor InputFile extension with direct (native) upload and progress indication

Starting with .NET 5 there is a InputFile component in Blazor which allows you to access content of the corresponding file(s) in form of a Stream (IBrowserFile.OpenReadStream).

If you run Blazor on server, the InputFile implementation sends the file content from browser to server through a SignalR connection almost at native speed (approx. 30% slower). In case of Blazor WebAssembly, you have to transfer the file to server on your own.

Due to unmarshalled interop Blazor is able to copy the file to WASM memory extremely quickly (approx. 100MB/sec). You can process the file on client-side (resize image etc.), but if you just want to upload the file, the cost of WASM is relatively high (in comparison to direct HTTP upload) and not suitable for larger files:

Direct (native) upload

To get over the limits of InputFile component in WASM, we can upload the files to server directly from JavaScript using XMLHttpRequest:

var data = new FormData();
data.append('file', file, file.name);

var request = new XMLHttpRequest();
request.open('POST', uploadEndpointUrl, true);
request.send(data);

(Unlike XMLHttpRequest the new fetch() API does not support progress indication.)

Progress indicator

XMLHttpRequest gives you nice progress indication by using the onprogress event:

request.upload.onprogress = function (e) {
    // e.loaded - bytes already uploaded
    // e.total - total upload size (slightly bigger than the file size)
};

HxInputFile / HxInputFileCore

Let’s put this all together and create a new component. We will call it HxInputFileCore (+ the HxInputFile which is a ready-made Bootstrap derivative).

The most important portions follow:

public partial class HxInputFileCore : InputFile, IAsyncDisposable
{
	[Parameter] public string UploadUrl { get; set; }
	[Parameter] public EventCallback<UploadProgressEventArgs> OnProgress { get; set; }
	[Parameter] public EventCallback<FileUploadedEventArgs> OnFileUploaded { get; set; }
	[Parameter] public EventCallback<UploadCompletedEventArgs> OnUploadCompleted { get; set; }
	[Parameter] public bool Multiple { get; set; }
	[Parameter] public string Id { get; set; } = "hx" + Guid.NewGuid().ToString("N");

	[Inject] protected IJSRuntime JSRuntime { get; set; }

	private DotNetObjectReference<HxInputFileCore> dotnetObjectReference;
	private IJSObjectReference jsModule;

	public HxInputFileCore()
	{
		dotnetObjectReference = DotNetObjectReference.Create(this);
	}

	protected override void OnParametersSet()
	{
		base.OnParametersSet();

		// TODO Temporary hack as base implementation of InputFile does not expose ElementReference (vNext: https://github.com/dotnet/aspnetcore/blob/main/src/Components/Web/src/Forms/InputFile.cs)
		AdditionalAttributes ??= new Dictionary<string, object>();
		AdditionalAttributes["id"] = this.Id;
		AdditionalAttributes["multiple"] = this.Multiple;
	}

	public async Task StartUploadAsync(string accessToken = null)
	{
		jsModule ??= await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./_content/Havit.Blazor.Components.Web/hxinputfilecore.js");
		await jsModule.InvokeVoidAsync("upload", Id, dotnetObjectReference, this.UploadUrl, accessToken);
	}

	[JSInvokable("HxInputFileCore_HandleUploadProgress")]
	public async Task HandleUploadProgress(int fileIndex, string fileName, long loaded, long total)
	{
		var uploadProgress = new UploadProgressEventArgs() { /*...*/	};
		await OnProgress.InvokeAsync(uploadProgress);
	}

	[JSInvokable("HxInputFileCore_HandleFileUploaded")]
	public async Task HandleFileUploaded(int fileIndex, string fileName, long fileSize, string fileType, long fileLastModified, int responseStatus, string responseText)
	{
		var fileUploaded = new FileUploadedEventArgs() { /* ... */ };
		await OnFileUploaded.InvokeAsync(fileUploaded);
	}

	[JSInvokable("HxInputFileCore_HandleUploadCompleted")]
	public async Task HandleUploadCompleted(int fileCount, long totalSize)
	{
		var uploadCompleted = new UploadCompletedEventArgs() { /* ... */
		};
		await OnUploadCompleted.InvokeAsync(uploadCompleted);
	}

	public async ValueTask DisposeAsync()
	{
		// ...
	}
}

…and the supportive JavaScript is:

export function upload(inputElementId, hxInputFileDotnetObjectReference, uploadEndpointUrl, accessToken) {
	var inputElement = document.getElementById(inputElementId);
	var dotnetReference = hxInputFileDotnetObjectReference;
	var files = inputElement.files;
	var totalSize = 0;
	var uploadedCounter = 0;

	for (var i = 0; i < files.length; i++) {
		(function (curr) {
			var index = curr;
			var file = files[curr];
			totalSize = totalSize + file.size;

			var data = new FormData();
			data.append('file', file, file.name);

			var request = new XMLHttpRequest();
			request.open('POST', uploadEndpointUrl, true);

			if (accessToken) {
				request.setRequestHeader('Authorization', 'Bearer ' + accessToken);
			}

			request.upload.onprogress = function (e) {
				dotnetReference.invokeMethodAsync('HxInputFileCore_HandleUploadProgress', index, file.name, e.loaded, e.total);
			};
			request.onreadystatechange = function () {
				if (request.readyState === 4) {
					dotnetReference.invokeMethodAsync('HxInputFileCore_HandleFileUploaded', index, file.name, file.size, file.type, file.lastModified, request.status, request.responseText);
				};

				uploadedCounter++;
				if (uploadedCounter === files.length) {
					dotnetReference.invokeMethodAsync('HxInputFileCore_HandleUploadCompleted', files.length, totalSize);
				}
			}

			request.send(data);
		}(i));
	}
}

The component is part of open-sourced library HAVIT Blazor published on GitHub.

Usage

<HxInputFile @ref="hxInputFileComponent" Label="HxInputFile" UploadUrl="/file-upload-streamed/" OnProgress="HandleProgress" OnFileUploaded="HandleFileUploaded" OnUploadCompleted="HandleUploadCompleted" Multiple="true" />

<HxButton Text="Upload" OnClick="HandleUploadClick" />

@code
{
	private HxInputFile hxInputFileComponent;

	private async Task HandleUploadClick()
	{
		files.Clear();

		string accessToken = null;
		
		var accessTokenResult = await ... // use IAccessTokenProvider
		await hxInputFileComponent.StartUploadAsync(accessToken);
	}

	private Task HandleProgress(UploadProgressEventArgs progress)
	{
		// indicate progress here
	}

	private Task HandleFileUploaded(FileUploadedEventArgs fileUploaded)
	{
		// individual file uploaded
	}

	private Task HandleUploadCompleted(UploadCompletedEventArgs uploadCompleted)
	{
		// all files uploaded
	}
}

TODOs

The presented component if not feature complete. There is some more work to do:

  • Maximum file size limit
  • Limit number of files being uploaded in parallel
  • Better error-handling
  • …?

Links

See also

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s