I. Introduction à Blazor▲
I-A. Qu’est-ce que Blazor ?▲
Les développeurs web .Net ont très souvent recours à JavaScript afin de réaliser des interfaces utilisateur riches, rapides et interactives. En effet les technologies JavaScript sont beaucoup utilisées pour gérer les appels asynchrones, faire des animations, rafraichir une partie de la page sans la recharger complètement et plein d’autres choses qui peuvent s’avérer très utiles pour les utilisateurs.
Et bien Blazor est un Framework permettant de créer des applications web interactives en utilisant C# au lieu de JavaScript. Il devient alors possible pour un développeur C# d’utiliser un seul langage pour écrire la partie Client et la partie Serveur d’une application web.
Blazor vient avec deux modèles différents : Blazor Server et Blazor Web Assembly. Les deux versions sont aujourd'hui disponibles pour un environnement de production .
I-B. Les différents modèles de projet Blazor▲
Blazor Web Assembly
L’application Blazor, ses dépendances et le Runtime .Net sont téléchargés dans le navigateur. Ainsi l’application s’exécute directement à partir du thread d’interface utilisateur du navigateur web :
Blazor Serveur
L’application Blazor est exécutée sur le serveur à partir d’une application ASP.Net Core. Les échanges entre l’interface utilisateur et le serveur sont effectués à l’aide de Signal R. Pour ceux qui ne savent pas, signal R est une technologie facilitant les échanges temps réel entre le client et le serveur. Le client peut réceptionner des informations du serveur sans avoir préalablement émis une demande.
C’est ce modèle qui sera utilisé au cours de ce tutoriel.
II. Création d’un projet Blazor Serveur▲
L’exemple consistera à enregistrer des plats dans une BD PostgreSQL, à afficher la liste des plats à l’écran et à permettre la modification et la suppression de plats enregistrés.
Étape 1 : dans Visual Studio 2019, on clique sur Créer un nouveau projet. Parmi la liste des types de projets qui apparaissent, on choisit Application Blazor.
Étape 2 : on nommera notre application DemoBlazorServerApp et ensuite on choisira le modèle Application serveur Blazor.
Lorsqu’on exécute l’application, on obtient cette page d’accueil :
III. Connexion à la base de données PostgreSQL▲
III-A. Entity Framework Core (EF Core)▲
Pour gérer les échanges avec la base de données nous allons utiliser EF Core.
Étape 1 : installer Entity Framework CLI tool afin de pouvoir utiliser les commandes EF Core dans la ligne de commande.
Pour cela nous devons exécuter la commande suivante dans la console de gestionnaire de package :
dotnet tool install --
global
dotnet-
ef
Une fois installé, il ne sera plus nécessaire d’exécuter cette commande à nouveau pour utiliser les commandes EF Core dans d’autres projets sur le même ordinateur.
Étape 2 : installer la bibliothèque EFCore pour PostGreSQL :
Install-
Package Npgsql.
EntityFrameworkCore.
PostgreSQL -
Version 3
.
1
.
4
III-B. Le modèle de données▲
Dans le répertoire Data, nous allons créer notre modèle qui permettra de manipuler les objets de type plat :
public
class
Meal
{
public
int
Id {
get
;
set
;
}
[
Required]
[
StringLength
(
20
,
ErrorMessage =
"Le nom du plat est trop long."
)]
public
string
Name {
get
;
set
;
}
public
string
Description {
get
;
set
;
}
[
Required]
[
Range
(
1
,
200
,
ErrorMessage =
"Le montant doit être compris entre 1 et 200 $"
)]
public
decimal
Price {
get
;
set
;
}
public
DateTime CreatedDate {
get
;
set
;
}
}
III-C. Le contexte de données▲
Cet objet se connectera à la base de données PostGreSQL afin d’effectuer les requêtes.
public
class
DemoBlazorServerAppContext :
DbContext
{
public
virtual
DbSet<
Meal>
Meals {
get
;
set
;
}
public
DemoBlazorServerAppContext
(
DbContextOptions<
DemoBlazorServerAppContext>
options) :
base
(
options) {
}
protected
override
void
OnModelCreating
(
ModelBuilder builder)
{
base
.
OnModelCreating
(
builder);
}
}
Pour que le contexte puisse fonctionner, nous avons besoin de définir la chaine de connexion à la BD. On le fera dans le fichier appsettings.json. Le fichier devrait ressembler à ceci :
{
"Logging"
:
{
"LogLevel"
:
{
"Default"
:
"Information"
,
"Microsoft"
:
"Warning"
,
"Microsoft.Hosting.Lifetime"
:
"Information"
}
},
"AllowedHosts"
:
"*"
,
"ConnectionStrings"
:
{
"DefaultConnection"
:
"Server=127.0.0.1;Port=5432;Database=postgres;User Id=postgres;Password=password_postgres;"
}
}
À la racine du projet, nous allons créer la classe ConfigurationHelper qui nous permettra de récupérer les données depuis le fichier appsettings.json. Il sera utilisé dans ce projet pour récupérer la chaine de connexion.
public
class
ConfigurationHelper
{
public
static
string
GetCurrentSettings
(
string
key)
{
var
builder =
new
ConfigurationBuilder
(
)
.
SetBasePath
(
System.
IO.
Directory.
GetCurrentDirectory
(
))
.
AddJsonFile
(
"appsettings.json"
,
optional:
false
,
reloadOnChange:
true
)
.
AddEnvironmentVariables
(
);
IConfigurationRoot configuration =
builder.
Build
(
);
return
configuration.
GetValue<
string
>(
key);
}
}
Pour conclure avec le contexte, nous allons définir une classe ContextFactory qui permet d’instancier un contexte qui se connecte à la BD PostGreSQL à l’aide de la chaine de connexion récupérée par l’objet ConfigurationHelper.
public
class
DemoBlazorServerAppContextFactory :
IDesignTimeDbContextFactory<
DemoBlazorServerAppContext>
{
public
DemoBlazorServerAppContext CreateDbContext
(
string
[]
args)
{
var
optionsBuilder =
new
DbContextOptionsBuilder<
DemoBlazorServerAppContext>(
);
var
connStr =
ConfigurationHelper.
GetCurrentSettings
(
"ConnectionStrings:DefaultConnection"
);
optionsBuilder.
UseNpgsql
(
connStr);
return
new
DemoBlazorServerAppContext
(
optionsBuilder.
Options);
}
}
III-D. La migration en base de données▲
Afin de migrer notre modèle de données dans la base de données, nous devons installer le package EntityFrameworkCore.Design :
Install-
Package Microsoft.
EntityFrameworkCore.
Design -
Version 3
.
1
.
4
Nous pouvons maintenant appliquer la première migration en exécutant ces deux commandes :
dotnet ef migrations add
InitialCreate --
project DemoBlazorServerApp
dotnet ef database update --
project DemoBlazorServerApp
Nous sommes désormais capables d’effectuer toutes les opérations de CRUD avec la BD PostGreSQL.
IV. Le service de gestion des plats▲
Nous allons user d’un service qui utilisera le contexte pour effectuer les requêtes de l’interface utilisateur. On nommera ce service MealService.
using
Microsoft.
EntityFrameworkCore;
using
System;
using
System.
Collections.
Generic;
using
System.
Linq;
using
System.
Threading.
Tasks;
namespace
DemoBlazorServerApp.
Data
{
public
class
MealService :
IMealService
{
DemoBlazorServerAppContext _context;
public
MealService
(
DemoBlazorServerAppContext context)
{
_context =
context;
}
public
async
Task<
List<
Meal>>
GetMealsAsync
(
)
{
return
await
_context.
Meals.
OrderBy
(
x =>
x.
CreatedDate).
ToListAsync
(
);
}
public
async
Task<
Meal>
GetMealByIdAsync
(
int
id)
{
return
await
_context.
Meals.
FindAsync
(
id);
}
public
async
Task<
Meal>
InsertMealAsync
(
Meal meal)
{
meal.
CreatedDate =
DateTime.
Now;
_context.
Meals.
Add
(
meal);
await
_context.
SaveChangesAsync
(
);
return
meal;
}
public
async
Task<
Meal>
UpdateMealAsync
(
int
id,
Meal m)
{
var
meal =
await
_context.
Meals.
FindAsync
(
id);
if
(
meal ==
null
)
return
null
;
meal.
Name =
m.
Name;
meal.
Description =
m.
Description;
meal.
Price =
m.
Price;
_context.
Meals.
Update
(
meal);
await
_context.
SaveChangesAsync
(
);
return
meal;
}
public
async
Task<
Meal>
DeleteMealAsync
(
int
id)
{
var
meal =
await
_context.
Meals.
FindAsync
(
id);
if
(
meal ==
null
)
return
null
;
_context.
Meals.
Remove
(
meal);
await
_context.
SaveChangesAsync
(
);
return
meal;
}
}
}
using
System;
using
System.
Collections.
Generic;
using
System.
Linq;
using
System.
Threading.
Tasks;
namespace
DemoBlazorServerApp.
Data
{
public
interface
IMealService
{
Task<
List<
Meal>>
GetMealsAsync
(
);
Task<
Meal>
GetMealByIdAsync
(
int
id);
Task<
Meal>
InsertMealAsync
(
Meal meal);
Task<
Meal>
UpdateMealAsync
(
int
id,
Meal m);
Task<
Meal>
DeleteMealAsync
(
int
id);
}
}
Il faut inscrire le service MealService et le contexte de données dans le fichier startup.cs avant de pouvoir les utiliser dans l’application :
public
void
ConfigureServices
(
IServiceCollection services)
{
services.
AddRazorPages
(
);
services.
AddServerSideBlazor
(
);
services.
AddSingleton<
WeatherForecastService>(
);
services.
AddScoped<
IMealService,
MealService>(
);
services.
AddDbContext<
DemoBlazorServerAppContext>(
option =>
option.
UseNpgsql
(
Configuration.
GetConnectionString
(
"DefaultConnection"
)));
}
V. Les vues▲
On décide d’afficher la liste des plats sur la page d’accueil par défaut de l’application. Pour cela nous allons modifier le composant Index.razor. La création, modification et suppression de plats se fera par des pop-ups qui seront appelés à partir du composant Index.razor. Nous utiliserons la bibliothèque BlazoredModal pour les popups.
V-A. Les pop-ups avec BlazoredModal▲
Étape 1 : installer le package BlazoredModal :
Install-
Package Blazored.
Modal
Étape 2 : inscrire le service BlazoredModal dans le startup.cs.
La version finale de la méthode ConfigureServices de la classe startup.cs devrait être :
public
void
ConfigureServices
(
IServiceCollection services)
{
services.
AddRazorPages
(
);
services.
AddServerSideBlazor
(
);
services.
AddSingleton<
WeatherForecastService>(
);
services.
AddScoped<
IMealService,
MealService>(
);
services.
AddDbContext<
DemoBlazorServerAppContext>(
option =>
option.
UseNpgsql
(
Configuration.
GetConnectionString
(
"DefaultConnection"
)));
services.
AddBlazoredModal
(
);
}
Étape 3 : ajouter les références dans le fichier _Imports.razor :
@using
System.
Net.
Http
@using
Microsoft.
AspNetCore.
Authorization
@using
Microsoft.
AspNetCore.
Components.
Authorization
@using
Microsoft.
AspNetCore.
Components.
Forms
@using
Microsoft.
AspNetCore.
Components.
Routing
@using
Microsoft.
AspNetCore.
Components.
Web
@using
Microsoft.
JSInterop
@using
DemoBlazorServerApp
@using
DemoBlazorServerApp.
Shared
@using
Blazored
@using
Blazored.
Modal
@using
Blazored.
Modal.
Services
Étape 4 : ajouter le composant BlazoredModal dans le MainLayout.Razor :
@inherits LayoutComponentBase
<
BlazoredModal />
<
div class
=
"sidebar"
>
<
NavMenu />
</
div>
<
div class
=
"main"
>
<
div class
=
"top-row px-4"
>
<
a href=
"https://docs.microsoft.com/aspnet/"
target=
"_blank"
>
About</
a>
</
div>
<
div class
=
"content px-4"
>
@Body
</
div>
</
div>
Étape 5 : mettre à jour le fichier _Host :
@page "/"
@namespace
DemoBlazorServerApp.
Pages
@addTagHelper *,
Microsoft.
AspNetCore.
Mvc.
TagHelpers
@addTagHelper *,
Blazored.
Modal
@{
Layout =
null
;
}
<!
DOCTYPE html>
<
html lang=
"en"
>
<
head>
<
meta charset=
"utf-8"
/>
<
meta name=
"viewport"
content=
"width=device-width, initial-scale=1.0"
/>
<
title>
DemoBlazorServerApp</
title>
<
base
href=
"~/"
/>
<
link rel=
"stylesheet"
href=
"css/bootstrap/bootstrap.min.css"
/>
<
link href=
"css/site.css"
rel=
"stylesheet"
/>
<
link href=
"_content/Blazored.Modal/blazored-modal.css"
rel=
"stylesheet"
/>
</
head>
<
body>
<
app>
<
component type=
"typeof(App)"
render-
mode=
"ServerPrerendered"
/>
</
app>
<
div id=
"blazor-error-ui"
>
<
environment include=
"Staging,Production"
>
An error has occurred.
This application may no longer respond until reloaded.
</
environment>
<
environment include=
"Development"
>
An unhandled exception has occurred.
See browser dev tools for
details.
</
environment>
<
a href=
""
class
=
"reload"
>
Reload</
a>
<
a class
=
"dismiss"
>
</
a>
</
div>
<
script src=
"_framework/blazor.server.js"
></
script>
</
body>
</
html>
V-B. Les vues▲
V-B-1. Vue d’affichage de la liste des plats▲
Comme on l’a dit plus haut, le composant Index.razor servira à afficher la liste des plats. Cette liste sera affichée sous forme de tableau :
@page "/"
@using
DemoBlazorServerApp.
Data
@inject IModalService Modal
@inject IMealService MealService
<
div class
=
"row"
>
<
div class
=
"col-12"
>
<
h4><
span class
=
"oi oi-list"
aria-
hidden=
"true"
></
span>
Liste de plats</
h4>
</
div>
</
div>
<
div class
=
"row"
>
<
div class
=
"col-6"
>
<
button @onclick=
"@(() => AddMeal())"
class
=
"btn btn-sm btn-primary"
><
span class
=
"oi oi-plus"
aria-
hidden=
"true"
></
span>
Ajouter plat </
button>
</
div>
</
div>
<
br />
@if
(
meals ==
null
)
{
<
p><
em>
Loading...</
em></
p>
}
else
{
<
table class
=
"table"
>
<
thead>
<
tr>
<
th>
Nom</
th>
<
th>
Description</
th>
<
th>
Prix</
th>
<
th>
Actions</
th>
</
tr>
</
thead>
<
tbody>
@foreach
(
var
meal in
meals)
{
<
tr>
<
td>
@meal.
Name</
td>
<
td>
@meal.
Description</
td>
<
td>
@meal.
Price $</
td>
<
th>
<
button @onclick=
"@(() => DeleteMeal(meal.Id))"
class
=
"btn btn-sm btn-primary"
>
Delete</
button>
|
<
button @onclick=
"@(() => EditMeal(meal.Id))"
class
=
"btn btn-sm btn-secondary"
>
Edit</
button>
</
th>
</
tr>
}
</
tbody>
</
table>
}
@code {
private
List<
Meal>
meals;
protected
override
async
Task OnInitializedAsync
(
)
{
meals =
await
MealService.
GetMealsAsync
(
);
}
async
Task AddMeal
(
)
{
var
mealModal =
Modal.
Show<
AddMeal>(
"Ajout de plat"
);
var
result =
await
mealModal.
Result;
if
(!
result.
Cancelled)
{
meals =
await
MealService.
GetMealsAsync
(
);
}
}
async
Task DeleteMeal
(
int
id)
{
var
parameters =
new
ModalParameters
(
);
parameters.
Add
(
nameof
(
Meal.
Id),
id);
var
mealModal =
Modal.
Show<
DeleteMeal>(
"Suppression de plat"
,
parameters);
var
result =
await
mealModal.
Result;
if
(!
result.
Cancelled)
{
meals =
await
MealService.
GetMealsAsync
(
);
}
}
async
Task EditMeal
(
int
id)
{
var
parameters =
new
ModalParameters
(
);
parameters.
Add
(
nameof
(
Meal.
Id),
id);
var
mealModal =
Modal.
Show<
EditMeal>(
"Mise à jour de plat"
,
parameters);
var
result =
await
mealModal.
Result;
if
(!
result.
Cancelled)
{
meals =
await
MealService.
GetMealsAsync
(
);
}
}
}
Comme le code source de cette page nous le montre, avant d’utiliser un service dans une page Razor, il faut d’abord l’injecter dans cette page.
V-B-2. Vue de création de nouveaux plats▲
Nous allons ajouter un composant AddMeal.razor qui sera utilisé comme popup pour l’ajout de plat.
Faisons un clic droit sur le répertoire Pages, cliquons sur Ajouter et ensuite sélectionnons Composant Razor :
Le code source du composant AddMeal :
@page "/addMeal"
@using DemoBlazorServerApp.Data
@inject IModalService ModalService
@inject IMealService MealService
<EditForm Model
=
"@meal"
OnValidSubmit
=
@FormSubmitted>
<DataAnnotationsValidator />
<div class
=
"form-group"
>
<label for
=
"nom"
>
Nom</label><br />
<InputText id
=
"nom"
@bind-Value
=
"meal.Name"
/>
<ValidationMessage For
=
"@(() => meal.Name)"
/>
</div>
<div class
=
"form-group"
>
<label for
=
"description"
>
Description</label><br/>
<InputTextArea id
=
"description"
@bind-Value
=
"meal.Description"
/>
</div>
<div class
=
"form-group"
>
<label for
=
"price"
>
Price</label><br />
<InputNumber id
=
"price"
@bind-Value
=
"meal.Price"
/>
<ValidationMessage For
=
"@(() => meal.Price)"
/>
</div>
<button type
=
"submit"
class
=
"btn btn-primary"
>
Ajouter</button>
<button @onclick
=
"BlazoredModal.Cancel"
class
=
"btn btn-secondary"
>
Annuler</button>
</EditForm>
@code {
private string StatusMessage;
private string StatusClass;
[CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; }
Meal meal = new Meal();
private async void FormSubmitted()
{
await MealService.InsertMealAsync(meal);
BlazoredModal.Close(ModalResult.Ok<Meal>(meal));
}
}
V-B-3. Vue de mise à jour de plat▲
On crée ensuite le composant EditMeal.razor pour la mise à jour des plats :
@page "/editMeal"
@using DemoBlazorServerApp.Data
@inject IModalService ModalService
@inject IMealService MealService
<EditForm Model
=
"@meal"
OnValidSubmit
=
@FormSubmitted>
<DataAnnotationsValidator />
<div class
=
"form-group"
>
<label for
=
"nom"
>
Nom</label><br />
<InputText id
=
"nom"
@bind-Value
=
"meal.Name"
/>
<ValidationMessage For
=
"@(() => meal.Name)"
/>
</div>
<div class
=
"form-group"
>
<label for
=
"description"
>
Description</label><br />
<InputTextArea id
=
"description"
@bind-Value
=
"meal.Description"
/>
</div>
<div class
=
"form-group"
>
<label for
=
"price"
>
Price</label><br />
<InputNumber id
=
"price"
@bind-Value
=
"meal.Price"
/>
<ValidationMessage For
=
"@(() => meal.Price)"
/>
</div>
<button type
=
"submit"
class
=
"btn btn-primary"
>
Modifier</button>
<button @onclick
=
"BlazoredModal.Cancel"
class
=
"btn btn-secondary"
>
Annuler</button>
</EditForm>
@code {
[CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; }
Meal meal = new Meal();
[Parameter] public int id { get; set; }
protected async override void OnInitialized()
{
meal = await MealService.GetMealByIdAsync(id);
}
private async void FormSubmitted()
{
await MealService.UpdateMealAsync(id, meal);
BlazoredModal.Close(ModalResult.Ok<Meal>(meal));
}
}
V-B-4. Vue de suppression de plat▲
Et enfin le composant DeleteMeal.razor pour la suppression de plats :
@inject IMealService MealService
<EditForm Model
=
"@meal"
OnSubmit
=
@FormSubmitted>
Voulez vous vraiment supprimer ?
<button type
=
"submit"
class
=
"btn btn-primary"
>
Oui</button>
<button @onclick
=
"BlazoredModal.Cancel"
class
=
"btn btn-secondary"
>
Annuler</button>
</EditForm>
@code {
[CascadingParameter] BlazoredModalInstance BlazoredModal { get; set; }
Meal meal = new Meal();
[Parameter] public int id { get; set; }
private async void FormSubmitted()
{
await MealService.DeleteMealAsync(id);
BlazoredModal.Close(ModalResult.Ok<Meal>(meal));
}
}
V-B-5. Résultat▲
Quand on lance l’application, on obtient une interface utilisateur rapide ; la page principale qui se met à jour sans rafraichissement total. Et tout cela, c’est fait en n’utilisant que du C-sharp.
|
VI. Remerciements Developpez.com▲
Nous tenons à remercier Claude Leloup pour la mise au gabarit et la relecture orthographique.