Ligne 5 :
Ligne 5 :
* [[MudBlazor]] components library
* [[MudBlazor]] components library
* [https://github.com/Blazored/LocalStorage LocalStorage]
* [https://github.com/Blazored/LocalStorage 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 =
= Description =
Dernière version du 9 mars 2025 à 22:03
Links
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)
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.
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
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}"
<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 to refresh the browser cache
Pages/Items.razor.css
.class { }
::deep .class { }
.class [b-t6dfgx6u46] { }
[b-t6dfgx6u46] .class { }
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 >
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 );
}
private async Task HandleValidSubmitAsync ()
{
await ItemService.UpdateItemAsync (item );
}
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 >
<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 >
afficher 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 :
afficher <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
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 ();
}
}
}
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" ;
}
}
}
Align multiple inputs
afficher <EditForm Model ="@ myModel " >
<table >
<tbody >
<tr >
<td > <label for ="login" > Login: </label > </td >
<td >
<InputText id ="login"
@bind-Value ="myModel.Login"
class ="form-control"
placeholder ="Lgin" >
</InputText >
</td >
</tr >
<tr >
<td > <label for ="password" > Password: </label > </td >
<td >
<InputText id ="password"
@bind-Value ="myModel.Password"
class ="form-control"
placeholder ="Password" >
</InputText >
</td >
</tr >
</tbody >
</table >
<div class ="mt-3" >
<button class ="btn btn-primary btn-sm" @onclick ="Save" > Save</button >
<button class ="btn btn-secondary btn-sm" @onclick ="Cancel" > Cancel</button >
</div >
</EditForm >
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" />
<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" />
@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 />
<InputText @bind-value ="item.Name"
placeholder ="Enter name" />
<ValidationMessage class ="offset-sm-3 col-sm-8" For ="@( () => Item.Name ) " />
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 );
var validator = new ItemQueryValidator ();
var validationResult = validator.Validate (query );
if (!validationResult.IsValid)
{
return new OperationResult <IReadOnlyCollection <TransactionResponse >>(validationResult.Errors );
}
afficher Clients/OperationResult.cs
public class OperationResult <T >
{
public T? Result { get ; set ; }
public IDictionary <string , string []> ValidationErrors { get ; }
public IReadOnlyCollection<string > Errors => ValidationErrors.SelectMany (x => x .Value ).ToList ();
public bool IsValid => ValidationErrors.Count == 0 ;
public OperationResult (IDictionary <string , string []> validationErrors )
{
ValidationErrors = validationErrors;
}
public OperationResult (IEnumerable <ValidationFailure > validationFailures )
{
ValidationErrors = validationFailures
.GroupBy (x => x .PropertyName , x => x .ErrorMessage )
.ToDictionary (x => x .Key , x => x .ToArray ());
}
public OperationResult (T result )
{
Result = result;
ValidationErrors = new Dictionary <string , string []>();
}
}
Form exemples
Deactivate the form while submitting
afficher 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 >
}
afficher 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 ; }
await MyEventCallback.InvokeAsync (true );
_Imports.razor
@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 ()
{
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
<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" );
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" );
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
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css' );
wwwroot/css/font-awesome
css/all.min.css
webfonts/*
LICENSE.txt
wwwroot/css/site.css
@import url('font-awesome/css/all.min.css' );
Program.cs
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 ()
{
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 );
Javascript interop
Pages/MyPage.razor.cs
[Inject ]
public IJSRuntime JS { get ; set ; }
await JS.InvokeVoidAsync ("import" , "./js/mypage.js" );
await JS.InvokeVoidAsync ("MyJSMethod" , "arg1" );
await JS.InvokeVoidAsync ("MyJSMethod" , (object )myArray );
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 >
<script src ="js/mypage.js" > </script >
</body >
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 >
<script src ="js/myscript.js" > </script >
</body >
External JS libraries
dotnet tool install -g Microsoft.Web.LibraryManager.Cli
libman init
libman install bootswatch@5.0.1 --files dist/darkly/bootstrap.min.css
libman uninstall bootstrap@5.0.1
Bootstrap
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
Pages/_Host.cshtml
<head >
<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 >
<link rel ="stylesheet" href ="~/lib/bootstrap/css/bootstrap.min.css" />
Delete the folder wwwroot/css/bootstrap
libman install bootstrap-icons@latest \
--files font/bootstrap-icons.min.css \
--files font/fonts/bootstrap-icons.woff \
--files font/fonts/bootstrap-icons.woff2
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
@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
.bi {
width : 2rem ;
font-size : 1.1rem ;
vertical-align : text-top;
top : -2px ;
}
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" />
<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 ();
}
App.razor
<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 ();
}
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
dotnet aspnet-codegenerator identity -h
dotnet aspnet-codegenerator identity -lf
dotnet aspnet-codegenerator identity -dc MyApplication.Data.ApplicationDbContext --files "Account.Login"
Infinite scrolling
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 );
}
}
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 );
}
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 ();
});
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
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
afficher 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" : [ ]
}
]
}
afficher 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"
}
]
}