Záznam ze Vzdělávacího okénka HAVIT, kde Jirka Kanda ukazoval komponentu HxGrid z balíčku Havit.Blazor:
Author Archives: Robert Haken
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.
OrderBy(e => e.NullableNavigationProperty.SomeValue) v EF Core
Při code-reviews se opakovaně setkávám se snahou ošetřit null v OrderBy/Where a v podobných LINQ extension metodách při použití EF Core. Například
.OrderBy(e => e.BossId.HasValue ? e.Boss.LastName : String.Empty)
Je to obvykle nadbytečné, ba dokonce nežádoucí.
LINQ provider výraz nevykonává, ale překládá do SQL, takže pohodlně funguje
.OrderBy(e => e.Boss.LastName)
přestože Boss může být NULL.
Možná takový zápis v C# tahá za oči, ale spíš bych se obával tu expression pro EF Core jakkoliv komplikovat, aby z toho nevznikl nějaký složitější dotaz než je potřeba.
Konkrétně krátká podoba vytvoří SQL klauzuli
ORDER BY [e].[LastName]
a SQL si s NULL pohodlně poradí, zatímco dlouhá podoba udělá
ORDER BY CASE
WHEN [e].[Id] IS NOT NULL THEN [e].[LastName]
ELSE N''''
END
…což obvykle nepotřebujete a v T-SQL by vás to nejspíš nikdy nenapadalo takhle řešit.
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.
Refit – REST API client [Robert Haken, Vzdělávací okénko, 10.3.2021]
Záznam ze Vzdělávacího okénka HAVIT z 10. března 2021, kde jsem povídal o Refit – knihovně umožňující snadné konzumování REST API z .NET pomocí strong-API.
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?
Novinky v TypeScript 4 [Lukáš Rada, HAVIT Vzdělávací okénko, 9.9.2020]
Záznam ze Vzdělávacího okénka HAVIT, kde Lukáš Rada mluvil o nových možnostech v jazyce TypeScript verzi 4.
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.
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.
Azure Static Web Apps [Lukáš Rada, Vzdělávací okénko, 22.7.2020]
Záznam ze Vzdělávacího okénka HAVIT, kde Lukáš Rada mluvil o Azure Static Web Apps (aktuálně v Preview), které nabízejí možnost snadno hostovat statický frontend ve spojení s Azure Functions pro API (backend).