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.
Tag Archives: Components
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é:
- 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)
Líbí se Vám Blazor?
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.
- Demo: https://github.com/hakenr/AskMe
- Slides: 2019-06-06 – CORESTART 3.0 – Blazor a novinky v ASP.NET Core 3.0.pdf
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