HxInputFile – rozšíření Blazor InputFile komponenty o přímý (nativní) upload a progress-indikaci

V .NET 5. dostal Blazor InputFile komponentu, která zpřístupňuje vybrané soubory v podobě streamu (IBrowserFile.OpenReadStream).

V případě Blazor Serveru je to celkem použitelné, implementace InputFile protlačí obsah souboru na server pomocí SignalR téměř plnou rychlostí (odhadem o 30% pomaleji než nativní HTTP upload). V případě Blazor WebAssembly je ale implementace přenosu dat na server ponechána na vývojářích a možnosti nejsou růžové.

Blazor dokáže přes unmarshalled interop dostat obsah souboru do WASM velmi rychle (cca 100MB/s), jenže vyšťouchat větší soubor z WASM na server je o dost horší a pro větší soubory nepoužitelné:

Přímý (nativní) upload

Abychom překonali limity InputFile komponenty ve WASM, můžeme soubory uploadovat na server přímo z JavaScriptu pomocí XMLHttpRequest (fetch() API se na to moc nehodí, protože zatím nedává rozumné informace o progressu):

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

Progress indikace

XMLHttpRequest nám dává hezké API v podobě události onprogress:

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

HxInputFile / HxInputFileCore

Když to dáme celé dohromady, můžeme vytvořit komponentu zděděnou z InputFile. Říkejme jí HxInputFileCore (a k tomu HxInputFile, který už je Bootstrap podobou s drobnými vylepšeními kolem).

Klíčové části zdrojového kódu komponenty pak budou:

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()
    {
        // ...
    }
}

…a podpůrný JavaScript k tomu:

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));
    }
}

Komponenty jsou součástí open-source balíčku Havit.Blazor, který najdete na GitHub.

Použití

<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

První verze komponenty neřeší některé detaily. Bude potřeba ještě:

  • Limit velikosti souborů
  • Limit počtu paralelně uploadovaných souborů
  • Vylepšit obsluhu chyb
  • …?

Odkazy

Viz též

Líbí se Vám Blazor?

Přidejte se k nám…

Zanechat odpověď

Vyplňte detaily níže nebo klikněte na ikonu pro přihlášení:

Logo WordPress.com

Komentujete pomocí vašeho WordPress.com účtu. Odhlásit /  Změnit )

Facebook photo

Komentujete pomocí vašeho Facebook účtu. Odhlásit /  Změnit )

Připojování k %s