« Blazor .NET Core 3.1 » : différence entre les versions
De Banane Atomic
Aller à la navigationAller à la recherche
Ligne 43 : | Ligne 43 : | ||
dotnet run | dotnet run | ||
</kode> | </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 webapi --no-https -o item.webapi | |||
dotnet sln add item.blazor/item.blazor.csproj | |||
dotnet sln add item.webapi/item.webapi.csproj | |||
</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] = |
Version du 4 janvier 2021 à 20:29
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 |
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 in the same page vs Code-behind
Code in the same page
MyPage.razor |
@page "/mypage" @inject IDataService DataService <h1>Title</h1> @code { private Data data; // some C# code } |
Code-behind
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 ItemId { 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/MyComponentBase.cs |
public class MyComponentBase : ComponentBase { // injection de IItemDataService (cette syntaxe n'est possible que dans les components) [Inject] public IItemDataService ItemDataService { get; set; } public 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); } } |
Form
EditItem.razor |
@page "/edititem/{ItemId:int}" @* to access to Model.Item *@ @using Model @inherits EditItemBase <EditForm Model="@Item" OnValidSubmit="@HandleValidSubmitAsync" OnInvalidSubmit="@HandleInvalidSubmitAsync"> <button type="submit">Save</button> <button @onclick="Cancel">Cancel</button> </EditForm> |
EditItemBase.cs |
[Parameter] public int ItemId { get; set; } [Inject] public IItemDataService ItemDataService { get; set; } public Item Item { get; set; } = new Item(); protected override async Task OnInitializedAsync() { Item = await ItemDataService.GetItemAsync(ItemId); } // when the submit button is clicked and the model is valid protected async Task HandleValidSubmitAsync() { await ItemDataService.UpdateItemAsync(Item); } // when the submit button is clicked and the model is not valid protected 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
<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
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
dotnet add package Blazored.FluentValidation |
_Imports.razor |
@using Blazored.FluentValidation |
Pages/EditItem.razor |
<EditForm Model="@Item"> <FluentValidationValidator /> <ValidationSummary /> |
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}"); } |
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.css" rel="stylesheet" /> |
Shared/MainLayout.razor |
@using Blazored.Toast.Configuration @* Position (Default: ToastPosition.TopRight) Timeout (Default: 5) SuccessClass: add css class to success toast SuccessIconClass: add css class to success toast icon ErrorIconClass: add css class to error toast icon *@ <BlazoredToasts Position="ToastPosition.BottomRight" Timeout="10" SuccessClass="success-toast-override" SuccessIconClass="fa fa-thumbs-up" ErrorIconClass="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; } |