Blazor ASP.NET Core 9.0
Links
- Microsoft doc
- Project example: eShopOnBlazor
- MudBlazor components library
- LocalStorage
New Capabilities in .NET 9
- mix and match rendering modes on a per page basis (static server-side rendering (SSR) and interactive modes)
- for Blazor Server, WebSocket compression is enabled by default, meaning your data will zip back and forth faster than ever
- for Blazor Server, the reconnection experience is way better: no more annoying waits when you navigate back to a disconnected app
- inject dependencies directly into the components' constructors
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.
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
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 to refresh the browser cache |
Pages/Items.razor.css |
/* scoped css only for this page */
/* @import is not allowed in scoped css */
.class { } /* apply on the first level tag with class */
::deep .class { } /* apply on all the tags with class */
|
/* rendered CSS */
.class[b-t6dfgx6u46] { }
[b-t6dfgx6u46] .class { }
|
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>
@* ValueChanged event *@
<InputText Value="@item.Name"
ValueChanged="ItemNameChanged"
ValueExpression="() => Item.Name">
</InputText>
|
private void ItemNameChanged(string value)
{
item.Name = value;
}
|
InputTextArea InputNumber InputDate
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 |
_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:
|
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
|
<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
|
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.
<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
<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 |
Form exemples
Deactivate the form while submitting
MyPage.razor |
MyPage.razor.cs |
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();
}
|
Go to the previous page
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")
|
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");
|
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 for the entire app, not a specific page */
/* 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');
|
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);
|
Javascript interop
Call JS from .NET
Pages/MyPage.razor.cs |
[Inject]
public IJSRuntime JS { get; set; }
// one way to import mypage.js
await JS.InvokeVoidAsync("import", "./js/mypage.js");
// invoke JavaScript functions without reading a returned value
await JS.InvokeVoidAsync("MyJSMethod", "arg1");
// pass an array as argument
await JS.InvokeVoidAsync("MyJSMethod", (object)myArray);
// invoke JavaScript functions and read a returned value
var result = await JS.InvokeAsync<string>("AddExclamationPoint", "arg1");
|
wwwroot/js/mypage.js |
window.MyJsMethod = function(arg1) {
alert(arg1);
}
window.AddExclamationPoint = function(arg1) {
return arg1 + ' !';
}
|
Another way to import mypage.js
Pages/_Layout.cshtml |
<body>
@* at the end of body *@
<script src="js/mypage.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>
|
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;
}
|
LightGallery
# download lightgallery
libman install lightgallery@2.3.0 \
--files lightgallery.min.js \
--files plugins/thumbnail/lg-thumbnail.min.js \
--files css/lightgallery.min.css \
--files css/lg-thumbnail.min.css \
--files fonts/lg.woff2 \
--files images/loading.gif
|
Pages/_Layout.cshtml |
<head>
<link rel="stylesheet" href="lib/lightgallery/css/lightgallery-bundle.min.css" />
@* or *@
<link rel="stylesheet" href="lib/lightgallery/css/lightgallery.min.css" />
<link rel="stylesheet" href="lib/lightgallery/css/lg-thumbnail.min.css" />
</head>
<body>
<script src="lib/lightgallery/lightgallery.min.js"></script>
<script src="lib/lightgallery/plugins/thumbnail/lg-thumbnail.min.js"></script>
<script src="js/site.js"></script>
</body>
|
Pages/LightGallery.razor |
<button id="open-gallery">Open gallery</button>
<button @onclick=OpenGallery>Open gallery</button>
|
Pages/LightGallery.razor.cs |
[Inject]
private IJSRuntime JS { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("import", "./lib/lightgallery/lightgallery.min.js");
await JS.InvokeVoidAsync("import", "./lib/lightgallery/plugins/thumbnail/lg-thumbnail.min.js");
await JS.InvokeVoidAsync("import", "./js/lightgallery.js");
}
}
private async Task OnProfileNameClicked(string profileName)
{
await JS.InvokeVoidAsync("OpenGallery", (object)pictureUrls);
}
|
wwwroot/js/lightgallery.js |
const $button = document.getElementById('open-gallery');
const dynamicGallery = lightGallery($button, {
dynamic: true,
dynamicEl: [
{
src: 'img/1.jpg',
thumb: 'img/thumb1.jpg',
subHtml: '<h4>Image 1 title</h4><p>Image 1 descriptions.</p>',
},
{
src: 'img/2.jpg',
thumb: 'img/thumb2.jpg',
subHtml: '<h4>Image 2 title</h4><p>Image 2 descriptions.</p>',
}
],
mode: 'lg-fade',
plugins: [lgThumbnail],
});
$button.addEventListener('click', function () {
dynamicGallery.openGallery();
});
|
wwwroot/js/site.js |
window.OpenGallery = function (picturePaths) {
let elements = picturePaths.map(x => ({
src: x,
thumb: x
}));
let gallery = lightGallery(document.getElementsByTagName('main')[0], {
dynamic: true,
dynamicEl: elements,
mode: 'lg-fade',
plugins: [lgThumbnail],
});
gallery.openGallery();
}
|
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
|
Authenticate with web api
Cookies x
Infinite scrolling
BlazorIntersectionObserver
dotnet add package BlazorIntersectionObserver |
Program.cs |
builder.Services.AddIntersectionObserver();
|
ItemGallery.razor |
<div class="d-flex flex-wrap">
@foreach (var item in Items)
{
<div class="card mx-5 my-2">
<img src=@item.Cover class="card-img-top">
</div>
}
</div>
<IntersectionObserve OnChange="@OnIntersectingChanged">
<div @ref="context.Ref.Current" class="@(allItemsLoaded || context.IsIntersecting ? "d-none": "")">
<div class="spinner-border d-block mx-auto" role="status">
<span class="sr-only">Loading...</span>
</div>
</div>
</IntersectionObserve>
|
ItemGallery.razor.cs |
private bool allProfilesLoaded;
private async Task OnIntersectingChanged(IntersectionObserverEntry entry)
{
if (entry.IsIntersecting)
{
CurrentItemQuery.PageIndex++;
var fetchedItems = await this.ProfileClient.GetAsync(CurrentItemQuery);
if (fetchedItems.Count == 0)
allProfilesLoaded = true;
else
Items.AddEach(fetchedItems);
}
}
|
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
|
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 |
item/.vscode/launch.json |