« Blazor .NET Core 3.1 » : différence entre les versions
(→Liens) |
|||
(104 versions intermédiaires par le même utilisateur non affichées) | |||
Ligne 1 : | Ligne 1 : | ||
[[Category: | [[Category:Blazor]] | ||
= Liens = | = Liens = | ||
* [https://docs.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-3.1 Introduction to ASP.NET Core Blazor] | * [https://docs.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-3.1 Introduction to ASP.NET Core Blazor] | ||
Ligne 19 : | Ligne 19 : | ||
|- | |- | ||
| [https://github.com/AdrienTorris/awesome-blazor Awesome Blazor] || projects | | [https://github.com/AdrienTorris/awesome-blazor Awesome Blazor] || projects | ||
|- | |||
| [https://antblazor.com/en-US/ Ant Blazor Design] || components | |||
|} | |} | ||
Ligne 32 : | Ligne 34 : | ||
= [https://docs.microsoft.com/en-us/aspnet/core/blazor/get-started?view=aspnetcore-3.1&tabs=visual-studio-code Créer une application] = | = [https://docs.microsoft.com/en-us/aspnet/core/blazor/get-started?view=aspnetcore-3.1&tabs=visual-studio-code Créer une application] = | ||
<kode lang='bash'> | <kode lang='bash'> | ||
# Blazor server | |||
dotnet new blazorserver --no-https -o [project-name] | |||
cd [project-name] | |||
dotnet run | |||
# Blazor WebAssembly | # Blazor WebAssembly | ||
dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.1.0-preview4.19579.2 | dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.1.0-preview4.19579.2 | ||
Ligne 37 : | Ligne 44 : | ||
cd blazorwasm | cd blazorwasm | ||
dotnet run | dotnet run | ||
</kode> | |||
== Solution Blazor / Webapi == | |||
<kode lang='bash'> | |||
# create item/item.sln | |||
dotnet new sln -o item | |||
cd item | |||
dotnet new blazorserver --no-https -o item.blazor | |||
dotnet new | dotnet new webapi --no-https -o item.webapi | ||
dotnet | dotnet sln add item.blazor/item.blazor.csproj | ||
dotnet sln add item.webapi/item.webapi.csproj | |||
</kode> | </kode> | ||
<filebox fn='item/.vscode/tasks.json' collapsed> | |||
{ | |||
"version": "2.0.0", | |||
"tasks": [ | |||
{ | |||
"label": "build blazor", | |||
"command": "dotnet", | |||
"type": "process", | |||
"args": [ | |||
"build", | |||
"${workspaceFolder}/item.blazor/item.blazor.csproj", | |||
"/property:GenerateFullPaths=true", | |||
"/consoleloggerparameters:NoSummary" | |||
], | |||
"problemMatcher": "$msCompile" | |||
}, | |||
{ | |||
"label": "build webapi", | |||
"command": "dotnet", | |||
"type": "process", | |||
"args": [ | |||
"build", | |||
"${workspaceFolder}/item.webapi/item.webapi.csproj", | |||
"/property:GenerateFullPaths=true", | |||
"/consoleloggerparameters:NoSummary" | |||
], | |||
"problemMatcher": "$msCompile" | |||
}, | |||
{ | |||
"label": "build all", | |||
"dependsOn": [ | |||
"build blazor", | |||
"build webapi" | |||
], | |||
"group": "build", | |||
"problemMatcher": [] | |||
} | |||
] | |||
} | |||
</filebox> | |||
<filebox fn='item/.vscode/launch.json' collapsed> | |||
{ | |||
"version": "0.2.0", | |||
"configurations": [ | |||
{ | |||
"name": "blazor", | |||
"type": "coreclr", | |||
"request": "launch", | |||
"preLaunchTask": "build blazor", | |||
"program": "${workspaceFolder}/item.blazor/bin/Debug/netcoreapp3.1/item.blazor.dll", | |||
"args": [], | |||
"cwd": "${workspaceFolder}/item.blazor", | |||
"stopAtEntry": false, | |||
"serverReadyAction": { | |||
"action": "openExternally", | |||
"pattern": "\\bNow listening on:\\s+(https?://\\S+)" | |||
}, | |||
"env": { | |||
"ASPNETCORE_ENVIRONMENT": "Development" | |||
}, | |||
"sourceFileMap": { | |||
"/Views": "${workspaceFolder}/Views" | |||
} | |||
}, | |||
{ | |||
"name": "webapi", | |||
"type": "coreclr", | |||
"request": "launch", | |||
"preLaunchTask": "build webapi", | |||
"program": "${workspaceFolder}/item.webapi/bin/Debug/netcoreapp3.1/item.webapi.dll", | |||
"args": [], | |||
"cwd": "${workspaceFolder}/item.webapi", | |||
"stopAtEntry": false, | |||
"serverReadyAction": { | |||
"action": "openExternally", | |||
"pattern": "\\bNow listening on:\\s+(https?://\\S+)" | |||
}, | |||
"env": { | |||
"ASPNETCORE_ENVIRONMENT": "Development" | |||
}, | |||
"sourceFileMap": { | |||
"/Views": "${workspaceFolder}/Views" | |||
} | |||
} | |||
], | |||
"compounds": [ | |||
{ | |||
"name": "blazor / webapi", | |||
"configurations": [ | |||
"blazor", | |||
"webapi" | |||
], | |||
"preLaunchTask": "build all" | |||
} | |||
] | |||
} | |||
</filebox> | |||
= [https://docs.microsoft.com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-3.1 Hosting models] = | = [https://docs.microsoft.com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-3.1 Hosting models] = | ||
Ligne 81 : | Ligne 195 : | ||
</filebox> | </filebox> | ||
= | = Code-behind = | ||
== Code in the | == Code in the blazor page == | ||
<filebox fn='MyPage.razor'> | <filebox fn='MyPage.razor'> | ||
@page "/mypage" | @page "/mypage" | ||
Ligne 96 : | Ligne 210 : | ||
</filebox> | </filebox> | ||
== Code | == Code in a partial class == | ||
<filebox fn='MyPage.razor'> | |||
@page "/mypage" | |||
<h1>Title</h1> | |||
</filebox> | |||
<filebox fn='MyPage.razor.cs'> | |||
public partial class MyPage : ComponentBase | |||
{ | |||
[Inject] | |||
private IDataService DataService { get; set; } | |||
private Data data; | |||
// some C# code | |||
} | |||
</filebox> | |||
== Code in an inherited class == | |||
<filebox fn='MyPage.razor'> | <filebox fn='MyPage.razor'> | ||
@page "/mypage" | @page "/mypage" | ||
@inherits MyPageBase | @inherits MyPageBase | ||
<h1>Title</h1> | <h1>Title</h1> | ||
</filebox> | </filebox> | ||
Ligne 119 : | Ligne 252 : | ||
@page "/items" | @page "/items" | ||
@* url avec un parameter *@ | @* url avec un parameter *@ | ||
@page "/item/{ | @page "/item/{id:int}" | ||
@* attendre que la valeur soit chargée *@ | @* attendre que la valeur soit chargée *@ | ||
Ligne 131 : | Ligne 264 : | ||
@code { | @code { | ||
[Parameter] | [Parameter] | ||
public int | public int Id { get; set; } | ||
} | } | ||
</filebox> | </filebox> | ||
* [https://docs.microsoft.com/en-us/aspnet/core/blazor/routing#route-constraints Route constraints] | * [https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/routing?view=aspnetcore-3.1#route-parameters Route parameters] | ||
* [https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/routing?view=aspnetcore-3.1#route-constraints Route constraints] | |||
= [https://docs.microsoft.com/en-us/aspnet/core/blazor/components?view=aspnetcore-3.1 Components] = | = [https://docs.microsoft.com/en-us/aspnet/core/blazor/components?view=aspnetcore-3.1 Components] = | ||
Ligne 294 : | Ligne 428 : | ||
== Component == | == Component == | ||
<filebox fn='Pages/ | <filebox fn='Pages/MyComponent.razor.cs'> | ||
public class | public partial class MyComponent : ComponentBase | ||
{ | { | ||
// injection de IItemDataService (cette syntaxe n'est possible que dans les components) | // injection de IItemDataService (cette syntaxe n'est possible que dans les components) | ||
[Inject] | [Inject] | ||
private IItemDataService ItemDataService { get; set; } | |||
private IEnumerable<Item> Items { get; set; } | |||
protected override async Task OnInitializedAsync() | protected override async Task OnInitializedAsync() | ||
Ligne 308 : | Ligne 442 : | ||
} | } | ||
</filebox> | </filebox> | ||
= [https://docs.microsoft.com/en-us/aspnet/core/blazor/data-binding Data binding] = | |||
<kode lang='blazor'> | |||
<input type="date" @bind="@SelectedDate" /> | |||
<p class="@(IsImportant ? "text-alert" : "")">Text</p> | |||
@code { | |||
DateTime SelectedDate { get; set; } | |||
bool IsImportant { get; set; } | |||
} | |||
</kode> | |||
= [https://docs.microsoft.com/en-us/aspnet/core/blazor/event-handling Event handling] = | |||
<kode lang='blazor'> | |||
<input type="date" @oninput="@OnValueChangedAsync" /> | |||
@code { | |||
async Task OnValueChangedAsync(ChangeEventArgs args) | |||
{ | |||
var selectedDate = DateTime.Parse(args.Value.ToString()); | |||
await MyAsyncCall(selectedDate); | |||
} | |||
} | |||
</kode> | |||
= [https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/handle-errors?view=aspnetcore-3.1&pivots=server Exception handling] = | |||
{{info | Blazor treats most unhandled exceptions as fatal to the circuit where they occur.<br>If a circuit is terminated due to an unhandled exception, the user can only continue to interact with the app by reloading the page to create a new circuit.}} | |||
{{warn | There is no way to handle unhandled exceptions. Each exception must be catched and handled.}} | |||
= Form = | = Form = | ||
Ligne 315 : | Ligne 477 : | ||
@using Model | @using Model | ||
<EditForm Model="@item" | |||
<EditForm Model="@ | |||
OnValidSubmit="@HandleValidSubmitAsync" | OnValidSubmit="@HandleValidSubmitAsync" | ||
OnInvalidSubmit="@HandleInvalidSubmitAsync"> | OnInvalidSubmit="@HandleInvalidSubmitAsync"> | ||
Ligne 326 : | Ligne 486 : | ||
</filebox> | </filebox> | ||
<filebox fn=' | <filebox fn='EditItem.razor.cs'> | ||
[Parameter] | [Parameter] | ||
public int ItemId { get; set; } | public int ItemId { get; set; } | ||
[Inject] | [Inject] | ||
private IItemDataService ItemDataService { get; set; } | |||
private Item item = new Item(); | |||
protected override async Task OnInitializedAsync() | protected override async Task OnInitializedAsync() | ||
{ | { | ||
item = await ItemDataService.GetItemAsync(ItemId); | |||
} | } | ||
// when the submit button is clicked and the model is valid | // when the submit button is clicked and the model is valid | ||
private async Task HandleValidSubmitAsync() | |||
{ | { | ||
await ItemDataService.UpdateItemAsync( | await ItemDataService.UpdateItemAsync(item); | ||
} | } | ||
// when the submit button is clicked and the model is not valid | // when the submit button is clicked and the model is not valid | ||
private async Task HandleInvalidSubmitAsync() | |||
{ | { | ||
Ligne 353 : | Ligne 513 : | ||
</filebox> | </filebox> | ||
== Input Text == | == [https://github.com/dotnet/aspnetcore/blob/master/src/Components/Web/src/Forms/InputText.cs Input Text] == | ||
<kode lang='razor'> | <kode lang='razor'> | ||
<div class="form-group row"> | <div class="form-group row"> | ||
Ligne 363 : | Ligne 523 : | ||
<InputText id="name" | <InputText id="name" | ||
@bind-Value=" | @bind-Value="Item.Name" | ||
class="form-control col-sm-8" | class="form-control col-sm-8" | ||
placeholder="Enter name"> | placeholder="Enter name"> | ||
Ligne 379 : | Ligne 539 : | ||
<InputTextArea id="comment" | <InputTextArea id="comment" | ||
class="form-control col-sm-8" | class="form-control col-sm-8" | ||
@bind-Value=" | @bind-Value="Item.Comment" | ||
placeholder="Enter comment"> | placeholder="Enter comment"> | ||
</InputTextArea> | </InputTextArea> | ||
Ligne 399 : | Ligne 559 : | ||
</kode> | </kode> | ||
== Input Date == | == [https://github.com/dotnet/aspnetcore/blob/master/src/Components/Web/src/Forms/InputDate.cs Input Date] == | ||
<kode lang='razor'> | <kode lang='razor'> | ||
<div class="form-group row"> | <div class="form-group row"> | ||
Ligne 410 : | Ligne 570 : | ||
<InputDate id="date" | <InputDate id="date" | ||
class="form-control col-sm-8" | class="form-control col-sm-8" | ||
@bind-Value=" | @bind-Value="Item.Date" | ||
placeholder="Enter date"> | placeholder="Enter date"> | ||
</InputDate> | </InputDate> | ||
Ligne 417 : | Ligne 577 : | ||
== Input Select == | == Input Select == | ||
{{warn | Only support binding of type string.}} | |||
<kode lang='razor'> | <kode lang='razor'> | ||
<div class="form-group row"> | <div class="form-group row"> | ||
<label for="country" class="col-sm-3">Color: </label> | <label for="country" class="col-sm-3">Color: </label> | ||
<InputSelect id="color" | <InputSelect id="color" | ||
@bind-Value=" | @bind-Value="CountryId" | ||
class="form-control col-sm-8"> | class="form-control col-sm-8"> | ||
@foreach (var color in Enum.GetValues(typeof(RgbColor))) | @foreach (var color in Enum.GetValues(typeof(RgbColor))) | ||
Ligne 433 : | Ligne 594 : | ||
</div> | </div> | ||
</kode> | </kode> | ||
=== [https://chrissainty.com/building-custom-input-components-for-blazor-using-inputbase/ Build your custom InputSelect to handle binding with int] === | |||
<filebox fn='Components/DropDown.cs'> | |||
public sealed class DropDown<T> : InputSelect<T> | |||
{ | |||
protected override bool TryParseValueFromString(string value, out T result, out string validationErrorMessage) | |||
{ | |||
if (typeof(T) == typeof(string)) | |||
{ | |||
result = (T)(object)value; | |||
validationErrorMessage = null; | |||
return true; | |||
} | |||
else if (typeof(T) == typeof(int)) | |||
{ | |||
int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue); | |||
result = (T)(object)parsedValue; | |||
validationErrorMessage = null; | |||
return true; | |||
} | |||
else if (typeof(T).IsEnum) | |||
{ | |||
try | |||
{ | |||
result = (T)Enum.Parse(typeof(T), value); | |||
validationErrorMessage = null; | |||
return true; | |||
} | |||
catch (ArgumentException) | |||
{ | |||
result = default; | |||
validationErrorMessage = $"The {FieldIdentifier.FieldName} field is not valid."; | |||
return false; | |||
} | |||
} | |||
throw new InvalidOperationException($"{GetType()} does not support the type '{typeof(T)}'."); | |||
} | |||
} | |||
</filebox> | |||
<filebox fn='_Imports.razor'> | |||
@using MyNamespace.Components | |||
</filebox> | |||
=== Error InputSelect`1[System.Int32] does not support the type 'System.Int32' === | === Error InputSelect`1[System.Int32] does not support the type 'System.Int32' === | ||
To bind to an {{boxx|int}}, use {{boxx|select}} instead of {{boxx|InputSelect}}: | |||
<kode lang='razor'> | <kode lang='razor'> | ||
<div class="form-group row"> | <div class="form-group row"> | ||
Ligne 451 : | Ligne 660 : | ||
== Input CheckBox == | == Input CheckBox == | ||
{{warn | {{boxx|InputCheckbox}} requires a cascading parameter of type {{boxx|EditContext}}.<br> | |||
For example, you can use {{boxx|InputCheckbox}} inside an {{boxx|EditForm}}.}} | |||
<kode lang='razor'> | <kode lang='razor'> | ||
<div class="form-group row"> | <div class="form-group row"> | ||
Ligne 456 : | Ligne 667 : | ||
class=" offset-sm-3"> | class=" offset-sm-3"> | ||
<InputCheckbox id="smoker" | <InputCheckbox id="smoker" | ||
@bind-Value=" | @bind-Value="Employee.Smoker"> | ||
</InputCheckbox> | </InputCheckbox> | ||
Smoker | Smoker | ||
</label> | </label> | ||
</div> | </div> | ||
</kode> | |||
<kode lang='razor'> | |||
<label> | |||
<input type="checkbox" @onchange="(e) => FilterAsync(e)" /> | |||
Only items with ... | |||
</label> | |||
@code { | |||
private async Task FilterAsync(ChangeEventArgs e) | |||
{ | |||
if ((bool)e.Value) | |||
{ | |||
Items = await this.ItemClient.GetAsync(); | |||
} | |||
else | |||
{ | |||
Items = await this.ItemClient.GetAsync(); | |||
} | |||
} | |||
} | |||
</kode> | |||
== [https://blog.stevensanderson.com/2019/09/13/blazor-inputfile/ InputFile] == | |||
<kode lang='bash'> | |||
dotnet add package BlazorInputFile | |||
</kode> | |||
<filebox fn='Pages/_Host.cshtml'> | |||
<script src="_content/BlazorInputFile/inputfile.js"></script> | |||
</body> | |||
</filebox> | |||
<filebox fn='_Imports.razor'> | |||
@using BlazorInputFile | |||
</filebox> | |||
<kode lang='blazor'> | |||
<InputFile OnChange="HandleSelectionAsync" /> | |||
<p>@OutputMessages</p> | |||
@code { | |||
async Task HandleSelectionAsync(IFileListEntry[] files) | |||
{ | |||
var file = files.FirstOrDefault(); | |||
if (file != null) | |||
{ | |||
OutputMessages += $"Loading {file.Name}"; | |||
string jsonContent; | |||
using (var streamReader = new StreamReader(file.Data)) | |||
{ | |||
jsonContent = await streamReader.ReadToEndAsync(); | |||
} | |||
var importedItems = JsonConvert.DeserializeObject<IReadOnlyCollection<ItemDto>>(jsonContent); | |||
OutputMessages += $"<br>{importedItems.Count} items extracted"; | |||
} | |||
} | |||
} | |||
</kode> | |||
* [https://github.com/SteveSandersonMS/BlazorInputFile InputFile] on GitHub | |||
== Align multiple inputs == | |||
<kode lang='razor'> | |||
<EditForm Model="@myModel"> | |||
<table> | |||
<tbody> | |||
<tr> | |||
<td><label for="login">Login: </label></td> | |||
<td> | |||
<InputText id="login" | |||
@bind-Value="myModel.Login" | |||
class="form-control" | |||
placeholder="Lgin"> | |||
</InputText> | |||
</td> | |||
</tr> | |||
<tr> | |||
<td><label for="password">Password: </label></td> | |||
<td> | |||
<InputText id="password" | |||
@bind-Value="myModel.Password" | |||
class="form-control" | |||
placeholder="Password"> | |||
</InputText> | |||
</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
<div class="mt-3"> | |||
<button class="btn btn-primary btn-sm" @onclick="Save">Save</button> | |||
<button class="btn btn-secondary btn-sm" @onclick="Cancel">Cancel</button> | |||
</div> | |||
</EditForm> | |||
</kode> | </kode> | ||
Ligne 479 : | Ligne 788 : | ||
<div class="form-group row"> | <div class="form-group row"> | ||
<label for="name" class="col-sm-3">Name: </label> | <label for="name" class="col-sm-3">Name: </label> | ||
<InputText id="name" @bind-value=" | <InputText id="name" @bind-value="Item.Name" class="form-control col-sm-8" placeholder="Enter name"></InputText>> | ||
@* affiche le message d'erreur de validation *@ | @* affiche le message d'erreur de validation *@ | ||
<ValidationMessage class="offset-sm-3 col-sm-8" For="@(() => Item.Name)" /> | <ValidationMessage class="offset-sm-3 col-sm-8" For="@(() => Item.Name)" /> | ||
Ligne 488 : | Ligne 797 : | ||
</filebox> | </filebox> | ||
== [https://github.com/blazored/FluentValidation Fluent validation] == | == [https://github.com/blazored/FluentValidation Fluent validation on client] == | ||
* [[Fluent_validation|Fluent validation]] | |||
<kode lang='bash'> | <kode lang='bash'> | ||
dotnet add package Blazored.FluentValidation | dotnet add package Blazored.FluentValidation | ||
Ligne 501 : | Ligne 811 : | ||
<FluentValidationValidator /> | <FluentValidationValidator /> | ||
<ValidationSummary /> | <ValidationSummary /> | ||
</filebox> | |||
=== [https://github.com/blazored/FluentValidation#finding-validators Finding Validators] === | |||
By default, the component will check for validators registered with DI first.<br> | |||
If it can't find any, it will then try scanning the applications assemblies to find validators using reflection.<br> | |||
Use {{boxx|DisableAssemblyScanning}} to load validators from DI only. | |||
<kode lang='xaml'> | |||
<FluentValidationValidator DisableAssemblyScanning="@true" /> | |||
</kode> | |||
<filebox fn='Startup.cs'> | |||
public void ConfigureServices(IServiceCollection services) | |||
{ | |||
services.AddTransient<IValidator<ItemDto>, ItemDtoValidator>(); | |||
</filebox> | |||
== [https://docs.microsoft.com/en-us/aspnet/core/blazor/forms-validation?view=aspnetcore-3.1#server-validation Display server validation errors] == | |||
<filebox fn='BusinessLogicValidator.cs' collapsed> | |||
public class BusinessLogicValidator : ComponentBase | |||
{ | |||
private ValidationMessageStore messageStore; | |||
[CascadingParameter] | |||
private EditContext CurrentEditContext { get; set; } | |||
protected override void OnInitialized() | |||
{ | |||
if (CurrentEditContext == null) | |||
{ | |||
throw new InvalidOperationException( | |||
$"{nameof(BusinessLogicValidator)} requires a cascading " + | |||
$"parameter of type {nameof(EditContext)}. " + | |||
$"For example, you can use {nameof(BusinessLogicValidator)} " + | |||
$"inside an {nameof(EditForm)}."); | |||
} | |||
messageStore = new ValidationMessageStore(CurrentEditContext); | |||
CurrentEditContext.OnValidationRequested += (s, e) => messageStore.Clear(); | |||
CurrentEditContext.OnFieldChanged += (s, e) => messageStore.Clear(e.FieldIdentifier); | |||
} | |||
public void DisplayErrors(IDictionary<string, string[]> errors) | |||
{ | |||
foreach (var err in errors) | |||
{ | |||
messageStore.Add(CurrentEditContext.Field(err.Key), err.Value); | |||
} | |||
CurrentEditContext.NotifyValidationStateChanged(); | |||
} | |||
public void ClearErrors() | |||
{ | |||
messageStore.Clear(); | |||
CurrentEditContext.NotifyValidationStateChanged(); | |||
} | |||
} | |||
</filebox> | |||
<filebox fn='ItemEdit.razor'> | |||
<EditForm Model="@item" | |||
OnValidSubmit="@HandleValidSubmitAsync"> | |||
<BusinessLogicValidator @ref="businessLogicValidator" /> | |||
</filebox> | |||
<filebox fn='ItemEdit.razor.cs'> | |||
private async Task HandleValidSubmitAsync() | |||
{ | |||
businessLogicValidator.ClearErrors(); | |||
var result = await ItemClient.CreateAsync(itemViewModel); | |||
if (!result.IsSuccess) | |||
{ | |||
businessLogicValidator.DisplayErrors(result.Errors); | |||
} | |||
</filebox> | |||
<filebox fn='CallResult.cs' collapsed> | |||
public sealed class CallResult | |||
{ | |||
public string SuccessMessage { get; } | |||
public IDictionary<string, string[]> Errors { get; } | |||
public bool IsSuccess => Errors.Count == 0; | |||
public CallResult(IDictionary<string, string[]> errors, string successMessage) | |||
{ | |||
this.SuccessMessage = successMessage; | |||
this.Errors = errors; | |||
} | |||
} | |||
</filebox> | |||
<filebox fn='ItemClient.cs' collapsed> | |||
public async Task<CallResult> CreateAsync(ItemViewModel itemViewModel) | |||
{ | |||
// ... | |||
var response = await this.httpClient.PostAsync(uri, createItemDtoJson); | |||
var errors = await ExtractErrorsAsync(response); | |||
// ... | |||
return new CallResult(errors, successMessage); | |||
} | |||
private async Task<IDictionary<string, string[]>> ExtractErrorsAsync(HttpResponseMessage response) | |||
{ | |||
IDictionary<string, string[]> errors = new Dictionary<string, string[]>(); | |||
if (!response.IsSuccessStatusCode) | |||
{ | |||
switch (response.StatusCode) | |||
{ | |||
case HttpStatusCode.BadRequest: | |||
var content = await response.Content.ReadAsStreamAsync(); | |||
var validationProblemDetails = | |||
await JsonSerializer.DeserializeAsync<ValidationProblemDetails>(content); | |||
errors = validationProblemDetails.Errors; | |||
break; | |||
case HttpStatusCode.NotFound: | |||
var id = await response.Content.ReadAsAsync<int>(); | |||
errors.Add("", new[] { $"Item id {id} not found." }); | |||
break; | |||
case HttpStatusCode.InternalServerError: | |||
errors.Add("", new[] { "Internal server error. Expense has not been added." }); | |||
break; | |||
default: | |||
errors.Add("", new[] { "Unknown error. Item has not been added." }); | |||
break; | |||
} | |||
} | |||
return errors; | |||
} | |||
</filebox> | </filebox> | ||
Ligne 523 : | Ligne 970 : | ||
} | } | ||
</filebox> | </filebox> | ||
= [https://docs.microsoft.com/en-us/aspnet/core/blazor/components/cascading-values-and-parameters?view=aspnetcore-3.1 Cascading values and parameters] = | |||
= Static files = | = Static files = | ||
Ligne 549 : | Ligne 998 : | ||
@import url('font-awesome/css/all.min.css'); | @import url('font-awesome/css/all.min.css'); | ||
</filebox> | </filebox> | ||
= [https://docs.microsoft.com/en-us/aspnet/core/data/ef-mvc/sort-filter-page#add-paging-links Pagination] = | |||
<kode lang='blazor'> | |||
<a @onclick="@PreviousPage" class="@(isPreviousPageAllowed ? "" : "disabled")"> | |||
<i class="fa fa-caret-square-left"></i> | |||
</a> | |||
<a @onclick="@NextPage" class="@(isNextPageAllowed ? "" : "disabled")"> | |||
<i class="fa fa-caret-square-right"></i> | |||
</a> | |||
Page @currentPageIndex | |||
@code { | |||
var currentPageIndex = 1; | |||
var isPreviousPageAllowed => currentPageIndex > 1; | |||
var isNextPageAllowed => AuditTrailLogs.Count == 10; | |||
async Task PreviousPage() | |||
{ | |||
if (IsPreviousPageAllowed) | |||
{ | |||
Items = await this.ItemClient.GetAsync(--currentPageIndex); | |||
} | |||
} | |||
async Task NextPage() | |||
{ | |||
if (IsNextPageAllowed) | |||
{ | |||
Items = await this.ItemClient.GetAsync(++currentPageIndex); | |||
} | |||
} | |||
} | |||
</kode> | |||
= [https://github.com/Blazored/Toast Toast message] = | = [https://github.com/Blazored/Toast Toast message] = | ||
Ligne 568 : | Ligne 1 050 : | ||
<filebox fn='Pages/_Host.cshtml'> | <filebox fn='Pages/_Host.cshtml'> | ||
<head> | <head> | ||
<link href="_content/Blazored.Toast/blazored-toast.css" rel="stylesheet" /> | <link href="_content/Blazored.Toast/blazored-toast.min.css" rel="stylesheet" /> | ||
</filebox> | </filebox> | ||
Ligne 577 : | Ligne 1 059 : | ||
Position (Default: ToastPosition.TopRight) | Position (Default: ToastPosition.TopRight) | ||
Timeout (Default: 5) | Timeout (Default: 5) | ||
IconType (Default: IconType.FontAwesome) | |||
SuccessClass: add css class to success toast | SuccessClass: add css class to success toast | ||
SuccessIcon: add css class to success toast icon | |||
ErrorIcon: add css class to error toast icon | |||
*@ | *@ | ||
<BlazoredToasts Position="ToastPosition.BottomRight" | <BlazoredToasts Position="ToastPosition.BottomRight" | ||
Timeout="10" | Timeout="10" | ||
IconType="IconType.FontAwesome" | |||
SuccessClass="success-toast-override" | SuccessClass="success-toast-override" | ||
SuccessIcon="fa fa-thumbs-up" | |||
ErrorIcon="fa fa-bug" /> | |||
</filebox> | </filebox> | ||
Ligne 594 : | Ligne 1 078 : | ||
this.ToastService.ShowSuccess("Success message", "Success title"); | this.ToastService.ShowSuccess("Success message", "Success title"); | ||
this.ToastService.ShowError("Error message", "Error title"); | this.ToastService.ShowError("Error message", "Error title"); | ||
</filebox> | |||
= [https://github.com/Blazored/Modal Modal popup] = | |||
* [https://github.com/Blazored/Modal/tree/main/samples/BlazorServer Samples] | |||
== Installation == | |||
<kode lang='bash'> | |||
dotnet add package Blazored.Modal | |||
</kode> | |||
<filebox fn='Startup.cs'> | |||
public void ConfigureServices(IServiceCollection services) | |||
{ | |||
services.AddBlazoredModal(); | |||
</filebox> | |||
<filebox fn='_Imports.razor'> | |||
@using Blazored.Modal | |||
@using Blazored.Modal.Services | |||
</filebox> | |||
<filebox fn='App.razor'> | |||
<CascadingBlazoredModal> | |||
<Router AppAssembly="typeof(Program).Assembly"> | |||
@* ... *@ | |||
</Router> | |||
</CascadingBlazoredModal> | |||
</filebox> | |||
<filebox fn='Pages/_Host.cshtml'> | |||
<link rel="stylesheet" href="_content/Blazored.Modal/blazored-modal.css" /> | |||
<link rel="stylesheet" href="css/themes/blazored-modal-dark-theme.css" /> | |||
<script src="_content/Blazored.Modal/blazored.modal.js"></script> | |||
</filebox> | |||
<filebox fn='css/themes/blazored-modal-dark-theme.css'> | |||
.blazored-modal { | |||
background-color: rgb(68, 68, 68); | |||
border: 1px solid black; | |||
box-shadow: 0 2px 2px rgba(0, 0, 0, .25); | |||
} | |||
</filebox> | |||
== Usage == | |||
<filebox fn='Pages/MyPageBase.cs'> | |||
[CascadingParameter] | |||
private IModalService Modal { get; set; } | |||
parameters.Add("MyProperty", value); | |||
Modal.Show<MyPopup>("Title", parameters); | |||
</filebox> | |||
<filebox fn='Pages/MyPopup.razor'> | |||
@inherits MyPopupBase | |||
<div> | |||
@MyProperty | |||
</div> | |||
</filebox> | |||
<filebox fn='Pages/MyPopupBase.cs'> | |||
public class MyPopupBase : ComponentBase | |||
{ | |||
[Parameter] | |||
public string MyProperty { get; set; } | |||
} | |||
</filebox> | |||
= [https://github.com/mariusmuntean/ChartJs.Blazor ChartJs.Blazor v1.1] = | |||
* [[Chart.js]] | |||
* [https://github.com/mariusmuntean/ChartJs.Blazor/blob/master/ChartJs.Blazor.Samples/Client/Pages/Charts/Bar/Vertical.razor Bar exemple] | |||
<kode lang='bash'> | |||
dotnet add package ChartJs.Blazor | |||
</kode> | |||
<filebox fn='Pages/_Hosts.cshtml'> | |||
<head> | |||
<link rel="stylesheet" href="_content/ChartJs.Blazor/ChartJSBlazor.css" /> | |||
<body> | |||
<script src="_content/ChartJs.Blazor/moment-with-locales.min.js" type="text/javascript" language="javascript"></script> | |||
<script src="_content/ChartJs.Blazor/Chart.min.js" type="text/javascript" language="javascript"></script> | |||
<script src="_content/ChartJs.Blazor/ChartJsBlazorInterop.js" type="text/javascript" language="javascript"></script> | |||
</filebox> | |||
<filebox fn='_Imports.razor'> | |||
@using ChartJs.Blazor; | |||
</filebox> | |||
<filebox fn='MyBarChart.razor'> | |||
@page "/my-bar-chart" | |||
@inherits MyBarChartBase | |||
@using ChartJs.Blazor.Charts | |||
@using ChartJs.Blazor.ChartJS.PieChart | |||
@using ChartJs.Blazor.ChartJS.Common.Properties | |||
@using ChartJs.Blazor.Util | |||
<ChartJsPieChart @ref="chart" Config="@config" Width="600" Height="300" /> | |||
</filebox> | |||
<filebox fn='MyBarChartBase.cs' collapsed> | |||
public class MyChartBase : ComponentBase | |||
{ | |||
protected BarConfig config; | |||
protected ChartJsBarChart chart; | |||
protected override void OnInitialized() | |||
{ | |||
config = new BarConfig | |||
{ | |||
Options = new BarOptions | |||
{ | |||
Title = new OptionsTitle | |||
{ | |||
Display = true, | |||
Text = "Sample chart from Blazor", | |||
FontColor = "white" | |||
}, | |||
Legend = new Legend | |||
{ | |||
Labels = new LegendLabelConfiguration | |||
{ | |||
FontColor = "white" | |||
} | |||
}, | |||
Scales = new BarScales | |||
{ | |||
XAxes = new List<CartesianAxis> | |||
{ | |||
new BarCategoryAxis | |||
{ | |||
Ticks = new CategoryTicks | |||
{ | |||
FontColor = "white" | |||
}, | |||
GridLines = new GridLines | |||
{ | |||
Display = false | |||
} | |||
} | |||
}, | |||
YAxes = new List<CartesianAxis> | |||
{ | |||
new BarLinearCartesianAxis | |||
{ | |||
Ticks = new LinearCartesianTicks | |||
{ | |||
FontColor = "white" | |||
}, | |||
GridLines = new GridLines | |||
{ | |||
Color = "DimGray" | |||
} | |||
} | |||
} | |||
} | |||
} | |||
}; | |||
} | |||
protected override Task OnInitializedAsync() | |||
{ | |||
var data = await DataClient.GetAsync(); | |||
var dataset = new BarDataset<DoubleWrapper> | |||
{ | |||
Label = "Amounts", | |||
BackgroundColor = Enumerable.Repeat(ColorUtil.RandomColorString(), data.Count).ToArray(), | |||
BorderWidth = 0, // default value | |||
BorderColor = "#ffffff", | |||
HoverBackgroundColor = ColorUtil.FromDrawingColor(Color.DodgerBlue), | |||
HoverBorderColor = ColorUtil.RandomColorString(), | |||
HoverBorderWidth = 1 | |||
}; | |||
var amounts = data.Select(x => (double)x.Amount); | |||
dataset.AddRange(amounts.Wrap()); | |||
var labels = data.Select(x => x.Month.ToString("MMM yyyy", CultureInfo.CurrentCulture)); | |||
config.Data.Labels.AddRange(labels); | |||
config.Data.Datasets.Add(dataset); | |||
await chart.Update(); | |||
} | |||
} | |||
</filebox> | </filebox> | ||
= Javascript interop = | = Javascript interop = | ||
== [https://docs.microsoft.com/en-us/aspnet/core/blazor/call-javascript-from-dotnet Call JS from .NET] == | |||
<filebox fn='Pages/MyPageBase.cs'> | <filebox fn='Pages/MyPageBase.cs'> | ||
[Inject] | [Inject] | ||
public IJSRuntime JSRuntime { get; set; } | public IJSRuntime JSRuntime { get; set; } | ||
var result = await JSRuntime.InvokeAsync< | await JSRuntime.InvokeVoidAsync("MyJSMethod", "arg1"); | ||
var result = await JSRuntime.InvokeAsync<string>("AddExclamationPoint", "arg1"); | |||
</filebox> | </filebox> | ||
<filebox fn='wwwroot/js/ | <filebox fn='wwwroot/js/site.js'> | ||
window.MyJsMethod = function(arg1) { | |||
alert(arg1); | |||
} | |||
window.AddExclamationPoint = function(arg1) { | |||
return arg1 + ' !'; | |||
} | |||
</filebox> | </filebox> | ||
<filebox fn='Pages/_Host.cshtml'> | <filebox fn='Pages/_Host.cshtml'> | ||
< | <body> | ||
< | @* à la fin de body *@ | ||
</ | <script src="js/site.js"></script> | ||
</body> | |||
</filebox> | |||
== [https://docs.microsoft.com/en-us/aspnet/core/blazor/call-dotnet-from-javascript Call .NET from JS] == | |||
<filebox fn='wwwroot/js/site.js'> | |||
window.UpdateButtonContent = function (dotnetHelper, newContent) { | |||
dotnetHelper.invokeMethodAsync('UpdateButtonContent', newContent); | |||
dotnetHelper.dispose(); | |||
}; | |||
</filebox> | |||
<filebox fn='Pages/MyPageBase.cs'> | |||
var objRef = DotNetObjectReference.Create(this); | |||
await JSRuntime.InvokeVoidAsync("UpdateButtonContentJS", objRef, newContent); | |||
objRef.Dispose(); | |||
[JSInvokable] | |||
public void UpdateButtonContent(string newContent) | |||
{ | |||
ButtonContent = newContent; | |||
} | |||
</filebox> | |||
<filebox fn='Pages/_Host.cshtml'> | |||
<body> | <body> | ||
@* à la fin de body *@ | |||
<script src=" | <script src="js/myscript.js"></script> | ||
</body> | </body> | ||
</filebox> | </filebox> | ||
<filebox fn=' | = [https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization?view=aspnetcore-3.1#localization-middleware Localization / Langue / Culture] = | ||
@ | <filebox fn='Startup.cs'> | ||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) | |||
{ | |||
var supportedCultures = new[] { "fr-FR" }; | |||
var localizationOptions = new RequestLocalizationOptions().SetDefaultCulture(supportedCultures[0]) | |||
.AddSupportedCultures(supportedCultures) | |||
.AddSupportedUICultures(supportedCultures); | |||
</filebox> | |||
= [https://stackoverflow.com/questions/58492389/how-to-make-a-html-text-multiline-using-a-c-sharp-bind-in-a-blazor-project Multi lines text] = | |||
<kode lang='blazor'> | |||
<p style="white-space: pre-wrap" >@message</p> | |||
@code { | |||
string message = "Line 1\nLine 2"; | |||
} | |||
</kode> | |||
= Sorting table = | |||
* [https://www.thecodehubs.com/sorting-table-in-blazor/ Sorting table in Blazor] | |||
* [https://exceptionnotfound.net/exploring-blazor-by-making-an-html-table-sortable-in-net-core/ Making an HTML table sortable] | |||
<filebox fn='MyTable.razor'> | |||
<table class="table table-striped"> | |||
<thead> | |||
<tr> | |||
<th class="text-center sortable-th" @onclick="@(() => SortItems("Description"))"> | |||
Description <span class="fa @(GetSortIconName("Description"))"></span> | |||
</th> | |||
</filebox> | </filebox> | ||
<filebox fn='MyTableBase.cs'> | |||
private string sortedColumnName; | |||
private bool isSortedAscending; | |||
protected void SortItems(string propertyName) | |||
{ | |||
var sortInAscendingOrder = propertyName != sortedColumnName ? false : !isSortedAscending; | |||
Items = OrderBy(Items, propertyName, sortInAscendingOrder); | |||
isSortedAscending = sortInAscendingOrder; | |||
sortedColumnName = propertyName; | |||
} | |||
protected string GetSortIconName(string propertyName) | |||
=> sortedColumnName != propertyName ? | |||
string.Empty : | |||
isSortedAscending ? "fa-long-arrow-alt-down" : "fa-long-arrow-alt-up"; | |||
private static IEnumerable<T> OrderBy<T>( | |||
IEnumerable<T> source, | |||
string propertyName, | |||
bool ascendingOrder) | |||
{ | |||
var propertyDescriptor = TypeDescriptor.GetProperties(typeof(T)) | |||
.Find(propertyName, false); | |||
return ascendingOrder ? | |||
source.OrderBy(x => propertyDescriptor.GetValue(x)) : | |||
source.OrderByDescending(x => propertyDescriptor.GetValue(x)); | |||
} | |||
</filebox> | |||
<kode lang='css'> | |||
th.sortable-th { | |||
cursor:pointer; | |||
} | |||
</kode> | |||
= [https://docs.microsoft.com/en-us/aspnet/core/blazor/handle-errors Handle errors] = | = [https://docs.microsoft.com/en-us/aspnet/core/blazor/handle-errors Handle errors] = | ||
* Unhandle exceptions crash the application ans there is no way to catch them. | |||
* Unhandle exceptions are logged in the journal. | |||
= Get caller ip address = | |||
== [https://stackoverflow.com/questions/59538318/how-to-use-the-httpcontext-object-in-server-side-blazor-to-retrieve-information Get the ip in the Host then pass it as a cascading value] == | |||
{{info | This is a workaround so it works with kestrel and apache as a reverse proxy}} | |||
<filebox fn='Pages/_Host.cshtml.cs'> | |||
public class HostModel : PageModel | |||
{ | |||
public string IPAddress { get; set; } | |||
public void OnGet() | |||
{ | |||
this.IPAddress = this.HttpContext.Connection.RemoteIpAddress.ToString(); | |||
} | |||
} | |||
</filebox> | |||
<filebox fn='Pages/_Host.cshtml'> | |||
@model HostModel | |||
<body> | |||
<app> | |||
<component type="typeof(App)" render-mode="ServerPrerendered" param-IPAddress="@Model.IPAddress" /> | |||
</app> | |||
</filebox> | |||
<filebox fn='App.razor'> | |||
<CascadingBlazoredModal> | |||
<CascadingValue Name="IPAddress" Value="@IPAddress"> | |||
<Router AppAssembly="@typeof(Program).Assembly"> | |||
@code | |||
{ | |||
[Parameter] | |||
public string IPAddress { get; set; } | |||
} | |||
</filebox> | |||
<filebox fn='MyPage.razor.cs'> | |||
public partial class MyPage : ComponentBase | |||
{ | |||
[CascadingParameter(Name = "IPAddress")] | |||
public string IPAddress { get; set; } | |||
</filebox> | |||
== [https://stackoverflow.com/questions/59469742/get-user-agent-and-ip-in-blazor-server-side-app Get caller remote ip] == | |||
{{warn | {{boxx|httpContextAccessor.HttpContext}} is null when deployed behind a reverse proxy (kestrel + apache).}} | |||
<filebox fn='Startup.cs'> | |||
public void ConfigureServices(IServiceCollection services) | |||
{ | |||
services.AddHttpContextAccessor(); | |||
</filebox> | |||
<filebox fn='MyPage.razor.cs'> | |||
[Inject] | |||
public IHttpContextAccessor httpContextAccessor { get; set; } | |||
protected override void OnInitialized() | |||
{ | |||
var userIp = httpContextAccessor.HttpContext.Connection?.RemoteIpAddress.ToString(); | |||
</filebox> | |||
= [https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/logging?view=aspnetcore-3.1&pivots=server Log] = | |||
<filebox fn='appsettings.json'> | |||
// display detailed exception message in the browser console | |||
"DetailedErrors": true | |||
</filebox> | |||
<filebox fn='MyPage.razor.cs'> | |||
[Inject] | |||
ILogger<MyPage> logger { get; set; } | |||
// log into the journal | |||
logger.LogWarning("Warn message"); | |||
</filebox> | |||
<filebox fn='MyPage.razor.cs'> | |||
[Inject] | |||
private IJSRuntime JsRuntime { get; set; } | |||
// log into the browser console log | |||
await JsRuntime.InvokeAsync<string>("console.log", "Message !"); | |||
</filebox> | |||
= [https://docs.microsoft.com/en-us/aspnet/core/blazor/security/?view=aspnetcore-3.1 Authentication and authorization] = | |||
* [https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-5.0&tabs=netcore-cli Introduction to Identity on ASP.NET Core] | |||
* [https://docs.microsoft.com/en-us/aspnet/core/security/authentication/customize-identity-model?view=aspnetcore-5.0 Identity model customization in ASP.NET Core] | |||
<filebox fn='App.razor'> | |||
@* replace RouteView by AuthorizeRouteView *@ | |||
<AuthorizeRouteView RouteData="@routeData" | |||
DefaultLayout="@typeof(MainLayout)" /> | |||
</filebox> | |||
<filebox fn='Shared/LoginDisplay.razor'> | |||
<AuthorizeView> | |||
<Authorized> | |||
<p>@context.User.Identity.Name</p> | |||
</Authorized> | |||
<NotAuthorized> | |||
<a href="login">Log in</a> | |||
</NotAuthorized> | |||
</AuthorizeView> | |||
</filebox> | |||
<filebox fn='Startup.cs'> | |||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) | |||
{ | |||
app.UseAuthentication(); | |||
app.UseAuthorization(); | |||
app.UseEndpoints(endpoints => {}); | |||
} | |||
public void ConfigureServices(IServiceCollection services) | |||
{ | |||
services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true) | |||
.AddEntityFrameworkStores<ApplicationDbContext>(); | |||
services.AddRazorPages(); | |||
services.AddServerSideBlazor(); | |||
} | |||
</filebox> | |||
== [https://docs.microsoft.com/en-us/aspnet/core/security/authentication/scaffold-identity?view=aspnetcore-3.1&tabs=netcore-cli#scaffold-identity-into-a-blazor-server-project-with-authorization Scaffold Identity] == | |||
<kode lang='bash'> | |||
dotnet tool install -g dotnet-aspnet-codegenerator | |||
dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design --version 3.1.3 | |||
dotnet add package Microsoft.EntityFrameworkCore.Design --version 3.1.3 | |||
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore --version 3.1.3 | |||
dotnet add package Microsoft.AspNetCore.Identity.UI --version 3.1.3 | |||
dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 3.1.3 | |||
dotnet add package Microsoft.EntityFrameworkCore.Tools --version 3.1.3 | |||
# help | |||
dotnet aspnet-codegenerator identity -h | |||
# list the files that can be scaffolded | |||
dotnet aspnet-codegenerator identity -lf | |||
# scaffold | |||
dotnet aspnet-codegenerator identity -dc MyApplication.Data.ApplicationDbContext --files "Account.Login" | |||
# create the page Area/Identity/Pages/Account/Login.cshtml | |||
</kode> | |||
* [https://medium.com/@nohorse/adding-a-custom-login-page-to-blazor-server-app-3d725a463927 Adding a custom login page to Blazor Server] | |||
== [https://chrissainty.com/securing-your-blazor-apps-authentication-with-clientside-blazor-using-webapi-aspnet-core-identity/ Authenticate with web api] == | |||
== [https://blazorhelpwebsite.com/ViewBlogPost/36 Cookies x] == | |||
* [https://docs.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-3.1 Use cookie authentication without ASP.NET Core Identity] |
Dernière version du 15 août 2021 à 14:23
Liens
Additional libraries
Blazorise | components, bootstrap, font-awesome |
Radzen Blazor Components | components |
Blazored | components |
BlazorStrap, Bootstrap 4 Components for Blazor Framework | bootstrap |
Blazor Extensions | |
Awesome Blazor | projects |
Ant Blazor Design | components |
Description
- Développement frontend avec C#
- Intégration des bibliothèques .NET existantes (nuget)
WebAssembly
Permet d'exécuter du bytecode (code intermediaire) dans le navigateur grâce à la javascript runtime sandbox.
WebAssembly est nativement présent dans les navigateurs moderne.
WebAssembly possède un runtime .NET (mono.wasm), ce qui permet d'exécuter des assemblies .NET dans le navigateur.
Créer une application
# Blazor server dotnet new blazorserver --no-https -o [project-name] cd [project-name] dotnet run # Blazor WebAssembly dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.1.0-preview4.19579.2 dotnet new blazorwasm -o blazorwasm cd blazorwasm dotnet run |
Solution Blazor / Webapi
# create item/item.sln dotnet new sln -o item cd item dotnet new blazorserver --no-https -o item.blazor dotnet new webapi --no-https -o item.webapi dotnet sln add item.blazor/item.blazor.csproj dotnet sln add item.webapi/item.webapi.csproj |
item/.vscode/tasks.json |
{ "version": "2.0.0", "tasks": [ { "label": "build blazor", "command": "dotnet", "type": "process", "args": [ "build", "${workspaceFolder}/item.blazor/item.blazor.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" }, { "label": "build webapi", "command": "dotnet", "type": "process", "args": [ "build", "${workspaceFolder}/item.webapi/item.webapi.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" }, { "label": "build all", "dependsOn": [ "build blazor", "build webapi" ], "group": "build", "problemMatcher": [] } ] } |
item/.vscode/launch.json |
{ "version": "0.2.0", "configurations": [ { "name": "blazor", "type": "coreclr", "request": "launch", "preLaunchTask": "build blazor", "program": "${workspaceFolder}/item.blazor/bin/Debug/netcoreapp3.1/item.blazor.dll", "args": [], "cwd": "${workspaceFolder}/item.blazor", "stopAtEntry": false, "serverReadyAction": { "action": "openExternally", "pattern": "\\bNow listening on:\\s+(https?://\\S+)" }, "env": { "ASPNETCORE_ENVIRONMENT": "Development" }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" } }, { "name": "webapi", "type": "coreclr", "request": "launch", "preLaunchTask": "build webapi", "program": "${workspaceFolder}/item.webapi/bin/Debug/netcoreapp3.1/item.webapi.dll", "args": [], "cwd": "${workspaceFolder}/item.webapi", "stopAtEntry": false, "serverReadyAction": { "action": "openExternally", "pattern": "\\bNow listening on:\\s+(https?://\\S+)" }, "env": { "ASPNETCORE_ENVIRONMENT": "Development" }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" } } ], "compounds": [ { "name": "blazor / webapi", "configurations": [ "blazor", "webapi" ], "preLaunchTask": "build all" } ] } |
Hosting models
Blazor WebAssembly
L'application est téléchargée et exécutée dans le navigateur.
- Excellentes performances
- Pas besoin d'un serveur web ASP.NET Core pour héberger l'application
- Permet une utilisation offline une fois chargée
- Le temps de chargement est long, il faut télécharger l'application, ses dépendances et le .NET runtime
- Les identifiants pour les accès aux bdd et web API sont chargés par chaque client
Blazor Server
L'application est exécutée sur le serveur. UI updates, event handling et les appels JavaScript sont pris en charge grâce à une connexion SignalR.
- Pas besoin de WebAssembly
- Chaque interaction utilisateur nécessite un appel au serveur
- Un serveur web ASP.NET Core est nécessaire pour héberger l'application
Blazor server application
C'est une ASP.NET Core application.
Arborescence de fichiers
- Program.cs: point d'entrée, appelle Startup.cs
- Startup.cs: configuration (DI)
- Pages: Components
- Shared: shared Components comme les layouts (main, menu)
- App.razor: routing
- _Imports.razor: import namespaces
- wwwroot: static files (css, js)
- Data: service to access data
Debug
Startup.cs |
public void ConfigureServices(IServiceCollection services) { // avoir des erreurs détaillées services.AddServerSideBlazor() .AddCircuitOptions(options => { options.DetailedErrors = true; }); |
Code-behind
Code in the blazor page
MyPage.razor |
@page "/mypage" @inject IDataService DataService <h1>Title</h1> @code { private Data data; // some C# code } |
Code in a partial class
MyPage.razor |
@page "/mypage" <h1>Title</h1> |
MyPage.razor.cs |
public partial class MyPage : ComponentBase { [Inject] private IDataService DataService { get; set; } private Data data; // some C# code } |
Code in an inherited class
MyPage.razor |
@page "/mypage" @inherits MyPageBase <h1>Title</h1> |
MyPageBase.cs |
public class MyPageBase : ComponentBase { [Inject] private IDataService DataService { get; set; } protected Data data; // some C# code } |
Pages
Pages/Items.razor |
@* url *@ @page "/items" @* url avec un parameter *@ @page "/item/{id:int}" @* attendre que la valeur soit chargée *@ @if (Items == null) { <p><em>Loading...</em></p> } else { /* ... */ } @code { [Parameter] public int Id { get; set; } } |
Components
Components/MyComponent.razor |
Components/MyComponentBase.cs |
[Parameter] public EventCallback<bool> MyEventCallback { get; set; } // invoke asynchronously callback await MyEventCallback.InvokeAsync(true); |
_Imports.razor |
@* Make components available *@ @using BlazorServerApp.Components |
Pages/MyPage.razor |
<MyComponent @ref="MyComponent" MyEventCallback="@MyComponent_OnSomethingHappened"></MyComponent> |
Pages/MyPageBase.cs |
protected MyComponentBase MyComponent { get; set; } public async void MyComponent_OnSomethingHappened() { // refresh the DOM if elements have changed StateHasChanged(); } |
Lifecycle methods
- OnInitialized OnInitializedAsync
- OnParametersSet OnParametersSetAsync
- OnAfterRender OnAfterRenderAsync
Layout
App.razor |
<Router AppAssembly="@typeof(Program).Assembly"> <Found Context="routeData"> @* définit le layout par défaut *@ <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> |
Shared/MainLayout.razor |
@inherits LayoutComponentBase <div class="sidebar"> <NavMenu /> </div> <div class="main"> <div class="content px-4"> @Body </div> </div> |
Data Service
Dependency Injection
Startup.cs |
public void ConfigureServices(IServiceCollection services) { // configuration du container de DI services.AddHttpClient<IItemDataService, ItemDataService>( client => client.BaseAddress = new Uri("http://localhost:5002") ); |
Data Service
Services/IItemDataService.cs |
public interface IItemDataService { Task<IEnumerable<Item>> GetAllItemsAsync(); Task<Item> GetItemAsync(int itemId); Task UpdateItemAsync(Item item); Task DeleteItemAsync(int itemId); Task<Item> AddItemAsync(Item item); } |
Services/ItemDataService.cs |
public class ItemDataService : IItemDataService { private readonly HttpClient _httpClient; // injection de HttpClient public ItemDataService(HttpClient httpClient) { _httpClient = httpClient; } public async Task<IEnumerable<Item>> GetAllItemsAsync() { return await JsonSerializer.DeserializeAsync<IEnumerable<Item>>( await _httpClient.GetStreamAsync($"api/item"), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); } public async Task<Item> GetItemAsync(int itemId) { return await JsonSerializer.DeserializeAsync<Item>( await _httpClient.GetStreamAsync($"api/item/{itemId}"), new JsonSerializerOptions() { PropertyNameCaseInsensitive = true }); } public async Task UpdateItemAsync(Item item) { var itemJson = new StringContent( JsonSerializer.Serialize(item), Encoding.UTF8, "application/json"); await _httpClient.PutAsync("api/item", itemJson); } public async Task DeleteItemAsync(int itemId) { await _httpClient.DeleteAsync($"item/{itemId}"); } public async Task<Item> AddItemAsync(Item item) { var itemJson = new StringContent( JsonSerializer.Serialize(item), Encoding.UTF8, "application/json"); var response = await _httpClient.PostAsync("item", itemJson); if (response.IsSuccessStatusCode) { return await JsonSerializer.DeserializeAsync<Item>( await response.Content.ReadAsStreamAsync()); } return null; } |
Component
Pages/MyComponent.razor.cs |
public partial class MyComponent : ComponentBase { // injection de IItemDataService (cette syntaxe n'est possible que dans les components) [Inject] private IItemDataService ItemDataService { get; set; } private IEnumerable<Item> Items { get; set; } protected override async Task OnInitializedAsync() { Items = await ItemDataService.GetAllItemsAsync(); } |
Data binding
Fichier:Blazor.svg | <input type="date" @bind="@SelectedDate" /> <p class="@(IsImportant ? "text-alert" : "")">Text</p> @code { DateTime SelectedDate { get; set; } bool IsImportant { get; set; } } |
Event handling
Fichier:Blazor.svg | <input type="date" @oninput="@OnValueChangedAsync" /> @code { async Task OnValueChangedAsync(ChangeEventArgs args) { var selectedDate = DateTime.Parse(args.Value.ToString()); await MyAsyncCall(selectedDate); } } |
Exception handling
Blazor treats most unhandled exceptions as fatal to the circuit where they occur. If a circuit is terminated due to an unhandled exception, the user can only continue to interact with the app by reloading the page to create a new circuit. |
There is no way to handle unhandled exceptions. Each exception must be catched and handled. |
Form
EditItem.razor |
@page "/edititem/{ItemId:int}" @* to access to Model.Item *@ @using Model <EditForm Model="@item" OnValidSubmit="@HandleValidSubmitAsync" OnInvalidSubmit="@HandleInvalidSubmitAsync"> <button type="submit">Save</button> <button @onclick="Cancel">Cancel</button> </EditForm> |
EditItem.razor.cs |
[Parameter] public int ItemId { get; set; } [Inject] private IItemDataService ItemDataService { get; set; } private Item item = new Item(); protected override async Task OnInitializedAsync() { item = await ItemDataService.GetItemAsync(ItemId); } // when the submit button is clicked and the model is valid private async Task HandleValidSubmitAsync() { await ItemDataService.UpdateItemAsync(item); } // when the submit button is clicked and the model is not valid private async Task HandleInvalidSubmitAsync() { } |
Input Text
<div class="form-group row"> <label for="name" class="col-sm-3"> Item: </label> <InputText id="name" @bind-Value="Item.Name" class="form-control col-sm-8" placeholder="Enter name"> </InputText> </div> |
Input Text Area
<div class="form-group row"> <label for="comment" class="col-sm-3"> Comment: </label> <InputTextArea id="comment" class="form-control col-sm-8" @bind-Value="Item.Comment" placeholder="Enter comment"> </InputTextArea> </div> |
Input Number
<div class="form-group row"> <label for="price" class="col-sm-3">Price: </label> <InputNumber id="price" @bind-Value="@Item.Price" class="form-control col-sm-8"> </InputNumber> </div> |
Input Date
<div class="form-group row"> <label for="date" class="col-sm-3"> Date: </label> <InputDate id="date" class="form-control col-sm-8" @bind-Value="Item.Date" placeholder="Enter date"> </InputDate> </div> |
Input Select
Only support binding of type string. |
<div class="form-group row"> <label for="country" class="col-sm-3">Color: </label> <InputSelect id="color" @bind-Value="CountryId" class="form-control col-sm-8"> @foreach (var color in Enum.GetValues(typeof(RgbColor))) { <option value="@color">@color</option> } <option value="@RgbColor.Red">@RgbColor.Red</option> <option value="@RgbColor.Green">@RgbColor.Green</option> <option value="@RgbColor.Blue">@RgbColor.Blue</option> </InputSelect> </div> |
Build your custom InputSelect to handle binding with int
Components/DropDown.cs |
public sealed class DropDown<T> : InputSelect<T> { protected override bool TryParseValueFromString(string value, out T result, out string validationErrorMessage) { if (typeof(T) == typeof(string)) { result = (T)(object)value; validationErrorMessage = null; return true; } else if (typeof(T) == typeof(int)) { int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue); result = (T)(object)parsedValue; validationErrorMessage = null; return true; } else if (typeof(T).IsEnum) { try { result = (T)Enum.Parse(typeof(T), value); validationErrorMessage = null; return true; } catch (ArgumentException) { result = default; validationErrorMessage = $"The {FieldIdentifier.FieldName} field is not valid."; return false; } } throw new InvalidOperationException($"{GetType()} does not support the type '{typeof(T)}'."); } } |
_Imports.razor |
@using MyNamespace.Components |
Error InputSelect`1[System.Int32] does not support the type 'System.Int32'
To bind to an int, use select instead of InputSelect:
<div class="form-group row"> <label for="group" class="col-sm-3">Color: </label> <select id="group" @bind="@User.Group.Id" class="form-control col-sm-8"> @foreach (var availableGroup in AvailableGroups) { <option value="@availableGroup.Id">@availableGroup.Name</option> } </select> </div> |
Input CheckBox
InputCheckbox requires a cascading parameter of type EditContext. For example, you can use InputCheckbox inside an EditForm. |
<div class="form-group row"> <label for="smoker" class=" offset-sm-3"> <InputCheckbox id="smoker" @bind-Value="Employee.Smoker"> </InputCheckbox> Smoker </label> </div> |
<label> <input type="checkbox" @onchange="(e) => FilterAsync(e)" /> Only items with ... </label> @code { private async Task FilterAsync(ChangeEventArgs e) { if ((bool)e.Value) { Items = await this.ItemClient.GetAsync(); } else { Items = await this.ItemClient.GetAsync(); } } } |
InputFile
dotnet add package BlazorInputFile |
Pages/_Host.cshtml |
<script src="_content/BlazorInputFile/inputfile.js"></script> </body> |
_Imports.razor |
@using BlazorInputFile |
Fichier:Blazor.svg | <InputFile OnChange="HandleSelectionAsync" /> <p>@OutputMessages</p> @code { async Task HandleSelectionAsync(IFileListEntry[] files) { var file = files.FirstOrDefault(); if (file != null) { OutputMessages += $"Loading {file.Name}"; string jsonContent; using (var streamReader = new StreamReader(file.Data)) { jsonContent = await streamReader.ReadToEndAsync(); } var importedItems = JsonConvert.DeserializeObject<IReadOnlyCollection<ItemDto>>(jsonContent); OutputMessages += $"<br>{importedItems.Count} items extracted"; } } } |
- InputFile on GitHub
Align multiple inputs
<EditForm Model="@myModel"> <table> <tbody> <tr> <td><label for="login">Login: </label></td> <td> <InputText id="login" @bind-Value="myModel.Login" class="form-control" placeholder="Lgin"> </InputText> </td> </tr> <tr> <td><label for="password">Password: </label></td> <td> <InputText id="password" @bind-Value="myModel.Password" class="form-control" placeholder="Password"> </InputText> </td> </tr> </tbody> </table> <div class="mt-3"> <button class="btn btn-primary btn-sm" @onclick="Save">Save</button> <button class="btn btn-secondary btn-sm" @onclick="Cancel">Cancel</button> </div> </EditForm> |
Form validation
Item.cs |
// nécessite le package nuget System.ComponentModel.Annotations [Required] [StringLength(50, ErrorMessage = "Name too long (max 50 char)")] public string Name { get; set; } |
Pages/ItemEdit.razor |
<EditForm Model="@Item"> <DataAnnotationsValidator /> @* affiche la liste des messages d'erreur de validation *@ <ValidationSummary /> <div class="form-group row"> <label for="name" class="col-sm-3">Name: </label> <InputText id="name" @bind-value="Item.Name" class="form-control col-sm-8" placeholder="Enter name"></InputText>> @* affiche le message d'erreur de validation *@ <ValidationMessage class="offset-sm-3 col-sm-8" For="@(() => Item.Name)" /> </div> <button type="submit">Save</button> <button type="button" @onclick="Cancel">Cancel</button> |
Fluent validation on client
dotnet add package Blazored.FluentValidation |
_Imports.razor |
@using Blazored.FluentValidation |
Pages/EditItem.razor |
<EditForm Model="@Item"> <FluentValidationValidator /> <ValidationSummary /> |
Finding Validators
By default, the component will check for validators registered with DI first.
If it can't find any, it will then try scanning the applications assemblies to find validators using reflection.
Use DisableAssemblyScanning to load validators from DI only.
<FluentValidationValidator DisableAssemblyScanning="@true" /> |
Startup.cs |
public void ConfigureServices(IServiceCollection services) { services.AddTransient<IValidator<ItemDto>, ItemDtoValidator>(); |
Display server validation errors
BusinessLogicValidator.cs |
public class BusinessLogicValidator : ComponentBase { private ValidationMessageStore messageStore; [CascadingParameter] private EditContext CurrentEditContext { get; set; } protected override void OnInitialized() { if (CurrentEditContext == null) { throw new InvalidOperationException( $"{nameof(BusinessLogicValidator)} requires a cascading " + $"parameter of type {nameof(EditContext)}. " + $"For example, you can use {nameof(BusinessLogicValidator)} " + $"inside an {nameof(EditForm)}."); } messageStore = new ValidationMessageStore(CurrentEditContext); CurrentEditContext.OnValidationRequested += (s, e) => messageStore.Clear(); CurrentEditContext.OnFieldChanged += (s, e) => messageStore.Clear(e.FieldIdentifier); } public void DisplayErrors(IDictionary<string, string[]> errors) { foreach (var err in errors) { messageStore.Add(CurrentEditContext.Field(err.Key), err.Value); } CurrentEditContext.NotifyValidationStateChanged(); } public void ClearErrors() { messageStore.Clear(); CurrentEditContext.NotifyValidationStateChanged(); } } |
ItemEdit.razor |
<EditForm Model="@item" OnValidSubmit="@HandleValidSubmitAsync"> <BusinessLogicValidator @ref="businessLogicValidator" /> |
ItemEdit.razor.cs |
private async Task HandleValidSubmitAsync() { businessLogicValidator.ClearErrors(); var result = await ItemClient.CreateAsync(itemViewModel); if (!result.IsSuccess) { businessLogicValidator.DisplayErrors(result.Errors); } |
CallResult.cs |
public sealed class CallResult { public string SuccessMessage { get; } public IDictionary<string, string[]> Errors { get; } public bool IsSuccess => Errors.Count == 0; public CallResult(IDictionary<string, string[]> errors, string successMessage) { this.SuccessMessage = successMessage; this.Errors = errors; } } |
ItemClient.cs |
public async Task<CallResult> CreateAsync(ItemViewModel itemViewModel) { // ... var response = await this.httpClient.PostAsync(uri, createItemDtoJson); var errors = await ExtractErrorsAsync(response); // ... return new CallResult(errors, successMessage); } private async Task<IDictionary<string, string[]>> ExtractErrorsAsync(HttpResponseMessage response) { IDictionary<string, string[]> errors = new Dictionary<string, string[]>(); if (!response.IsSuccessStatusCode) { switch (response.StatusCode) { case HttpStatusCode.BadRequest: var content = await response.Content.ReadAsStreamAsync(); var validationProblemDetails = await JsonSerializer.DeserializeAsync<ValidationProblemDetails>(content); errors = validationProblemDetails.Errors; break; case HttpStatusCode.NotFound: var id = await response.Content.ReadAsAsync<int>(); errors.Add("", new[] { $"Item id {id} not found." }); break; case HttpStatusCode.InternalServerError: errors.Add("", new[] { "Internal server error. Expense has not been added." }); break; default: errors.Add("", new[] { "Unknown error. Item has not been added." }); break; } } return errors; } |
MyPage.razor |
<a @onclick="@NavigateToHome">Home</a> <a @onclick="@(() => NavigateToEdit(item.Id)">Home</a> |
MyPageBase.cs |
[Inject] public NavigationManager NavigationManager { get; set; } protected void NavigateToHome() { NavigationManager.NavigateTo("home"); } protected void NavigateToEdit(int itemId) { NavigationManager.NavigateTo($"edit/{itemId}"); } |
Cascading values and parameters
Static files
Pages/_Host.cshtml |
<head> <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" /> <link href="css/site.css" rel="stylesheet" /> |
wwwroot/css/site.css |
/* custom css */ /* import Iconic font */ @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); |
- wwwroot/favicon.ico
Font-Awesome
- wwwroot/css/font-awesome
- css/all.min.css
- webfonts/*
- LICENSE.txt
wwwroot/css/site.css |
@import url('font-awesome/css/all.min.css'); |
Pagination
Fichier:Blazor.svg | <a @onclick="@PreviousPage" class="@(isPreviousPageAllowed ? "" : "disabled")"> <i class="fa fa-caret-square-left"></i> </a> <a @onclick="@NextPage" class="@(isNextPageAllowed ? "" : "disabled")"> <i class="fa fa-caret-square-right"></i> </a> Page @currentPageIndex @code { var currentPageIndex = 1; var isPreviousPageAllowed => currentPageIndex > 1; var isNextPageAllowed => AuditTrailLogs.Count == 10; async Task PreviousPage() { if (IsPreviousPageAllowed) { Items = await this.ItemClient.GetAsync(--currentPageIndex); } } async Task NextPage() { if (IsNextPageAllowed) { Items = await this.ItemClient.GetAsync(++currentPageIndex); } } } |
Toast message
dotnet add package Blazored.Toast |
Startup.cs |
public void ConfigureServices(IServiceCollection services) { services.AddBlazoredToast(); |
_Imports.razor |
@using Blazored.Toast @using Blazored.Toast.Services |
Pages/_Host.cshtml |
<head> <link href="_content/Blazored.Toast/blazored-toast.min.css" rel="stylesheet" /> |
Shared/MainLayout.razor |
@using Blazored.Toast.Configuration @* Position (Default: ToastPosition.TopRight) Timeout (Default: 5) IconType (Default: IconType.FontAwesome) SuccessClass: add css class to success toast SuccessIcon: add css class to success toast icon ErrorIcon: add css class to error toast icon *@ <BlazoredToasts Position="ToastPosition.BottomRight" Timeout="10" IconType="IconType.FontAwesome" SuccessClass="success-toast-override" SuccessIcon="fa fa-thumbs-up" ErrorIcon="fa fa-bug" /> |
Pages/MyPageBase.cs |
[Inject] public IToastService ToastService { get; set; } this.ToastService.ShowSuccess("Success message", "Success title"); this.ToastService.ShowError("Error message", "Error title"); |
Modal popup
Installation
dotnet add package Blazored.Modal |
Startup.cs |
public void ConfigureServices(IServiceCollection services) { services.AddBlazoredModal(); |
_Imports.razor |
@using Blazored.Modal @using Blazored.Modal.Services |
App.razor |
<CascadingBlazoredModal> <Router AppAssembly="typeof(Program).Assembly"> @* ... *@ </Router> </CascadingBlazoredModal> |
Pages/_Host.cshtml |
<link rel="stylesheet" href="_content/Blazored.Modal/blazored-modal.css" /> <link rel="stylesheet" href="css/themes/blazored-modal-dark-theme.css" /> <script src="_content/Blazored.Modal/blazored.modal.js"></script> |
css/themes/blazored-modal-dark-theme.css |
.blazored-modal { background-color: rgb(68, 68, 68); border: 1px solid black; box-shadow: 0 2px 2px rgba(0, 0, 0, .25); } |
Usage
Pages/MyPageBase.cs |
[CascadingParameter] private IModalService Modal { get; set; } parameters.Add("MyProperty", value); Modal.Show<MyPopup>("Title", parameters); |
Pages/MyPopup.razor |
@inherits MyPopupBase <div> @MyProperty </div> |
Pages/MyPopupBase.cs |
public class MyPopupBase : ComponentBase { [Parameter] public string MyProperty { get; set; } } |
ChartJs.Blazor v1.1
dotnet add package ChartJs.Blazor |
Pages/_Hosts.cshtml |
<head> <link rel="stylesheet" href="_content/ChartJs.Blazor/ChartJSBlazor.css" /> <body> <script src="_content/ChartJs.Blazor/moment-with-locales.min.js" type="text/javascript" language="javascript"></script> <script src="_content/ChartJs.Blazor/Chart.min.js" type="text/javascript" language="javascript"></script> <script src="_content/ChartJs.Blazor/ChartJsBlazorInterop.js" type="text/javascript" language="javascript"></script> |
_Imports.razor |
@using ChartJs.Blazor; |
MyBarChart.razor |
@page "/my-bar-chart" @inherits MyBarChartBase @using ChartJs.Blazor.Charts @using ChartJs.Blazor.ChartJS.PieChart @using ChartJs.Blazor.ChartJS.Common.Properties @using ChartJs.Blazor.Util <ChartJsPieChart @ref="chart" Config="@config" Width="600" Height="300" /> |
MyBarChartBase.cs |
public class MyChartBase : ComponentBase { protected BarConfig config; protected ChartJsBarChart chart; protected override void OnInitialized() { config = new BarConfig { Options = new BarOptions { Title = new OptionsTitle { Display = true, Text = "Sample chart from Blazor", FontColor = "white" }, Legend = new Legend { Labels = new LegendLabelConfiguration { FontColor = "white" } }, Scales = new BarScales { XAxes = new List<CartesianAxis> { new BarCategoryAxis { Ticks = new CategoryTicks { FontColor = "white" }, GridLines = new GridLines { Display = false } } }, YAxes = new List<CartesianAxis> { new BarLinearCartesianAxis { Ticks = new LinearCartesianTicks { FontColor = "white" }, GridLines = new GridLines { Color = "DimGray" } } } } } }; } protected override Task OnInitializedAsync() { var data = await DataClient.GetAsync(); var dataset = new BarDataset<DoubleWrapper> { Label = "Amounts", BackgroundColor = Enumerable.Repeat(ColorUtil.RandomColorString(), data.Count).ToArray(), BorderWidth = 0, // default value BorderColor = "#ffffff", HoverBackgroundColor = ColorUtil.FromDrawingColor(Color.DodgerBlue), HoverBorderColor = ColorUtil.RandomColorString(), HoverBorderWidth = 1 }; var amounts = data.Select(x => (double)x.Amount); dataset.AddRange(amounts.Wrap()); var labels = data.Select(x => x.Month.ToString("MMM yyyy", CultureInfo.CurrentCulture)); config.Data.Labels.AddRange(labels); config.Data.Datasets.Add(dataset); await chart.Update(); } } |
Javascript interop
Call JS from .NET
Pages/MyPageBase.cs |
[Inject] public IJSRuntime JSRuntime { get; set; } await JSRuntime.InvokeVoidAsync("MyJSMethod", "arg1"); var result = await JSRuntime.InvokeAsync<string>("AddExclamationPoint", "arg1"); |
wwwroot/js/site.js |
window.MyJsMethod = function(arg1) { alert(arg1); } window.AddExclamationPoint = function(arg1) { return arg1 + ' !'; } |
Pages/_Host.cshtml |
<body> @* à la fin de body *@ <script src="js/site.js"></script> </body> |
Call .NET from JS
wwwroot/js/site.js |
window.UpdateButtonContent = function (dotnetHelper, newContent) { dotnetHelper.invokeMethodAsync('UpdateButtonContent', newContent); dotnetHelper.dispose(); }; |
Pages/MyPageBase.cs |
var objRef = DotNetObjectReference.Create(this); await JSRuntime.InvokeVoidAsync("UpdateButtonContentJS", objRef, newContent); objRef.Dispose(); [JSInvokable] public void UpdateButtonContent(string newContent) { ButtonContent = newContent; } |
Pages/_Host.cshtml |
<body> @* à la fin de body *@ <script src="js/myscript.js"></script> </body> |
Localization / Langue / Culture
Startup.cs |
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) { var supportedCultures = new[] { "fr-FR" }; var localizationOptions = new RequestLocalizationOptions().SetDefaultCulture(supportedCultures[0]) .AddSupportedCultures(supportedCultures) .AddSupportedUICultures(supportedCultures); |
Multi lines text
Fichier:Blazor.svg | <p style="white-space: pre-wrap" >@message</p> @code { string message = "Line 1\nLine 2"; } |
Sorting table
MyTable.razor |
<table class="table table-striped"> <thead> <tr> <th class="text-center sortable-th" @onclick="@(() => SortItems("Description"))"> Description <span class="fa @(GetSortIconName("Description"))"></span> </th> |
MyTableBase.cs |
private string sortedColumnName; private bool isSortedAscending; protected void SortItems(string propertyName) { var sortInAscendingOrder = propertyName != sortedColumnName ? false : !isSortedAscending; Items = OrderBy(Items, propertyName, sortInAscendingOrder); isSortedAscending = sortInAscendingOrder; sortedColumnName = propertyName; } protected string GetSortIconName(string propertyName) => sortedColumnName != propertyName ? string.Empty : isSortedAscending ? "fa-long-arrow-alt-down" : "fa-long-arrow-alt-up"; private static IEnumerable<T> OrderBy<T>( IEnumerable<T> source, string propertyName, bool ascendingOrder) { var propertyDescriptor = TypeDescriptor.GetProperties(typeof(T)) .Find(propertyName, false); return ascendingOrder ? source.OrderBy(x => propertyDescriptor.GetValue(x)) : source.OrderByDescending(x => propertyDescriptor.GetValue(x)); } |
th.sortable-th { cursor:pointer; } |
Handle errors
- Unhandle exceptions crash the application ans there is no way to catch them.
- Unhandle exceptions are logged in the journal.
Get caller ip address
Get the ip in the Host then pass it as a cascading value
This is a workaround so it works with kestrel and apache as a reverse proxy |
Pages/_Host.cshtml.cs |
public class HostModel : PageModel { public string IPAddress { get; set; } public void OnGet() { this.IPAddress = this.HttpContext.Connection.RemoteIpAddress.ToString(); } } |
Pages/_Host.cshtml |
@model HostModel <body> <app> <component type="typeof(App)" render-mode="ServerPrerendered" param-IPAddress="@Model.IPAddress" /> </app> |
App.razor |
<CascadingBlazoredModal> <CascadingValue Name="IPAddress" Value="@IPAddress"> <Router AppAssembly="@typeof(Program).Assembly"> @code { [Parameter] public string IPAddress { get; set; } } |
MyPage.razor.cs |
public partial class MyPage : ComponentBase { [CascadingParameter(Name = "IPAddress")] public string IPAddress { get; set; } |
Get caller remote ip
httpContextAccessor.HttpContext is null when deployed behind a reverse proxy (kestrel + apache). |
Startup.cs |
public void ConfigureServices(IServiceCollection services) { services.AddHttpContextAccessor(); |
MyPage.razor.cs |
[Inject] public IHttpContextAccessor httpContextAccessor { get; set; } protected override void OnInitialized() { var userIp = httpContextAccessor.HttpContext.Connection?.RemoteIpAddress.ToString(); |
Log
appsettings.json |
// display detailed exception message in the browser console "DetailedErrors": true |
MyPage.razor.cs |
[Inject] ILogger<MyPage> logger { get; set; } // log into the journal logger.LogWarning("Warn message"); |
MyPage.razor.cs |
[Inject] private IJSRuntime JsRuntime { get; set; } // log into the browser console log await JsRuntime.InvokeAsync<string>("console.log", "Message !"); |
Authentication and authorization
App.razor |
@* replace RouteView by AuthorizeRouteView *@ <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /> |
Shared/LoginDisplay.razor |
<AuthorizeView> <Authorized> <p>@context.User.Identity.Name</p> </Authorized> <NotAuthorized> <a href="login">Log in</a> </NotAuthorized> </AuthorizeView> |
Startup.cs |
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => {}); } public void ConfigureServices(IServiceCollection services) { services.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true) .AddEntityFrameworkStores<ApplicationDbContext>(); services.AddRazorPages(); services.AddServerSideBlazor(); } |
Scaffold Identity
dotnet tool install -g dotnet-aspnet-codegenerator dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design --version 3.1.3 dotnet add package Microsoft.EntityFrameworkCore.Design --version 3.1.3 dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore --version 3.1.3 dotnet add package Microsoft.AspNetCore.Identity.UI --version 3.1.3 dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 3.1.3 dotnet add package Microsoft.EntityFrameworkCore.Tools --version 3.1.3 # help dotnet aspnet-codegenerator identity -h # list the files that can be scaffolded dotnet aspnet-codegenerator identity -lf # scaffold dotnet aspnet-codegenerator identity -dc MyApplication.Data.ApplicationDbContext --files "Account.Login" # create the page Area/Identity/Pages/Account/Login.cshtml |