« Blazor ASP.NET Core 7.0 » : différence entre les versions
De Banane Atomic
Aller à la navigationAller à la recherche
(20 versions intermédiaires par le même utilisateur non affichées) | |||
Ligne 5 : | Ligne 5 : | ||
* [https://github.com/dotnet-architecture/eShopOnBlazor Project example: eShopOnBlazor] | * [https://github.com/dotnet-architecture/eShopOnBlazor Project example: eShopOnBlazor] | ||
== Components | == Components libraries == | ||
* [[Radzen]] | * [[Radzen]] | ||
* [[MudBlazor]] | * [[MudBlazor]] | ||
== Useful librairies == | |||
* [https://github.com/Blazored/LocalStorage LocalStorage] | |||
= Pages = | = Pages = | ||
Ligne 45 : | Ligne 48 : | ||
== [https://learn.microsoft.com/en-us/aspnet/core/blazor/components/css-isolation?view=aspnetcore-7.0 CSS isolation] == | == [https://learn.microsoft.com/en-us/aspnet/core/blazor/components/css-isolation?view=aspnetcore-7.0 CSS isolation] == | ||
{{info | You may need of the {{boxx|::deep}} pseudo-element to modify the style of child components.}} | |||
<filebox fn='Pages/Items.razor.css'> | <filebox fn='Pages/Items.razor.css'> | ||
/* scoped css only for this page */ | /* scoped css only for this page */ | ||
/* @import is not allowed in scoped css */ | /* @import is not allowed in scoped css */ | ||
first-level-tag { } | |||
first-level-tag ::deep second-level-tag { } | |||
</filebox> | </filebox> | ||
Ligne 119 : | Ligne 125 : | ||
</a> | </a> | ||
</div> | </div> | ||
</kode> | |||
= foreach = | |||
<kode lang='razor'> | |||
<ul> | |||
@foreach (var item in items) | |||
{ | |||
<li> | |||
@item | |||
</li> | |||
} | |||
</ul> | |||
</kode> | </kode> | ||
Ligne 346 : | Ligne 364 : | ||
= [https://docs.microsoft.com/en-us/aspnet/core/blazor/components?view=aspnetcore-7.0 Components] = | = [https://docs.microsoft.com/en-us/aspnet/core/blazor/components?view=aspnetcore-7.0 Components] = | ||
<filebox fn='Components/MyComponent.razor'> | <filebox fn='Components/MyComponent.razor'> | ||
</filebox> | </filebox> | ||
<filebox fn='Components/ | <filebox fn='Components/MyComponent.cs'> | ||
[Parameter] | [Parameter] | ||
public EventCallback<bool> MyEventCallback { get; set; } | public EventCallback<bool> MyEventCallback { get; set; } | ||
Ligne 377 : | Ligne 394 : | ||
</filebox> | </filebox> | ||
== Lifecycle methods == | == [https://learn.microsoft.com/en-us/aspnet/core/blazor/components/lifecycle?view=aspnetcore-7.0#handle-incomplete-async-actions-at-render Lifecycle methods] == | ||
{{info | Due to pre-rendering in Blazor Server you can't perform any JS interop until the OnAfterRender lifecycle method.}} | |||
{| class="wikitable wtp" | |||
! Method | |||
! Start / End | |||
! Show | |||
|- | |||
| SetParametersAsync || Start || false | |||
|- | |||
| OnInitializedAsync || Start || false | |||
|- | |||
| OnAfterRenderAsync || Start || false | |||
|- | |||
| OnAfterRenderAsync || End || false | |||
|- | |||
| OnInitializedAsync || End || true | |||
|- | |||
| OnParametersSetAsync || Start || true | |||
|- | |||
| OnParametersSetAsync || End || true | |||
|- | |||
| OnAfterRenderAsync || Start || true | |||
|- | |||
| OnAfterRenderAsync || End || true | |||
|- | |||
| SetParametersAsync || End || true | |||
|} | |||
# {{boxx|OnInitialized}} {{boxx|OnInitializedAsync}} | # {{boxx|OnInitialized}} {{boxx|OnInitializedAsync}} | ||
# {{boxx|OnParametersSet}} {{boxx|OnParametersSetAsync}} | # {{boxx|OnParametersSet}} {{boxx|OnParametersSetAsync}} | ||
# {{boxx|OnAfterRender}} {{boxx|OnAfterRenderAsync}} | # {{boxx|OnAfterRender}} {{boxx|OnAfterRenderAsync}} | ||
== [https://learn.microsoft.com/en-us/aspnet/core/blazor/components/event-handling?view=aspnetcore-7.0#eventcallback EventCallback] == | |||
<filebox fn='ChildComponent.razor'> | |||
<button @onclick="OnClickCallback">Click</button> | |||
@code { | |||
[Parameter] | |||
public EventCallback<MouseEventArgs> OnClickCallback { get; set; } | |||
} | |||
</filebox> | |||
<filebox fn='Parent.razor'> | |||
@* The ShowMessage is passed to the ChildComponent | |||
When the button will be pressed in the ChildComponent, the ShowMessage will be triggered in the parent *@ | |||
<ChildComponent OnClickCallback="@ShowMessage"> | |||
Content of the child component is supplied by the parent component. | |||
</ChildComponent> | |||
@code { | |||
private void ShowMessage(MouseEventArgs e) | |||
{ } | |||
} | |||
</filebox> | |||
= Navigation = | = Navigation = | ||
Ligne 388 : | Ligne 455 : | ||
NavigationManager.NavigateTo("url"); | NavigationManager.NavigateTo("url"); | ||
// to implement redirection, use the OnInitializedAsync method otherwise you will get an NavigationException | |||
protected override async Task OnInitializedAsync() | |||
{ | |||
NavigationManager.NavigateTo("/item", true, true); | |||
await base.OnInitializedAsync(); | |||
} | |||
</kode> | </kode> | ||
* [https://stackoverflow.com/questions/58076758/navigationerror-on-navigateto NavigationException with NavigationManager called from OnInitialized] | |||
= [https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-7.0 Dependency injection] = | = [https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-7.0 Dependency injection] = | ||
Ligne 415 : | Ligne 490 : | ||
* The user selects the browser's reload/refresh button. | * The user selects the browser's reload/refresh button. | ||
|} | |} | ||
= [https://learn.microsoft.com/en-us/aspnet/core/blazor/globalization-localization?view=aspnetcore-7.0&pivots=server Localization / Langue / Culture] = | |||
<filebox fn='Program.cs'> | |||
builder.Services.AddLocalization(); | |||
app.UseRequestLocalization("fr-FR"); | |||
</filebox> | |||
= [https://learn.microsoft.com/en-us/aspnet/core/blazor/call-web-api?view=aspnetcore-7.0&pivots=server Call a web API from ASP.NET Core Blazor] = | = [https://learn.microsoft.com/en-us/aspnet/core/blazor/call-web-api?view=aspnetcore-7.0&pivots=server Call a web API from ASP.NET Core Blazor] = | ||
Ligne 474 : | Ligne 556 : | ||
{ | { | ||
// create a DbContext for this query, then dispose it once the query has been executed | // create a DbContext for this query, then dispose it once the query has been executed | ||
using var context = contextFactory. | using var context = await contextFactory.CreateDbContextAsync(); | ||
} | } | ||
</filebox> | </filebox> | ||
= [https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management?view=aspnetcore-7.0&pivots=server State management] = | |||
<kode lang='cs'> | |||
[Inject] | |||
public ProtectedSessionStorage ProtectedSessionStore { get; set; } | |||
protected override async Task OnAfterRenderAsync(bool firstRender) | |||
{ | |||
if (firstRender) | |||
{ | |||
await LoadStateAsync(); | |||
StateHasChanged(); | |||
} | |||
} | |||
private async Task LoadStateAsync() | |||
{ | |||
var fetchValueResult = await ProtectedSessionStore.GetAsync<string>("key"); | |||
var value = fetchValueResult.Success ? fetchValueResult.Value : null; | |||
} | |||
await ProtectedSessionStore.SetAsync("key", value); | |||
</kode> | |||
= External JS libraries = | = External JS libraries = | ||
Ligne 563 : | Ligne 668 : | ||
vertical-align: text-top; | vertical-align: text-top; | ||
top: -2px; | top: -2px; | ||
} | |||
</filebox> | |||
= [https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/handle-errors?view=aspnetcore-7.0 Handle exceptions] = | |||
<filebox fn='Shared/MainLayout.razor'> | |||
<ErrorBoundary @ref="errorBoundary"> | |||
<ChildContent> | |||
@Body | |||
</ChildContent> | |||
<ErrorContent Context="e"> | |||
@{ OnError(@e); } | |||
<MudAlert Severity="Severity.Error">An exception occured: @e.Message</MudAlert> | |||
</ErrorContent> | |||
</ErrorBoundary> | |||
@code { | |||
[Inject] | |||
private ILogger<MainLayout> Logger { get; set; } = null!; | |||
private ErrorBoundary? errorBoundary; | |||
private void OnError(Exception e) | |||
{ | |||
Logger.LogCritical(e, e.Message); | |||
} | |||
// reset the ErrorBoundary to a non-error state on subsequent page navigation events by calling the error boundary's Recover method. | |||
protected override void OnParametersSet() | |||
{ | |||
errorBoundary?.Recover(); | |||
} | |||
} | } | ||
</filebox> | </filebox> | ||
= [https://stackoverflow.com/questions/57514541/how-to-turn-on-circuitoptions-detailederrors Turn on detailed errors] = | = [https://stackoverflow.com/questions/57514541/how-to-turn-on-circuitoptions-detailederrors Turn on detailed errors] = | ||
{{info | Display detailed error in the client. Never do that in Production, rely better on server log.}} | |||
<filebox fn='Program.cs'> | <filebox fn='Program.cs'> | ||
builder.Services.AddServerSideBlazor() | builder.Services.AddServerSideBlazor() |
Dernière version du 27 février 2024 à 08:24
Links
Components libraries
Useful librairies
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
You may need of the ::deep pseudo-element to modify the style of child components. |
Pages/Items.razor.css |
/* scoped css only for this page */ /* @import is not allowed in scoped css */ first-level-tag { } first-level-tag ::deep second-level-tag { } |
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(); } |
Event handling
MyPage.razor |
<button @onclick=OnButtonClicked>Click me</button> <button @onclick="() => OnButtonClicked(Item.Name)">Click me</button> |
MyPage.razor.cs |
void OnButtonClicked() { } async Task OnButtonClicked(string name) { } |
preventDefault and stopPropagation
<div @onclick="OnDivClicked"> <a href="/page1" @onclick=OnLinkClicked @onclick:preventDefault @* prevent the navigation to the url but still executes the OnLinkClicked method *@ @onclick:stopPropagation> @* stop the propagation of the event (bubbling), so the OnDivClicked method is never called *@ Page 1 </a> </div> |
foreach
<ul> @foreach (var item in items) { <li> @item </li> } </ul> |
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> |
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="Name" /> @* by default uses the 'on change' event (focus lost) *@ <input type="text" @bind="Name" @bind:event="oninput" @* use the oninput event instead of the default onchange, the event is triggered on each input key event *@ @bind:after="DoActionAsync" /> @* trigger the DoActionAsync method when the UI text is modified *@ @code { string Name { get; set; } = "Text"; async Task DoActionAsync() { } } |
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 *@ |
FluentValidation
dotnet add package Blazored.FluentValidation |
_Imports.razor |
@using Blazored.FluentValidation |
Pages/MyForm.razor |
<EditForm Model="@model" OnValidSubmit="@SubmitValidForm"> <FluentValidationValidator /> |
Manual validation
Clients/ItemClient.cs |
public async Task<OperationResult<IReadOnlyCollection<ItemResponse>>> GetItemsAsync( ItemQuery query, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(query); // client validation var validator = new ItemQueryValidator(); var validationResult = validator.Validate(query); if (!validationResult.IsValid) { return new OperationResult<IReadOnlyCollection<TransactionResponse>>(validationResult.Errors); } |
Clients/OperationResult.cs |
public class OperationResult<T> { public T? Result { get; set; } public IDictionary<string, string[]> ValidationErrors { get; } // error messages grouped by property name public IReadOnlyCollection<string> Errors => ValidationErrors.SelectMany(x => x.Value).ToList(); public bool IsValid => ValidationErrors.Count == 0; public OperationResult(IDictionary<string, string[]> validationErrors) // for server side validation error { ValidationErrors = validationErrors; } public OperationResult(IEnumerable<ValidationFailure> validationFailures) // for client side validation errors { ValidationErrors = validationFailures .GroupBy(x => x.PropertyName, x => x.ErrorMessage) .ToDictionary(x => x.Key, x => x.ToArray()); } public OperationResult(T result) // for valid result { Result = result; ValidationErrors = new Dictionary<string, string[]>(); } } |
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; } |
Components
Components/MyComponent.razor |
Components/MyComponent.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
Due to pre-rendering in Blazor Server you can't perform any JS interop until the OnAfterRender lifecycle method. |
Method | Start / End | Show |
---|---|---|
SetParametersAsync | Start | false |
OnInitializedAsync | Start | false |
OnAfterRenderAsync | Start | false |
OnAfterRenderAsync | End | false |
OnInitializedAsync | End | true |
OnParametersSetAsync | Start | true |
OnParametersSetAsync | End | true |
OnAfterRenderAsync | Start | true |
OnAfterRenderAsync | End | true |
SetParametersAsync | End | true |
- OnInitialized OnInitializedAsync
- OnParametersSet OnParametersSetAsync
- OnAfterRender OnAfterRenderAsync
EventCallback
ChildComponent.razor |
<button @onclick="OnClickCallback">Click</button> @code { [Parameter] public EventCallback<MouseEventArgs> OnClickCallback { get; set; } } |
Parent.razor |
@* The ShowMessage is passed to the ChildComponent When the button will be pressed in the ChildComponent, the ShowMessage will be triggered in the parent *@ <ChildComponent OnClickCallback="@ShowMessage"> Content of the child component is supplied by the parent component. </ChildComponent> @code { private void ShowMessage(MouseEventArgs e) { } } |
[Inject] private NavigationManager NavigationManager { get; set; } = default!; NavigationManager.NavigateTo("url"); // to implement redirection, use the OnInitializedAsync method otherwise you will get an NavigationException protected override async Task OnInitializedAsync() { NavigationManager.NavigateTo("/item", true, true); await base.OnInitializedAsync(); } |
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:
|
Localization / Langue / Culture
Program.cs |
builder.Services.AddLocalization(); app.UseRequestLocalization("fr-FR"); |
Call a web API from ASP.NET Core Blazor
Program.cs |
//builder.Services.AddServerSideBlazor(); var myWebapiUrl = builder.Configuration["MyWebapiUrl"]; if (string.IsNullOrWhiteSpace(myWebapiUrl)) { throw new Exception("MyWebapiUrl setting is null"); } builder.Services.AddHttpClient<IMyClient, MyClient>( client => client.BaseAddress = new Uri(myWebapiUrl) ); |
Entity Framework Core
Program.cs |
builder.Services.AddScoped(_ => new MyDbContext(builder.Configuration.GetConnectionString("MyDbContext"))); |
appsettings.json |
{ "ConnectionStrings": { "MyDbContext": "Data Source=MyServer; Initial Catalog=MyDb; Integrated Security=True; MultipleActiveResultSets=True;" }, |
MyRepository.cs |
private readonly MyDbContext context; public MyRepository(MyDbContext context) { this.context= context; } public Task<Data> FetchDataAsync() => context.Data.ToListAsync(); |
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 MyRepository(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 = await contextFactory.CreateDbContextAsync(); } |
State management
[Inject] public ProtectedSessionStorage ProtectedSessionStore { get; set; } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await LoadStateAsync(); StateHasChanged(); } } private async Task LoadStateAsync() { var fetchValueResult = await ProtectedSessionStore.GetAsync<string>("key"); var value = fetchValueResult.Success ? fetchValueResult.Value : null; } await ProtectedSessionStore.SetAsync("key", value); |
External JS libraries
LibMan library manager
# install the LibMan CLI dotnet tool install -g Microsoft.Web.LibraryManager.Cli # create the libman.js file libman init # available providers: cdnjs (default), jsdelivr (choose this one), unpkg, filesystem libman install bootswatch@5.0.1 --files dist/darkly/bootstrap.min.css # uninstall bootstrap libman uninstall bootstrap@5.0.1 |
Bootstrap
# jsdelivr provider # 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 \ --files LICENSE # 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> |
Pages/Error.cshtml |
<head> <!-- REPLACE <link href="~/css/bootstrap/bootstrap.min.css" rel="stylesheet" /> BY --> <link rel="stylesheet" href="~/lib/bootstrap/css/bootstrap.min.css" /> |
Delete the folder wwwroot/css/bootstrap |
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'); |
Delete the folder wwwroot/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; } |
Handle exceptions
Shared/MainLayout.razor |
<ErrorBoundary @ref="errorBoundary"> <ChildContent> @Body </ChildContent> <ErrorContent Context="e"> @{ OnError(@e); } <MudAlert Severity="Severity.Error">An exception occured: @e.Message</MudAlert> </ErrorContent> </ErrorBoundary> @code { [Inject] private ILogger<MainLayout> Logger { get; set; } = null!; private ErrorBoundary? errorBoundary; private void OnError(Exception e) { Logger.LogCritical(e, e.Message); } // reset the ErrorBoundary to a non-error state on subsequent page navigation events by calling the error boundary's Recover method. protected override void OnParametersSet() { errorBoundary?.Recover(); } } |
Turn on detailed errors
Display detailed error in the client. Never do that in Production, rely better on server log. |
Program.cs |
builder.Services.AddServerSideBlazor() .AddCircuitOptions(o => { o.DetailedErrors = true; o.DetailedErrors = builder.Environment.IsDevelopment(); // only add details when debugging }); |
Create a new project
mkdir MySolution cd MySolution dotnet new sln --name MySolution dotnet new blazorserver -o MyBlazorProject --no-https dotnet sln add ./MyBlazorProject/MyBlazorProject.csproj |