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.
Category Archives: Blazor
FluentValidation s Blazorem [Robert Haken, Vzdělávací okénko, 4.8.2021]
Záznam ze Vzdělávacího okénka HAVIT, kde jsem ukazoval, jak na validací formulářů v Blazoru s pomocí Fluent Validation knihovny.
Blazor – Validace formulářů [Jiří Kanda, Vzdělávací okénko, 2.6.2021]
Záznam ze Vzdělávacího okénka HAVIT, kde Jirka Kanda ukazoval, jak na validací formulářů v Blazoru:
Cancellation Tokens [Jiří Kanda, Vzdělávací okénko, 5.5.2021]
Záznam ze Vzdělávacího okénka HAVIT, kde Jirka Kanda ukazoval využití a práci s CancellationTokens v asynchronním programování.
Blazor – životní cyklus komponent [Jiří Kanda, Vzdělávací okénko, 24.3.2021]
Záznam ze Vzdělávacího okénka HAVIT, kde Jirka Kanda ukazoval životní cyklus Razor (Blazor) komponent:
Havit.Blazor – HxGrid [Jiří Kanda, Vzdělávací okénko, 24.3.2021]
Záznam ze Vzdělávacího okénka HAVIT, kde Jirka Kanda ukazoval komponentu HxGrid z balíčku Havit.Blazor:
I[Async]Disposable [Jiří Kanda, Vzdělávací okénko, 17.3.2021]
Záznam ze Vzdělávacího okénka HAVIT, kde Jirka Kanda mluvil o IDisposable
, IAsyncDisposable
a vztahu mezi nimi.
Havit.Blazor stack onboarding [Robert Haken, Vzdělávací okénko, 16.3.2021]
Záznam ze Vzdělávacího okénka HAVIT z 16. března 2021, kde nabízím pohled do nitra naší kuchyně – vstupní seznámení s naším Havit.Blazor stackem.
Nahrávka je publikována na našem HAVIT YouTube Channelu.
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 – 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.