Tag Archives: WebAssembly

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…

Blazor – Stav světa a co se chystá v .NET5 – záznam, slides, dema [WUG Days Online 2020]

Záznam z přednášky pro konferenci WUG Days Online 2020, kterou pořádalo sdružení WUG Česká Republika ve dnech 21. až 23. října 2020.

DEMA: https://github.com/hakenr/BlazorNet5Demos

HAVIT & Blazor – pohled do kuchyně [Jiří Kanda, Vzdělávací okénko, 29.7.2020]

Záznam ze Vzdělávacího okénka HAVIT, kde Jirka Kanda ukazoval, jaký stack máme v HAVITu připravený pro vývoj aplikací v Blazor.

Blazor WebAssembly + gRPC-Web code-first [Robert Haken, Vzdělávací okénko, 15.4.2020]

Záznam ze Vzdělávacího okénka HAVIT z 15. dubna 2020, kde jsem povídal o komunikaci mezi Blazor WebAssembly front-endem a ASP.NET Core backendem prostřednictvím gRPC-Web protokolu a s code-first přístupem (bez .proto souborů).

Nahrávka je publikována na našem HAVIT YouTube Channelu.

Blazor – Úvod, hosting, modely [Jiří Kanda, HAVIT Vzdělávací okénko, 31.10.2019]

Záznam ze Vzdělávacího okénka HAVIT z 31. října 2019, kde Jiří Kanda povídal o Blazoru – jednalo se o úvod – první ze chystané série vystoupení na toto téma.

Nahrávka je publikována na našem HAVIT YouTube Channelu.

Blazor – záznam, slides, dema [Dotnet Days Praha 2019]

Záznam z přednášky pro konferenci Dotnet Days Praha z 2. listopadu 2019. Je publikován na našem HAVIT YouTube Channelu.

Dotčená témata:

  • Blazor intro
  • Blazor hosting model – server-side vs. client-side, WebAssembly
  • Supported platforms
  • Blazor Now
  • Blazor Plans
  • Page = Component + Route
  • Hello World
  • AskMe Blazor demo
  • Layouts
  • Routing
  • Component Lifecycle Methods

WebAssembly aneb smrt JavaScriptu [Pavel Kříž, HAVIT Vzdělávací okénko 10.5.2018]

Záznam ze Vzdělávacího okénka HAVIT z 10. května 2018, kde Pavel Kříž prezentoval vývoj v oblasti front-endového programování – WebAssemblies a jejich vztah k JavaScriptu. Nahrávka je publikována na našem HAVIT YouTube Channelu.

Podcast .NET.CZ (Episode.25) – Blazor, webový frontend, WebAssembly

Před pár dny jsem byl hostem podcastového pořadu .NET.CZ, kde jsme se bavili od vývoji webových frontendů, WebAssembly a Blazoru, frameworku pro vývoj single page applications (SPA) front-endů v .NET – C# a Razoru.