Tag Archives: Components

HAVIT Blazor – Bootstrap 5 Components Bundle [Robert Haken, Vzdělávací okénko, 13.10.2021]

Záznam ze Vzdělávacího okénka HAVIT, kde jsem nechával nahlédnout pod pokličku vývoje naší sady komponent pro Blazor, jak jsme daleko, jak si stojíme, co to umí, co nás ještě čeká, atp.

https://havit.blazor.eu

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 – záznam, slides, dema [Robert Haken, Corestart 3.0, Update Days 2019]

Záznam z přednášky pro konferenci Corestart 3.0, Update Days 2019 z 6. června 2019. Je publikován na našem HAVIT YouTube Channelu.

Dotčená témata:

  • Blazor intro
  • Blazor hosting model – server-side vs. client-side
  • Supported platforms
  • Page
  • Components
  • Built-in Components
  • Routing
  • Layouts
  • Components –Event Handling
  • Data Binding
  • Coded Components
  • Components Lifecycle methods
  • Templated Components
  • Razor Templates
  • JavaScript Interop