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é:
- HttpClient (v době psaní tohoto postu) nepodporuje request-streaming. I když použijete StreamContent, celý request je nejprve nabufferován do paměti a pak teprve tlačen na server.
- HttpClient musí data z WASM dostat ven.
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
- havit/Havit.Blazor: Libraries of components and supportive classes for Blazor development. (github.com)
- Havit.Blazor/HxInputFileCore.cs at master · havit/Havit.Blazor (github.com)
- Havit.Blazor/HxInputFile.cs at master · havit/Havit.Blazor (github.com)
- NuGet Gallery | Havit.Blazor.Components.Web – HxInputFileCore
- NuGet Gallery | Havit.Blazor.Components.Web.Bootstrap – HxInputFile
Viz též
- File uploads with Blazor (stevensanderson.com) – původní
BlazorInputFile
, než bylo přidáno do Blazoru - [browser][wasm] Request Streaming upload via http handler · Issue #36634 · dotnet/runtime (github.com)
- WebAssemblyHttpHandler buffers StreamContent into browser memory · Issue #19969 · mono/mono (github.com)