« Blazor ASP.NET Core 7.0 » : différence entre les versions
De Banane Atomic
Aller à la navigationAller à la recherche
Ligne 334 : | Ligne 334 : | ||
<script src="_framework/blazor.server.js"></script> | <script src="_framework/blazor.server.js"></script> | ||
</body> | </body> | ||
</filebox> | |||
== Bootstrap icons == | |||
<kode lang='bash'> | |||
# install the css and fonts only | |||
libman install bootstrap-icons@latest \ | |||
--files font/bootstrap-icons.min.css \ | |||
--files font/fonts/bootstrap-icons.woff \ | |||
--files font/fonts/bootstrap-icons.woff2 | |||
# default destination path: wwwroot/lib/bootstrap-icons | |||
</kode> | |||
<filebox fn='Pages/_Host.cshtml'> | |||
<head> | |||
<link rel="stylesheet" href="lib/bootstrap-icons/font/bootstrap-icons.min.css" /> | |||
<link href="css/site.css" rel="stylesheet" /> | |||
</filebox> | |||
<filebox fn='wwroot/css/site.css'> | |||
/* remove this line */ | |||
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); | |||
</filebox> | |||
{{info | Remove the folder {{boxx|wwroot/css/open-iconic}}}} | |||
<filebox fn='Shared/NavMenu.razor'> | |||
<NavLink class="nav-link" href="" Match="NavLinkMatch.All"> | |||
<i class="bi bi-house"></i> Home | |||
</NavLink> | |||
</filebox> | |||
<filebox fn='Shared/NavMenu.razor.css'> | |||
/* replace .oi by .bi */ | |||
.bi { | |||
width: 2rem; | |||
font-size: 1.1rem; | |||
vertical-align: text-top; | |||
top: -2px; | |||
} | |||
</filebox> | </filebox> | ||
Version du 28 mars 2023 à 14:34
Pages
Pages/Items.razor |
@page "/items" @* url *@ @page "/item/{id:int}" @* url avec un parameter *@ <PageTitle>Items</PageTitle> @if (Items == null) @* attendre que la valeur soit chargée *@ { <p><em>Loading...</em></p> } else { /* ... */ } |
Pages/Items.razor.cs |
public partial class Table : ComponentBase { private IReadOnlyCollection<ItemDto> items; [Parameter] public int Id { get; set; } protected override async Task OnInitializedAsync() { items = await this.ItemClient.GetAsync(); } protected override async Task OnAfterRenderAsync(bool firstRender) { } |
CSS isolation
Pages/Items.razor.css |
/* scoped css only for this page */ /* @import is not allowed in scoped css */ |
JS isolation
Pages/MyPage.razor.js |
export function myFunction() { } export function myFunctionWithArg(arg) { return 'ok'; } |
Pages/MyPage.razor.cs |
public partial class MyPage : ComponentBase, IAsyncDisposable { [Inject] private IJSRuntime JS { get; set; } = default!; private IJSObjectReference? module; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { module = await JS.InvokeAsync<IJSObjectReference>("import", "./Pages/MyPage.razor.js"); } } private async Task MyJsFunction() { if (module is not null) await module.InvokeVoidAsync("myFunction"); } private async ValueTask<string?> MyJsFunctionWithArg(string arg) { string result = null; if (module is not null) await module.InvokeAsync<string>("myFunctionWithArg", arg); return result; } async ValueTask IAsyncDisposable.DisposeAsync() { if (module is not null) await module.DisposeAsync(); } |
Form
EditItem.razor |
@page "/edititem/{ItemId:int}" <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 IItemService ItemService { get; set; } private Item item = null; 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 ItemService.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> |
Form validation
Data Annotation
Item.cs |
[Required, StringLength(50, MinimumLength = 3, ErrorMessage = "Name must be between 3 and 50 characters")] public string Name { get; set; } |
Pages/ItemEdit.razor |
<EditForm Model="@item"> <DataAnnotationsValidator /> <ValidationSummary /> @* affiche la liste des messages d'erreur de validation *@ <InputText @bind-value="item.Name" placeholder="Enter name" /> <ValidationMessage class="offset-sm-3 col-sm-8" For="@(() => Item.Name)" /> @* affiche le message d'erreur de validation *@ |
Data binding
One way
Changes made in the UI doesn't change the binded property.
The onchange event can be used to manually update the property.
Fichier:Blazor.svg | <input type="text" value="@Description" @onchange=@((ChangeEventArgs __e) => Description = __e.Value.ToString()) @onchange=OnInputChanged /> @code { string Description { get; set; } = "Text"; void OnInputChanged(ChangeEventArgs e) => Description = e.Value.ToString(); } |
Two way
Fichier:Blazor.svg | <input type="text" @bind="Description" /> @* by default uses the 'on change' event (focus lost) *@ <input type="text" @bind="Description" @bind:event="oninput" /> @* trigger the binding 'on input' event *@ @code { string Description { get; set; } = "Text"; } |
Form exemples
Deactivate the form while submitting
MyPage.razor |
<EditForm Model="@search" OnValidSubmit="@HandleValidSubmitAsync"> <DataAnnotationsValidator /> <fieldset disabled="@isSearching"> <div class="row justify-content-center"> <InputText @bind-Value="search.Text" class="col-sm-6" placeholder="Nom de table" /> <button type="submit" class="btn btn-primary col-auto"> @if (!isSearching) { <span class="oi oi-magnifying-glass" aria-hidden="true"></span> } else { <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> } </button> </div> <div class="row justify-content-center"> <ValidationMessage class="text-danger col-sm-6" For="@(() => search.Text)" /> </div> </fieldset> </EditForm> @if (isSearching) { <h5 class="text-center mt-5 mb-2">Loading <span class="spinner-border spinner-border-sm" role="status"></span> </h5> } else if (tables.Count > 0) { <h5 class="text-center mt-5 mb-2">Résultat de la recherche</h5> } else if (validSearchHasBeenStarted) { <h5 class="text-center mt-5 mb-2">Pas de résultat pour cette recherche</h5> } |
MyPage.razor.cs |
private bool isSearching { get; set; } = false; private bool validSearchHasBeenStarted { get; set; } = false; private async Task HandleValidSubmitAsync() { validSearchHasBeenStarted = true; isSearching = true; var data = await myClient.FetchDataAsync(search.Text); isSearching = false; } |
Dependency injection
Program.cs |
builder.Services.AddTransient<IMyService, MyService>(); |
Pages/MyPage.razor.cs |
[Inject] private IMyService myService { get; set; } |
Lifetime | Description |
---|---|
Singleton | creates a single instance of the service. All components requiring a Singleton service receive the same instance of the service. |
Transient | creates a new instance of the service on each component request. |
Scoped | creates a new instance of the service on each HTTP request but not across SignalR connection/circuit messages. Which means only when:
|
Entity Framework Core
Using a DbContext factory
DbContext isn't thread safe and isn't designed for concurrent use.
To handle multi-threads scenarios, use a AddDbContextFactory to create a DbContext for each query.
Program.cs |
builder.Services.AddDbContextFactory<MyDbContext>(); |
MyRepository.cs |
private readonly IDbContextFactory<MyDbContext> contextFactory; public DataSummaryRepository(IDbContextFactory<MyDbContext> contextFactory) { this.contextFactory = contextFactory; } public async Task<Data> FetchDataAsync() { // create a DbContext for this query, then dispose it once the query has been executed using var context = contextFactory.CreateDbContext(); } |
External JS libraries
LibMan library manager
# install the LibMan CLI dotnet tool install -g Microsoft.Web.LibraryManager.Cli # create a libman.js file libman init # available providers: cdnjs, jsdelivr, unpkg, filesystem libman install bootswatch@5.0.1 --files dist/darkly/bootstrap.min.css # uninstall bootstrap libman uninstall bootstrap@5.0.1 |
Bootstrap
# install only bootstrap.min.css and bootstrap.bundle.min.js (with popper) libman install bootstrap@latest \ --files dist/css/bootstrap.min.css \ --files dist/css/bootstrap.min.css.map \ --files dist/js/bootstrap.bundle.min.js \ --files dist/js/bootstrap.bundle.min.js.map # default destination path: wwwroot/lib/bootstrap |
Pages/_Host.cshtml |
<head> <!-- REPLACE <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" /> BY --> <link rel="stylesheet" href="lib/bootstrap/css/bootstrap.min.css" /> <link href="css/site.css" rel="stylesheet" /> <script src="lib/bootstrap/js/bootstrap.bundle.min.js"></script> <script src="_framework/blazor.server.js"></script> </body> |
Bootstrap icons
# install the css and fonts only libman install bootstrap-icons@latest \ --files font/bootstrap-icons.min.css \ --files font/fonts/bootstrap-icons.woff \ --files font/fonts/bootstrap-icons.woff2 # default destination path: wwwroot/lib/bootstrap-icons |
Pages/_Host.cshtml |
<head> <link rel="stylesheet" href="lib/bootstrap-icons/font/bootstrap-icons.min.css" /> <link href="css/site.css" rel="stylesheet" /> |
wwroot/css/site.css |
/* remove this line */ @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); |
Remove the folder wwroot/css/open-iconic |
Shared/NavMenu.razor |
<NavLink class="nav-link" href="" Match="NavLinkMatch.All"> <i class="bi bi-house"></i> Home </NavLink> |
Shared/NavMenu.razor.css |
/* replace .oi by .bi */ .bi { width: 2rem; font-size: 1.1rem; vertical-align: text-top; top: -2px; } |
Turn on detailed errors
Program.cs |
builder.Services.AddServerSideBlazor() .AddCircuitOptions(o => { o.DetailedErrors = true; o.DetailedErrors = builder.Environment.IsDevelopment(); // only add details when debugging }); |