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) { }
|
|
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 { }
|
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();
}
|
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>
|
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() { }
|
|
<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>
|
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
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 *@
|
|
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/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();
}
|
|
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
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)
{ }
}
|
Navigation
|
[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();
}
|
There is no native way in Blazor to go back to the previous page, but there is still the JS way.
|
private IJSRuntime JSRuntime { get; set; } = default!;
await JSRuntime.InvokeVoidAsync("history.back")
|
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:
- The user closes the browser's window, then opens a new window and navigates back to the app.
- The user closes a tab of the app in a browser window, then opens a new tab and navigates back to the app.
- The user selects the browser's reload/refresh button.
|
Program.cs
|
builder.Services.AddLocalization();
app.UseRequestLocalization("fr-FR");
|
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)
);
|
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();
|
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();
}
|
|
[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
|
# 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 |
|
# 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;
}
|
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();
}
}
|
|
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
|