📝 Add documentation

pull/6/head
Julien Riboulet 2 years ago
commit 5c718f6358

@ -0,0 +1,25 @@
kind: pipeline
type: docker
name: CI
trigger:
event:
- push
steps:
- name: generate-and-deploy-docs
image: hub.codefirst.iut.uca.fr/thomas.bellembois/codefirst-docdeployer
volumes:
- name: docs
path: /docs
commands:
- /entrypoint.sh
when:
branch:
- master
depends_on: [ build ]
volumes:
- name: docs
temp: {}

@ -0,0 +1,20 @@
# Dependencies
/node_modules
# Production
/build
# Generated files
.docusaurus
.cache-loader
# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

@ -0,0 +1,26 @@
stages:
- 🚧 Build
- 🚀 Delivery
🚧 Build:
stage: 🚧 Build
tags:
- docker
image: node:16.13.0
script:
- yarn install
- yarn build
pages:
image: node:16.13.0
stage: 🚀 Delivery
tags:
- docker
script:
- yarn install
- yarn build
- mkdir -p ${CI_PROJECT_DIR}/public
- cp -r ${CI_PROJECT_DIR}/build/* ${CI_PROJECT_DIR}/public
artifacts:
paths:
- public

@ -0,0 +1,3 @@
module.exports = {
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
};

@ -0,0 +1,3 @@
label: 'Add an item'
position: 6
collapsed: true

@ -0,0 +1,83 @@
---
sidebar_position: 4
title: Add button
---
In order to access your add page, we are going to add a button above our grid.
Open the `Pages/List.razor` file and add the highlighted changes:
```cshtml title="Pages/List.razor"
@page "/list"
@using MyBeautifulAdmin.Models
<h3>List</h3>
// highlight-start
<div>
<NavLink class="btn btn-primary" href="Add" Match="NavLinkMatch.All">
<i class="fa fa-plus"></i> Ajouter
</NavLink>
</div>
// highlight-end
<DataGrid TItem="Data"
Data="@data"
ReadData="@OnReadData"
TotalItems="@totalData"
PageSize="10"
ShowPager
Responsive>
<DataGridColumn TItem="Data" Field="@nameof(Data.Id)" Caption="#" />
<DataGridColumn TItem="Data" Field="@nameof(Data.FirstName)" Caption="First Name" />
<DataGridColumn TItem="Data" Field="@nameof(Data.LastName)" Caption="Last Name" />
<DataGridColumn TItem="Data" Field="@nameof(Data.DateOfBirth)" Caption="Date Of Birth" DisplayFormat="{0:d}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo("fr-FR")" />
<DataGridColumn TItem="Data" Field="@nameof(Data.Roles)" Caption="Roles">
<DisplayTemplate>
@(string.Join(", ", ((Data)context).Roles))
</DisplayTemplate>
</DataGridColumn>
</DataGrid>
```
## Concept: Routing
### Routing patterns
The `Router` component allows routing to Razor components in a Blazor application. The `Router` component is used in the `App` component of Blazor apps.
```cshtml title="App.razor"
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<p>Sorry, there's nothing at this address.</p>
</NotFound>
</Router>
```
When a Razor component (`.razor`) with an `@page` directive is compiled, the generated component class is supplied as a RouteAttribute specifying the component's routing model.
When the app starts, the assembly specified as the `AppAssembly` router is scanned to collect routing information for app components that have a RouteAttribute .
At runtime, the RouteView component:
* Receives all route parameters from the RouteData Router.
* Renders the specified component with its layout, including additional nested layouts.
Components support multiple routing patterns using multiple `@page` directives.
The following sample component loads on requests for `/BlazorRoute` and `/DifferentBlazorRoute`.
```cshtml title="Pages/BlazorRoute.razor"
// highlight-start
@page "/BlazorRoute"
@page "/DifferentBlazorRoute"
// highlight-end
<h1>Blazor routing</h1>
```
:::info
For URLs to resolve correctly, the application must include a &lt;base&gt; in its `wwwroot/index.html` file (Blazor WebAssembly) or `Pages/_Layout.cshtml` file (Blazor Server) with the application base path specified in the `href` attribute.
:::

@ -0,0 +1,161 @@
---
sidebar_position: 5
title: Creation of the add model
---
## Add model
In order to add an element we will create an object representing our item.
For this in create a new class `Models/ItemModel.cs`, this class will include all the information of our API object.
We will also add a property so that the user accepts the conditions of addition.
Similarly, annotations will be present to validate the fields of our forms.
```csharp title="Models/ItemModel.cs"
public class ItemModel
{
public int Id { get; set; }
[Required]
[StringLength(50, ErrorMessage = "The display name must not exceed 50 characters.")]
public string DisplayName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "The name must not exceed 50 characters.")]
[RegularExpression(@"^[a-z''-'\s]{1,40}$", ErrorMessage = "Only lowercase characters are accepted.")]
public string Name { get; set; }
[Required]
[Range(1, 64)]
public int StackSize { get; set; }
[Required]
[Range(1, 125)]
public int MaxDurability { get; set; }
public List<string> EnchantCategories { get; set; }
public List<string> RepairWith { get; set; }
[Required]
[Range(typeof(bool), "true", "true", ErrorMessage = "You must agree to the terms.")]
public bool AcceptCondition { get; set; }
[Required(ErrorMessage = "The image of the item is mandatory!")]
public byte[] ImageContent { get; set; }
}
```
## Concept: Data Annotation
### Validation attributes
Validation attributes allow you to specify validation rules for model properties.
The following example shows a model class that is annotated with validation attributes.
The `[ClassicMovie]` attribute is a custom validation attribute, and the others are predefined.
```csharp
public class Movie
{
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Title { get; set; }
[ClassicMovie(1960)]
[DataType(DataType.Date)]
[Display(Name = "Release Date")]
public DateTime ReleaseDate { get; set; }
[Required]
[StringLength(1000)]
public string Description { get; set; }
[Range(0, 999.99)]
public decimal Price { get; set; }
public Genre Genre { get; set; }
public bool Preorder { get; set; }
}
```
### Attributs prédéfinis
Here are some of the predefined validation attributes:
* `[ValidateNever]`: ValidateNeverAttribute Indicates that a property or parameter should be excluded from validation.
* `[CreditCard]`: Checks that the property has a credit card format. Requires additional jQuery validation methods.
* `[Compare]`: Validates that two properties of a model match.
* `[EmailAddress]`: Checks that the property has an email format.
* `[Phone]`: Checks that the property has a phone number format.
* `[Range]`: Checks that the property value is within a specified range.
* `[RegularExpression]`: Validates that the property value matches a specified regular expression.
* `[Required]`: Checks that the field is not null. For more information on the behavior of this attribute, see [Required] attribute.
* `[StringLength]`: Validates that a property value of type String does not exceed a specified length limit.
* `[Url]`: Checks that the property has a URL format.
* `[Remote]`: Validates input on the client by calling an action method on the server. For more information on the behavior of this attribute, see [Remote] attribute.
You can find the full list of validation attributes in the [System.ComponentModel.DataAnnotations](https://docs.microsoft.com/fr-fr/dotnet/api/system.componentmodel.dataannotations) namespace.
### Error Messages
Validation attributes allow you to specify the error message to display for invalid input. For instance :
```csharp
[StringLength(8, ErrorMessage = "Name length can't be more than 8.")]
```
Internally the attributes call `String.Format` with a placeholder for the field name and sometimes other placeholders. For instance :
```csharp
[StringLength(8, ErrorMessage = "{0} length must be between {2} and {1}.", MinimumLength = 6)]
```
Applied to a `Name` property, the error message created by the previous code would be "Name length must be between 6 and 8".
To find out what parameters are passed to `String.Format` for the error message of a particular attribute, see the [DataAnnotations source code](https://github.com/dotnet/runtime/tree/main/src /libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations).
### Custom attributes
For scenarios not handled by the predefined validation attributes, you can create custom validation attributes. Create a class that inherits from ValidationAttribute and override the IsValid method.
The `IsValid` method accepts an object named value, which is the input to validate. An overload also accepts a `ValidationContext` object, which provides additional information such as the model instance created by the model binding.
The following example checks that the release date of a movie belonging to the Classic genre is not later than a specified year. The `[ClassicMovie]` attribute:
* Runs only on the server.
* For classic films, validate the publication date:
```csharp
public class ClassicMovieAttribute : ValidationAttribute
{
public ClassicMovieAttribute(int year)
{
Year = year;
}
public int Year { get; }
public string GetErrorMessage() =>
$"Classic movies must have a release year no later than {Year}.";
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
var movie = (Movie)validationContext.ObjectInstance;
var releaseYear = ((DateTime)value).Year;
if (movie.Genre == Genre.Classic && releaseYear > Year)
{
return new ValidationResult(GetErrorMessage());
}
return ValidationResult.Success;
}
}
```
The `movie` variable in the previous example represents a `Movie` object that contains the form submission data. When validation fails, a `ValidationResult` with an error message is returned.

@ -0,0 +1,23 @@
---
sidebar_position: 3
title: Creation of the add page
---
## Creation of the page
As before, create a new page which will be named `Add.razor` and the partial class `Add.razor.cs`.
## Final source code
```cshtml title="Pages/Add.razor"
@page "/add"
<h3>Add</h3>
```
```csharp title="Pages/Add.razor.cs"
public partial class Add
{
}
```

@ -0,0 +1,239 @@
---
sidebar_position: 6
title: Creation of the form
---
Open the `Pages/Add.razor` file and edit the following file:
```cshtml title="Pages/Add.razor"
@page "/add"
<h3>Add</h3>
<EditForm Model="@itemModel" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<p>
<label for="display-name">
Display name:
<InputText id="display-name" @bind-Value="itemModel.DisplayName" />
</label>
</p>
<p>
<label for="name">
Name:
<InputText id="name" @bind-Value="itemModel.Name" />
</label>
</p>
<p>
<label for="stack-size">
Stack size:
<InputNumber id="stack-size" @bind-Value="itemModel.StackSize" />
</label>
</p>
<p>
<label for="max-durability">
Max durability:
<InputNumber id="max-durability" @bind-Value="itemModel.MaxDurability" />
</label>
</p>
<p>
Enchant categories:
<div>
@foreach (var item in enchantCategories)
{
<label>
<input type="checkbox" @onchange="@(e => OnEnchantCategoriesChange(item, e.Value))" />@item
</label>
}
</div>
</p>
<p>
Repair with:
<div>
@foreach (var item in repairWith)
{
<label>
<input type="checkbox" @onchange="@(e => OnRepairWithChange(item, e.Value))" />@item
</label>
}
</div>
</p>
<p>
<label>
Item image:
<InputFile OnChange="@LoadImage" accept=".png" />
</label>
</p>
<p>
<label>
Accept Condition:
<InputCheckbox @bind-Value="itemModel.AcceptCondition" />
</label>
</p>
<button type="submit">Submit</button>
</EditForm>
```
* The `EditForm` component is rendered where the &lt;EditForm&gt; appears.
* The model is created in the component code and kept in a private field ( itemModel ). The field is assigned to the `Model` attribute of the &lt;EditForm&gt; .
* The `InputText` component ( id="display-name" ) is an input component for modifying string values. The `@bind-Value` directive attribute binds the `itemModel.DisplayName` model property to the `Value` property of the `InputText` component.
* The `HandleValidSubmit` method is assigned to `OnValidSubmit`. The handler is called if the form passes validation.
* The Data Annotations Validator (`DataAnnotationsValidator`) attaches support for validation using Data Annotations:
* If the form field &lt;input&gt; is not populated when the Submit button is selected, an error is displayed in the validation summary (`ValidationSummary`) ("The DisplayName field is required.") and `HandleValidSubmit` is not called.
* If the form field &lt;input&gt; contains more than fifty characters when the submit button is selected, an error is displayed in the validation summary ("Displayed name must not exceed 50 characters.") and `HandleValidSubmit` is not called.
* If the form field &lt;input&gt; contains a valid value when the Submit button is selected, `HandleValidSubmit` is called.
## Form code
Open the `Pages/Add.razor.cs` file and edit the following file:
```csharp title="Pages/Add.razor.cs"
public partial class Add
{
[Inject]
public ILocalStorageService LocalStorage { get; set; }
[Inject]
public IWebHostEnvironment WebHostEnvironment { get; set; }
/// <summary>
/// The default enchant categories.
/// </summary>
private List<string> enchantCategories = new List<string>() { "armor", "armor_head", "armor_chest", "weapon", "digger", "breakable", "vanishable" };
/// <summary>
/// The default repair with.
/// </summary>
private List<string> repairWith = new List<string>() { "oak_planks", "spruce_planks", "birch_planks", "jungle_planks", "acacia_planks", "dark_oak_planks", "crimson_planks", "warped_planks" };
/// <summary>
/// The current item model
/// </summary>
private ItemModel itemModel = new()
{
EnchantCategories = new List<string>(),
RepairWith = new List<string>()
};
private async void HandleValidSubmit()
{
// Get the current data
var currentData = await LocalStorage.GetItemAsync<List<Item>>("data");
// Simulate the Id
itemModel.Id = currentData.Max(s => s.Id) + 1;
// Add the item to the current data
currentData.Add(new Item
{
Id = itemModel.Id,
DisplayName = itemModel.DisplayName,
Name = itemModel.Name,
RepairWith = itemModel.RepairWith,
EnchantCategories = itemModel.EnchantCategories,
MaxDurability = itemModel.MaxDurability,
StackSize = itemModel.StackSize,
CreatedDate = DateTime.Now
});
// Save the image
var imagePathInfo = new DirectoryInfo($"{WebHostEnvironment.WebRootPath}/images");
// Check if the folder "images" exist
if (!imagePathInfo.Exists)
{
imagePathInfo.Create();
}
// Determine the image name
var fileName = new FileInfo($"{imagePathInfo}/{itemModel.Name}.png");
// Write the file content
await File.WriteAllBytesAsync(fileName.FullName, itemModel.ImageContent);
// Save the data
await LocalStorage.SetItemAsync("data", currentData);
}
private async Task LoadImage(InputFileChangeEventArgs e)
{
// Set the content of the image to the model
using (var memoryStream = new MemoryStream())
{
await e.File.OpenReadStream().CopyToAsync(memoryStream);
itemModel.ImageContent = memoryStream.ToArray();
}
}
private void OnEnchantCategoriesChange(string item, object checkedValue)
{
if ((bool)checkedValue)
{
if (!itemModel.EnchantCategories.Contains(item))
{
itemModel.EnchantCategories.Add(item);
}
return;
}
if (itemModel.EnchantCategories.Contains(item))
{
itemModel.EnchantCategories.Remove(item);
}
}
private void OnRepairWithChange(string item, object checkedValue)
{
if ((bool)checkedValue)
{
if (!itemModel.RepairWith.Contains(item))
{
itemModel.RepairWith.Add(item);
}
return;
}
if (itemModel.RepairWith.Contains(item))
{
itemModel.RepairWith.Remove(item);
}
}
}
```
You can now add a new item, if you return to the list when your new item is present.
## Concept: Form and validation
### Built-in form components
The Blazor framework provides built-in form components to receive and validate user input.
Inputs are validated when they are changed and when a form is submitted.
The available input components are listed in the following table.
| Composant dentrée | Rendu comme… |
| ----------- | ----------- |
| InputCheckbox | `<input type="checkbox">` |
| InputDate&lt;TValue&gt; | `<input type="date">` |
| InputFile | `<input type="file">` |
| InputNumber&lt;TValue&gt; | `<input type="number">` |
| InputRadio&lt;TValue&gt; | `<input type="radio">` |
| InputRadioGroup&lt;TValue&gt; | Groupe denfants InputRadio&lt;TValue&gt; |
| InputSelect&lt;TValue&gt; | `<select>` |
| InputText | `<input>` |
| InputTextArea | `<textarea>` |
For more information on the InputFile component, see [ASP.NET Core Blazor file uploads](https://docs.microsoft.com/en-us/aspnet/core/blazor/file-uploads).
All input components, including EditForm , support arbitrary attributes. Any attribute that does not correspond to a component parameter is added to the rendered HTML element.
Input components provide the default behavior to validate when a field is changed, including updating the CSS Field class to reflect the state of the field as valid or invalid.
Some components include useful parsing logic.
For example, InputDate&lt;TValue&gt; and InputNumber&lt;TValue&gt; Correctly handle unparsed values by registering unparsed values as validation errors.
Types that can accept NULL values also support nullability of the target field (for example, int? for a nullable integer).

@ -0,0 +1,112 @@
---
sidebar_position: 8
title: Image display
---
We have now added a new item with its image, so it's time to manage it in our table.
Open the `Pages/List.razor` file and add the highlighted lines:
```cshtml title="Pages/List.razor"
<DataGrid TItem="Item"
Data="@items"
ReadData="@OnReadData"
TotalItems="@totalItem"
PageSize="10"
ShowPager
Responsive>
<DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="#" />
// highlight-start
<DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="Image">
<DisplayTemplate>
<img src="images/@(context.Name).png" class="img-thumbnail" title="@context.DisplayName" alt="@context.DisplayName" style="max-width: 150px" />
</DisplayTemplate>
</DataGridColumn>
// highlight-end
<DataGridColumn TItem="Item" Field="@nameof(Item.DisplayName)" Caption="Display name" />
<DataGridColumn TItem="Item" Field="@nameof(Item.StackSize)" Caption="Stack size" />
<DataGridColumn TItem="Item" Field="@nameof(Item.MaxDurability)" Caption="Maximum durability" />
<DataGridColumn TItem="Item" Field="@nameof(Item.EnchantCategories)" Caption="Enchant categories">
<DisplayTemplate>
@(string.Join(", ", ((Item)context).EnchantCategories))
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="Item" Field="@nameof(Item.RepairWith)" Caption="Repair with">
<DisplayTemplate>
@(string.Join(", ", ((Item)context).RepairWith))
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="Item" Field="@nameof(Item.CreatedDate)" Caption="Created date" DisplayFormat="{0:d}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo("fr-FR")" />
</DataGrid>
```
Our image is well displayed but on the other hand the images of our false data are not present.
Let's modify the code to take into account the following image as the default image.
![Image par défaut](/img/ajouter-item/default.png)
Download the image and place it in the `wwwroot/images/default.png` folder.
![Image par défaut](/img/ajouter-item/default-image.png)
Open the `Pages/List.razor.cs` file and add the highlighted lines:
```csharp title="Pages/List.razor.cs"
...
[Inject]
public ILocalStorageService LocalStorage { get; set; }
// highlight-start
[Inject]
public IWebHostEnvironment WebHostEnvironment { get; set; }
// highlight-end
[Inject]
public NavigationManager NavigationManager { get; set; }
...
```
Open the `Pages/List.razor` file and edit the highlighted lines:
```cshtml title="Pages/List.razor"
<DataGrid TItem="Item"
Data="@items"
ReadData="@OnReadData"
TotalItems="@totalItem"
PageSize="10"
ShowPager
Responsive>
<DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="#" />
// highlight-start
<DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="Image">
<DisplayTemplate>
@if (File.Exists($"{WebHostEnvironment.WebRootPath}/images/{context.Name}.png"))
{
<img src="images/@(context.Name).png" class="img-thumbnail" title="@context.DisplayName" alt="@context.DisplayName" style="max-width: 150px"/>
}
else
{
<img src="images/default.png" class="img-thumbnail" title="@context.DisplayName" alt="@context.DisplayName" style="max-width: 150px"/>
}
</DisplayTemplate>
</DataGridColumn>
// highlight-end
<DataGridColumn TItem="Item" Field="@nameof(Item.DisplayName)" Caption="Display name" />
<DataGridColumn TItem="Item" Field="@nameof(Item.StackSize)" Caption="Stack size" />
<DataGridColumn TItem="Item" Field="@nameof(Item.MaxDurability)" Caption="Maximum durability" />
<DataGridColumn TItem="Item" Field="@nameof(Item.EnchantCategories)" Caption="Enchant categories">
<DisplayTemplate>
@(string.Join(", ", ((Item)context).EnchantCategories))
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="Item" Field="@nameof(Item.RepairWith)" Caption="Repair with">
<DisplayTemplate>
@(string.Join(", ", ((Item)context).RepairWith))
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="Item" Field="@nameof(Item.CreatedDate)" Caption="Created date" DisplayFormat="{0:d}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo("fr-FR")" />
</DataGrid>
```

@ -0,0 +1,26 @@
---
sidebar_position: 1
title: Introduction
---
## Add a new item
This lab will allow you to add a new item.
### List of steps
* Store our data in the LocalStorage
* Creation of a new page
* Add an `Add` button
* Creation of the add-on model
* Creation of the form
* Redirect to listing page
* Display of downloaded image
### List of concepts
* Blazor Storage
* Routing
* Data Annotation
* Form and validation
* URI and navigation state helpers

@ -0,0 +1,151 @@
---
sidebar_position: 2
title: Store our data
---
## Use local data storage
We used our `fake-data.json` file to populate our grid, but in the next steps we will need to add/edit the data.
The easiest way would be to modify the `fake-data.json` file directly but this solution is too simple 😁.
For this we will use the `LocalStorage` of the browser.
## Using Blazored.LocalStorage
In order to facilitate its use, we will use the `Blazored.LocalStorage` nuget.
Install the package with the command `Install-Package Blazored.LocalStorage`
## Parametrer Blazored.LocalStorage
Open the `Program.cs` file and add the highlighted change:
```csharp title="Program.cs"
...
builder.Services
.AddBlazorise()
.AddBootstrapProviders()
.AddFontAwesomeIcons();
// highlight-next-line
builder.Services.AddBlazoredLocalStorage();
var app = builder.Build();
...
```
## Save data to LocalStorage
Open the `Pages/List.razor.cs` file and modify it as follows:
```csharp title="Pages/List.razor.cs"
public partial class List
{
private List<Item> items;
private int totalItem;
[Inject]
public HttpClient Http { get; set; }
[Inject]
public ILocalStorageService LocalStorage { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// Do not treat this action if is not the first render
if (!firstRender)
{
return;
}
var currentData = await LocalStorage.GetItemAsync<Item[]>("data");
// Check if data exist in the local storage
if (currentData == null)
{
// this code add in the local storage the fake data (we load the data sync for initialize the data before load the OnReadData method)
var originalData = Http.GetFromJsonAsync<Item[]>($"{NavigationManager.BaseUri}fake-data.json").Result;
await LocalStorage.SetItemAsync("data", originalData);
}
}
private async Task OnReadData(DataGridReadDataEventArgs<Item> e)
{
if (e.CancellationToken.IsCancellationRequested)
{
return;
}
// When you use a real API, we use this follow code
//var response = await Http.GetJsonAsync<Data[]>( $"http://my-api/api/data?page={e.Page}&pageSize={e.PageSize}" );
var response = (await LocalStorage.GetItemAsync<Item[]>("data")).Skip((e.Page - 1) * e.PageSize).Take(e.PageSize).ToList();
if (!e.CancellationToken.IsCancellationRequested)
{
totalItem = (await LocalStorage.GetItemAsync<List<Item>>("data")).Count;
items = new List<Item>(response); // an actual data for the current page
}
}
}
```
## Concept: Blazor Storage
### Preserve state between browser sessions
As a general rule, persist state across browser sessions where users are actively creating data, not just data that already exists.
To persist state across browser sessions, the application must persist data in a storage location other than browser memory.
State persistence is not automatic. You must take steps during application development to implement stateful data persistence.
Data persistence is typically required only for high-value state that users have spent creating.
In the following examples, persistent state saves time or contributes to business activities:
* Multi-step Web Forms: It is cumbersome for a user to re-enter data for multiple completed steps of a multi-step web form if their state is lost. A user loses state in this scenario if they exit the form and return it later.
* Shopping Carts: Any commercially significant component of an app that represents potential revenue can be maintained. A user who loses their status, and therefore their shopping cart, may purchase fewer products or services when they return to the site later.
An application can persist only Application state. User interfaces cannot be persisted, such as component instances and their render trees. Components and render trees are generally not serializable. To persist UI state, such as the expanded nodes of a Tree View control, the application must use custom code to model the behavior of UI state as the state of the serializable application.
### State preservation location
Common locations exist for persistence state:
* Server side storage
* URLs
* Browser Storage
* In-memory state container service
### Browser storage
For temporary data that the user actively creates, a commonly used storage location is the `localStorage` & `sessionStorage` pools and the browser:
* `localStorage` is limited to the browser window. If the user reloads the page or closes and reopens the browser, the state persists. If the user opens multiple browser tabs, the state is shared across the tabs. Data is retained in `localStorage` until explicitly cleared.
* `sessionStorage` is extended to the browser tab. If the user reloads the tab, the state persists. If the user closes the tab or browser, the state is lost. If the user opens multiple browser tabs, each tab has its own independent version of the data.
:::info
`localStorage` and `sessionStorage` can be used in Blazor WebAssembly apps, but only by writing custom code or using a third-party package.
:::
:::caution
Users can view or alter data stored in `localStorage` and `sessionStorage`.
:::
### Where to find local session data
All recent browsers allow you to view local session data.
To do this, in your browser, press the `F12` key, in the new window:
* Select the `Application` tab
* In `Storage`:
* Select `Local Storage`
* Select your app url
You now have access to all the stored data, you can modify them as well as delete them.
![Ou trouver les données de session locales](/img/ajouter-item/local-session-location.png)

@ -0,0 +1,94 @@
---
sidebar_position: 7
title: Redirection
---
## Perform a redirect
Now that you can add an item, it would be interesting to redirect the user to the list when adding it.
Open the `Pages/Add.razor.cs` file and add the highlighted lines:
```csharp title="Pages/Add.razor.cs"
public partial class Add
{
[Inject]
public ILocalStorageService LocalStorage { get; set; }
// highlight-start
[Inject]
public NavigationManager NavigationManager { get; set; }
// highlight-end
/// <summary>
/// The default roles.
/// </summary>
private List<string> roles = new List<string>() { "admin", "writter", "reader", "member" };
/// <summary>
/// The current data model
/// </summary>
private DataModel dataModel = new()
{
DateOfBirth = DateTime.Now,
Roles = new List<string>()
};
private async void HandleValidSubmit()
{
// Get the current data
var currentData = await LocalStorage.GetItemAsync<List<Data>>("data");
// Simulate the Id
dataModel.Id = currentData.Max(s => s.Id) + 1;
// Add the item to the current data
currentData.Add(new Data
{
LastName = dataModel.LastName,
DateOfBirth = dataModel.DateOfBirth,
FirstName = dataModel.FirstName,
Id = dataModel.Id,
Roles = dataModel.Roles
});
// Save the data
await LocalStorage.SetItemAsync("data", currentData);
// highlight-next-line
NavigationManager.NavigateTo("list");
}
private void OnRoleChange(string item, object checkedValue)
{
if ((bool)checkedValue)
{
if (!dataModel.Roles.Contains(item))
{
dataModel.Roles.Add(item);
}
return;
}
if (dataModel.Roles.Contains(item))
{
dataModel.Roles.Remove(item);
}
}
}
```
## Concept: URI and navigation state helpers
Use `NavigationManager` to manage URIs and navigation in C# code. `NavigationManager` provides the event and methods shown in the following table.
| Membre | Description |
| ---- | ---- |
| Uri | Gets the current absolute URI. |
| BaseUri | Gets the base URI (with a trailing slash) that can be prepended to relative URI paths to produce an absolute URI. Typically, `BaseUri` corresponds to the `href` attribute on the document's `<base>` element in `wwwroot/index.html` (Blazor WebAssembly) or `Pages/_Layout.cshtml` (Blazor Server). |
| NavigateTo | Navigates to the specified URI. If `forceLoad` is `true`: <ul><li>Client-side routing is bypassed.</li><li>The browser is forced to load the new page from the server, whether or not the URI is normally handled by the client-side router.</li></ul>If `replace` is `true`, the current URI in the browser history is replaced instead of pushing a new URI onto the history stack. |
| LocationChanged | An event that fires when the navigation location has changed. |
| ToAbsoluteUri | Converts a relative URI into an absolute URI. |
| ToBaseRelativePath | Given a base URI (for example, a URI previously returned by BaseUri), converts an absolute URI into a URI relative to the base URI prefix. |
| GetUriWithQueryParameter | Returns a URI constructed by updating NavigationManager.Uri with a single parameter added, updated, or removed. |

@ -0,0 +1,3 @@
label: 'API'
position: 11
collapsed: true

@ -0,0 +1,314 @@
---
sidebar_position: 2
title: Image display
---
## Use Base64 images
The image files is not the best way to save image, in many architecture the image are saving in the database.
We modify the code to save the image in the database ;)
### Update the model
Open the item model and add a property which will contain the base64 image.
```csharp title="Models/Item.cs"
public class Item
{
public int Id { get; set; }
public string DisplayName { get; set; }
public string Name { get; set; }
public int StackSize { get; set; }
public int MaxDurability { get; set; }
public List<string> EnchantCategories { get; set; }
public List<string> RepairWith { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime? UpdatedDate { get; set; }
// highlight-next-line
public string ImageBase64 { get; set; }
}
```
```csharp title="Models/ItemModel.cs"
public class ItemModel
{
...
// highlight-next-line
public string ImageBase64 { get; set; }
}
```
### Update the local service
We will also modify the local service to save the image in base64, we remove all reference to save image.
Remove the highlight code !
```csharp title="Services/DataLocalService.cs"
public async Task Add(ItemModel model)
{
// Get the current data
var currentData = await _localStorage.GetItemAsync<List<Item>>("data");
// Simulate the Id
model.Id = currentData.Max(s => s.Id) + 1;
// Add the item to the current data
currentData.Add(ItemFactory.Create(model));
// highlight-start
// Save the image
var imagePathInfo = new DirectoryInfo($"{_webHostEnvironment.WebRootPath}/images");
// Check if the folder "images" exist
if (!imagePathInfo.Exists)
{
imagePathInfo.Create();
}
// Determine the image name
var fileName = new FileInfo($"{imagePathInfo}/{model.Name}.png");
// Write the file content
await File.WriteAllBytesAsync(fileName.FullName, model.ImageContent);
// highlight-end
// Save the data
await _localStorage.SetItemAsync("data", currentData);
}
public async Task Update(int id, ItemModel model)
{
// Get the current data
var currentData = await _localStorage.GetItemAsync<List<Item>>("data");
// Get the item int the list
var item = currentData.FirstOrDefault(w => w.Id == id);
// Check if item exist
if (item == null)
{
throw new Exception($"Unable to found the item with ID: {id}");
}
// highlight-start
// Save the image
var imagePathInfo = new DirectoryInfo($"{_webHostEnvironment.WebRootPath}/images");
// Check if the folder "images" exist
if (!imagePathInfo.Exists)
{
imagePathInfo.Create();
}
// Delete the previous image
if (item.Name != model.Name)
{
var oldFileName = new FileInfo($"{imagePathInfo}/{item.Name}.png");
if (oldFileName.Exists)
{
File.Delete(oldFileName.FullName);
}
}
// Determine the image name
var fileName = new FileInfo($"{imagePathInfo}/{model.Name}.png");
// Write the file content
await File.WriteAllBytesAsync(fileName.FullName, model.ImageContent);
// highlight-end
// Modify the content of the item
ItemFactory.Update(item, model);
// Save the data
await _localStorage.SetItemAsync("data", currentData);
}
public async Task Delete(int id)
{
// Get the current data
var currentData = await _localStorage.GetItemAsync<List<Item>>("data");
// Get the item int the list
var item = currentData.FirstOrDefault(w => w.Id == id);
// Delete item in
currentData.Remove(item);
// highlight-start
// Delete the image
var imagePathInfo = new DirectoryInfo($"{_webHostEnvironment.WebRootPath}/images");
var fileName = new FileInfo($"{imagePathInfo}/{item.Name}.png");
if (fileName.Exists)
{
File.Delete(fileName.FullName);
}
// highlight-end
// Save the data
await _localStorage.SetItemAsync("data", currentData);
}
```
### Update the factory
This is in the factory that we transform the image to base64.
Change the code of the factory :
```csharp title="Factories/ItemFactory.cs"
public static ItemModel ToModel(Item item, byte[] imageContent)
{
return new ItemModel
{
Id = item.Id,
DisplayName = item.DisplayName,
Name = item.Name,
RepairWith = item.RepairWith,
EnchantCategories = item.EnchantCategories,
MaxDurability = item.MaxDurability,
StackSize = item.StackSize,
ImageContent = imageContent,
// highlight-next-line
ImageBase64 = string.IsNullOrWhiteSpace(item.ImageBase64) ? Convert.ToBase64String(imageContent) : item.ImageBase64
};
}
public static Item Create(ItemModel model)
{
return new Item
{
Id = model.Id,
DisplayName = model.DisplayName,
Name = model.Name,
RepairWith = model.RepairWith,
EnchantCategories = model.EnchantCategories,
MaxDurability = model.MaxDurability,
StackSize = model.StackSize,
CreatedDate = DateTime.Now,
// highlight-next-line
ImageBase64 = Convert.ToBase64String(model.ImageContent)
};
}
public static void Update(Item item, ItemModel model)
{
item.DisplayName = model.DisplayName;
item.Name = model.Name;
item.RepairWith = model.RepairWith;
item.EnchantCategories = model.EnchantCategories;
item.MaxDurability = model.MaxDurability;
item.StackSize = model.StackSize;
item.UpdatedDate = DateTime.Now;
// highlight-next-line
item.ImageBase64 = Convert.ToBase64String(model.ImageContent);
}
```
### Update the views
Remove the highlight code !
```csharp title="Pages/Edit.razor.cs"
...
protected override async Task OnInitializedAsync()
{
var item = await DataService.GetById(Id);
var fileContent = await File.ReadAllBytesAsync($"{WebHostEnvironment.WebRootPath}/images/default.png");
// highlight-start
if (File.Exists($"{WebHostEnvironment.WebRootPath}/images/{itemModel.Name}.png"))
{
fileContent = await File.ReadAllBytesAsync($"{WebHostEnvironment.WebRootPath}/images/{item.Name}.png");
}
// highlight-end
// Set the model with the item
itemModel = ItemFactory.ToModel(item, fileContent);
}
...
```
```cshtml title="Pages/Edit.razor"
...
<p>
<label>
Current Item image:
<img src="data:image/png;base64, @(itemModel.ImageBase64)" class="img-thumbnail" title="@itemModel.DisplayName" alt="@itemModel.DisplayName" style="min-width: 50px; max-width: 150px"/>
</label>
</p>
...
```
```cshtml title="Pages/List.razor"
...
<DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="Image">
<DisplayTemplate>
@if (!string.IsNullOrWhiteSpace(context.ImageBase64))
{
<img src="data:image/png;base64, @(context.ImageBase64)" class="img-thumbnail" title="@context.DisplayName" alt="@context.DisplayName" style="min-width: 50px; max-width: 150px" />
}
else
{
<img src="images/default.png" class="img-thumbnail" title="@context.DisplayName" alt="@context.DisplayName" style="max-width: 150px"/>
}
</DisplayTemplate>
</DataGridColumn>
...
```
## Concept: Base64 Images
To understand base64 images, first lets understand why we need it.
Web developers from all around the world is currently trying to minimise unusual data to maximise their website performance.
Website performance can be optimised by reducing page size, reducing number of requests, position of scripts and stylesheets & many other factors affect webpage speed.
Base64 method is used to minimise the server requests by integrating image data in HTML code.
Base64 method uses Data URI and its syntax is as below:
```html title="Base64 Syntax"
data:[<MIMETYPE>][;charset=<CHARSET>][;base64],<DATA>
```
To integrate it in HTML you have to write html img tag code like this:
```html title="Base64 image in img TagXHTML"
<img alt="coderiddles" src="data:image/jpg;base64,S)OWE902WEIWUOLKASJODIIWJ9878978JKKJKIWEURU4954590EJ09JT9T99TIR32EQ2EKJDKSDFDNXZCNBAC3SASDASD45ASD5ASD5A4SDASD54DS56DB67V6VBN78M90687LKJKSDFJSDF76F7S7D78F9SDF78S6FSDF9SDFSFSDGFSDFSDLFJSDF7SD86FSDFSDFSDFS8F89SDIFOSDFSFJHKJL" />
```
We can easily convert base64 images using [online tool](https://www.base64-image.de/).
We can also integrate base64 images inside stylesheet (background-image property).
```css title="Base64 image inside StylesheetCSS"
.imagediv
{
background: url(data:image/jpg;base64,S)OWE902WEIWUOLKASJODIIWJ9878978JKKJKIWEURU4954590EJ09JT9T99TIR32EQ2EKJDKSDFDNXZCNBAC3SASDASD45ASD5ASD5A4SDASD54DS56DB67V6VBN78M90687LKJKSDFJSDF76F7S7D78F9SDF78S6FSDF9SDFSFSDGFSDFSDLFJSDF7SD86FSDFSDFSDFS8F89SDIFOSDFSFJHKJL);
}
```
### Base64 images can increase performance?
If we encode all our websites small images, wrap them inside css or html code. Doing this will reduce the number of HTTP requests because client (browser) dont have to issue separate request for images.
That will definitely increase performance of website. Because most of page load time is taken for transferring data over internet. Not to process it on server.
**Advantages of Base64 images:**
* Removes separate HTTP Requests for image loading by wrapping encoded image code inside css or HTML.
* Image encoded data can be saved inside database and can generate image file. Just incase we lost image file copy.
**Disadvantages Base64 images:**
* Though Base64 increases performance but be careful. Doing so will increase image size approximately 20-25%. than what it is actually in its binary form. So more data has to be transferred on internet. For mobile devices it is a bit drawback.
* Even if we apply gzip compression, doing so will only decrease css file size to around 10-12%.
* IE6 & IE7 not supports Data URI which means base64 images will not be loaded in ie6 & ie7 browsers.
* If you apply base64 encoding to lots of medium sized images it will increase HTML content size or CSS content size. So browser has to do roundtrip to get full content.

@ -0,0 +1,18 @@
---
sidebar_position: 1
title: Introduction
---
## API
We will see how API calls work.
### List of steps
* Image display
* Make HTTP requests
### List of concepts
* Base64 Images
* IHttpClientFactory

@ -0,0 +1,448 @@
---
sidebar_position: 3
title: Make HTTP requests
---
## Data format
Two operations exist to retrieve data from an API, the first is to retrieve raw data in JSON format, the second is to use serialization / deserialization.
In our case, the library used implements serialization / deserialization by default.
In order to be able to manipulate our data we will use our `Item` component class locally, the data received comes from the serialization of the `Item` class of the server.
```csharp title="Models/Item.cs"
public class Item
{
public int Id { get; set; }
public string DisplayName { get; set; }
public string Name { get; set; }
public int StackSize { get; set; }
public int MaxDurability { get; set; }
public List<string> EnchantCategories { get; set; }
public List<string> RepairWith { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime? UpdatedDate { get; set; }
public string ImageBase64 { get; set; }
}
```
:::info
Adding or deleting fields in our model does not generate an error when retrieving data.
:::
## Using the IOC
Thanks to our IOC we will therefore call our API in a specific service implementing the `IDataService` interface.
Create the `DataApiService` class:
```csharp title="Services/DataApiService.cs"
public class DataApiService : IDataService
{
private readonly HttpClient _http;
public DataApiService(
HttpClient http)
{
_http = http;
}
public async Task Add(ItemModel model)
{
// Get the item
var item = ItemFactory.Create(model);
// Save the data
await _http.PostAsJsonAsync("https://localhost:7234/api/Crafting/", item);
}
public async Task<int> Count()
{
return await _http.GetFromJsonAsync<int>("https://localhost:7234/api/Crafting/count");
}
public async Task<List<Item>> List(int currentPage, int pageSize)
{
return await _http.GetFromJsonAsync<List<Item>>($"https://localhost:7234/api/Crafting/?currentPage={currentPage}&pageSize={pageSize}");
}
public async Task<Item> GetById(int id)
{
return await _http.GetFromJsonAsync<Item>($"https://localhost:7234/api/Crafting/{id}");
}
public async Task Update(int id, ItemModel model)
{
// Get the item
var item = ItemFactory.Create(model);
await _http.PutAsJsonAsync($"https://localhost:7234/api/Crafting/{id}", item);
}
public async Task Delete(int id)
{
await _http.DeleteAsync($"https://localhost:7234/api/Crafting/{id}");
}
public async Task<List<CraftingRecipe>> GetRecipes()
{
return await _http.GetFromJsonAsync<List<CraftingRecipe>>("https://localhost:7234/api/Crafting/recipe");
}
}
```
We now have a class allowing us to pass all of our calls through an API.
## Register the data service
Open the `Program.cs` file and edit the following line:
```csharp title="Program.cs"
...
builder.Services.AddScoped<IDataService, DataApiService>();
...
```
## Add the API sample to your project
Download this [project](/Minecraft.Crafting.Api.zip).
Unzip the file in the directory of your project, at the same place of the directory of the Blazor Project.
Example:
![Sample Api Location](/img/api/sample-api-location.png)
On Visual Studio, click right on the solution and choose `Add => Existing Project...`
![Add Existing Project](/img/api/add-existing-project.png)
Select the file `Minecraft.Crafting.Api.csproj` in the directory `Minecraft.Crafting.Api`.
Your solution now contains two projects, your client and the sample API.
## How to start two projects at same time in Visual Studio
For test your client with the API, you have to start the two projects at same time.
For this, on Visual Studio, click right on the solution and choose `Properties`
![Solution Properties](/img/api/solution-properties.png)
On the new screen select "Multiple startup projects" and select "Start" for the two projects.
![Multiple startup projects](/img/api/multiple-startup-projects.png)
When you start debug mode, the two projects are started.
## Concept: IHttpClientFactory
An `IHttpClientFactory` can be registered and used to configure and create HttpClient instances in an app.
`IHttpClientFactory` offers the following benefits:
* Provides a central location for naming and configuring logical `HttpClient` instances. For example, a client named github could be registered and configured to access GitHub. A default client can be registered for general access.
* Codifies the concept of outgoing middleware via delegating handlers in `HttpClient`. Provides extensions for Polly-based middleware to take advantage of delegating handlers in `HttpClient`.
* Manages the pooling and lifetime of underlying `HttpClientMessageHandler` instances. Automatic management avoids common DNS (Domain Name System) problems that occur when manually managing `HttpClient` lifetimes.
* Adds a configurable logging experience (via `ILogger`) for all requests sent through clients created by the factory.
### Consumption patterns
There are several ways IHttpClientFactory can be used in an app:
* Basic usage
* Named clients
* Typed clients
* Generated clients
The best approach depends upon the app's requirements.
#### Basic usage
Register `IHttpClientFactory` by calling `AddHttpClient` in `Program.cs`:
```csharp
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// highlight-next-line
builder.Services.AddHttpClient();
```
An `IHttpClientFactory` can be requested using dependency injection (DI). The following code uses `IHttpClientFactory` to create an `HttpClient` instance:
```csharp
public class BasicModel : PageModel
{
private readonly IHttpClientFactory _httpClientFactory;
// highlight-next-line
public BasicModel(IHttpClientFactory httpClientFactory) => _httpClientFactory = httpClientFactory;
public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }
public async Task OnGet()
{
var httpRequestMessage = new HttpRequestMessage(
HttpMethod.Get,
"https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
{
Headers =
{
{ HeaderNames.Accept, "application/vnd.github.v3+json" },
{ HeaderNames.UserAgent, "HttpRequestsSample" }
}
};
// highlight-next-line
var httpClient = _httpClientFactory.CreateClient();
var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);
if (httpResponseMessage.IsSuccessStatusCode)
{
using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync();
GitHubBranches = await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubBranch>>(contentStream);
}
}
}
```
Using `IHttpClientFactory` like in the preceding example is a good way to refactor an existing app.
It has no impact on how `HttpClient` is used. In places where `HttpClient` instances are created in an existing app, replace those occurrences with calls to `CreateClient`.
#### Named clients
Named clients are a good choice when:
* The app requires many distinct uses of `HttpClient`.
* Many `HttpClients` have different configuration.
Specify configuration for a named `HttpClient` during its registration in `Program.cs`:
```csharp
builder.Services.AddHttpClient("GitHub", httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");
// using Microsoft.Net.Http.Headers;
// The GitHub API requires two headers.
httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
httpClient.DefaultRequestHeaders.Add(
HeaderNames.UserAgent, "HttpRequestsSample");
});
```
In the preceding code the client is configured with:
* The base address https://api.github.com/.
* Two headers required to work with the GitHub API.
##### CreateClient
Each time CreateClient is called:
* A new instance of `HttpClient` is created.
* The configuration action is called.
To create a named client, pass its name into `CreateClient`:
```csharp
public class NamedClientModel : PageModel
{
private readonly IHttpClientFactory _httpClientFactory;
public NamedClientModel(IHttpClientFactory httpClientFactory) =>
_httpClientFactory = httpClientFactory;
public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }
public async Task OnGet()
{
// highlight-next-line
var httpClient = _httpClientFactory.CreateClient("GitHub");
var httpResponseMessage = await httpClient.GetAsync(
"repos/dotnet/AspNetCore.Docs/branches");
if (httpResponseMessage.IsSuccessStatusCode)
{
using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync();
GitHubBranches = await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubBranch>>(contentStream);
}
}
}
```
In the preceding code, the request doesn't need to specify a hostname. The code can pass just the path, since the base address configured for the client is used.
#### Typed clients
Typed clients:
* Provide the same capabilities as named clients without the need to use strings as keys.
* Provides IntelliSense and compiler help when consuming clients.
* Provide a single location to configure and interact with a particular `HttpClient`. For example, a single typed client might be used:
* For a single backend endpoint.
* To encapsulate all logic dealing with the endpoint.
* Work with DI and can be injected where required in the app.
A typed client accepts an `HttpClient` parameter in its constructor:
```csharp
public class GitHubService
{
private readonly HttpClient _httpClient;
// highlight-next-line
public GitHubService(HttpClient httpClient)
{
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri("https://api.github.com/");
// using Microsoft.Net.Http.Headers;
// The GitHub API requires two headers.
_httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
_httpClient.DefaultRequestHeaders.Add(
HeaderNames.UserAgent, "HttpRequestsSample");
}
public async Task<IEnumerable<GitHubBranch>?> GetAspNetCoreDocsBranchesAsync() =>
await _httpClient.GetFromJsonAsync<IEnumerable<GitHubBranch>>(
"repos/dotnet/AspNetCore.Docs/branches");
}
```
In the preceding code:
* The configuration is moved into the typed client.
* The provided `HttpClient` instance is stored as a private field.
API-specific methods can be created that expose `HttpClient` functionality. For example, the `GetAspNetCoreDocsBranches` method encapsulates code to retrieve docs GitHub branches.
The following code calls `AddHttpClient` in `Program.cs` to register the `GitHubService` typed client class:
```csharp
builder.Services.AddHttpClient<GitHubService>();
```
The typed client is registered as transient with DI. In the preceding code, `AddHttpClient` registers `GitHubService` as a transient service. This registration uses a factory method to:
* Create an instance of `HttpClient`.
* Create an instance of `GitHubService`, passing in the instance of `HttpClient` to its constructor.
The typed client can be injected and consumed directly:
```csharp
public class TypedClientModel : PageModel
{
private readonly GitHubService _gitHubService;
// highlight-next-line
public TypedClientModel(GitHubService gitHubService) => _gitHubService = gitHubService;
public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }
public async Task OnGet()
{
try
{
// highlight-next-line
GitHubBranches = await _gitHubService.GetAspNetCoreDocsBranchesAsync();
}
catch (HttpRequestException)
{
// ...
}
}
}
```
The configuration for a typed client can also be specified during its registration in `Program.cs`, rather than in the typed client's constructor:
```csharp
builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");
// ...
});
```
#### Generated clients
`IHttpClientFactory` can be used in combination with third-party libraries such as Refit. Refit is a REST library for .NET. It converts REST APIs into live interfaces.
Call `AddRefitClient` to generate a dynamic implementation of an interface, which uses `HttpClient` to make the external HTTP calls.
A custom interface represents the external API:
```csharp
public interface IGitHubClient
{
[Get("/repos/dotnet/AspNetCore.Docs/branches")]
Task<IEnumerable<GitHubBranch>> GetAspNetCoreDocsBranchesAsync();
}
```
Call `AddRefitClient` to generate the dynamic implementation and then call `ConfigureHttpClient` to configure the underlying `HttpClient`:
```csharp
// highlight-next-line
builder.Services.AddRefitClient<IGitHubClient>()
.ConfigureHttpClient(httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");
// using Microsoft.Net.Http.Headers;
// The GitHub API requires two headers.
httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
httpClient.DefaultRequestHeaders.Add(
HeaderNames.UserAgent, "HttpRequestsSample");
});
```
Use DI to access the dynamic implementation of `IGitHubClient`:
```csharp
public class RefitModel : PageModel
{
private readonly IGitHubClient _gitHubClient;
// highlight-next-line
public RefitModel(IGitHubClient gitHubClient) => _gitHubClient = gitHubClient;
public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }
public async Task OnGet()
{
try
{
// highlight-next-line
GitHubBranches = await _gitHubClient.GetAspNetCoreDocsBranchesAsync();
}
catch (ApiException)
{
// ...
}
}
}
```
### HttpClient request type
HttpClient supports other HTTP verbs:
| Properties | Verbe |
| ---- | ---- |
| Delete | Represents an HTTP DELETE protocol method. |
| Get | Represents an HTTP GET protocol method. |
| Head | Represents an HTTP HEAD protocol method. The HEAD method is identical to GET except that the server only returns message-headers in the response, without a message-body. |
| Method | An HTTP method. |
| Options | Represents an HTTP OPTIONS protocol method. |
| Patch | Gets the HTTP PATCH protocol method. |
| Post | Represents an HTTP POST protocol method that is used to post a new entity as an addition to a URI. |
| Put | Represents an HTTP PUT protocol method that is used to replace an entity identified by a URI. |
| Trace | Represents an HTTP TRACE protocol method. |

@ -0,0 +1,3 @@
label: 'Bonus'
position: 18
collapsed: true

@ -0,0 +1,652 @@
---
sidebar_position: 3
title: Authentication
---
ASP.NET Core supports configuration and management of security in Blazor apps.
Security scenarios differ between Blazor Server and Blazor WebAssembly apps.
Because Blazor Server apps run on the server, permission checks can determine the following:
* User interface options presented to a user (for example, menu entries available to a user).
* Access rules for application areas and components.
Blazor WebAssembly apps run on the client.
Authorization is only used to determine which UI options to display.
Because client-side controls can be modified or overridden by a user, a Blazor WebAssembly app can't enforce authorization access rules.
For our examples we will use a small example project available [here](/DemoAuthentication.zip).
## Creating a client application
We are going to create a client application to manage local authentication.
Create a new Blazor WASM app.
Install the `Microsoft.AspNetCore.Components.Authorization` package in version 5.0.13.
![required library](/img/authentication/nuget-Microsoft.AspNetCore.Components.Authorization.png)
Or using the Package Manager console: `PM> Install-Package Microsoft.AspNetCore.Components.Authorization -Version 5.0.13`
:::caution
In Blazor WebAssembly apps, authentication checks can be skipped because all client-side code can be modified by users.
This also applies to all client-side application technologies, including JavaScript SPA application frameworks or native applications for any operating system.
:::
## Models
As usual, we need to create the model classes that would take various authentication parameters for login and registration of new users.
We will create these classes in the `Models` folder.
```csharp title="Models/RegisterRequest.cs"
public class RegisterRequest
{
[Required]
public string Password { get; set; }
[Required]
[Compare(nameof(Password), ErrorMessage = "Passwords do not match!")]
public string PasswordConfirm { get; set; }
[Required]
public string UserName { get; set; }
}
```
```csharp title="Models/LoginRequest.cs"
public class LoginRequest
{
[Required]
public string Password { get; set; }
[Required]
public string UserName { get; set; }
}
```
```csharp title="Models/CurrentUser.cs"
public class CurrentUser
{
public Dictionary<string, string> Claims { get; set; }
public bool IsAuthenticated { get; set; }
public string UserName { get; set; }
}
```
```csharp title="Models/AppUser.cs"
public class AppUser
{
public string Password { get; set; }
public List<string> Roles { get; set; }
public string UserName { get; set; }
}
```
We now have classes to help persist authentication settings.
## Creating the authentication service
We will now create our authentication service, this one will use a list of users in memory, only the user `Admin` will be defined by default.
Add a new interface in the project.
```csharp title="Services/IAuthService.cs"
public interface IAuthService
{
CurrentUser GetUser(string userName);
void Login(LoginRequest loginRequest);
void Register(RegisterRequest registerRequest);
}
```
Let's add a concrete class and implement the previous interface.
```csharp title="Services/AuthService.cs"
public class AuthService : IAuthService
{
private static readonly List<AppUser> CurrentUser;
static AuthService()
{
CurrentUser = new List<AppUser>
{
new AppUser { UserName = "Admin", Password = "123456", Roles = new List<string> { "admin" } }
};
}
public CurrentUser GetUser(string userName)
{
var user = CurrentUser.FirstOrDefault(w => w.UserName == userName);
if (user == null)
{
throw new Exception("User name or password invalid !");
}
var claims = new List<Claim>();
claims.AddRange(user.Roles.Select(s => new Claim(ClaimTypes.Role, s)));
return new CurrentUser
{
IsAuthenticated = true,
UserName = user.UserName,
Claims = claims.ToDictionary(c => c.Type, c => c.Value)
};
}
public void Login(LoginRequest loginRequest)
{
var user = CurrentUser.FirstOrDefault(w => w.UserName == loginRequest.UserName && w.Password == loginRequest.Password);
if (user == null)
{
throw new Exception("User name or password invalid !");
}
}
public void Register(RegisterRequest registerRequest)
{
CurrentUser.Add(new AppUser { UserName = registerRequest.UserName, Password = registerRequest.Password, Roles = new List<string> { "guest" } });
}
}
```
## Authentication State Provider
As the name suggests, this class provides the authentication state of the user in Blazor Applications.
`AuthenticationStateProvider` is an abstract class in the `Authorization` namespace.
Blazor uses this class which will be inherited and replaced by us with a custom implementation to get user state.
This state can come from session storage, cookies or local storage as in our case.
Let's start by adding the Provider class to the `Services` folder. Let's call it `CustomStateProvider`. As mentioned, this class will inherit from the `AuthenticationStateProvider` class.
```csharp title="Services/CustomStateProvider.cs"
public class CustomStateProvider : AuthenticationStateProvider
{
private readonly IAuthService _authService;
private CurrentUser _currentUser;
public CustomStateProvider(IAuthService authService)
{
this._authService = authService;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var identity = new ClaimsIdentity();
try
{
var userInfo = GetCurrentUser();
if (userInfo.IsAuthenticated)
{
var claims = new[] { new Claim(ClaimTypes.Name, _currentUser.UserName) }.Concat(_currentUser.Claims.Select(c => new Claim(c.Key, c.Value)));
identity = new ClaimsIdentity(claims, "Server authentication");
}
}
catch (HttpRequestException ex)
{
Console.WriteLine("Request failed:" + ex);
}
return new AuthenticationState(new ClaimsPrincipal(identity));
}
public async Task Login(LoginRequest loginParameters)
{
_authService.Login(loginParameters);
// No error - Login the user
var user = _authService.GetUser(loginParameters.UserName);
_currentUser = user;
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task Logout()
{
_currentUser = null;
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task Register(RegisterRequest registerParameters)
{
_authService.Register(registerParameters);
// No error - Login the user
var user = _authService.GetUser(registerParameters.UserName);
_currentUser = user;
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
private CurrentUser GetCurrentUser()
{
if (_currentUser != null && _currentUser.IsAuthenticated)
{
return _currentUser;
}
return new CurrentUser();
}
}
```
You can see that, by default, we manage to implement a `GetAuthenticationStateAsync` function.
Now, this function is quite important because Blazor calls it very often to check the authentication status of the user in the application.
Additionally, we will inject the `AuthService` service and the `CurrentUser` model to the constructor of this class.
The idea behind this is that we will not directly use the service instance in the view (Razor components), rather we will inject the `CustomStateProvider` instance in our views which will in turn access the services.
**Explanation**
`GetAuthenticationStateAsync` - Get the current user from the service object. if the user is authenticated, we will add their claims to a list and create a claims identity. After that, we will return an authentication state with the required data.
The other 3 methods are quite simple. We will simply call the required service methods. But here is one more thing I would like to explain.
Now, with each connection, registration, disconnection, there is technically a change of state in the authentication.
We need to notify the entire application that the user's state has changed.
Therefore, we use a notify method and pass the current authentication state by calling `GetAuthenticationStateAsync`. Pretty logical, huh?
Now, to enable these services and dependencies in the project, we need to register them in the IOC, right?
To do this, navigate to the project's `Program.cs` and make the following additions.
```csharp {6-10} title="Program.cs"
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<CustomStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(s => s.GetRequiredService<CustomStateProvider>());
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
}
```
Now let's work on adding the Razor components for the UI.
Here we are going to add 2 components i.e. Login component and Register component.
We will protect the entire application by making it secure.
This means that only an authenticated user will be allowed to view page data.
Thus, we will have a separate layout component for the login/registration components.
Go to the `Shared` folder of the project.
Here is where you should ideally add all shared Razor components.
In our case, let's add a new Razor component and call it, `AuthLayout.razor`.
```html title="Shared/AuthLayout.razor"
@inherits LayoutComponentBase
<div class="main">
<div class="content px-4">
@Body
</div>
</div>
```
You can see it's a pretty basic html file with an inheritance tag.
We won't focus on css/html.
However, this layout component will act as a container that will hold the login and register component itself.
## Login Component
```html title="Pages/Authentication/Login.razor"
@page "/login"
@layout AuthLayout
<h1 class="h2 font-weight-normal login-title">
Login
</h1>
<EditForm class="form-signin" OnValidSubmit="OnSubmit" Model="loginRequest">
<DataAnnotationsValidator />
<label for="inputUsername" class="sr-only">User Name</label>
<InputText id="inputUsername" class="form-control" @bind-Value="loginRequest.UserName" autofocus placeholder="Username" />
<ValidationMessage For="@(() => loginRequest.UserName)" />
<label for="inputPassword" class="sr-only">Password</label>
<InputText type="password" id="inputPassword" class="form-control" placeholder="Password" @bind-Value="loginRequest.Password" />
<ValidationMessage For="@(() => loginRequest.Password)" />
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
<label class="text-danger">@error</label>
<NavLink href="register">
<h6 class="font-weight-normal text-center">Create account</h6>
</NavLink>
</EditForm>
```
```csharp title="Pages/Authentication/Login.razor.cs"
public partial class Login
{
[Inject]
public CustomStateProvider AuthStateProvider { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
private string error { get; set; }
private LoginRequest loginRequest { get; set; } = new LoginRequest();
private async Task OnSubmit()
{
error = null;
try
{
await AuthStateProvider.Login(loginRequest);
NavigationManager.NavigateTo("");
}
catch (Exception ex)
{
error = ex.Message;
}
}
}
```
## Register Component
```html title="Pages/Authentication/Register.razor"
@page "/register"
@layout AuthLayout
<h1 class="h2 font-weight-normal login-title">
Register
</h1>
<EditForm class="form-signin" OnValidSubmit="OnSubmit" Model="registerRequest">
<DataAnnotationsValidator />
<label for="inputUsername" class="sr-only">User Name</label>
<InputText id="inputUsername" class="form-control" placeholder="Username" autofocus @bind-Value="@registerRequest.UserName" />
<ValidationMessage For="@(() => registerRequest.UserName)" />
<label for="inputPassword" class="sr-only">Password</label>
<InputText type="password" id="inputPassword" class="form-control" placeholder="Password" @bind-Value="@registerRequest.Password" />
<ValidationMessage For="@(() => registerRequest.Password)" />
<label for="inputPasswordConfirm" class="sr-only">Password Confirmation</label>
<InputText type="password" id="inputPasswordConfirm" class="form-control" placeholder="Password Confirmation" @bind-Value="@registerRequest.PasswordConfirm" />
<ValidationMessage For="@(() => registerRequest.PasswordConfirm)" />
<button class="btn btn-lg btn-primary btn-block" type="submit">Create account</button>
<label class="text-danger">@error</label>
<NavLink href="login">
<h6 class="font-weight-normal text-center">Already have an account? Click here to login</h6>
</NavLink>
</EditForm>
```
```csharp title="Pages/Authentication/Register.razor.cs"
public partial class Register
{
[Inject]
public CustomStateProvider AuthStateProvider { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
private string error { get; set; }
private RegisterRequest registerRequest { get; set; } = new RegisterRequest();
private async Task OnSubmit()
{
error = null;
try
{
await AuthStateProvider.Register(registerRequest);
NavigationManager.NavigateTo("");
}
catch (Exception ex)
{
error = ex.Message;
}
}
}
```
## Enable authentication
Now we need to let Blazor know that authentication is enabled and we need to pass the `Authorize` attribute throughout the application.
To do this, modify the main component, i.e. the `App.razor` component.
```html title="App.razor"
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<CascadingAuthenticationState>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</CascadingAuthenticationState>
</NotFound>
</Router>
```
### What is `AuthorizeRoteView`?
It is a combination of `AuthorizeView` and `RouteView`, so it shows the specific page but only to authorized users.
### What is `CascadingAuthenticationState`?
This provides a cascading parameter to all descendant components.
Let's add a logout button to the top navigation bar of the app to allow the user to logout as well as force redirect to the `Login` page if the user is not logged in.
We will need to make changes to the `MainLayout.razor` component for this.
```html {11} title="Shared/MainLayout.razor"
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<div class="top-row px-4">
<a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
<button type="button" class="btn btn-link ml-md-auto" @onclick="@LogoutClick">Logout</button>
</div>
<div class="content px-4">
@Body
</div>
</div>
</div>
```
```csharp title="Shared/MainLayout.razor.cs"
public partial class MainLayout
{
[Inject]
public CustomStateProvider AuthStateProvider { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
[CascadingParameter]
private Task<AuthenticationState> AuthenticationState { get; set; }
protected override async Task OnParametersSetAsync()
{
if (!(await AuthenticationState).User.Identity.IsAuthenticated)
{
NavigationManager.NavigateTo("/login");
}
}
private async Task LogoutClick()
{
await AuthStateProvider.Logout();
NavigationManager.NavigateTo("/login");
}
}
```
We want to display our username and these roles on the index page soon after we log in.
Go to `Pages/Index.razor` and make the following changes.
```html title="Pages/Index.razor"
@page "/"
<AuthorizeView>
<Authorized>
<h1>Hello @context.User.Identity.Name !!</h1>
<p>Welcome to Blazor Learner.</p>
<ul>
@foreach (var claim in context.User.Claims)
{
<li>@claim.Type: @claim.Value</li>
}
</ul>
</Authorized>
<Authorizing>
<h1>Loading ...</h1>
</Authorizing>
<NotAuthorized>
<h1>Authentication Failure!</h1>
<p>You're not signed in.</p>
</NotAuthorized>
</AuthorizeView>
```
You can see there's a bunch of unsolvable errors that probably indicate few things are undefined in the namespace or something.
This is because we didn't import the new namespaces.
To do this, navigate to `_Import.razor` and add these lines at the bottom.
```html title="_Imports.razor"
@using Microsoft.AspNetCore.Components.Authorization
```
Similarly we want to create an administration page accessible only by users with the `admin` role.
Create the `Pages/Admin.razor` page:
```html title="Pages/Admin.razor"
@page "/admin"
<h3>Admin Page</h3>
```
We are going to put in our menu our new page:
```html {10-16} title="Shared/NavMenu.razor"
...
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<ul class="nav flex-column">
<li class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>
</li>
<AuthorizeView Roles="admin">
<li class="nav-item px-3">
<NavLink class="nav-link" href="admin" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Admin page
</NavLink>
</li>
</AuthorizeView>
</ul>
</div>
...
```
## Concept: Composant AuthorizeView
The `AuthorizeView` component displays UI content selectively, depending on whether the user is authorized or not.
This approach is useful when you only need to display user data and don't need to use the user's identity in procedural logic.
The component exposes a context variable of type `AuthenticationState`, which you can use to access information about the logged-in user:
```html
<AuthorizeView>
<h1>Hello, @context.User.Identity.Name!</h1>
<p>You can only see this content if you're authenticated.</p>
</AuthorizeView>
```
You can also provide different content for display if the user is not authorized:
```html
<AuthorizeView>
<Authorized>
<h1>Hello, @context.User.Identity.Name!</h1>
<p>You can only see this content if you're authorized.</p>
<button @onclick="SecureMethod">Authorized Only Button</button>
</Authorized>
<NotAuthorized>
<h1>Authentication Failure!</h1>
<p>You're not signed in.</p>
</NotAuthorized>
</AuthorizeView>
@code {
private void SecureMethod() { ... }
}
```
The content of the `<Authorized>` and `<NotAuthorized>` tags can include arbitrary elements, such as other interactive components.
A default event handler for an authorized element, such as the `SecureMethod` method of the `<button>` element in the preceding example, can be called only by an authorized user.
Authorization requirements, such as roles or policies that control user interface or access options, are covered in the `Authorization` section.
If authorization conditions are not specified, `AuthorizeView` uses a default policy and processes:
* Users authenticated (logged in) as authorized.
* Unauthenticated (logged out) users as unauthorized.
## Concept: Authorization based on role and policy
The `AuthorizeView` component supports both role-based and policy-based authorization.
For role-based authorization, use the `Roles` parameter:
```html
<AuthorizeView Roles="admin, superuser">
<p>You can only see this if you're an admin or superuser.</p>
</AuthorizeView>
```
## Concept: Attribut [Authorize]
The [Authorize] attribute can be used in Razor components:
```html
@page "/"
@attribute [Authorize]
You can only see this if you're signed in.
```
:::caution
Only use `[Authorize]` on `@page` components reached through the Blazor router.
Authorization is done only as an aspect of routing and not for child components rendered in a page.
To authorize viewing specific items in a page, use `AuthorizeView` instead.
:::
The [Authorize] attribute also supports role-based or policy-based authorization. For role-based authorization, use the `Roles` parameter:
```html
@page "/"
@attribute [Authorize(Roles = "admin, superuser")]
<p>You can only see this if you're in the 'admin' or 'superuser' role.</p>
```

@ -0,0 +1,332 @@
---
sidebar_position: 2
title: Graphql
---
For our examples we will use a small example project based on an SQLite database.
This example is available [here](/DemoGraphQL.zip).
## Creating a client application
We will create a client application to consume GraphQL.
Create a new Blazor WASM app.
Edit the `appsettings.json` file, adding an address to the GraphQL app:
```csharp title="wwwroot/appsettings.json"
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"GraphQLURI": "https://localhost:44371/graphql",
"AllowedHosts": "*"
}
```
Install the `GraphQL.Client` package in its latest version.
![required library](/img/graphql/29-GraphQL.CLient-new-version.png)
Or using the Package Manager console: `PM> Install-Package GraphQL.Client`
Install the GraphQL serialization nuget `GraphQL.Client.Serializer.Newtonsoft`:
![required library](/img/graphql/30-GraphQL-Serializer-Newtonsoft.png)
`PM> Install-Package GraphQL.Client.Serializer.Newtonsoft`
After installation, we will save it in the `Program` class:
```csharp {8} title="Program.cs"
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<IGraphQLClient>(s => new GraphQLHttpClient(builder.Configuration["GraphQLURI"], new NewtonsoftJsonSerializer()));
await builder.Build().RunAsync();
}
```
The next step is to create the `OwnerConsumer` class, which will store all requests and mutations:
```csharp title="OwnerConsumer.cs"
public class OwnerConsumer
{
private readonly IGraphQLClient _client;
public OwnerConsumer(IGraphQLClient client)
{
_client = client;
}
}
```
Now let's register this class:
```csharp {9} title="Program.cs"
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<IGraphQLClient>(s => new GraphQLHttpClient(builder.Configuration["GraphQLURI"], new NewtonsoftJsonSerializer()));
builder.Services.AddScoped<OwnerConsumer>();
await builder.Build().RunAsync();
}
```
We have finished everything regarding the configuration.
## Creating model classes
We will create the model classes so that we can use the data from our queries:
```csharp title="Models/TypeOfAccount.cs"
public enum TypeOfAccount
{
Cash,
Savings,
Expense,
Income
}
```
```csharp title="Models/Account.cs"
public class Account
{
public Guid Id { get; set; }
public TypeOfAccount Type { get; set; }
public string Description { get; set; }
}
```
```csharp title="Models/Owner.cs"
public class Owner
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public ICollection<Account> Accounts { get; set; }
}
```
Now we are going to create the `Input` class for the mutation actions.
```csharp title="Models/OwnerInput.cs"
public class OwnerInput
{
public string Name { get; set; }
public string Address { get; set; }
}
```
We have now prepared everything and are ready to start creating queries and mutations.
## Creating queries and mutations to consume the GraphQL API
Open the `OwnerConsumer` class and add the `GetAllOwners` method:
```csharp title="OwnerConsumer.cs"
public async Task<List<Owner>> GetAllOwners()
{
var query = new GraphQLRequest
{
Query = @"
query ownersQuery{
owners {
id
name
address
accounts {
id
type
description
}
}
}"
};
var response = await _client.SendQueryAsync<ResponseOwnerCollectionType>(query);
return response.Data.Owners;
}
```
As you can see, we are creating a new `GraphQLRequest` object which contains a `Query` property for the query we want to send to the GraphQL API.
This query is the same one you can use with the `UI.Playground` tool.
To execute the query, call the `SenQueryAsync` method which accepts a response type (as a generic parameter) and the query.
Finally, the code returns the list of owners from this response.
We don't have the `ResponseOwnerCollectionType` class, so let's create a new `ResponseTypes` folder and inside it create the two new classes:
```csharp title="ResponseTypes/ResponseOwnerCollectionType.cs"
public class ResponseOwnerCollectionType
{
public List<Owner> Owners { get; set; }
}
```
```csharp title="ResponseTypes/ResponseOwnerType.cs"
public class ResponseOwnerType
{
public Owner Owner { get; set; }
}
```
### Get Query
```csharp title="OwnerConsumer.cs"
public async Task<Owner> GetOwner(Guid id)
{
var query = new GraphQLRequest
{
Query = @"
query ownerQuery($ownerID: ID!) {
owner(ownerId: $ownerID) {
id
name
address
accounts {
id
type
description
}
}
}",
Variables = new { ownerID = id }
};
var response = await _client.SendQueryAsync<ResponseOwnerType>(query);
return response.Data.Owner;
}
```
### Create Mutation
```csharp title="OwnerConsumer.cs"
public async Task<Owner> CreateOwner(OwnerInput ownerToCreate)
{
var query = new GraphQLRequest
{
Query = @"
mutation($owner: ownerInput!){
createOwner(owner: $owner){
id,
name,
address
}
}",
Variables = new {owner = ownerToCreate}
};
var response = await _client.SendMutationAsync<ResponseOwnerType>(query);
return response.Data.Owner;
}
```
### Update Mutation
```csharp title="OwnerConsumer.cs"
public async Task<Owner> UpdateOwner(Guid id, OwnerInput ownerToUpdate)
{
var query = new GraphQLRequest
{
Query = @"
mutation($owner: ownerInput!, $ownerId: ID!){
updateOwner(owner: $owner, ownerId: $ownerId){
id,
name,
address
}
}",
Variables = new { owner = ownerToUpdate, ownerId = id }
};
var response = await _client.SendMutationAsync<ResponseOwnerType>(query);
return response.Data.Owner;
}
```
### Delete Mutation
```csharp title="OwnerConsumer.cs"
public async Task<Owner> DeleteOwner(Guid id)
{
var query = new GraphQLRequest
{
Query = @"
mutation($ownerId: ID!){
deleteOwner(ownerId: $ownerId)
}",
Variables = new { ownerId = id }
};
var response = await _client.SendMutationAsync<ResponseOwnerType>(query);
return response.Data.Owner;
}
```
## Use in a Blazor page
Create a new `Consume.razor` page to test our code:
```html title="Pages/Consume.razor"
@page "/consume"
<h3>Consume</h3>
@if (Owner != null)
{
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Address</th>
</tr>
</thead>
<tbody>
@foreach (var item in Owner)
{
<tr>
<td>@item.Id</td>
<td>@item.Name</td>
<td>@item.Address</td>
</tr>
}
</tbody>
</table>
}
```
Create the page code:
```csharp title="Pages/Consume.razor.cs"
public partial class Consume
{
private List<Owner> Owner;
[Inject]
public OwnerConsumer Consumer { get; set; }
protected override async Task OnInitializedAsync()
{
this.Owner = await Consumer.GetAllOwners();
}
}
```

@ -0,0 +1,100 @@
---
sidebar_position: 1
title: Websocket
---
This course explains how to get started with WebSockets in ASP.NET Core.
[WebSocket](https://wikipedia.org/wiki/WebSocket) ([RFC 6455](https://tools.ietf.org/html/rfc6455)) is a protocol that allows persistent two-way communication channels over TCP connections.
Its use benefits applications that take advantage of fast, real-time communication, such as chat, dashboard, and game applications.
For our examples we will use a small example project available [here](/DemoWebsocket.zip).
## WebSocket definition
In the traditional web paradigm, the client was responsible for initiating communication with a server, and the server could not send data back unless it had been previously requested by the client.
With WebSockets, you can send data between server and client over a single TCP connection, and typically WebSockets are used to provide real-time functionality to modern applications.
![Définition WebSocket](/img/websocket/sockets.png)
## What is a SignalR Hub?
The SignalR hubs API lets you call methods on connected clients from the server.
* In server code, you define methods that are called by the client.
* In client code, you define methods that are called from the server.
SignalR takes care of everything in the background that makes real-time client-to-server and server-to-client communications possible.
## Install the SignalR .NET client package
The `Microsoft.AspNetCore.SignalR.Client` package is required for .NET clients to connect to SignalR hubs.
Install the `Microsoft.AspNetCore.SignalR.Client` package in its latest version.
![required library](/img/websocket/nuget-Microsoft.AspNetCore.SignalR.Client.png)
Or using the Package Manager console: `PM> Install-Package Microsoft.AspNetCore.SignalR.Client`
## Use SignalR
To establish a connection, create a `HubConnectionBuilder` and call the `Build` method.
Hub URL, protocol, transport type, logging level, headers, and other options can be configured when creating a connection.
Configure all required options by inserting one of the `HubConnectionBuilder` methods into the `Build` method.
Start the connection with `StartAsync`.
```csharp title="DemoSignalR.razor.cs"
public partial class DemoSignalR
{
private HubConnection connection;
private string connectionUrl = "https://localhost:44391/ChatHub";
private List<Chat> logs = new List<Chat>();
private string message = "";
private string userName = "UserName";
public DemoSignalR()
{
// Create the new SignalR Hub
connection = new HubConnectionBuilder()
.WithUrl(connectionUrl)
.Build();
}
public void Dispose()
{
OnClose();
}
private async void OnClose()
{
// Send message for user disconnect
await connection.InvokeAsync("SendMessage", new Chat { Type = "disconnect", Name = userName });
// Stop the connection
await connection.StopAsync();
}
private async void OnConnect()
{
// Handler to treat the receive message
connection.On<Chat>("ReceiveMessage", chat =>
{
logs.Add(chat);
StateHasChanged();
});
// Start the connection
await connection.StartAsync();
// Send message for user connect
await connection.InvokeAsync("SendMessage", new Chat { Type = "connect", Name = userName });
}
private async Task SendMessageAsync()
{
// Send the user message
await connection.InvokeAsync("SendMessage", new Chat { Type = "message", Name = userName, Message = message });
}
}
```

@ -0,0 +1,289 @@
---
sidebar_position: 13
title: Configuration
---
Configuration in ASP.NET Core is done using one or more configuration providers.
Configuration providers read configuration data from key-value pairs using a variety of configuration sources:
* Settings files, such as `appsettings.json`
* Environment Variables
* Azure Key Vault
* Azure App Setup
* Command line arguments
* Custom, installed or created providers
* Directory files
* .NET objects in memory
:::caution
Configuration and settings files in a Blazor WebAssembly app are visible to users. Don't store application secrets, credentials, or other sensitive data in a WebAssembly application's configuration or files.
:::
## Default configuration
ASP.NET Core web apps created with dotnet new or Visual Studio generate the following code:
```csharp
public static async Task Main(string[] args)
{
// highlight-next-line
var builder = WebApplication.CreateBuilder(args);
...
```
`CreateBuilder` provides the default application configuration in the following order:
* `ChainedConfigurationProvider`: adds an existing `IConfiguration` as a source. In the case of default configuration, adds the host configuration and sets it as the first source of the application configuration.
* `appsettings.json` using default JSON provider.
* appsettings.`Environment`.json using the default JSON provider. For example, appsettings.**Production**.json and appsettings.**Development**.json.
* Application secrets when the application is running in the `Development` environment.
* Environment variables using the environment variables configuration provider.
* Command line arguments using the command line configuration provider.
Configuration providers added later override previous key settings.
For example, if `MyKey` is defined in both `appsettings.json` and the environment, the environment value is used.
Using the default configuration providers, the command-line configuration provider overrides all other providers.
## appsettings.json
Consider the following `appsettings.json` file:
```json title="appsettings.json"
{
"Position": {
"Title": "Editor",
"Name": "Joe Smith"
},
"MyKey": "My appsettings.json Value",
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
```
The following code displays several of the previous configuration parameters:
```html title="Config.razor"
@page "/config"
<h3>Config</h3>
<div>
<div>MyKey: @Configuration["MyKey"]</div>
<div>Position:Title: @Configuration["Position:Title"]</div>
<div>Position:Name: @Configuration["Position:Name"]</div>
<div>Logging:LogLevel:Default: @Configuration["Logging:LogLevel:Default"]</div>
</div>
```
```csharp title="Config.razor.cs"
using Microsoft.Extensions.Configuration;
public partial class Config
{
[Inject]
public IConfiguration Configuration { get; set; }
}
```
The default `JsonConfigurationProvider` configuration loads in the following order:
* appsettings.json
* appsettings.`Environment`.json: for example, appsettings.**Production**.json and appsettings.**Development**.json. The environment version of the file is loaded from `IHostingEnvironment.EnvironmentName`.
Values in appsettings.`Environment`.json override keys in appsettings.json. For example, by default:
* In development, the appsettings.**Development**.json file overrides the values found in appsettings.json.
* In production, the appsettings.**Production**.json file overrides the values found in appsettings.json. For example, when deploying the application to Azure.
If a configuration value must be guaranteed, use `GetValue`. The preceding example only reads strings and does not support a default value.
Using the default configuration of appsettings.json and appsettings.`Environment`.json. files are enabled with `reloadOnChange: true`.
Changes made to the appsettings.json and appsettings.`Environment`.json files after the application starts are read by the JSON configuration provider.
## Bind hierarchical configuration data using the options model
The recommended way to read related configuration values is using the options pattern. For example, to read the following configuration values:
```json title="appsettings.json"
"Position": {
"Title": "Editor",
"Name": "Joe Smith"
}
```
Create the following `PositionOptions` class:
```csharp title="PositionOptions.cs"
public class PositionOptions
{
public const string Position = "Position";
public string Title { get; set; }
public string Name { get; set; }
}
```
An option class:
* Must be non-abstract with a public parameterless constructor.
* All public read/write properties of the type are bound.
* The fields are not linked. In the preceding code, `Position` is not bound. The `Position` property is used so that the "Position" string does not have to be hard-coded into the application when binding the class to a configuration provider.
The following code:
* Calls `ConfigurationBinder.Bind` to bind the `PositionOptions` class to the `Position` section.
* Displays `Position` configuration data.
```html title="Config.razor"
@page "/config"
<h3>Config</h3>
@if (positionOptions != null)
{
<div>
<div>Title: @positionOptions.Title</div>
<div>Name: @positionOptions.Name</div>
</div>
}
```
```csharp title="Config.razor.cs"
private PositionOptions positionOptions;
public partial class Config
{
[Inject]
public IConfiguration Configuration { get; set; }
private PositionOptions positionOptions;
protected override void OnInitialized()
{
base.OnInitialized();
positionOptions = new PositionOptions();
Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);
}
}
```
In the preceding code, by default, changes made to the JSON configuration file after the application starts are read.
`ConfigurationBinder.Get<T>` binds and returns the specified type. `ConfigurationBinder.Get<T>` may be more convenient than using `ConfigurationBinder.Bind`.
The following code shows how to use `ConfigurationBinder.Get<T>` with the `PositionOptions` class:
```csharp title="Config.razor.cs"
public partial class Config
{
[Inject]
public IConfiguration Configuration { get; set; }
private PositionOptions positionOptions;
protected override void OnInitialized()
{
base.OnInitialized();
positionOptions = Configuration.GetSection(PositionOptions.Position).Get<PositionOptions>();
}
}
```
Another approach to using the options pattern is to bind the section and add it to the dependency injection service container.
In the following code, `PositionOptions` is added to the service container with `Configure` and bound to the configuration:
```csharp title="Program.cs"
...
builder.Services.Configure<PositionOptions>(option =>
{
var positionOptions = builder.Configuration.GetSection(PositionOptions.Position).Get<PositionOptions>();
option.Name = positionOptions.Name;
option.Title = positionOptions.Title;
});
...
```
Using the previous code, the following code reads the position options:
```csharp title="Config.razor.cs"
public partial class Config
{
[Inject]
public IConfiguration Configuration { get; set; }
[Inject]
public IOptions<PositionOptions> OptionsPositionOptions { get; set; }
private PositionOptions positionOptions;
protected override void OnInitialized()
{
base.OnInitialized();
positionOptions = OptionsPositionOptions.Value;
}
}
```
## User Security and Secrets
Instructions for configuration data:
* Never store passwords or other sensitive data in configuration provider code or plain text configuration files. The Secret Manager tool can be used to store secrets in development.
* Do not use any production secrets in development or test environments.
* Specify secrets outside of the project so they can't be inadvertently committed to a source code repository.
By default, the user secrets configuration source is listed after the JSON configuration sources.
Therefore, user secrets keys take precedence over keys in appsettings.json and appsettings.`Environment`.json.
## Environment Variables
Using the default configuration, the `EnvironmentVariablesConfigurationProvider` loads the configuration from environment variable key-value pairs after reading the `appsettings.json` file, appsettings.`Environment`.json and secrets of the user.
Therefore, key values read from the environment override values read from `appsettings.json`, appsettings.`Environment`.json and user secrets.
The `:` separator does not work with hierarchical environment variable keys on all platforms. `__`, the double underscore, is:
* Supported by all platforms. For example, the `:` separator is not supported by `Bash`, but `__` is.
* Automatically replaced by a `:`
The following `set` commands:
* Set the environment keys and values from the previous example to Windows.
* Test the settings during use. The dotnet run command should be run in the project directory.
```shell
set MyKey="My key from Environment"
set Position__Title=Environment_Editor
set Position__Name=Environment_Rick
dotnet run
```
Previous environment settings:
* Are only defined in processes launched from the command window in which they were defined.
* Will not be read by browsers launched with Visual Studio.
The following `setx` commands can be used to set environment keys and values on Windows.
Unlike `set`, the `setx` parameters are preserved. `/M` sets the variable in the system environment.
If the `/M` switch is not used, a user environment variable is set.
```shell
setx MyKey "My key from setx Environment" /M
setx Position__Title Environment_Editor /M
setx Position__Name Environment_Rick /M
```
To verify that the previous commands override `appsettings.json` and appsettings.`Environment`.json:
* with Visual Studio: quit and restart Visual Studio.
* With CLI interface: start a new command window and enter `dotnet run`.
One of the great interest of environment variables and the use of it with Docker.

@ -0,0 +1,24 @@
---
sidebar_position: 4
title: Creation of the project
---
## Create a new Blazor site
Open Visual Studio and select `Create a new Project`
![Create a new Blazor site](/img/creation-projet/creation-projet-01.png)
Search in the list of `Blazor Server` templates, select the project type and click `Next`.
![Create a new Blazor site](/img/creation-projet/creation-projet-02.png)
Fill in the information as well as the location of your project and click on `Next`.
![Create a new Blazor site](/img/creation-projet/creation-projet-03.png)
Leave the default options and click on `Create`.
![Create a new Blazor site](/img/creation-projet/creation-projet-04.png)
Congratulations, your site is now available.

@ -0,0 +1,3 @@
label: 'Delete an item'
position: 8
collapsed: true

@ -0,0 +1,37 @@
---
sidebar_position: 3
title: Add Delete Action
---
## Add Delete Action
In order to delete an element we will therefore modify the grid in order to add an action allowing the deletion of our element.
For this we are going to modify the column already containing our navigation link to the modification.
Open the `Pages/List.razor` file and add the highlighted changes as follows:
```cshtml title="Pages/List.razor"
...
<DataGridColumn TItem="Item" Field="@nameof(Item.CreatedDate)" Caption="Created date" DisplayFormat="{0:d}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo("fr-FR")" />
<DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="Action">
<DisplayTemplate>
<a href="Edit/@(context.Id)" class="btn btn-primary"><i class="fa fa-edit"></i> Editer</a>
// highlight-next-line
<button type="button" class="btn btn-primary" @onclick="() => OnDelete(context.Id)"><i class="fa fa-trash"></i> Supprimer</button>
</DisplayTemplate>
</DataGridColumn>
</DataGrid>
```
In the code of our page `Pages/List.razor.cs` add the method:
```csharp title="Pages/List.razor.cs"
...
private void OnDelete(int id)
{
}
```

@ -0,0 +1,58 @@
---
sidebar_position: 4
title: Install Blazored.Modal
---
## Using Blazored Modal
Add the `Blazored.Modal` nuget.
### Add service
```csharp title="Program.cs"
// highlight-next-line
using Blazored.Modal;
public static async Task Main(string[] args)
{
...
// highlight-next-line
builder.Services.AddBlazoredModal();
...
}
```
### Add Imports
```cshtml title="_Imports.razor"
@using Blazored.Modal
@using Blazored.Modal.Services
```
### Add CascadingBlazoredModal component around existing Router component
```cshtml title="App.razor"
// highlight-next-line
<CascadingBlazoredModal>
<Router AppAssembly="typeof(Program).Assembly">
...
</Router>
// highlight-next-line
</CascadingBlazoredModal>
```
### Add JavaScript & CSS references
```cshtml title="Pages/_Layout.cshtml"
<link href="_content/Blazored.Modal/blazored-modal.css" rel="stylesheet" />
```
```html
<script src="_content/Blazored.Modal/blazored.modal.js"></script>
```
## Documentation
Link to documentation [Blazored.Modal](https://github.com/Blazored/Modal).

@ -0,0 +1,356 @@
---
sidebar_position: 5
title: Creation of a confirmation popup
---
## Creation of a confirmation popup
In order to create our popup, create the `Modals` folder at the root of the site.
Create the razor component `DeleteConfirmation.razor` and its class `DeleteConfirmation.razor.cs`.
```cshtml title="Modals/DeleteConfirmation.razor"
<div class="simple-form">
<p>
Are you sure you want to delete @item.DisplayName ?
</p>
<button @onclick="ConfirmDelete" class="btn btn-primary">Delete</button>
<button @onclick="Cancel" class="btn btn-secondary">Cancel</button>
</div>
```
```csharp title="Modals/DeleteConfirmation.razor.cs"
public partial class DeleteConfirmation
{
[CascadingParameter]
public BlazoredModalInstance ModalInstance { get; set; }
[Inject]
public IDataService DataService { get; set; }
[Parameter]
public int Id { get; set; }
private Item item = new Item();
protected override async Task OnInitializedAsync()
{
// Get the item
item = await DataService.GetById(Id);
}
void ConfirmDelete()
{
ModalInstance.CloseAsync(ModalResult.Ok(true));
}
void Cancel()
{
ModalInstance.CancelAsync();
}
}
```
Open the `Pages/List.razor.cs` file and add the changes:
```csharp title="Pages/List.razor.cs"
...
[Inject]
public NavigationManager NavigationManager { get; set; }
[CascadingParameter]
public IModalService Modal { get; set; }
...
private async void OnDelete(int id)
{
var parameters = new ModalParameters();
parameters.Add(nameof(Item.Id), id);
var modal = Modal.Show<DeleteConfirmation>("Delete Confirmation", parameters);
var result = await modal.Result;
if (result.Cancelled)
{
return;
}
await DataService.Delete(id);
// Reload the page
NavigationManager.NavigateTo("list", true);
}
```
## Concept: Cascading Parameters
### Component `CascadingValue`
An ancestor component provides cascading value using the `CascadingValue` framework Blazor component, which encapsulates a subtree of a component hierarchy and provides a unique value to all components in its subtree.
The following example shows the flow of theme information through the component hierarchy of a layout component to provide a CSS style class to child component buttons.
The following `ThemeInfo` C# class is placed in a folder named `UIThemeClasses` and specifies theme information.
:::info
For the samples in this section, the application namespace is BlazorSample . When experimenting with the code in your own sample application, replace the application namespace with the namespace of your sample application.
:::
```csharp title="UIThemeClasses/ThemeInfo.cs"
namespace BlazorSample.UIThemeClasses
{
public class ThemeInfo
{
public string? ButtonClass { get; set; }
}
}
```
The following layout specifies theme information ( `ThemeInfo` ) as a cascading value for all components that make up the layout body of the `Body` property.
The value `ButtonClass` is assigned to `btn-success`, which is a style of start button. Any component descending in the component hierarchy can use the `ButtonClass` property through the cascading `ThemeInfo` value.
```cshtml title="Shared/MainLayout.razor"
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<CascadingValue Value="theme">
<div class="content px-4">
@Body
</div>
</CascadingValue>
</div>
</div>
@code {
private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
```
### Attribute `[CascadingParameter]`
To use cascading values, descendant components declare cascading parameters using the `[CascadingParameter]` attribute.
Cascading values are related to cascading parameters by type.
The following component binds the cascading `ThemeInfo` value to a cascading parameter, optionally using the same `ThemeInfo` name. The parameter is used to define the CSS class for the `Increment Counter (Themed)` button.
```cshtml title="Pages/ThemedCounter.razor"
@page "/themed-counter"
@using BlazorSample.UIThemeClasses
<h1>Themed Counter</h1>
<p>Current count: @currentCount</p>
<p>
<button class="btn" @onclick="IncrementCount">
Increment Counter (Unthemed)
</button>
</p>
<p>
<button
class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)"
@onclick="IncrementCount">
Increment Counter (Themed)
</button>
</p>
@code {
private int currentCount = 0;
[CascadingParameter]
protected ThemeInfo? ThemeInfo { get; set; }
private void IncrementCount()
{
currentCount++;
}
}
```
### Cascading Multiple Values
To cascade multiple values of the same type in the same subtree, supply a unique `Name` string to each `CascadingValue` component and its corresponding `[CascadingParameter]` attributes.
In the following example, two `CascadingValue` components cascade different instances of `CascadingType`:
```cshtml
<CascadingValue Value="@parentCascadeParameter1" Name="CascadeParam1">
<CascadingValue Value="@ParentCascadeParameter2" Name="CascadeParam2">
...
</CascadingValue>
</CascadingValue>
@code {
private CascadingType parentCascadeParameter1;
[Parameter]
public CascadingType ParentCascadeParameter2 { get; set; }
...
}
```
In a descendant component, cascading parameters receive their cascading values from the ancestor component with the `Name` attribute:
```cshtml
...
@code {
[CascadingParameter(Name = "CascadeParam1")]
protected CascadingType ChildCascadeParameter1 { get; set; }
[CascadingParameter(Name = "CascadeParam2")]
protected CascadingType ChildCascadeParameter2 { get; set; }
}
```
### Pass data in a component hierarchy
Cascading parameters also allow components to pass data through a component hierarchy.
Consider the following UI tab set example, where a tab set component manages a series of individual tabs.
Create an `ITab` interface that tabs implement in a folder named `UIInterfaces`.
```csharp title="UIInterfaces/ITab.cs"
using Microsoft.AspNetCore.Components;
namespace BlazorSample.UIInterfaces
{
public interface ITab
{
RenderFragment ChildContent { get; }
}
}
```
The following `TabSet` component manages a set of tabs. The components of the `Tab` tab set, which are created later in this section, provide the list items ( `<li>...</li>` ) of the list ( `<ul>... </ul>` ).
`Tab` child components are not explicitly passed as parameters to `TabSet`.
Instead, `Tab` child components are part of `TabSet` child content.
However, the `TabSet` still requires a `Tab` reference to each component in order to display the headers and the active tab.
To enable this coordination without requiring additional code, the `TabSet` component can present itself as a cascading value which is then retrieved by descendant `Tab` components.
```cshtml title="Shared/TabSet.razor"
@using BlazorSample.UIInterfaces
<!-- Display the tab headers -->
<CascadingValue Value=this>
<ul class="nav nav-tabs">
@ChildContent
</ul>
</CascadingValue>
<!-- Display body for only the active tab -->
<div class="nav-tabs-body p-4">
@ActiveTab?.ChildContent
</div>
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
public ITab ActiveTab { get; private set; }
public void AddTab(ITab tab)
{
if (ActiveTab == null)
{
SetActiveTab(tab);
}
}
public void SetActiveTab(ITab tab)
{
if (ActiveTab != tab)
{
ActiveTab = tab;
StateHasChanged();
}
}
}
```
Descendant `Tab` components capture the `TabSet` container as a cascading parameter. The `Tab` components add to the `TabSet` coordinate and to set the active tab.
```cshtml title="Shared/Tab.razor"
@using BlazorSample.UIInterfaces
@implements ITab
<li>
<a @onclick="ActivateTab" class="nav-link @TitleCssClass" role="button">
@Title
</a>
</li>
@code {
[CascadingParameter]
public TabSet ContainerTabSet { get; set; }
[Parameter]
public string Title { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
private string TitleCssClass =>
ContainerTabSet.ActiveTab == this ? "active" : null;
protected override void OnInitialized()
{
ContainerTabSet.AddTab(this);
}
private void ActivateTab()
{
ContainerTabSet.SetActiveTab(this);
}
}
```
The following `ExampleTabSet` component uses the `TabSet` component, which contains three `Tab` components.
```cshtml title="Pages/ExampleTabSet.razor"
@page "/example-tab-set"
<TabSet>
<Tab Title="First tab">
<h4>Greetings from the first tab!</h4>
<label>
<input type="checkbox" @bind="showThirdTab" />
Toggle third tab
</label>
</Tab>
<Tab Title="Second tab">
<h4>Hello from the second tab!</h4>
</Tab>
@if (showThirdTab)
{
<Tab Title="Third tab">
<h4>Welcome to the disappearing third tab!</h4>
<p>Toggle this tab from the first tab.</p>
</Tab>
}
</TabSet>
@code {
private bool showThirdTab;
}
```

@ -0,0 +1,61 @@
---
sidebar_position: 2
title: Changing the data service
---
In order to take into account the deletion of an element, we will first add this functionality to our data service.
## Add method to interface
Open the `Services/IDataService.cs` file and add the highlighted item:
```csharp title="Services/IDataService.cs"
public interface IDataService
{
Task Add(ItemModel model);
Task<int> Count();
Task<List<Item>> List(int currentPage, int pageSize);
Task<Item> GetById(int id);
Task Update(int id, ItemModel model);
// highlight-start
Task Delete(int id);
// highlight-end
}
```
## Add method to implementation
Open the `Services/DataLocalService.cs` file and add the following method:
```csharp title="Services/DataLocalService.cs"
...
public async Task Delete(int id)
{
// Get the current data
var currentData = await _localStorage.GetItemAsync<List<Item>>("data");
// Get the item int the list
var item = currentData.FirstOrDefault(w => w.Id == id);
// Delete item in
currentData.Remove(item);
// Delete the image
var imagePathInfo = new DirectoryInfo($"{_webHostEnvironment.WebRootPath}/images");
var fileName = new FileInfo($"{imagePathInfo}/{item.Name}.png");
if (fileName.Exists)
{
File.Delete(fileName.FullName);
}
// Save the data
await _localStorage.SetItemAsync("data", currentData);
}
```

@ -0,0 +1,19 @@
---
sidebar_position: 1
title: Introduction
---
## Delete an item
This lab will allow you to delete an item.
### List of steps
* Modification of the data service
* Add Delete action
* Install Blazored.Modal
* Creation of a confirmation popup
### List of concepts
* Cascading Parameters

@ -0,0 +1,3 @@
label: 'Deploy Apps'
position: 16
collapsed: true

@ -0,0 +1,215 @@
---
sidebar_position: 2
sidebar_label: Create CI file
title: Create CI file
---
The CI of your project use Drone.
## Create initial CI file
At the root of your project create a new empty file with the name `.drone.yml`.
Fill the file with the initial data of a CI file:
```yml
kind: pipeline
type: docker
name: default
trigger:
event:
- push
steps:
```
This code permit:
* Define the type of the pipeline and is name.
* Launch the CI when a push is commit on the repository.
* Under `steps` the yaml file contains the list of jobs.
### Add the build job
In first for a CI is the build of your project, add the below code in the CI file.
```yml
- name: build
image: mcr.microsoft.com/dotnet/sdk:6.0
commands:
- cd Sources/
- dotnet restore MySolution.sln
- dotnet build MySolution.sln -c Release --no-restore
```
### Add the test job
The second step it is for test your project.
```yml
- name: tests
image: mcr.microsoft.com/dotnet/sdk:6.0
commands:
- cd Sources/
- dotnet restore MySolution.sln
- dotnet test MySolution.sln --no-restore
depends_on: [build]
```
### Add the analysis job
A CI is also present for analysis your code !
```yml
- name: code-analysis
image: hub.codefirst.iut.uca.fr/thomas.bellembois/codefirst-dronesonarplugin-dotnet6
commands:
- cd Sources/
- dotnet restore MySolution.sln
- dotnet sonarscanner begin /k:$REPO_NAME /d:sonar.host.url=$$$${PLUGIN_SONAR_HOST} /d:sonar.coverageReportPaths="coveragereport/SonarQube.xml" /d:sonar.coverage.exclusions="Tests/**" /d:sonar.login=$$$${PLUGIN_SONAR_TOKEN}
- dotnet build MySolution.sln -c Release --no-restore
- dotnet test MySolution.sln --logger trx --no-restore /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura --collect "XPlat Code Coverage"
- reportgenerator -reports:"**/coverage.cobertura.xml" -reporttypes:SonarQube -targetdir:"coveragereport"
- dotnet publish MySolution.sln -c Release --no-restore -o $CI_PROJECT_DIR/build/release
- dotnet sonarscanner end /d:sonar.login=$$$${PLUGIN_SONAR_TOKEN}
secrets: [ SECRET_SONAR_LOGIN ]
settings:
# accessible en ligne de commande par $${PLUGIN_SONAR_HOST}
sonar_host: https://codefirst.iut.uca.fr/sonar/
# accessible en ligne de commande par $${PLUGIN_SONAR_TOKEN}
sonar_token:
from_secret: SECRET_SONAR_LOGIN
depends_on: [tests]
```
### Add the documentation job
Your project containts also a documentation, create a job to generate this.
```yml
- name: generate-and-deploy-docs
image: hub.codefirst.iut.uca.fr/thomas.bellembois/codefirst-docdeployer
failure: ignore
volumes:
- name: docs
path: /docs
commands:
- /entrypoint.sh
when:
branch:
- master
depends_on: [ build ]
# The volumes declaration appear at the end of the file, after all steps
volumes:
- name: docs
temp: {}
```
## Add the CD job
For the CD we deploy a docker image of your project, add the job to build and deploy your docker image in a registry.
```yaml
- name: docker-build
image: plugins/docker
settings:
dockerfile: Sources/Dockerfile
context: .
registry: hub.codefirst.iut.uca.fr
repo: hub.codefirst.iut.uca.fr/my-group/my-application-client
username:
from_secret: SECRET_REGISTRY_USERNAME
password:
from_secret: SECRET_REGISTRY_PASSWORD
when:
branch:
- master
```
## Example of full file
Discover here a full file with all jobs.
```yaml
kind: pipeline
type: docker
name: default
trigger:
event:
- push
steps:
- name: build
image: mcr.microsoft.com/dotnet/sdk:6.0
commands:
- cd Sources/
- dotnet restore MySolution.sln
- dotnet build MySolution.sln -c Release --no-restore
- name: tests
image: mcr.microsoft.com/dotnet/sdk:6.0
commands:
- cd Sources/
- dotnet restore MySolution.sln
- dotnet test MySolution.sln --no-restore
depends_on: [build]
- name: code-analysis
image: hub.codefirst.iut.uca.fr/thomas.bellembois/codefirst-dronesonarplugin-dotnet6
commands:
- cd Sources/
- dotnet restore MySolution.sln
- dotnet sonarscanner begin /k:$REPO_NAME /d:sonar.host.url=$$$${PLUGIN_SONAR_HOST} /d:sonar.coverageReportPaths="coveragereport/SonarQube.xml" /d:sonar.coverage.exclusions="Tests/**" /d:sonar.login=$$$${PLUGIN_SONAR_TOKEN}
- dotnet build MySolution.sln -c Release --no-restore
- dotnet test MySolution.sln --logger trx --no-restore /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura --collect "XPlat Code Coverage"
- reportgenerator -reports:"**/coverage.cobertura.xml" -reporttypes:SonarQube -targetdir:"coveragereport"
- dotnet publish MySolution.sln -c Release --no-restore -o $CI_PROJECT_DIR/build/release
- dotnet sonarscanner end /d:sonar.login=$$$${PLUGIN_SONAR_TOKEN}
secrets: [ SECRET_SONAR_LOGIN ]
settings:
# accessible en ligne de commande par $${PLUGIN_SONAR_HOST}
sonar_host: https://codefirst.iut.uca.fr/sonar/
# accessible en ligne de commande par $${PLUGIN_SONAR_TOKEN}
sonar_token:
from_secret: SECRET_SONAR_LOGIN
depends_on: [tests]
- name: generate-and-deploy-docs
image: hub.codefirst.iut.uca.fr/thomas.bellembois/codefirst-docdeployer
failure: ignore
volumes:
- name: docs
path: /docs
commands:
- /entrypoint.sh
when:
branch:
- master
depends_on: [ build ]
- name: docker-build
image: plugins/docker
settings:
dockerfile: Sources/Dockerfile
context: .
registry: hub.codefirst.iut.uca.fr
repo: hub.codefirst.iut.uca.fr/my-group/my-application-client
username:
from_secret: SECRET_REGISTRY_USERNAME
password:
from_secret: SECRET_REGISTRY_PASSWORD
when:
branch:
- master
volumes:
- name: docs
temp: {}
```

@ -0,0 +1,57 @@
---
sidebar_position: 1
sidebar_label: Generate Docker file
title: Generate Docker file
---
## How to generate my docker file
It is possible to generate a `Dockerfile` automaticly with Visual Studio.
On your project, click right and select `Add` => `Docker Support...`:
![Docker Support](/img/deploy/docker-support-visual-studio.png)
Select the `Linux` target:
![Target OS](/img/deploy/docker-file-options.png)
A new file `Dockerfile` are created in your project.
## The nuget configuration
If you use specific repositories, you must use a `nuget.config` file.
Don't forget to add your `nuget.config` to your build image, just after `WORKDIR /src` add `COPY ["nuget.config", "."]`
:::caution
If you don't add the file, restore nuget does not work, by default restore search on nuget.org
:::
## Example of Dockerfile
```txt
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY ["nuget.config", "."]
COPY ["MyBeautifulApp/MyBeautifulApp.csproj", "MyBeautifulApp/"]
RUN dotnet restore "MyBeautifulApp/MyBeautifulApp.csproj"
COPY . .
WORKDIR "/src/MyBeautifulApp"
RUN dotnet build "MyBeautifulApp.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "MyBeautifulApp.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyBeautifulApp.dll"]
```

@ -0,0 +1,3 @@
label: 'DI & IOC'
position: 7
collapsed: true

@ -0,0 +1,251 @@
---
sidebar_position: 3
title: Creating a data service
---
As we previously added the addition of an item, we now have two places in the code in our code where we access our local storage.
In order to simplify the management of our code, we will therefore use the IOC & DI.
In our framework, we are going to use the native IOC of AspNetCore, we are going to use the DI to manage our data.
We have already used the AspNetCore IOC with properties using the `[Inject]` attribute, these being managed by the system or external libraries.
## Creating the data service interface
We will therefore create a `Services` directory at the root of our site and create our interface.
```csharp title="Services/IDataService.cs"
public interface IDataService
{
Task Add(ItemModel model);
Task<int> Count();
Task<List<Item>> List(int currentPage, int pageSize);
}
```
Our interface contains the methods to manage our data.
## Creating the Data Service Implementation
We will now create our class to manage our data locally.
```csharp title="Services/DataLocalService.cs"
public class DataLocalService : IDataService
{
private readonly HttpClient _http;
private readonly ILocalStorageService _localStorage;
private readonly NavigationManager _navigationManager;
private readonly IWebHostEnvironment _webHostEnvironment;
public DataLocalService(
ILocalStorageService localStorage,
HttpClient http,
IWebHostEnvironment webHostEnvironment,
NavigationManager navigationManager)
{
_localStorage = localStorage;
_http = http;
_webHostEnvironment = webHostEnvironment;
_navigationManager = navigationManager;
}
public async Task Add(ItemModel model)
{
// Get the current data
var currentData = await _localStorage.GetItemAsync<List<Item>>("data");
// Simulate the Id
model.Id = currentData.Max(s => s.Id) + 1;
// Add the item to the current data
currentData.Add(new Item
{
Id = model.Id,
DisplayName = model.DisplayName,
Name = model.Name,
RepairWith = model.RepairWith,
EnchantCategories = model.EnchantCategories,
MaxDurability = model.MaxDurability,
StackSize = model.StackSize,
CreatedDate = DateTime.Now
});
// Save the image
var imagePathInfo = new DirectoryInfo($"{_webHostEnvironment.WebRootPath}/images");
// Check if the folder "images" exist
if (!imagePathInfo.Exists)
{
imagePathInfo.Create();
}
// Determine the image name
var fileName = new FileInfo($"{imagePathInfo}/{model.Name}.png");
// Write the file content
await File.WriteAllBytesAsync(fileName.FullName, model.ImageContent);
// Save the data
await _localStorage.SetItemAsync("data", currentData);
}
public async Task<int> Count()
{
return (await _localStorage.GetItemAsync<Item[]>("data")).Length;
}
public async Task<List<Item>> List(int currentPage, int pageSize)
{
// Load data from the local storage
var currentData = await _localStorage.GetItemAsync<Item[]>("data");
// Check if data exist in the local storage
if (currentData == null)
{
// this code add in the local storage the fake data
var originalData = await _http.GetFromJsonAsync<Item[]>($"{_navigationManager.BaseUri}fake-data.json");
await _localStorage.SetItemAsync("data", originalData);
}
return (await _localStorage.GetItemAsync<Item[]>("data")).Skip((currentPage - 1) * pageSize).Take(pageSize).ToList();
}
}
```
You will notice that we use another way to inject dependencies into this class through the constructor.
`ILocalStorageService` & `HttpClient` & `IWebHostEnvironment` & `NavigationManager` are automatically resolved by the IOC.
## Editing Pages
Let's modify our pages to take into account our new service:
```csharp title="Pages/List.razor.cs"
public partial class List
{
private List<Item> items;
private int totalItem;
[Inject]
public IDataService DataService { get; set; }
[Inject]
public IWebHostEnvironment WebHostEnvironment { get; set; }
private async Task OnReadData(DataGridReadDataEventArgs<Item> e)
{
if (e.CancellationToken.IsCancellationRequested)
{
return;
}
if (!e.CancellationToken.IsCancellationRequested)
{
items = await DataService.List(e.Page, e.PageSize);
totalItem = await DataService.Count();
}
}
}
```
```csharp title="Pages/Add.razor.cs"
public partial class Add
{
/// <summary>
/// The default enchant categories.
/// </summary>
private List<string> enchantCategories = new List<string>() { "armor", "armor_head", "armor_chest", "weapon", "digger", "breakable", "vanishable" };
/// <summary>
/// The current item model
/// </summary>
private ItemModel itemModel = new()
{
EnchantCategories = new List<string>(),
RepairWith = new List<string>()
};
/// <summary>
/// The default repair with.
/// </summary>
private List<string> repairWith = new List<string>() { "oak_planks", "spruce_planks", "birch_planks", "jungle_planks", "acacia_planks", "dark_oak_planks", "crimson_planks", "warped_planks" };
[Inject]
public IDataService DataService { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
private async void HandleValidSubmit()
{
await DataService.Add(itemModel);
NavigationManager.NavigateTo("list");
}
private async Task LoadImage(InputFileChangeEventArgs e)
{
// Set the content of the image to the model
using (var memoryStream = new MemoryStream())
{
await e.File.OpenReadStream().CopyToAsync(memoryStream);
itemModel.ImageContent = memoryStream.ToArray();
}
}
private void OnEnchantCategoriesChange(string item, object checkedValue)
{
if ((bool)checkedValue)
{
if (!itemModel.EnchantCategories.Contains(item))
{
itemModel.EnchantCategories.Add(item);
}
return;
}
if (itemModel.EnchantCategories.Contains(item))
{
itemModel.EnchantCategories.Remove(item);
}
}
private void OnRepairWithChange(string item, object checkedValue)
{
if ((bool)checkedValue)
{
if (!itemModel.RepairWith.Contains(item))
{
itemModel.RepairWith.Add(item);
}
return;
}
if (itemModel.RepairWith.Contains(item))
{
itemModel.RepairWith.Remove(item);
}
}
}
```
## Register the data service
Now we have to define in the IOC of our application the resolution of our interface / class.
Open the `Program.cs` file and add the following line:
```csharp title="Program.cs"
...
builder.Services.AddScoped<IDataService, DataLocalService>();
...
```
Later we will implement data management through an API, it will simply be enough to create a new class `Services/DataApiService.cs` implementing the interface `IDataService` with API calls and modify the IOC with this new service.

@ -0,0 +1,17 @@
---
sidebar_position: 1
title: Introduction
---
## DI & IOC
This lab will allow you to understand dependency injection and inversion of control.
### List of steps
* Creating a data service
### List of concepts
* Dependency injection (DI)
* Inversion of control (IOC)

@ -0,0 +1,189 @@
---
sidebar_position: 2
title: DI & IOC
---
## Definition
When you have understood the fundamentals of object-oriented programming (OOP), you must quickly understand and assimilate several design patterns.
Indeed, this makes it possible to deal with known problems and thus to impose good practices which are based on the experience of software designers.
For me, and surely for many other software designers, one of the primary goals of design is to keep the interface clear, easily maintainable and above all scalable. As any developer knows, technologies evolve very very quickly.
This is why we are going to see dependency injection (DI) and inversion of control (Ioc).
### DI (Dependency Injection)
In C# it is simple to instantiate a new object via the “new” operator. Instantiating a new object into a class imposes a coupling (i.e. a tightly bound connection), or even a reference to an assembly.
We could get by if the project was not too complex with simple objects and very little coupling. But, let's say you want to create a service, how are you going to quickly implement it in a new class/assembly? And in the worst case, the day you want to change the type of service in your simple project, which over time has become complex, how are you going to proceed?
As you have understood, this pattern is for any developer who cares about the quality of his software and seeks to make his application as maintainable as possible.
Indeed, the injection makes it possible to establish in a dynamic way the relation between several classes. It consists in dividing the responsibilities between the different modules, the different parts and even facilitates the subsequent modification of the classes.
### IOC (Inversion Of Control)
Once we have understood the principle of dependency injection, we can tackle another higher-level concept: inversion of control.
Indeed, if we have few or many classes calling on our service, we will create upstream `IMyService` instances as many times as necessary.
So we will end up with multiple instances, which was not meant to happen. Also, the fact of instantiating our interface in several places, we will find ourselves faced with as much code duplication as instantiations, which will make our application difficult to maintain.
Therefore, the first idea that comes to mind when we are unfamiliar with dependency inversion is to make our `MyService` class static.
Which is a very bad idea. We no longer have any problem with instances, however, now we will have to make our class `thread-safe` and properly manage `multi-threading`.
Not to mention the problems we will face when we want to do unit tests.
Others will have thought of the `singleton`. This is not the case, there are often too many memory leak problems.
Then, when we look in its fundamentals, we naturally think of one of the design patterns described by the "Gof": the `Factory`.
Indeed, a factory with a singleton could be the solution, unfortunately, there would always remain a weak coupling between the class and the call to each factory.
In our example, we would only have one, so we don't see the point of it.
But, in an application there are far more factories and there would therefore be as many couplings as there are factories. Without counting on the instantiations which can be more or less long.
But under certain conditions we could see it immediately without having to wait for the instantiation time.
To be able to outsource even more code, such as the creation of our object, and not slow down our program if there are long instantiations, we are going to have to map the contracts with their implementations at the start of the application.
The IOC is defined as a container that determines what must be instantiated and returned to the client to prevent the latter from explicitly calling the constructor with the "new" operator.
In a nutshell, it's an object that acts as a cache for the instances we need in various parts of our applications.
## Add services to a Blazor Server app
After creating a new app, examine part of the `Program.cs` file:
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
```
The `builder` variable represents a `Microsoft.AspNetCore.Builder.WebApplicationBuilder` with an `IServiceCollection`, which is a list of service descriptor objects.
Services are added by providing service descriptors to the service collection.
The following example demonstrates the concept with the `IDataAccess` interface and its concrete implementation `DataAccess`:
```csharp
builder.Services.AddSingleton<IDataAccess, DataAccess>();
```
## Service lifetime
Services can be configured with the following lifetimes:
### `Scoped`
Blazor WebAssembly apps don't currently have a concept of DI scopes. `Scoped`-registered services behave like `Singleton` services.
The Blazor Server hosting model supports the `Scoped` lifetime across HTTP requests but not across SignalR connection/circuit messages among components that are loaded on the client. The Razor Pages or MVC portion of the app treats scoped services normally and recreates the services on each HTTP request when navigating among pages or views or from a page or view to a component. Scoped services aren't reconstructed when navigating among components on the client, where the communication to the server takes place over the SignalR connection of the user's circuit, not via HTTP requests. In the following component scenarios on the client, scoped services are reconstructed because a new circuit is created for the user:
* The user closes the browser's window. The user opens a new window and navigates back to the app.
* The user closes a tab of the app in a browser window. The user opens a new tab and navigates back to the app.
* The user selects the browser's reload/refresh button.
### `Singleton`
DI creates a single instance of the service. All components requiring a `Singleton` service receive the same instance of the service.
### `Transient`
Whenever a component obtains an instance of a `Transient` service from the service container, it receives a new instance of the service.
## Request a service in a component
After services are added to the service collection, inject the services into the components using the @inject Razor directive, which has two parameters:
* Type: The type of the service to inject.
* Property: The name of the property receiving the injected app service. The property doesn't require manual creation. The compiler creates the property.
Use multiple `@inject` statements to inject different services.
The following example shows how to use `@inject`.
The service implementing `Services.IDataAccess` is injected into the component's property `DataRepository`. Note how the code is only using the `IDataAccess` abstraction:
```csharp
@page "/customer-list"
@inject IDataAccess DataRepository
@if (customers != null)
{
<ul>
@foreach (var customer in customers)
{
<li>@customer.FirstName @customer.LastName</li>
}
</ul>
}
@code {
private IReadOnlyList<Customer>? customers;
protected override async Task OnInitializedAsync()
{
customers = await DataRepository.GetAllCustomersAsync();
}
private class Customer
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
private interface IDataAccess
{
public Task<IReadOnlyList<Customer>> GetAllCustomersAsync();
}
}
```
Internally, the generated property (`DataRepository`) uses the `[Inject]` attribute.
Typically, this attribute isn't used directly.
If a base class is required for components and injected properties are also required for the base class, manually add the `[Inject]` attribute:
```csharp
using Microsoft.AspNetCore.Components;
public class ComponentBase : IComponent
{
[Inject]
protected IDataAccess DataRepository { get; set; }
...
}
```
## Use DI in services
Complex services might require additional services.
In the following example, `DataAccess` requires the HttpClient default service. `@inject` (or the `[Inject]` attribute) isn't available for use in services.
Constructor injection must be used instead.
Required services are added by adding parameters to the service's constructor.
When DI creates the service, it recognizes the services it requires in the constructor and provides them accordingly.
In the following example, the constructor receives an `HttpClient` via DI. `HttpClient` is a default service.
```csharp
using System.Net.Http;
public class DataAccess : IDataAccess
{
public DataAccess(HttpClient http)
{
...
}
}
```
Prerequisites for constructor injection:
* One constructor must exist whose arguments can all be fulfilled by DI. Additional parameters not covered by DI are allowed if they specify default values.
* The applicable constructor must be public.
* One applicable constructor must exist. In case of an ambiguity, DI throws an exception.

@ -0,0 +1,3 @@
label: 'Edit an item'
position: 7
collapsed: true

@ -0,0 +1,24 @@
---
sidebar_position: 3
title: Add Edit action
---
In order to reach the editing page, we will therefore modify the grid in order to add a link to our new page with the identifier of our element.
For this we will create a new `DataGridColumn` column with a `DisplayTemplate`.
Open the `Pages/List.razor` file and add the highlighted changes as follows:
```cshtml title="Pages/List.razor"
...
<DataGridColumn TItem="Item" Field="@nameof(Item.CreatedDate)" Caption="Created date" DisplayFormat="{0:d}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo("fr-FR")" />
// highlight-start
<DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="Action">
<DisplayTemplate>
<a href="Edit/@(context.Id)" class="btn btn-primary"><i class="fa fa-edit"></i> Editer</a>
</DisplayTemplate>
</DataGridColumn>
// highlight-end
</DataGrid>
```

@ -0,0 +1,365 @@
---
sidebar_position: 4
title: Creation of page editor
---
## Creation of the page
As before, create a new page which will be named `Edit.razor` and the partial class `Edit.razor.cs`.
## Final source code
```cshtml title="Pages/Edit.razor"
<h3>Edit</h3>
```
```csharp title="Pages/Edit.razor.cs"
public partial class Edit
{
}
```
## Set page url
We are going to define the url of our page by specifying that we want our identifier.
Open the `Pages/Edit.razor` file and add the highlighted edits as follows:
```cshtml title="Pages/Edit.razor"
// highlight-next-line
@page "/edit/{Id:int}"
<h3>Edit</h3>
```
## Pass a parameter
We passed in the url the id of our item to edit, now we are going to retrieve it in a `Parameter` in order to be able to use it in our code.
Open the `Pages/Edit.razor.cs` file and add the highlighted edits as follows:
```cshtml title="Pages/Edit.razor.cs"
public partial class Edit
{
[Parameter]
public int Id { get; set; }
}
```
## View Parameter
In order to verify the passage of our identifier we will display it.
Open the `Pages/Edit.razor` file and add the highlighted edits as follows:
```cshtml title="Pages/Edit.razor"
@page "/edit/{Id:int}"
<h3>Edit</h3>
// highlight-next-line
<div>My paremeter: @Id</div>
```
## Concept: URL parameters
### Route settings
The router uses routing parameters to populate corresponding component parameters with the same name.
Route parameter names are case insensitive. In the following example, the `text` parameter assigns the value of the road segment to the component's `Text` property.
When a request is made for `/RouteParameter/amazing`, the `<h1>` tag content is rendered as `Blazor is amazing!`.
```cshtml title="Pages/RouteParameter.razor"
@page "/RouteParameter/{text}"
<h1>Blazor is @Text!</h1>
@code {
[Parameter]
public string? Text { get; set; }
}
```
Optional parameters are supported.
In the following example, the optional `text` parameter assigns the value of the route segment to the component's `Text` property. If the segment is not present, the value of `Text` is set to `fantastic`.
```cshtml title="Pages/RouteParameter.razor"
@page "/RouteParameter/{text?}"
<h1>Blazor is @Text!</h1>
@code {
[Parameter]
public string? Text { get; set; }
protected override void OnInitialized()
{
Text = Text ?? "fantastic";
}
}
```
Use `OnParametersSet OnInitialized{Async}` instead to allow navigation to the same component with a different optional parameter value.
Based on the previous example, use `OnParametersSet` when the user needs to navigate from `/RouteParameter` to `/RouteParameter/amazing` or from `/RouteParameter/amazing` to `/RouteParameter`:
```html
protected override void OnParametersSet()
{
Text = Text ?? "fantastic";
}
```
### Route Constraints
A route constraint applies type matching on a route segment to a component.
In the following example, the route to the `User` component matches only if:
* A routing `Id` segment is present in the request URL.
* The `Id` segment is an integer type ( `int` ).
```cshtml title="Pages/User.razor"
@page "/user/{Id:int}"
<h1>User Id: @Id</h1>
@code {
[Parameter]
public int Id { get; set; }
}
```
The route constraints shown in the following table are available.
For more information about route constraints that correspond to the indifferent culture, see the disclaimer below the table.
| Constraint | Example | Matching examples | Invariant culture correspondence
| ---- | ---- | ---- | ----
| `bool` | `{active:bool}` | `true`, FALSE | No
| `datetime` | `{dob:datetime}` | `2016-12-31`, `2016-12-31 7:32pm` | Yes
| `decimal` | `{price:decimal}` | `49.99`, `-1,000.01` | Yes
| `double` | `{weight:double}` | `1.234`, `-1,001.01e8` | Yes
| `float` | `{weight:float}` | `1.234, -1`,`001.01e8` | Yes
| `guid` | `{id:guid}` | `CD2C1638-1638-72D5-1638-DEADBEEF1638`, `{CD2C1638-1638-72D5-1638-DEADBEEF1638}` | No
| `int` | `{id:int}` | `123456789`, `-123456789` | Yes
| `long` | `{ticks:long}` | `123456789`, `-123456789` | Yes
:::caution
Routing constraints that check that the URL can be converted to a CLR type (like `int` or `DateTime`) always use the invariant culture. these constraints assume that the URL is not localizable.
:::
Route constraints also work with optional parameters. In the following example, `Id` is required, but `Option` is an optional boolean route parameter.
```cshtml title="Pages/User.razor"
@page "/user/{Id:int}/{Option:bool?}"
<p>
Id: @Id
</p>
<p>
Option: @Option
</p>
@code {
[Parameter]
public int Id { get; set; }
[Parameter]
public bool Option { get; set; }
}
```
## Concept: Component Parameters
Component parameters pass data to components and are set using Public C# Properties on the component class with the `[Parameter]` attribute.
In the following example, a built-in reference type ( `System.String` ) and a user-defined reference type ( `PanelBody` ) are passed as component parameters.
```csharp title="PanelBody.cs"
public class PanelBody
{
public string? Text { get; set; }
public string? Style { get; set; }
}
```
```cshtml title="Shared/ParameterChild.razor"
<div class="card w-25" style="margin-bottom:15px">
<div class="card-header font-weight-bold">@Title</div>
<div class="card-body" style="font-style:@Body.Style">
@Body.Text
</div>
</div>
@code {
[Parameter]
public string Title { get; set; } = "Set By Child";
[Parameter]
public PanelBody Body { get; set; } =
new()
{
Text = "Set by child.",
Style = "normal"
};
}
```
:::caution
Providing initial values for component parameters is supported, but does not create a component that writes to its own parameters after the component is first rendered.
:::
The `Title` & `Body` component and `ParameterChild` component parameters are defined by arguments in the HTML tag that renders the component instance.
The following `ParameterParent` component renders two `ParameterChild` components:
* The first `ParameterChild` component is rendered without providing any parameter arguments.
* The second `ParameterChild` component receives values for `Title` and `Body` from the `ParameterParent` component, which uses an explicit C# expression to set the values for the properties of `PanelBody`.
```cshtml title="Pages/ParameterParent.razor"
@page "/parameter-parent"
<h1>Child component (without attribute values)</h1>
<ParameterChild />
<h1>Child component (with attribute values)</h1>
<ParameterChild Title="Set by Parent" Body="@(new PanelBody() { Text = "Set by parent.", Style = "italic" })" />
```
The following rendered HTML markup of the `ParameterParent` component shows default `ParameterChild` values for components when the `ParameterParent` component does not provide component parameter values.
When the `ParameterParent` component provides component parameter values, they override the component's default `ParameterChild` values.
:::note
For clarity, rendered CSS style classes are not shown in the following rendered HTML markup.
:::
```html
<h1>Child component (without attribute values)</h1>
<div>
<div>Set By Child</div>
<div>Set by child.</div>
</div>
<h1>Child component (with attribute values)</h1>
<div>
<div>Set by Parent</div>
<div>Set by parent.</div>
</div>
```
Assign a C# field, property, or result of a method to a component parameter as an HTML attribute value using the `@` symbol.
The following `ParameterParent2` component displays four instances of the preceding `ParameterChild` component and sets their parameter `Title` values to:
* Value of the `title` field.
* Result of C# `GetTitle` method.
* Current local date in long format with `ToLongDateString`, which uses an implicit C# expression.
* `panelData` Property of the `Title` object.
```cshtml title="Pages/ParameterParent2.razor"
@page "/parameter-parent-2"
<ParameterChild Title="@title" />
<ParameterChild Title="@GetTitle()" />
<ParameterChild Title="@DateTime.Now.ToLongDateString()" />
<ParameterChild Title="@panelData.Title" />
@code {
private string title = "From Parent field";
private PanelData panelData = new();
private string GetTitle()
{
return "From Parent method";
}
private class PanelData
{
public string Title { get; set; } = "From Parent object";
}
}
```
:::info
When assigning a C# member to a component parameter, prefix the member with the `@` symbol and never prefix the parameter's HTML attribute.
Correct use:
```html
<ParameterChild Title="@title" />
```
Wrong:
```html
<ParameterChild @Title="title" />
```
:::
Using an explicit Razor expression to concatenate text with an expression result for assignment to a parameter is not supported.
The following example attempts to concatenate the text "Set by" with the property value of an object. The following Razor syntax is not supported:
```html
<ParameterChild Title="Set by @(panelData.Title)" />
```
The code in the previous example generates a Compiler Error when building the application:
```
Les attributs de composant ne prennent pas en charge le contenu complexe (mixte C# et balisage).
```
To support assigning a compound value, use a method, field, or property. The following example performs the concatenation of "`Set by`" and the property value of an object in the C# `GetTitle` method:
```cshtml title="Pages/ParameterParent3.razor"
@page "/parameter-parent-3"
<ParameterChild Title="@GetTitle()" />
@code {
private PanelData panelData = new();
private string GetTitle() => $"Set by {panelData.Title}";
private class PanelData
{
public string Title { get; set; } = "Parent";
}
}
```
Component parameters must be declared as Automatic Properties, which means they must not contain custom logic in their `get` or `set` accessors.
For example, the following `StartData` property is an automatic property:
```csharp
[Parameter]
public DateTime StartData { get; set; }
```
Do not put custom logic in the `get` or `set` accessor, as component parameters are purely for use as a channel from a parent component to pass information to a child component.
If a `set` accessor of a child component property contains logic that causes the parent component to be re-rendered, the results of an infinite render loop.
To transform a received parameter value:
* Leave the parameter property as auto property to represent the raw data provided.
* Create another property or method to provide the transformed data based on the parameter property.
Override `OnParametersSetAsync` to transform a received parameter each time new data is received.
Writing an initial value to a component parameter is supported because initial value assignments don't interfere with automatic component Blazor rendering.
The following assignment of the current locale `DateTime` with `DateTime.Now` to `StartData` is valid syntax in a component:
```csharp
[Parameter]
public DateTime StartData { get; set; } = DateTime.Now;
```
After the initial assignment of `DateTime.Now`, do not assign a value to `StartData` in code.

@ -0,0 +1,98 @@
---
sidebar_position: 5
title: Creation of the form
---
## Creation of the form
We will use the same form as the one on the `Add.razor` page, the only differences are:
* The default value for the "Enchant categories" & "Repair with" lists so that they are checked when displaying the form.
* Image display
Open the `Pages/Edit.razor` page and modify it as follows:
```cshtml title="Pages/Edit.razor"
@page "/edit/{Id:int}"
<h3>Edit</h3>
<EditForm Model="@itemModel" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<p>
<label for="display-name">
Display name:
<InputText id="display-name" @bind-Value="itemModel.DisplayName" />
</label>
</p>
<p>
<label for="name">
Name:
<InputText id="name" @bind-Value="itemModel.Name" />
</label>
</p>
<p>
<label for="stack-size">
Stack size:
<InputNumber id="stack-size" @bind-Value="itemModel.StackSize" />
</label>
</p>
<p>
<label for="max-durability">
Max durability:
<InputNumber id="max-durability" @bind-Value="itemModel.MaxDurability" />
</label>
</p>
<p>
Enchant categories:
<div>
@foreach (var item in enchantCategories)
{
<label>
<input type="checkbox" @onchange="@(e => OnEnchantCategoriesChange(item, e.Value))" checked="@(itemModel.EnchantCategories.Contains(item) ? "checked" : null)" />@item
</label>
}
</div>
</p>
<p>
Repair with:
<div>
@foreach (var item in repairWith)
{
<label>
<input type="checkbox" @onchange="@(e => OnRepairWithChange(item, e.Value))" checked="@(itemModel.RepairWith.Contains(item) ? "checked" : null)" />@item
</label>
}
</div>
</p>
<p>
<label>
Current Item image:
@if (File.Exists($"{WebHostEnvironment.WebRootPath}/images/{itemModel.Name}.png"))
{
<img src="images/@(itemModel.Name).png" class="img-thumbnail" title="@itemModel.DisplayName" alt="@itemModel.DisplayName" style="max-width: 150px"/>
}
else
{
<img src="images/default.png" class="img-thumbnail" title="@itemModel.DisplayName" alt="@itemModel.DisplayName" style="max-width: 150px"/>
}
</label>
</p>
<p>
<label>
Item image:
<InputFile OnChange="@LoadImage" accept=".png" />
</label>
</p>
<p>
<label>
Accept Condition:
<InputCheckbox @bind-Value="itemModel.AcceptCondition" />
</label>
</p>
<button type="submit">Submit</button>
</EditForm>
```

@ -0,0 +1,108 @@
---
sidebar_position: 2
title: Changing the data service
---
In order to take into account the modification of an element, we will first add the following functionalities to our data service:
* Retrieve an item by its identifier
* Update an item
## Add the methods to the interface
Open the `Services/IDataService.cs` file and add the highlighted items:
```csharp title="Services/IDataService.cs"
public interface IDataService
{
Task Add(ItemModel model);
Task<int> Count();
Task<List<Item>> List(int currentPage, int pageSize);
// highlight-start
Task<Item> GetById(int id);
Task Update(int id, ItemModel model);
// highlight-end
}
```
## Add the methods to the implementation
Open the `Services/DataLocalService.cs` file and add the following methods:
```csharp title="Services/DataLocalService.cs"
...
public async Task<Item> GetById(int id)
{
// Get the current data
var currentData = await _localStorage.GetItemAsync<List<Item>>("data");
// Get the item int the list
var item = currentData.FirstOrDefault(w => w.Id == id);
// Check if item exist
if (item == null)
{
throw new Exception($"Unable to found the item with ID: {id}");
}
return item;
}
public async Task Update(int id, ItemModel model)
{
// Get the current data
var currentData = await _localStorage.GetItemAsync<List<Item>>("data");
// Get the item int the list
var item = currentData.FirstOrDefault(w => w.Id == id);
// Check if item exist
if (item == null)
{
throw new Exception($"Unable to found the item with ID: {id}");
}
// Save the image
var imagePathInfo = new DirectoryInfo($"{_webHostEnvironment.WebRootPath}/images");
// Check if the folder "images" exist
if (!imagePathInfo.Exists)
{
imagePathInfo.Create();
}
// Delete the previous image
if (item.Name != model.Name)
{
var oldFileName = new FileInfo($"{imagePathInfo}/{item.Name}.png");
if (oldFileName.Exists)
{
File.Delete(oldFileName.FullName);
}
}
// Determine the image name
var fileName = new FileInfo($"{imagePathInfo}/{model.Name}.png");
// Write the file content
await File.WriteAllBytesAsync(fileName.FullName, model.ImageContent);
// Modify the content of the item
item.DisplayName = model.DisplayName;
item.Name = model.Name;
item.RepairWith = model.RepairWith;
item.EnchantCategories = model.EnchantCategories;
item.MaxDurability = model.MaxDurability;
item.StackSize = model.StackSize;
item.UpdatedDate = DateTime.Now;
// Save the data
await _localStorage.SetItemAsync("data", currentData);
}
```

@ -0,0 +1,23 @@
---
sidebar_position: 1
title: Introduction
---
## Edit an item
This lab will allow you to edit an item.
### List of steps
* Modification of the data service
* Add Edit action
* Creation page edit
* Creation of the edit form
* Using the model
* Use the Pattern Factory
### List of concepts
* URL parameters
* Component Parameters
* Factory Pattern

@ -0,0 +1,178 @@
---
sidebar_position: 7
title: Use the Pattern Factory
---
In the code of the `Edit.razor.cs` page, we have transformed our item into the model and vice versa.
We currently use this code only in two pages, if we added a new field, it would be necessary to pass on each page in order to carry out the modifications of assignment.
We are therefore going to use the Factory pattern.
## Creation of our factory
To do this, create the `Factories` folder at the root of the site.
In this folder create a new `ItemFactory` class and modify it as follows:
```csharp title="Factories/ItemFactory.cs"
public static class ItemFactory
{
public static ItemModel ToModel(Item item, byte[] imageContent)
{
return new ItemModel
{
Id = item.Id,
DisplayName = item.DisplayName,
Name = item.Name,
RepairWith = item.RepairWith,
EnchantCategories = item.EnchantCategories,
MaxDurability = item.MaxDurability,
StackSize = item.StackSize,
ImageContent = imageContent
};
}
public static Item Create(ItemModel model)
{
return new Item
{
Id = model.Id,
DisplayName = model.DisplayName,
Name = model.Name,
RepairWith = model.RepairWith,
EnchantCategories = model.EnchantCategories,
MaxDurability = model.MaxDurability,
StackSize = model.StackSize,
CreatedDate = DateTime.Now
};
}
public static void Update(Item item, ItemModel model)
{
item.DisplayName = model.DisplayName;
item.Name = model.Name;
item.RepairWith = model.RepairWith;
item.EnchantCategories = model.EnchantCategories;
item.MaxDurability = model.MaxDurability;
item.StackSize = model.StackSize;
item.UpdatedDate = DateTime.Now;
}
}
```
A factory is always `static` ie it never instantiates, it can be considered as a converter.
## Using our factory
Open the `Services/DataLocalService.cs` file and modify as follows:
```csharp title="Services/DataLocalService.cs"
...
public async Task Add(ItemModel model)
{
...
// Simulate the Id
model.Id = currentData.Max(s => s.Id) + 1;
// Add the item to the current data
// highlight-next-line
currentData.Add(ItemFactory.Create(model));
// Save the image
var imagePathInfo = new DirectoryInfo($"{_webHostEnvironment.WebRootPath}/images");
...
}
public async Task Update(int id, ItemModel model)
{
...
// Write the file content
await File.WriteAllBytesAsync(fileName.FullName, model.ImageContent);
// Modify the content of the item
// highlight-next-line
ItemFactory.Update(item, model);
// Save the data
await _localStorage.SetItemAsync("data", currentData);
}
...
```
```csharp title="Pages/Edit.razor.cs"
...
protected override async Task OnInitializedAsync()
{
var item = await DataService.GetById(Id);
var fileContent = await File.ReadAllBytesAsync($"{WebHostEnvironment.WebRootPath}/images/default.png");
if (File.Exists($"{WebHostEnvironment.WebRootPath}/images/{itemModel.Name}.png"))
{
fileContent = await File.ReadAllBytesAsync($"{WebHostEnvironment.WebRootPath}/images/{item.Name}.png");
}
// Set the model with the item
// highlight-next-line
itemModel = ItemFactory.ToModel(item, fileContent);
}
...
```
## Concept: Factory Pattern
### What is the factory pattern?
The factory pattern describes a programming approach that allows you to create objects without having to specify the exact class of those objects.
This makes exchanging the created item flexible and convenient.
For the implementation, developers use the factory design pattern, also called factory pattern, which gives its name to the model.
This method is either specified in an interface and implemented by a child class, or implemented by a base class and possibly overridden (by derived classes).
The pattern thus replaces the usual class constructor to detach the creation of objects from the objects themselves, thus making it possible to follow the so-called SOLID principles.
:::tip
**SOLID Principles** are a subset of object-oriented programming (OOP) principles that aim to improve the object-oriented software development process. The acronym "SOLID" refers to the following five principles:
- Principle of **S**ingle-Responsibility: each class should have only one responsibility.
- Principle of **O**pen-Closed: the software units must be able to be extended without having to modify their behavior.
- Principle of **L**iskov Substitutions: a derived class must always be usable instead of its base class.
- Principle of **I**nterface-Segregation: the interfaces must be perfectly adapted to the needs of the customers who access them.
- Principle of **D**ependency-Inversion: classes at a higher level of abstraction should never depend on classes at a lower level of abstraction.
:::
### What is the purpose of the factory pattern?
The factory pattern aims to solve a fundamental problem during instantiation, i.e. the creation of a concrete object of a class, in object-oriented programming:
**creating an object directly within the class**, which needs this object or should use it, is possible in principle, but **very rigid**.
It binds the class to that particular object and makes it impossible to modify the instantiation independent of the class.
The factory pattern avoids such code by first defining a separate operation for creating the object: the factory.
When called, it generates the object, instead of the class constructor mentioned above.
### The pros and cons of the factory pattern
In the factory pattern, calling a program method is completely separate from implementing new classes, which has some advantages.
This has an effect in particular on the **extensibility of software**: instances of the factory pattern have a high degree of autonomy and allow you to **add new classes** without the application having to change in any way, alongside execution.
Just implement the factory interface and instantiate the creator accordingly.
Another advantage is the good testability of the factory components.
If a `factory` implements three classes, for example, their functionality can be tested individually and independently of the calling class.
In the case of the latter, you just need to make sure that it calls the `factory` correctly, even if the software is extended to this step later.
The ability to give a meaningful name to the factory pattern (as opposed to a class constructor) is also beneficial.
The great weakness of the factory pattern is the fact that its implementation leads to a large increase in the classes included.
As cost-effective as the factory pattern approach is in principle when it comes to extending software, it is also disadvantageous when it comes to effort:
if a product family is to be extended, not only the interface, but also all subordinate classes of the `factory` must be adapted accordingly.
Good advance planning of the types of products required is therefore essential.

@ -0,0 +1,129 @@
---
sidebar_position: 6
title: Using the model
---
## Using the model
We are going to set up the code for editing an item.
Open the file `Pages/Edit.razor.cs` and modify as follows:
```csharp title="Pages/Edit.razor.cs"
public partial class Edit
{
[Parameter]
public int Id { get; set; }
/// <summary>
/// The default enchant categories.
/// </summary>
private List<string> enchantCategories = new List<string>() { "armor", "armor_head", "armor_chest", "weapon", "digger", "breakable", "vanishable" };
/// <summary>
/// The current item model
/// </summary>
private ItemModel itemModel = new()
{
EnchantCategories = new List<string>(),
RepairWith = new List<string>()
};
/// <summary>
/// The default repair with.
/// </summary>
private List<string> repairWith = new List<string>() { "oak_planks", "spruce_planks", "birch_planks", "jungle_planks", "acacia_planks", "dark_oak_planks", "crimson_planks", "warped_planks" };
[Inject]
public IDataService DataService { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
[Inject]
public IWebHostEnvironment WebHostEnvironment { get; set; }
protected override async Task OnInitializedAsync()
{
var item = await DataService.GetById(Id);
var fileContent = await File.ReadAllBytesAsync($"{WebHostEnvironment.WebRootPath}/images/default.png");
if (File.Exists($"{WebHostEnvironment.WebRootPath}/images/{itemModel.Name}.png"))
{
fileContent = await File.ReadAllBytesAsync($"{WebHostEnvironment.WebRootPath}/images/{item.Name}.png");
}
// Set the model with the item
itemModel = new ItemModel
{
Id = item.Id,
DisplayName = item.DisplayName,
Name = item.Name,
RepairWith = item.RepairWith,
EnchantCategories = item.EnchantCategories,
MaxDurability = item.MaxDurability,
StackSize = item.StackSize,
ImageContent = fileContent
};
}
private async void HandleValidSubmit()
{
await DataService.Update(Id, itemModel);
NavigationManager.NavigateTo("list");
}
private async Task LoadImage(InputFileChangeEventArgs e)
{
// Set the content of the image to the model
using (var memoryStream = new MemoryStream())
{
await e.File.OpenReadStream().CopyToAsync(memoryStream);
itemModel.ImageContent = memoryStream.ToArray();
}
}
private void OnEnchantCategoriesChange(string item, object checkedValue)
{
if ((bool)checkedValue)
{
if (!itemModel.EnchantCategories.Contains(item))
{
itemModel.EnchantCategories.Add(item);
}
return;
}
if (itemModel.EnchantCategories.Contains(item))
{
itemModel.EnchantCategories.Remove(item);
}
}
private void OnRepairWithChange(string item, object checkedValue)
{
if ((bool)checkedValue)
{
if (!itemModel.RepairWith.Contains(item))
{
itemModel.RepairWith.Add(item);
}
return;
}
if (itemModel.RepairWith.Contains(item))
{
itemModel.RepairWith.Remove(item);
}
}
}
```
The changes from the add page are:
* Using the `OnInitializedAsync` method to retrieve the item relative to its identifier
* Retrieve an item instead of creating a new one
* Using template to modify data

@ -0,0 +1,3 @@
label: 'Globalization & Localization'
position: 9
collapsed: true

@ -0,0 +1,20 @@
---
sidebar_position: 1
title: Introduction
---
## Globalization & Localization
This lab will allow you to switch our site to multilingual.
### List of steps
* User language selection
* Translate the site with the resources files
### List of concepts
* Controllers
* Resource files
* Culture

@ -0,0 +1,160 @@
---
sidebar_position: 3
title: Translate the site using resources
---
## Location files
In order to be able to use localization, we will use resource files.
Create the `Resources` directory at the root of the site.
In this directory must be the resource files corresponding to your pages, the naming pattern is the directory of the page, a point, the name of the page then the language.
Example :
```
MyBeautifulAdmin
└───Resources
│ │ Pages.List.fr-FR.resx
│ │ Pages.List.resx
│ │ ...
└───Pages
│ │ List.razor
│ │ ...
```
## Creation of localization files
In the `Resources` directory, right click `Add => New Item...`, in the list select `Resources File`.
The file name will be `Pages.List.resx`, the file with no language will be the default if the language is not found.
Add your first translation, resource file works in key/value:
| Name | Value
| ---- | ----
| Title | Items List
Create the file for the French language `Pages.List.fr-FR.resx`
| Name | Value
| ---- | ----
| Title | Liste des éléments
Here is an example rendering of one of the resource files:
![Création des fichiers de localisation](/img/globalisation-localisation/ressource-file.png)
## Using Localization Files
Open the class `Pages/List.razor.cs` and add the service allowing to use the localization.
```csharp title="Pages/List.razor.cs"
public partial class List
{
[Inject]
public IStringLocalizer<List> Localizer { get; set; }
...
```
Open the `Pages/List.razor` page and use the `Localizer` to call up the resources:
```cshtml title="Pages/List.razor"
@page "/list"
@using MyBeautifulAdmin.Models
<h3>@Localizer["Title"]</h3>
...
```
## Concept: Resource files
You can include resources, such as strings, images, or object data, in resources files to make them easily available to your application.
The .NET Framework offers five ways to create resources files:
* Create a text file that contains string resources. You can use Resource File Generator (resgen.exe) to convert the text file into a binary resource (.resources) file. You can then embed the binary resource file in an application executable or an application library by using a language compiler, or you can embed it in a satellite assembly by using Assembly Linker (Al.exe).
* Create an XML resource (.resx) file that contains string, image, or object data. You can use Resource File Generator (resgen.exe) to convert the .resx file into a binary resource (.resources) file. You can then embed the binary resource file in an application executable or an application library by using a language compiler, or you can embed it in a satellite assembly by using Assembly Linker (Al.exe).
* Create an XML resource (.resx) file programmatically by using types in the System.Resources namespace. You can create a .resx file, enumerate its resources, and retrieve specific resources by name.
* Create a binary resource (.resources) file programmatically. You can then embed the file in an application executable or an application library by using a language compiler, or you can embed it in a satellite assembly by using Assembly Linker (Al.exe).
* Use Visual Studio to create a resource file and include it in your project. Visual Studio provides a resource editor that lets you add, delete, and modify resources. At compile time, the resource file is automatically converted to a binary .resources file and embedded in an application assembly or satellite assembly.
:::danger
Do not use resource files to store passwords, security-sensitive information, or private data.
:::
### Resources in .resx files
Unlike text files, which can only store string resources, XML resource (.resx) files can store strings, binary data such as images, icons, and audio clips, and programmatic objects.
A .resx file contains a standard header, which describes the format of the resource entries and specifies the versioning information for the XML that is used to parse the data.
The resource file data follows the XML header.
Each data item consists of a name/value pair that is contained in a `data` tag.
Its `name` attribute defines the resource name, and the nested `value` tag contains the resource value.
For string data, the `value` tag contains the string.
For example, the following `data` tag defines a string resource named `prompt` whose value is `"Enter your name:"`.
```xml
<data name="prompt" xml:space="preserve">
<value>Enter your name:</value>
</data>
```
The following example shows a portion of a .resx file that includes an Int32 resource and a bitmap image.
```xml
<data name="i1" type="System.Int32, mscorlib">
<value>20</value>
</data>
<data name="flag" type="System.Drawing.Bitmap, System.Drawing,
Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>
AAEAAAD/////AQAAAAAAAAAMAgAAADtTeX…
</value>
</data>
```
:::caution
Because .resx files must consist of well-formed XML in a predefined format, we do not recommend working with .resx files manually, particularly when the .resx files contain resources other than strings.
Instead, Visual Studio provides a transparent interface for creating and manipulating .resx files.
:::
## Concept: Culture
### Culture names and identifiers
The `CultureInfo` class specifies a unique name for each culture, based on RFC 4646.
The name is a combination of an ISO 639 two-letter lowercase culture code associated with a language and an ISO 3166 two-letter uppercase subculture code associated with a country or region.
In addition, for apps that target .NET Framework 4 or later and are running under Windows 10 or later, culture names that correspond to valid BCP-47 language tags are supported.
The format for the culture name based on RFC 4646 is `languagecode2`-`country/regioncode2`, where `languagecode2` is the two-letter language code and `country/regioncode2` is the two-letter subculture code.
Examples include `ja-JP` for Japanese (Japan) and `en-US` for English (United States).
In cases where a two-letter language code is not available, a three-letter code derived from ISO 639-2 is used.
Some culture names also specify an ISO 15924 script.
For example, `Cyrl` specifies the Cyrillic script and `Latn` specifies the Latin script.
A culture name that includes a script uses the pattern `languagecode2`-`scripttag`-`country/regioncode2`.
An example of this type of culture name is `uz-Cyrl-UZ` for Uzbek (Cyrillic, Uzbekistan).
On Windows operating systems before Windows Vista, a culture name that includes a script uses the pattern `languagecode2`-`country/regioncode2`-`scripttag`, for example, `uz-UZ-Cyrl` for Uzbek (Cyrillic, Uzbekistan).
A neutral culture is specified by only the two-letter, lowercase language code.
For example, `fr` specifies the neutral culture for French, and `de` specifies the neutral culture for German.
:::info
There are two culture names that contradict this rule. The cultures Chinese (Simplified), named `zh-Hans`, and Chinese (Traditional), named `zh-Hant`, are neutral cultures.
The culture names represent the current standard and should be used unless you have a reason for using the older names `zh-CHS` and `zh-CHT`.
:::
### The current culture and current UI culture
Every thread in a .NET application has a current culture and a current UI culture.
The current culture determines the formatting conventions for dates, times, numbers, and currency values, the sort order of text, casing conventions, and the ways in which strings are compared.
The current UI culture is used to retrieve culture-specific resources at run time.

@ -0,0 +1,224 @@
---
sidebar_position: 2
title: User language selection
---
## Install localization
Install the `Microsoft.Extensions.Localization` package.
## Configure location
Open the `Program.cs` file:
```csharp title="Program.cs"
...
builder.Services.AddBlazoredModal();
// highlight-start
// Add the controller of the app
builder.Services.AddControllers();
// Add the localization to the app and specify the resources path
builder.Services.AddLocalization(opts => { opts.ResourcesPath = "Resources"; });
// Configure the localtization
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
// Set the default culture of the web site
options.DefaultRequestCulture = new RequestCulture(new CultureInfo("en-US"));
// Declare the supported culture
options.SupportedCultures = new List<CultureInfo> { new CultureInfo("en-US"), new CultureInfo("fr-FR") };
options.SupportedUICultures = new List<CultureInfo> { new CultureInfo("en-US"), new CultureInfo("fr-FR") };
});
// highlight-end
var app = builder.Build();
...
app.UseRouting();
// highlight-start
// Get the current localization options
var options = ((IApplicationBuilder)app).ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
if (options?.Value != null)
{
// use the default localization
app.UseRequestLocalization(options.Value);
}
// Add the controller to the endpoint
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
// highlight-end
app.MapBlazorHub();
...
```
In order to be able to change the culture, we will have to declare a controller.
Create a new folder at the root of the site named `Controllers`.
Create a new class called `CultureController`.
```csharp title="Controllers/CultureController.cs"
using Microsoft.AspNetCore.Localization;
using Microsoft.AspNetCore.Mvc;
/// <summary>
/// The culture controller.
/// </summary>
[Route("[controller]/[action]")]
public class CultureController : Controller
{
/// <summary>
/// Sets the culture.
/// </summary>
/// <param name="culture">The culture.</param>
/// <param name="redirectUri">The redirect URI.</param>
/// <returns>
/// The action result.
/// </returns>
public IActionResult SetCulture(string culture, string redirectUri)
{
if (culture != null)
{
// Define a cookie with the selected culture
this.HttpContext.Response.Cookies.Append(
CookieRequestCultureProvider.DefaultCookieName,
CookieRequestCultureProvider.MakeCookieValue(
new RequestCulture(culture)));
}
return this.LocalRedirect(redirectUri);
}
}
```
## Language selector component
The following `CultureSelector` component shows how to set the user's culture selection.
The component is placed in the `Shared` folder for use throughout the application.
Create the `Shared/CultureSelector.razor` component.
```cshtml title="Shared/CultureSelector.razor"
@using System.Globalization
@inject NavigationManager NavigationManager
<p>
<label>
Select your locale:
<select @bind="Culture">
@foreach (var culture in supportedCultures)
{
<option value="@culture">@culture.DisplayName</option>
}
</select>
</label>
</p>
@code
{
private CultureInfo[] supportedCultures = new[]
{
new CultureInfo("en-US"),
new CultureInfo("fr-FR")
};
private CultureInfo Culture
{
get => CultureInfo.CurrentCulture;
set
{
if (CultureInfo.CurrentUICulture == value)
{
return;
}
var culture = value.Name.ToLower(CultureInfo.InvariantCulture);
var uri = new Uri(this.NavigationManager.Uri).GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped);
var query = $"?culture={Uri.EscapeDataString(culture)}&" + $"redirectUri={Uri.EscapeDataString(uri)}";
// Redirect the user to the culture controller to set the cookie
this.NavigationManager.NavigateTo("/Culture/SetCulture" + query, forceLoad: true);
}
}
}
```
Add in the `<div class="top-row px-4">` div in `Shared/MainLayout.razor`, add the `CultureSelector` component:
```cshtml title="Shared/MainLayout.razor"
...
<main>
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
// highlight-start
<div class="px-4">
<CultureSelector />
</div>
// highlight-end
</div>
...
```
Add the following code in the `Pages/Index.razor` file to visualize the language change.
```cshtml title="Pages/Index.razor"
@using System.Globalization
...
<p>
<b>CurrentCulture</b>: @CultureInfo.CurrentCulture
</p>
...
```
## Concept: Controllers
Some actions are not possible with the razor model, such as the declaration of cookies on the fly, to declare a cookie it is absolutely necessary that no HTML element be displayed before.
In order to overcome this problem, we therefore use an MVC type controller without the View and Model part.
Here is a small definition of the MVC architecture:
The Model-View-Controller (MVC) architectural pattern separates an app into three main components: **M**odel, **V**iew, and **C**ontroller.
The MVC pattern helps you create apps that are more testable and easier to update than traditional monolithic apps.
MVC-based apps contain:
* **M**odels: Classes that represent the data of the app. The model classes use validation logic to enforce business rules for that data. Typically, model objects retrieve and store model state in a database.
* **V**iews: Views are the components that display the app's user interface (UI). Generally, this UI displays the model data.
* **C**ontrollers: Classes that:
* Handle browser requests.
* Retrieve model data.
* Call view templates that return a response.
In an MVC app, the view only displays information.
The controller handles and responds to user input and interaction.
For example, the controller handles URL segments and query-string values, and passes these values to the model. The model might use these values to query the database.
For example:
* `https://localhost:5001/Home/Privacy`: specifies the `Home` controller and the `Privacy` action.
* `https://localhost:5001/Movies/Edit/5`: is a request to edit the movie with `ID=5` using the `Movies` controller and the `Edit` action.
The MVC architectural pattern separates an app into three main groups of components: Models, Views, and Controllers.
This pattern helps to achieve separation of concerns: The UI logic belongs in the view. Input logic belongs in the controller.
Business logic belongs in the model.
This separation helps manage complexity when building an app, because it enables work on one aspect of the implementation at a time without impacting the code of another.
For example, you can work on the view code without depending on the business logic code.

@ -0,0 +1,42 @@
---
sidebar_position: 1
title: Notation
---
# Scoring grid
Please note that this scoring grid is given for information only and the scale may change.
## Expected final rendering:
- The project on the gitlab, the forge, github ... (please invite me to the project if it is private)
- The documentation (preferably in a docs directory at the root of your project)
- A readme that explains how to launch the project, which branch, don't forget to explain the specific manipulations and the data to be created
:::caution
Without any indication from you, the master/main branch will be taken into account and the project launched by default.
:::
## Blazor Apps (*30 points*)
- Implementation of a data visualization page with pagination (*2 points*)
- Page for adding an element with validation (*2 point*)
- Edit page of an element with validation (*2 point*)
- Deletion of an element with a confirmation (*2 point*)
- Complex component (*5 point*)
- Use API (Get / Insert / Update / Delete) (*3 point*)
- IOC & DI use (*4 point*)
- Localization & Globalization (at least two languages) (*1 point*)
- Use of the configuration (*1 point*)
- Logs (*2 points*)
- Code cleanliness (You can use sonarqube) (*2 point*)
- GUI (Global design, placement of buttons, ...) (*2 point*)
- Code location (No code in views) (*2 point*)
## To go further (see list of bonuses in the progress section)
## Documentation (10 points)
- The Readme (*2 points*)
- Description of how the client solution works (code-level illustration) (*6 points*)
- Merge request (*2 points*)

@ -0,0 +1,199 @@
---
sidebar_position: 3
title: Introduction
---
## What is Blazor?
Blazor is an open-source web framework from the house of Microsoft.
It was released in the year 2018.
It allows developers to build applications using C# and the .Net libraries instead of JavaScript on the client-side.
It helps in running the application in the browser, and it can be hosted with several techniques.
The main technique are Blazor Web Assembly (WASM) and Blazor Web Server (Server).
* As the name suggests, Blazor relies on Razor syntax to build web applications.
* For this, the writing of `.razor` and `.cs` files, as well as the classic .css files for the design part will be necessary.
* Most obvious point: Since the Blazor application is written in C#, sharing code between client and server parts is obviously very simple, and one gets access to NuGet packages as well, etc.!
## Multiple versions of Blazor?
**Blazor WebAssembly**
![Blazor WebAssembly](/img/introduction/Blazor-WebAssembly.png)
Your .Net dlls are sent directly to the web browser and executed from there, like any static file.
* Not supported on IE11 and old browser
* API call possibly refused by firewall
* Executed on the client
**Blazor Server**
![Blazor Server](/img/introduction/Blazor-Server.png)
Based on SignalR technology, offers to keep all the execution of your app on the server side.
* Source code not accessible
* All access executed in server
* Requires high-performance servers
## Blazor WebAssembly, more details
Blazor Web Assembly (WASM) was released in May 2020.
It works very similarly to Single Page Application (SPA) frameworks (Angular, React, Vue..).
Here, the C# and Razor components are compiled as .Net assemblies, then downloaded and run on the browser side (client-side).
Once the application is downloaded on the client-side and as it begins to run, there is no requirement of SignalR here for maintaining the connection between Server and client.
Server-side functionalities cannot be integrated with this application, and it requires a Web API to communicate and exchange required data.
### Here are some of the positives of using Blazor Web Assembly
* After the initial load, the UI reacts faster to user requests (except external API calls), as the entire content of the website is being downloaded on the client-side.
* Since the application is downloaded on the client-side, offline support is possible in case of network issues.
### Here are some of the disadvantages of using Blazor Web Assembly
* The high initial load time of the application.
* File manipulation.
* Interaction with another system.
## Blazor Server, more details
Blazor Server is a hosting technique that was released along with the .Net Core.
It uses the ASP.Net Core application, which helps integrate the server-side functionality.
Along with this, the client-side pages are created using Razor components.
On running the application, a connection is established between the browser and server-side using SignalR (an open-source library for ASP.Net-based applications used to send asynchronous notifications to client-side web applications).
The server then sends the payloads to the client, which updates the page. It also enables two-way communication between server and client-side.
### Here are some of the advantages of using Blazor Server
* Application is loaded and rendered in a quick time as users do not download application libraries.
* The backend code is isolated as the C# files are not being sent to the client-side.
* It supports and works in older versions of browsers as well.
### Here are some of the disadvantages of using Blazor Server
* As the pages are being sent from the server to the client-side, whenever there are network instabilities, offline support will be a problem.
* As the application creates new WebSockets for each connection, scalability will be challenging as the amount of memory consumed will be huge.
## Summary of differences
| Characteristics | Blazor WebAssembly | Blazor Server |
| ---- |:----:|:----:|
| Can use C# code for Web Apps | ✔️ | ✔️ |
| Small download size | Have to download .NET Runtime + dependencies | ✔️ |
| Works well with devices with limited resources | All the code have to be run in the browser | ✔️ |
| Execution speed | ✔️ | Can have latency |
| Serverless | ✔️ | Needs a server |
| Independent from ASP.NET Core | ✔️ | Require ASP.NET Core |
| Independent from WebAssembly | Requires WebAssembly | ✔️ |
| Scability | ✔️ | Can be a challenge after a thousand users depending on server capabilities |
| Can be served from a CDN | ✔️ | Needs a server |
| Offline mode | ✔️ | Needs a continuous connection |
| Sensitive code are protected | The code is available of the user | ✔️ |
| Use .NET tooling support | No | ✔️ |
## Blazor Hybrid (.NET MAUI Blazor app) ???
.NET Multi-platform App UI (MAUI) is a cross-platform framework.
It enables you to build cross-platform apps with a shared code base that can run natively on Android, iOS, macOS and Windows.
Out of the box, .NET MAUI provides support for data-binding; cross-platform APIs for accessing native device features like GPS, accelerometer, battery and network states; a layout engine (for designing pages) and cross-platform graphics functionality (for drawing, painting shapes and images).
The promise of .NET MAUI is that youll be able to build a single app and have it run on all four operating systems. But it goes even further when you add Blazor to the mix.
## Create my first app
```cmd
dotnet new blazorwasm -o MyBeautifulFirstApp
```
![Create my first app](/img/introduction/creer-ma-premiere-application.png)
## Launch my first app
```cmd
cd MyBeautifulFirstApp
dotnet run
```
![Launch my first app](/img/introduction/lancer-ma-premiere-application.png)
Open in browser: [https://localhost:5001](https://localhost:5001)
## Anatomy of the app
* Based on classic ASP.NET Core, a `Program.cs` file will call a `Startup.cs` file. Responsible for referencing the Blazor root component, conventionally called App.
* For the Blazor part, an `App.razor` file defining the root component, as well as a default page, usually called `Index.razor`, placed inside a Pages folder.
* Finally, a linking file `index.html`, contained in the `wwwroot` folder.
* In this file, we will find in particular a reference to the `blazor.webassembly.js` framework which will allow you to load the runtime in its webassembly version, as well as all the .NET libraries (.dll).
* Below is a short summary of all the files mentioned:
* A C# project `.csproj` file.
* A `Program.cs` input file.
* A `Startup.cs` file.
* A root Blazor component defined in an `App.razor` file.
* An `Index.razor` page defined in a `Pages` folder (by convention).
* Then finally an `index.html` file inside the `wwwroot` folder which will be the entry point.
## Host a Blazor app
* IIS Server
* Blazor offers "out-of-the-box" integration, and provides a `web.config` file necessary for hosting, which can be found directly among our application's publication files. the installation of the URL Rewrite module is necessary.
* Docker container
* With a Dockerfile, use Nginx or Apache servers.
* Azure Blob Storage
* Use of the Static website functionality which allows to expose the files contained in the storage in http.
* Github pages
* With some modification, it is possible to host Blazor (WASM) apps.
* ASP.NET Core
* The best option for hosting Blazor is still the aspnetcore app with a Kestrel server.
## Design
Blazor's default graphics system is `Bootstrap v4.3.1`.
Example :
![Example design bootstrap](/img/introduction/exemple-design-bootstrap.png)
## Binding
Razor provides data binding functionality with `@bind`.
![Binding](/img/introduction/binding.png)
## Code location
By default the code is in the `.razor` page.
In order to separate the code and the design of your pages, you must create a class file containing your code.
![Code location](/img/introduction/emplacement-code-01.png)
To do this you just need to create a new class with the name `MyRazorPage.razor.cs`.
This must be partial:
![Code location](/img/introduction/emplacement-code-02.png)
## Navigation
Declare the url of the page:
![Navigation](/img/introduction/navigation-01.png)
Navigate to a page from code:
![Navigation](/img/introduction/navigation-02.png)
## Create a new page
To create a new page, nothing could be simpler, just create a new Razor component.
![Create a new page](/img/introduction/creer-une-nouvelle-page-01.png)
![Create a new page](/img/introduction/creer-une-nouvelle-page-02.png)

@ -0,0 +1,38 @@
---
sidebar_position: 15
title: Coding live
---
## What is dotnet watch?
`dotnet watch` is a function that executes [.NET Core CLI commands](https://docs.microsoft.com/fr-fr/dotnet/core/tools/) each time a source file changes (In big as soon as we record).
Therefore, the command that we will associate will be `run`!
In short, we will execute our project each time we save our work.
```
dotnet watch run
```
This feature saves time during Blazor app development.
## Usage in Visual Studio 2019
You can directly open a Powershell window from the `View` -> `Terminal` menu.
Once in the terminal, change the current directory to that of your application, where your `csproj` file is.
Execute the command:
```
dotnet watch run
```
Your browser opens with your application, now return to Visual Studio, with each new backup your site will be compiled and updated in your browser.
## Usage in Visual Studio 2022
Visual Studio 2022 natively supports `Hot Reload` which eliminates the need to use this command.

@ -0,0 +1,155 @@
---
sidebar_position: 2
title: Progress
---
## Introducing Blazor
* Steps
* Structure
* Pages
* Browsing
* Code location
* Binding
* Use HTML/Css/JS
## Create a new Blazor Server project
* Steps
* Creation of a new project with Visual Studio
## Display data with Blazor
* Steps
* Creation of a new page
* Generation of false data
* Use data
* Use an HTML table
* Use Blazorise
* Use paging with Blazorise
* Concepts
* Layout
* Serialization
* Life cycle events
* NuGet
## Add a new item
* Steps
* Store our data in the LocalStorage
* Creation of a new page
* Add an `Add` button
* Creation of the add-on model
* Creation of the form
* Redirect to listing page
* Display of downloaded image
* Concepts
* Blazor Storage
* Routing
* Data Annotation
* Form and validation
* Uri and browsing state assistance
## DI & IOC
* Steps
* Creation of a data service
* Concepts
* Dependency Injection (DI)
* Inversion of Control (IOC)
## Edit an item
* Steps
* Modification of the data service
* Add Edit action
* Creation page edit
* Creation of the edit form
* Using the model
* Use the Pattern Factory
* Concepts
* URL parameters
* Component Parameters
* Factory Pattern
## Delete an item
* Steps
* Modification of the data service
* Add Delete action
* Install Blazored.Modal
* Creation of a confirmation popup
* Concepts
* Cascading Parameters
## Globalization & Localization
* Steps
* User language selection
* Translate the site thanks to the resources
* Concepts
* Controllers
* Resource files
* Culture
## Razor component
* Steps
* Creation of a generic component
* Creation of a complex component
* Component Description
* Classes appendix of the component
* Creation of the `item` component element
* Creation of the `craft` component element
* Component Usage
* Concepts
* RenderFragment
* Event handling
* IJSRuntime
* CascadingValue & CascadingParameter
## API
* Steps
* Make HTTP requests
* Concepts
* IHttpClientFactory
## Setup
* Steps
* Using Setup
## Logger
* Steps
* Use of logs
:::info
Bonus part (likely to change over time)
:::
## Using graphQL API / Grpc
* Steps
* Filter management
## Using Websockets
* Steps
* Customer notification
## Authentication
* Steps
* Creation of an authentication page
* Use authentication with APIs
## Various
* Steps
* Dockerization of the project or part of the project
* Using SonarQube
* Using OpenId Connect and/or OAuth
* Implementation of an identity server (Identity Server 4 for example)
* MVVM

@ -0,0 +1,3 @@
label: 'Razor Component'
position: 10
collapsed: true

@ -0,0 +1,29 @@
---
sidebar_position: 4
title: Component appendix classes
---
In order to make our component work we will need several additional classes.
Typically, components are in a `Components` folder, so we'll do the same and create that folder.
As seen in the description, we need to trace user actions, so for that we need a class with the information to trace.
```csharp title="Components/CraftingAction.cs"
public class CraftingAction
{
public string Action { get; set; }
public int Index { get; set; }
public Item Item { get; set; }
}
```
Our component must also accept recipes, so we will create a class representing a recipe.
```csharp title="Components/CraftingRecipe.cs"
public class CraftingRecipe
{
public Item Give { get; set; }
public List<List<string>> Have { get; set; }
}
```

@ -0,0 +1,24 @@
---
sidebar_position: 3
title: Component Description
---
## Description
In order to better understand our complex component, we will first make a short description of its content and the behavior expected by it.
The purpose of this component is to allow us to test recipes, for this we must have on the left a table containing the list of elements available for our recipe.
On the right we have to find the locations that will contain our recipe elements, once our recipe is valid, a last element will be displayed under our locations with the result of the recipe.
A list below will contain all the actions performed by the user and will be managed in JavaScript.
## Mockup
For a better visualization of our component, a mockup can be made in order to better realize the final.
For this we can use the site https://wireframe.cc/.
The result of the mockup is:
![Mockup](/img/blazor-component/mockup.png)

@ -0,0 +1,250 @@
---
sidebar_position: 5
title: Component subcomponent
---
Our component will mainly call for an item, whether to fill the grid, display the boxes of the recipe and even the result.
In order not to have to repeat code in our component, we will therefore create a sub-component which will allow the display of our elements.
Our component remains simple but uses the Drag & Drop method.
## Creating our base component view
```cshtml title="Components/CraftingItem.razor"
<div
class="item"
ondragover="event.preventDefault();"
draggable="true"
@ondragstart="@OnDragStart"
@ondrop="@OnDrop"
@ondragenter="@OnDragEnter"
@ondragleave="@OnDragLeave">
@if (Item != null)
{
@Item.DisplayName
}
</div>
```
We use Razor's envent handling for Drag & Drop methods.
## Creating our component code
The code of our item remains simple, we call on our parent in order to pass it the action information as well as Drag & Drop.
```csharp title="Components/CraftingItem.razor.cs"
public partial class CraftingItem
{
[Parameter]
public int Index { get; set; }
[Parameter]
public Item Item { get; set; }
[Parameter]
public bool NoDrop { get; set; }
[CascadingParameter]
public Crafting Parent { get; set; }
internal void OnDragEnter()
{
if (NoDrop)
{
return;
}
Parent.Actions.Add(new CraftingAction { Action = "Drag Enter", Item = this.Item, Index = this.Index });
}
internal void OnDragLeave()
{
if (NoDrop)
{
return;
}
Parent.Actions.Add(new CraftingAction { Action = "Drag Leave", Item = this.Item, Index = this.Index });
}
internal void OnDrop()
{
if (NoDrop)
{
return;
}
this.Item = Parent.CurrentDragItem;
Parent.RecipeItems[this.Index] = this.Item;
Parent.Actions.Add(new CraftingAction { Action = "Drop", Item = this.Item, Index = this.Index });
// Check recipe
Parent.CheckRecipe();
}
private void OnDragStart()
{
Parent.CurrentDragItem = this.Item;
Parent.Actions.Add(new CraftingAction { Action = "Drag Start", Item = this.Item, Index = this.Index });
}
}
```
It is also possible to create CSS style files directly for our component, this file will be automatically rendered with our component.
```css title="Components/CraftingItem.razor.cs"
.item {
width: 64px;
height: 64px;
border: 1px solid;
overflow: hidden;
}
```
## Concept: Event handling
Specify delegate event handlers in Razor component markup with `@on{DOM EVENT}="{DELEGATE}"` Razor syntax:
* The `{DOM EVENT}` placeholder is a Document Object Model (DOM) event (for example, `click`).
* The `{DELEGATE}` placeholder is the C# delegate event handler.
For event handling:
* Asynchronous delegate event handlers that return a Task are supported.
* Delegate event handlers automatically trigger a UI render, so there's no need to manually call StateHasChanged.
* Exceptions are logged.
The following code:
* Calls the `UpdateHeading` method when the button is selected in the UI.
* Calls the `CheckChanged` method when the checkbox is changed in the UI.
```cshtml title="Pages/EventHandlerExample1.razor"
@page "/event-handler-example-1"
<h1>@currentHeading</h1>
<p>
<label>
New title
<input @bind="newHeading" />
</label>
// highlight-next-line
Update heading
</button>
</p>
<p>
<label>
// highlight-next-line
<input type="checkbox" @onchange="CheckChanged" />
@checkedMessage
</label>
</p>
@code {
private string currentHeading = "Initial heading";
private string? newHeading;
private string checkedMessage = "Not changed yet";
// highlight-start
private void UpdateHeading()
{
currentHeading = $"{newHeading}!!!";
}
private void CheckChanged()
{
checkedMessage = $"Last changed at {DateTime.Now}";
}
// highlight-end
}
```
In the following example, `UpdateHeading`:
* Is called asynchronously when the button is selected.
* Waits two seconds before updating the heading.
```cshtml title="Pages/EventHandlerExample2.razor"
@page "/event-handler-example-2"
<h1>@currentHeading</h1>
<p>
<label>
New title
<input @bind="newHeading" />
</label>
// highlight-next-line
<button @onclick="UpdateHeading">
Update heading
</button>
</p>
@code {
private string currentHeading = "Initial heading";
private string? newHeading;
// highlight-start
private async Task UpdateHeading()
{
await Task.Delay(2000);
currentHeading = $"{newHeading}!!!";
}
// highlight-end
}
```
### Event arguments
For events that support an event argument type, specifying an event parameter in the event method definition is only necessary if the event type is used in the method.
In the following example, `MouseEventArgs` is used in the `ReportPointerLocation` method to set message text that reports the mouse coordinates when the user selects a button in the UI.
```cshtml title="Pages/EventHandlerExample3.razor"
@page "/event-handler-example-3"
@for (var i = 0; i < 4; i++)
{
<p>
<button @onclick="ReportPointerLocation">
Where's my mouse pointer for this button?
</button>
</p>
}
<p>@mousePointerMessage</p>
@code {
private string? mousePointerMessage;
// highlight-start
private void ReportPointerLocation(MouseEventArgs e)
{
mousePointerMessage = $"Mouse coordinates: {e.ScreenX}:{e.ScreenY}";
}
// highlight-end
}
```
### Supported EventArgs
| Event | Class | Document Object Model (DOM) events |
| ---- | ---- | ---- |
| Clipboard | ClipboardEventArgs | `oncut`, `oncopy`, `onpaste` |
| Drag | DragEventArgs | `ondrag`, `ondragstart`, `ondragenter`, `ondragleave`, `ondragover`, `ondrop`, `ondragend` |
| Error | ErrorEventArgs | `onerror` |
| General | EventArgs | `onactivate`, `onbeforeactivate`, `onbeforedeactivate`, `ondeactivate`, `onfullscreenchange`, `onfullscreenerror`, `onloadeddata`, `onloadedmetadata`, `onpointerlockchange`, `onpointerlockerror`, `onreadystatechange`, `onscroll` |
| Clipboard | EventArgs | `onbeforecut`, `onbeforecopy`, `onbeforepaste` |
| Input | EventArgs | `oninvalid`, `onreset`, `onselect`, `onselectionchange`, `onselectstart`, `onsubmit` |
| Media | EventArgs | `oncanplay`, `oncanplaythrough`, `oncuechange`, `ondurationchange`, `onemptied`, `onended`, `onpause`, `onplay`, `onplaying`, `onratechange`, `onseeked`, `onseeking`, `onstalled`, `onstop`, `onsuspend`, `ontimeupdate`, `ontoggle`, `onvolumechange`, `onwaiting` |
| Focus | FocusEventArgs | `onfocus`, `onblur`, `onfocusin`, `onfocusout` |
| Input | ChangeEventArgs | `onchange`, `oninput` |
| Keyboard | KeyboardEventArgs | `onkeydown`, `onkeypress`, `onkeyup` |
| Mouse | MouseEventArgs | `onclick`, `oncontextmenu`, `ondblclick`, `onmousedown`, `onmouseup`, `onmouseover`, `onmousemove`, `onmouseout` |
| Mouse pointer | PointerEventArgs | `onpointerdown`, `onpointerup`, `onpointercancel`, `onpointermove`, `onpointerover`, `onpointerout`, `onpointerenter`, `onpointerleave`, `ongotpointercapture`, `onlostpointercapture` |
| Mouse wheel | WheelEventArgs | `onwheel`, `onmousewheel` |
| Progress | ProgressEventArgs | `onabort`, `onload`, `onloadend`, `onloadstart`, `onprogress`, `ontimeout` |
| Touch | TouchEventArgs | `ontouchstart`, `ontouchend`, `ontouchmove`, `ontouchenter`, `ontouchleave`, `ontouchcancel` |

@ -0,0 +1,533 @@
---
sidebar_position: 6
title: Component code
---
Here is the code of our main component, it is he who will call our basic item and perform a large part of the actions.
## Creation of the view
```cshtml title="Components/Crafting.razor"
<CascadingValue Value="@this">
<div class="container">
<div class="row">
<div class="col-6">
<div>Available items:</div>
<div>
<div class="css-grid">
@foreach (var item in Items)
{
<CraftingItem Item="item" NoDrop="true"/>
}
</div>
</div>
</div>
<div class="col-6">
<div>Recipe</div>
<div>
<div class="css-recipe">
<CraftingItem Index="0"/>
<CraftingItem Index="1"/>
<CraftingItem Index="2"/>
<CraftingItem Index="3"/>
<CraftingItem Index="4"/>
<CraftingItem Index="5"/>
<CraftingItem Index="6"/>
<CraftingItem Index="7"/>
<CraftingItem Index="8"/>
</div>
</div>
<div>Result</div>
<div>
<CraftingItem Item="RecipeResult"/>
</div>
</div>
<div class="col-12">
<div>Actions</div>
<div class="actions" id="actions">
</div>
</div>
</div>
</div>
</CascadingValue>
```
## Code creation
We use an `ObservableCollection` to allow an action on a change in the list with the `CollectionChanged` event.
```csharp title="Components/Crafting.razor.cs"
public partial class Crafting
{
private Item _recipeResult;
public Crafting()
{
Actions = new ObservableCollection<CraftingAction>();
Actions.CollectionChanged += OnActionsCollectionChanged;
this.RecipeItems = new List<Item> { null, null, null, null, null, null, null, null, null };
}
public ObservableCollection<CraftingAction> Actions { get; set; }
public Item CurrentDragItem { get; set; }
[Parameter]
public List<Item> Items { get; set; }
public List<Item> RecipeItems { get; set; }
public Item RecipeResult
{
get => this._recipeResult;
set
{
if (this._recipeResult == value)
{
return;
}
this._recipeResult = value;
this.StateHasChanged();
}
}
[Parameter]
public List<CraftingRecipe> Recipes { get; set; }
/// <summary>
/// Gets or sets the java script runtime.
/// </summary>
[Inject]
internal IJSRuntime JavaScriptRuntime { get; set; }
public void CheckRecipe()
{
RecipeResult = null;
// Get the current model
var currentModel = string.Join("|", this.RecipeItems.Select(s => s != null ? s.Name : string.Empty));
this.Actions.Add(new CraftingAction { Action = $"Items : {currentModel}" });
foreach (var craftingRecipe in Recipes)
{
// Get the recipe model
var recipeModel = string.Join("|", craftingRecipe.Have.SelectMany(s => s));
this.Actions.Add(new CraftingAction { Action = $"Recipe model : {recipeModel}" });
if (currentModel == recipeModel)
{
RecipeResult = craftingRecipe.Give;
}
}
}
private void OnActionsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
JavaScriptRuntime.InvokeVoidAsync("Crafting.AddActions", e.NewItems);
}
}
```
As for our element we create a CSS file for our component.
```css title="Components/Crafting.razor.css"
.css-grid {
grid-template-columns: repeat(4,minmax(0,1fr));
gap: 10px;
display: grid;
width: 286px;
}
.css-recipe {
grid-template-columns: repeat(3,minmax(0,1fr));
gap: 10px;
display: grid;
width: 212px;
}
.actions {
border: 1px solid black;
height: 250px;
overflow: scroll;
}
```
A JavaScript class is also created to interact with the page.
```js title="Components/Crafting.razor.js"
window.Crafting =
{
AddActions: function (data) {
data.forEach(element => {
var div = document.createElement('div');
div.innerHTML = 'Action: ' + element.action + ' - Index: ' + element.index;
if (element.item) {
div.innerHTML += ' - Item Name: ' + element.item.name;
}
document.getElementById('actions').appendChild(div);
});
}
}
```
## Concept: CascadingValue & CascadingParameter
In our component we need to know our parent when we are in an item in order to call the `CheckRecipe()` methods as well as to add actions or to know the element we are moving.
We therefore use in the view of our component `Crafting.razor` the following code:
```html title="Crafting.razor"
<CascadingValue Value="@this">
...
<CraftingItem Item="item" NoDrop="true"/>
...
</CascadingValue>
```
This code makes it possible to make our component available in all the sub-components found in the `CascadingValue` tag.
To retrieve our `CascadingValue` we must use in our component a `CascadingParameter` as follows:
```csharp
[CascadingParameter]
public Crafting Parent { get; set; }
```
It would be possible to go through `Parameter` but in case of multiple levels, it would be necessary in each level to add our `Parameter` to nest them:
```cshtml title="Components/MyRootComponent.razor"
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public string Text { get; set; }
}
<div style="border: 1px solid black; padding: 10px;">
<strong>MyRootComponent - @Text</strong>
<div>
@ChildContent
</div>
</div>
```
```cshtml title="Components/MyFirstChildComponent.razor"
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public MyRootComponent RootComponent { get; set; }
}
<div style="border: 1px solid black; padding: 10px;">
<strong>MyFirstChildComponent - @RootComponent.Text</strong>
<div>
@ChildContent
</div>
</div>
```
```cshtml title="Components/MySecondChildComponent.razor"
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public MyRootComponent RootComponent { get; set; }
}
<div style="border: 1px solid black; padding: 10px;">
<strong>MySecondChildComponent - @RootComponent.Text</strong>
<div>
@ChildContent
</div>
</div>
```
```cshtml title="Pages/Index.razor"
@code
{
MyRootComponent MyRootComponent;
}
<MyRootComponent @ref="MyRootComponent" Text="RootComponentText">
<MyFirstChildComponent RootComponent="@MyRootComponent">
<MySecondChildComponent RootComponent="@MyRootComponent">
<div>MySecondChildComponent - Content</div>
</MySecondChildComponent>
</MyFirstChildComponent>
</MyRootComponent>
```
This also forces the developer to declare the component's usage variable each time it is used, as well as having to specify an attribute with its value.
Whereas with `CascadingValue` & `CascadingParameter` the code becomes simpler:
```cshtml title="Components/MyRootComponent.razor"
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
[Parameter]
public string Text { get; set; }
}
<div style="border: 1px solid black; padding: 10px;">
<strong>MyRootComponent - @Text</strong>
<div>
<CascadingValue Value="@this">
@ChildContent
</CascadingValue>
</div>
</div>
```
```cshtml title="Components/MyFirstChildComponent.razor"
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
[CascadingParameter]
public MyRootComponent RootComponent { get; set; }
}
<div style="border: 1px solid black; padding: 10px;">
<strong>MyFirstChildComponent - @RootComponent.Text</strong>
<div>
@ChildContent
</div>
</div>
```
```cshtml title="Components/MySecondChildComponent.razor"
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
[CascadingParameter]
public MyRootComponent RootComponent { get; set; }
}
<div style="border: 1px solid black; padding: 10px;">
<strong>MySecondChildComponent - @RootComponent.Text</strong>
<div>
@ChildContent
</div>
</div>
```
```cshtml title="Pages/Index.razor"
<MyRootComponent Text="RootComponentText">
<MyFirstChildComponent>
<MySecondChildComponent>
<div>MySecondChildComponent - Content</div>
</MySecondChildComponent>
</MyFirstChildComponent>
</MyRootComponent>
```
## Concept: IJSRuntime
To call JS from .net, inject the `IJSRuntime` abstraction and call one of the following methods:
* IJSRuntime.InvokeAsync
* JSRuntimeExtensions.InvokeAsync
* JSRuntimeExtensions.InvokeVoidAsync
For previous .NET methods that call JS functions:
* The function identifier ( `String` ) is relative to the global scope ( `window` ). To call `window.someScope.someFunction`, the identifier is `someScope.someFunction`. There is no need to register the function before it is called.
* Pass any number of on-Serializable JS arguments in `Object[]` to a JS function.
* The cancellation token ( `CancellationToken` ) propagates a notification that operations should be canceled.
* `TimeSpan` represents a time limit for a JS operation.
* The `TValue` return type must also be serializable. `TValue` should be the .NET type that best matches the JS returned type.
* `JS A Promise` is returned for `InvokeAsync` methods. `InvokeAsync` unwraps `Promise` and returns the value expected by `Promise`.
For apps that have Blazor Server pre-render enabled, invoking JS is not possible during the initial pre-render. Interop JS calls should be deferred until the connection with the browser is established.
The following example is based on `TextDecoder`, a JS-based decoder.
The sample demonstrates how to call a JS function from a C# method that offloads a specification from developer code to an existing JS API.
The JS function accepts a byte array from a C# method, decodes the array, and returns the text to the component for display.
Add the following JS code inside the closing `</body>` tag in the `wwwroot/index.html` file:
```html
<script>
window.convertArray = (win1251Array) => {
var win1251decoder = new TextDecoder('windows-1251');
var bytes = new Uint8Array(win1251Array);
var decodedArray = win1251decoder.decode(bytes);
console.log(decodedArray);
return decodedArray;
};
</script>
```
The following `CallJsExample1` component:
* Calls the JS function `convertArray` with `InvokeAsync` when selecting a button ( `Convert Array` ).
* Once the JS function is called, the passed array is converted to a string. The string is returned to the component for display ( `text` ).
```cshtml title="Pages/CallJsExample1.razor"
@page "/call-js-example-1"
@inject IJSRuntime JS
<h1>Call JS <code>convertArray</code> Function</h1>
<p>
<button @onclick="ConvertArray">Convert Array</button>
</p>
<p>
@text
</p>
<p>
<a href="https://www.imdb.com/title/tt0379786/" target="_blank">Serenity</a><br>
<a href="https://www.imdb.com/name/nm0472710/" target="_blank">David Krumholtz on IMDB</a>
</p>
@code {
private MarkupString text;
private uint[] quoteArray =
new uint[]
{
60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
105, 118, 101, 114, 115, 101, 10, 10,
};
private async Task ConvertArray()
{
text = new(await JS.InvokeAsync<string>("convertArray", quoteArray));
}
}
```
### Call JavaScript functions without reading a returned value ( `InvokeVoidAsync` )
Use `InvokeVoidAsync` in the following cases:
* .NET is not required to read the result of a JS call.
* JS functions return `void (0)`/`void 0` or undefined.
Inside the closing `</body>` tag of `wwwroot/index.html`, provide a JS function `displayTickerAlert1`. The function is called with `InvokeVoidAsync` and does not return a value:
```html
<script>
window.displayTickerAlert1 = (symbol, price) => {
alert(`${symbol}: $${price}!`);
};
</script>
```
`TickerChanged` calls the `handleTickerChanged1` method in the following component `CallJsExample2`.
```cshtml
@page "/call-js-example-2"
@inject IJSRuntime JS
<h1>Call JS Example 2</h1>
<p>
<button @onclick="SetStock">Set Stock</button>
</p>
@if (stockSymbol is not null)
{
<p>@stockSymbol price: @price.ToString("c")</p>
}
@code {
private Random r = new();
private string? stockSymbol;
private decimal price;
private async Task SetStock()
{
stockSymbol =
$"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
price = r.Next(1, 101);
await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
}
}
```
### Calling JavaScript functions and reading a returned value ( `InvokeAsync` )
Use `InvokeAsync` when .net needs to read the result of a JS call.
Inside the closing `</body>` tag of `wwwroot/index.html`, provide a JS function `displayTickerAlert2`. The following example returns a string to be displayed by the caller:
```html
<script>
window.displayTickerAlert2 = (symbol, price) => {
if (price < 20) {
alert(`${symbol}: $${price}!`);
return "User alerted in the browser.";
} else {
return "User NOT alerted.";
}
};
</script>
```
`TickerChanged` calls the `handleTickerChanged2` method and displays the returned string in the following component `CallJsExample3`.
```cshtml title="Pages/CallJsExample3.razor"
@page "/call-js-example-3"
@inject IJSRuntime JS
<h1>Call JS Example 3</h1>
<p>
<button @onclick="SetStock">Set Stock</button>
</p>
@if (stockSymbol is not null)
{
<p>@stockSymbol price: @price.ToString("c")</p>
}
@if (result is not null)
{
<p>@result</p>
}
@code {
private Random r = new();
private string? stockSymbol;
private decimal price;
private string? result;
private async Task SetStock()
{
stockSymbol =
$"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
price = r.Next(1, 101);
var interopResult =
await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
result = $"Result of TickerChanged call for {stockSymbol} at " +
$"{price.ToString("c")}: {interopResult}";
}
}
```

@ -0,0 +1,103 @@
---
sidebar_position: 7
title: Using the component
---
Now that our component is functional, we just have to use it, for that we just have to declare it in our page `Pages/Index.razor` with a fixed data.
## Add method in the data service
Open the interface of the service and add a new method for get the recipes.
```csharp title="Services/IDataService.cs"
public interface IDataService
{
Task Add(ItemModel model);
Task<int> Count();
Task<List<Item>> List(int currentPage, int pageSize);
Task<Item> GetById(int id);
Task Update(int id, ItemModel model);
Task Delete(int id);
// highlight-next-line
Task<List<CraftingRecipe>> GetRecipes();
}
```
## Add method to the local service
Open the local service and implement the new method for get the recipes.
```csharp title="Services/DataLocalService.cs"
public class DataLocalService : IDataService
{
...
public Task<List<CraftingRecipe>> GetRecipes()
{
var items = new List<CraftingRecipe>
{
new CraftingRecipe
{
Give = new Item { DisplayName = "Diamond", Name = "diamond" },
Have = new List<List<string>>
{
new List<string> { "dirt", "dirt", "dirt" },
new List<string> { "dirt", null, "dirt" },
new List<string> { "dirt", "dirt", "dirt" }
}
}
};
return Task.FromResult(items);
}
}
```
## Component declaration
```cshtml title="Pages/Index.razor"
<div>
<Crafting Items="Items" Recipes="Recipes" />
</div>
```
```csharp title="Pages/Index.razor.cs"
public partial class Index
{
[Inject]
public IDataService DataService { get; set; }
public List<Item> Items { get; set; } = new List<Item>();
private List<CraftingRecipe> Recipes { get; set; } = new List<CraftingRecipe>();
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
Items = await DataService.List(0, await DataService.Count());
Recipes = await DataService.GetRecipes();
}
}
```
## Import JavaScript files
The component is displayed but the user actions are not displayed, quite simply because we have not imported the JavaScript file for it.
Open the `Pages/_Layout.cshtml` file and add the following script tag:
```cshtml title="Pages/_Layout.cshtml"
...
<script src="Components/Crafting.razor.js"></script>
...
```

@ -0,0 +1,567 @@
---
sidebar_position: 2
title: Generic component
---
Generic components are components that accept one or more UI templates as parameters, which can then be used as part of the component's rendering logic.
Generic components allow you to author higher-level components that are more reusable than regular components. A couple of examples include:
* A table component that allows a user to specify templates for the table's header, rows, and footer.
* A list component that allows a user to specify a template for rendering items in a list.
## Component creation
Create a new component that will serve as the basis for the generic component:
```cshtml title="Components/Card.razor"
<h3>Card</h3>
<div class="card text-center">
@CardHeader
@CardBody
@CardFooter
</div>
```
```csharp title="Components/Card.razor.cs"
public partial class Card
{
[Parameter]
public RenderFragment CardBody { get; set; }
[Parameter]
public RenderFragment CardFooter { get; set; }
[Parameter]
public RenderFragment CardHeader { get; set; }
}
```
`@CardHeader`, `@CardBody` and `@CardFooter` are parameter template components of type `RenderFragment`.
```cshtml title="Pages/Index.razor"
...
<Card>
<CardHeader>
<div class="card-header">
My Templated Component
</div>
</CardHeader>
<CardBody>
<div class="card-body">
<h5>Welcome To Template Component</h5>
</div>
</CardBody>
<CardFooter>
<div class="card-footer text-muted">
Click Here
</div>
</CardFooter>
</Card>
...
```
Each Template parameter of type `RenderFragment` will have a corresponding Html tag. The HTML tag must match the parameter name.
**Render:**
![Render](/img/blazor-component/RenderFragmentCard.png)
## Html tag duplication
The Html tag which represents the parameter of the Template component, duplicating it will result in the rendering of the last Html tag or the bottom one in priority.
```cshtml title="Pages/Index.razor"
...
<Card>
<CardHeader>
<div class="card-header">
Templated Component
</div>
</CardHeader>
<CardHeader>
<div class="card-header">
Hi I'm duplicated header
</div>
</CardHeader>
</Card>
...
```
**Render:**
![Render](/img/blazor-component/duplicate.png)
## Parameter typed `RenderFragment<T>`
* `RenderFragmetnt<T>` is a typed parameter that represents a portion of UI rendering with dynamic content of the type it implements.
* Declare the generic type parameter at the top of the component using `@typeparam`. This represents the component type at runtime.
* This type of typed component parameter that is passed as an element will have an implicit parameter called `context`.
* Implicit context parameter helps with dynamic data binding. It has access to the property type implemented by `RenderFragment<T>`.
```cshtml title="Components/Card.razor"
@typeparam TItem
<div class="card text-center">
@CardHeader(Item)
@CardBody(Item)
@CardFooter
</div>
```
```csharp title="Components/Card.razor.cs"
public partial class Card<TItem>
{
[Parameter]
public RenderFragment<TItem> CardBody { get; set; }
[Parameter]
public RenderFragment CardFooter { get; set; }
[Parameter]
public RenderFragment<TItem> CardHeader { get; set; }
[Parameter]
public TItem Item { get; set; }
}
```
`CardHeader`, `CardBody` are parameters of type `RenderFragment<T>`. These Html modeled properties have access to the implicit `context` parameter for dynamic binding data.
```csharp title="Models/Cake.cs"
public class Cake
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Cost { get; set; }
}
```
```cshtml title="Pages/Index.razor"
...
<Card Item="CakeItem">
<CardHeader>
<div class="card-header">
Cake Token Number - @context.Id
</div>
</CardHeader>
<CardBody>
<div class="card-body">
<div>@context.Name</div>
<div>$ @context.Cost</div>
</div>
</CardBody>
<CardFooter>
<div class="card-footer text-muted">
Click Here
</div>
</CardFooter>
</Card>
...
```
```csharp title="Pages/Index.razor.cs"
public partial class Index
{
private Cake CakeItem = new Cake
{
Id = 1,
Name = "Black Forest",
Cost = 50
};
}
```
**Render:**
![Render](/img/blazor-component/typeRenderFragment.png)
## Context attribute on Template HTML element
In the component, the context can be declared explicitly as an Html attribute.
The value assigned to the context (Html attribute) will be used for dynamic linking in all component templates.
Declaration of the Context attribute at the component level.
```cshtml title="Pages/Index.razor"
...
<Card Item="CakeItem" Context="cakeContext">
<CardHeader>
<div class="card-header">
Cake Token Number - @cakeContext.Id
</div>
</CardHeader>
<CardBody>
<div class="card-body">
<div>@cakeContext.Name</div>
<div>$ @cakeContext.Cost</div>
</div>
</CardBody>
</Card>
...
```
Declaration context attribute at the level of each template.
```cshtml title="Pages/Index.razor"
...
<Card Item="CakeItem">
<CardHeader Context="headContext">
<div class="card-header">
Cake Token Number - @headContext.Id
</div>
</CardHeader>
<CardBody Context="bodyContext">
<div class="card-body">
<div>@bodyContext.Name</div>
<div>$ @bodyContext.Cost</div>
</div>
</CardBody>
</Card>
...
```
## Generic component
The template component is itself a generic component. `@typeparam` defines the type of the generic component.
The generic template component will be defined by our own implementation and it can be reused for different types.
The best choice of its implementation is like the component with only two properties.
The first property is of type `RenderFragment<T>` to render the content.
The second property is of type `List<T>` a collection of data to bind to render.
The most widely used scenarios for generic component are display elements,
this will help avoid creating new components to display items in an application.
A single component for all display elements.
```cshtml title="Components/ShowItems.razor"
@typeparam TItem
<div>
@if ((Items?.Count ?? 0) != 0)
{
@foreach (var item in Items)
{
@ShowTemplate(item);
}
}
</div>
```
```cshtml title="Components/ShowItems.razor.cs"
public partial class ShowItems<TItem>
{
[Parameter]
public List<TItem> Items { get; set; }
[Parameter]
public RenderFragment<TItem> ShowTemplate { get; set; }
}
```
`ShowTemplate` and `Items` are two parameters of our generic type component.
`ShowTemplate` is a property of type `RenderFragment<TItem>` which will render the Html to be displayed (this Html can be an array, a list, a content, etc.).
`Items` is a collection whose type matches our component type, this data will be used in data binding inside our rendered Html.
```cshtml title="Pages/Index.razor"
...
<ShowItems Items="Cakes" >
<ShowTemplate Context="CakeContext">
<div class="card text-center">
<div class="card-header">
Cake Token Id - @CakeContext.Id
</div>
<div class="card-body">
<h5 class="card-title">@CakeContext.Name</h5>
<p class="card-text">Price $@CakeContext.Cost</p>
</div>
<div class="card-footer text-muted">
Click Here
</div>
</div>
</ShowTemplate>
</ShowItems>
...
```
The `ShowItems` template represents our generic type component. `ShowTemplate` represents the HTML rendering property of the component.
```csharp title="Pages/Index.razor.cs"
public partial class Index
{
...
public List<Cake> Cakes { get; set; }
protected override Task OnAfterRenderAsync(bool firstRender)
{
LoadCakes();
StateHasChanged();
return base.OnAfterRenderAsync(firstRender);
}
public void LoadCakes()
{
Cakes = new List<Cake>
{
// items hidden for display purpose
new Cake
{
Id = 1,
Name = "Red Velvet",
Cost = 60
},
};
}
...
}
```
Inside the `OnAfterRenderAsync` method we call the `StageHasChanged()` method to reflect the data changes in the component.
You can try removing the `StateHasChanged()` method and you may observe an empty page without displaying any data because we are filling some data before rendering Html.
**Render:**
![Render](/img/blazor-component/genericStatic.png)
## Concept: RenderFragment
Template components are components that accept one or more UI templates as parameters, which can then be used as part of the component's rendering logic.
Template components allow you to create higher level components that are more reusable than normal components.
Here are some examples :
* Table component that allows a user to specify templates for the table header, rows, and footer.
* List component that allows a user to specify a template to display items in a list.
A template component is defined by specifying one or more component parameters of type `RenderFragment` or `RenderFragment<TValue>`.
A render fragment represents a segment of the user interface to be rendered. `RenderFragment<TValue>` takes a type parameter which can be specified when calling the render fragment.
Often, template components are generically typed, as shown in the following `TableTemplate` component.
The `<T>` generic type in this example is used to render `IReadOnlyList<T>` values, which in this case is a series of PET rows in a component that displays a table of pets.
```cshtml title="Shared/TableTemplate.razor"
@typeparam TItem
@using System.Diagnostics.CodeAnalysis
<table class="table">
<thead>
<tr>@TableHeader</tr>
</thead>
<tbody>
@foreach (var item in Items)
{
if (RowTemplate is not null)
{
<tr>@RowTemplate(item)</tr>
}
}
</tbody>
</table>
@code {
[Parameter]
public RenderFragment? TableHeader { get; set; }
[Parameter]
public RenderFragment<TItem>? RowTemplate { get; set; }
[Parameter, AllowNull]
public IReadOnlyList<TItem> Items { get; set; }
}
```
When using a template component, template parameters can be specified using child elements that correspond to parameter names.
In the following example, `<TableHeader>...</TableHeader>` and `<RowTemplate>...<RowTemplate>` provide `RenderFragment<TValue>` templates for `TableHeader` and `RowTemplate` of component ` TableTemplate`.
Specify the `Context` attribute on the component element when you want to specify the content parameter name for implicit child content (without encapsulating child element).
In the following example, the `Context` attribute appears on the `TableTemplate` element and applies to all `RenderFragment<TValue>` template parameters.
```cshtml title="Pages/Pets1.razor"
@page "/pets1"
<h1>Pets</h1>
<TableTemplate Items="pets" Context="pet">
<TableHeader>
<th>ID</th>
<th>Name</th>
</TableHeader>
<RowTemplate>
<td>@pet.PetId</td>
<td>@pet.Name</td>
</RowTemplate>
</TableTemplate>
@code {
private List<Pet> pets = new()
{
new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
new Pet { PetId = 4, Name = "Salem Saberhagen" },
new Pet { PetId = 7, Name = "K-9" }
};
private class Pet
{
public int PetId { get; set; }
public string? Name { get; set; }
}
}
```
You can also change the parameter name using the `Context` attribute on the `RenderFragment<TValue>` child element.
In the following example, the `Context` is set to `RowTemplate` instead of `TableTemplate`:
```cshtml title="Pages/Pets2.razor"
@page "/pets2"
<h1>Pets</h1>
<TableTemplate Items="pets">
<TableHeader>
<th>ID</th>
<th>Name</th>
</TableHeader>
<RowTemplate Context="pet">
<td>@pet.PetId</td>
<td>@pet.Name</td>
</RowTemplate>
</TableTemplate>
@code {
private List<Pet> pets = new()
{
new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
new Pet { PetId = 4, Name = "Salem Saberhagen" },
new Pet { PetId = 7, Name = "K-9" }
};
private class Pet
{
public int PetId { get; set; }
public string? Name { get; set; }
}
}
```
Component arguments of type `RenderFragment<TValue>` have an implicit parameter named `context`, which can be used.
In the following example, `Context` is not set. `@context.{PROPERTY}` provides `PET` values to the model, where `{PROPERTY}` is a `Pet` property:
```cshtml title="Pages/Pets3.razor"
@page "/pets3"
<h1>Pets</h1>
<TableTemplate Items="pets">
<TableHeader>
<th>ID</th>
<th>Name</th>
</TableHeader>
<RowTemplate>
<td>@context.PetId</td>
<td>@context.Name</td>
</RowTemplate>
</TableTemplate>
@code {
private List<Pet> pets = new()
{
new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
new Pet { PetId = 4, Name = "Salem Saberhagen" },
new Pet { PetId = 7, Name = "K-9" }
};
private class Pet
{
public int PetId { get; set; }
public string? Name { get; set; }
}
}
```
When using generic components, the type parameter is inferred if possible.
However, you can explicitly specify the type with an attribute whose name matches the type parameter, which is `TItem` in the previous example:
```cshtml title="Pages/Pets4.razor"
@page "/pets4"
<h1>Pets</h1>
<TableTemplate Items="pets" TItem="Pet">
<TableHeader>
<th>ID</th>
<th>Name</th>
</TableHeader>
<RowTemplate>
<td>@context.PetId</td>
<td>@context.Name</td>
</RowTemplate>
</TableTemplate>
@code {
private List<Pet> pets = new()
{
new Pet { PetId = 2, Name = "Mr. Bigglesworth" },
new Pet { PetId = 4, Name = "Salem Saberhagen" },
new Pet { PetId = 7, Name = "K-9" }
};
private class Pet
{
public int PetId { get; set; }
public string? Name { get; set; }
}
}
```
By default a new blazor component does not accept content if you do not declare a `RenderFragment` named `ChildContent`.
```cshtml title="Components/TestRenderFragment.razor"
<h3>TestRenderFragment</h3>
@code {
}
```
```cshtml title="Pages/Index.razor"
...
<TestRenderFragment>
<div>Content of my TestRenderFragment</div>
</TestRenderFragment>
...
```
This code does not generate a compilation error but will trigger the following error at runtime:
```
Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
Unhandled exception rendering component: Object of type 'MyBeautifulAdmin.Components.TestRenderFragment' does not have a property matching the name 'ChildContent'.
System.InvalidOperationException: Object of type 'MyBeautifulAdmin.Components.TestRenderFragment' does not have a property matching the name 'ChildContent'.
```
In order to render the child code, use the following code:
```cshtml title="Components/TestRenderFragment.razor"
<h3>TestRenderFragment</h3>
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
}
@ChildContent
```

@ -0,0 +1,25 @@
---
sidebar_position: 1
title: Introduction
---
## Razor component
We're going to see how a Blazor component works.
### List of steps
* Creating a Generic Component
* Creating a complex component
* Component Description
* Component appendix classes
* Creating the `item` component item
* Creating the `craft` component element
* Using the component
### List of concepts
* RenderFragment
* Event handling
* IJSRuntime
* CascadingValue & CascadingParameter

@ -0,0 +1,262 @@
---
sidebar_position: 14
title: Use the logs
---
## Install logging
By default, a **Blazor Server** app is **already set up** to use logging.
With Blazor WebAssembly, we are now able to build Single Page Applications (SPAs) using C# and the ASP.NET Core framework.
Coming from ASP.NET Core MVC, you may wonder what .NET features are available, limited, or not available when running in the browser.
One of them is logging, which is a basic means for debugging in production environments and during development.
For a **Blazor WASM** application the installation of the `Microsoft.Extensions.Logging.Configuration` nuget will be required.
In the application settings file, provide the logging configuration. The logging configuration is loaded into `Program.cs`.
```json title="wwwroot/appsettings.json"
{
"Logging": {
"LogLevel": {
"Default": "Trace",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
```
Similarly for **Blazor WASM** adding the `Microsoft.Extensions.Logging.Configuration` namespace and logging configuration to `Program.cs` will be required:
```csharp title="Program.cs"
using Microsoft.Extensions.Logging;
...
builder.Logging.AddConfiguration(builder.Configuration.GetSection("Logging"));
```
## Use the logs
To create logs, use an `ILogger<TCategoryName>` object from the `ILogger<TCategoryName>` DI.
The following example:
* Creates an event logger, `ILogger<CreateLog>` , which uses a fully qualified log `ILogger<CreateLog>` of type `CreateLog` . The log category is a string associated with each log.
* Calls each log level with the `Log` method.
```csharp title="Pages/CreateLog.razor"
@page "/logs"
<h3>CreateLog</h3>
<button @onclick="CreateLogs">Create logs</button>
```
```csharp title="Pages/CreateLog.razor.cs"
using Microsoft.Extensions.Logging;
public partial class CreateLog
{
[Inject]
public ILogger<CreateLog> Logger { get; set; }
private void CreateLogs()
{
var logLevels = Enum.GetValues(typeof(LogLevel)).Cast<LogLevel>();
foreach (var logLevel in logLevels.Where(l => l != LogLevel.None))
{
Logger.Log(logLevel, $"Log message for the level: {logLevel}");
}
}
}
```
Here is the result after clicking the button:
![Use the logs](/img/logs.png)
## Configuring logging
Logging configuration is usually provided by the `Logging` section of the appsettings.`{Environment}`.json files.
The following sample code `appsettings.Development.json` is generated by the web application templates:
```json title="appsettings.Development.json"
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
```
In the preceding JSON code:
* The `"Default"`, `"Microsoft"` and `"Microsoft.Hosting.Lifetime"` categories are specified.
* The `"Microsoft"` category applies to all categories that start with `"Microsoft"`. For example, this setting applies to the `"Microsoft.AspNetCore.Routing.EndpointMiddleware"` category.
* The `"Microsoft"` category saves logs at the `Warning` level and above.
* The `"Microsoft.Hosting.Lifetime"` category is more specific than the `"Microsoft"` category, so the `"Microsoft.Hosting.Lifetime"` category records logs at the `Information` level and above.
The `Logging` property can have different `LogLevel` and provider.
`LogLevel` specifies the minimum LogLevel to record for the selected categories.
In the preceding JSON, the `Information` & `Warning` levels of logging are specified. `LogLevel` indicates the severity of the log and ranges from 0 to 6:
`Trace`=0, `Debug`=1, `Information`=2, `Warning`=3, `Error`=4, `Critical`=5 and `None`=6.
When a `LogLevel` is specified, logging is enabled for messages at the specified level and above.
In the preceding JSON, the `Default` category is logged for `Information` and later.
For example, `Information`, `Warning`, `Error` and `Critical` messages are logged.
If no `LogLevel` is specified, logging defaults to the `Information` level.
A provider property can specify a `LogLevel` property. `LogLevel` under a provider specifies logging levels for that provider, and overrides non-provider log settings.
Consider the following `appsettings.json` file:
```json title="appsettings.json"
{
"Logging": {
"LogLevel": { // All providers, LogLevel applies to all the enabled providers.
"Default": "Error", // Default logging, Error and higher.
"Microsoft": "Warning" // All Microsoft* categories, Warning and higher.
},
"Debug": { // Debug provider.
"LogLevel": {
"Default": "Information", // Overrides preceding LogLevel:Default setting.
"Microsoft.Hosting": "Trace" // Debug:Microsoft.Hosting category.
}
},
"EventSource": { // EventSource provider
"LogLevel": {
"Default": "Warning" // All categories of EventSource provider.
}
}
}
}
```
Settings in `Logging.{providername}.LogLevel` override settings in `Logging.LogLevel`. In the preceding JSON code, the default value of the `Debug` provider is `Information`:
`Logging:Debug:LogLevel:Default:Information`
## How filter rules are applied
When creating an `ILogger<TCategoryName>` object, the `ILoggerFactory` object selects a single rule to apply to this event logging per provider.
All messages written by an `ILogger` instance are filtered according to the selected rules.
The most specific rule for each vendor and category pair is selected from the available rules.
The following algorithm is used for each provider when an `ILogger` object is created for a given category:
* Select all rules that match the provider or its alias. If no match is found, select all rules with an empty provider.
* From the result of the previous step, select the rules with the longest matching category prefix. If no match is found, select all rules that don't specify a category.
* If multiple rules are selected, take the **last**.
* If no rule is selected, use `MinimumLevel`.
## Log level
The following table lists the values for `LogLevel`:
| Log Level | Value | Method | Description
| ---- | ---- | ---- | ---- |
| Track | 0 | LogTrace | Contains the most detailed messages. These messages may contain sensitive application data. These messages are disabled by default and should not be enabled in production.
| Debug | 1 | LogDebug | For debugging and development. Use with caution in production due to high volume.
| Information | 2 | Log Information | Tracks the general progress of the application. May have long term value.
| Disclaimer | 3 | LogWarning | For abnormal or unexpected events. Usually includes errors or conditions that do not cause the application to fail.
| Error | 4 | LogError | Provides information about errors and exceptions that cannot be handled. These messages indicate a failure in the current operation or request, not the entire application.
| Review | 5 | LogCritical | Provides information about failures that require immediate investigation. Examples: data loss, insufficient disk space.
| None | 6 | | Specifies that a logging category should write no messages.
In the previous table, the `LogLevel` is listed from lowest to highest severity level.
The first parameter of the logging method, `LogLevel`, indicates the severity of the log.
Instead of calling `Log(LogLevel, ...)`, most developers call `Log{LogLevel}` extension methods.
The `Log{LogLevel}` extension methods call the `Log` method specifying the `LogLevel`.
For example, the following two logging calls are functionally equivalent and produce the same log:
```csharp
var message = "My Log Message";
Logger.Log(LogLevel.Information, message);
Logger.LogInformation(message);
```
The following code creates `Information` and `Warning` logs:
```csharp
var id = 10;
Logger.LogInformation("Getting item {Id}", id);
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
Logger.LogWarning("Get({Id}) NOT FOUND", id);
return NotFound();
}
```
In the preceding code, the first argument to `Log{LogLevel}` is a message template containing placeholders for the argument values provided by the other method parameters.
## Log message template
Each log API uses a message template. The latter may contain placeholders for which the arguments are provided. Use names, not numbers, for placeholders.
```csharp
var id = 10;
Logger.LogInformation("Getting item {Id}", id);
var todoItem = await _context.TodoItems.FindAsync(id);
if (todoItem == null)
{
Logger.LogWarning("Get({Id}) NOT FOUND", id);
return NotFound();
}
```
The order of parameters, not their placeholder names, determines which parameters are used to provide placeholder values in log messages.
In the following code, the parameter names are out of sequence in the message template placeholders:
```csharp
int apples = 1;
int pears = 2;
int bananas = 3;
Logger.LogInformation("Parameters: {pears}, {bananas}, {apples}", apples, pears, bananas);
```
However, parameters are assigned to placeholders in the following order: apples , pears , bananas .
The log message reflects the order of the parameters:
```
Parameters: 1, 2, 3
```
This approach allows logging providers to implement semantic or structured logging.
The actual arguments, not just the formatted message template, are passed to the logging system.
This allows logging providers to store parameter values as fields.
For example, consider the following logger method:
```csharp
Logger.LogInformation("Getting item {Id} at {RequestTime}", id, DateTime.Now);
```
for example, when logging to Azure Table Storage:
* Each Azure table entity can have `ID` and `RequestTime` properties.
* Tables with properties simplify queries on logged data. For example, a query can find all logs in a given `RequestTime` range without having to parse the text message timeout.

@ -0,0 +1,3 @@
label: 'View data'
position: 5
collapsed: true

@ -0,0 +1,82 @@
---
sidebar_position: 7
title: Blazorise pagination
---
## Use pagination with Blazorise
Now that you have a functional grid, it is possible that your application returns a lot more data to you, in order to keep a fluid display, it is possible to set up pagination.
## Add paging to grid
In order to set up pagination open the `Pages/List.razor` file and add the highlighted code to your page:
```cshtml title="Pages/List.razor"
@page "/list"
@using Minecraft.Crafting.Models
<h3>List</h3>
<DataGrid TItem="Item"
Data="@items"
// highlight-start
ReadData="@OnReadData"
TotalItems="@totalItem"
PageSize="10"
ShowPager
// highlight-end
Responsive>
<DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="#" />
<DataGridColumn TItem="Item" Field="@nameof(Item.DisplayName)" Caption="Display name" />
<DataGridColumn TItem="Item" Field="@nameof(Item.StackSize)" Caption="Stack size" />
<DataGridColumn TItem="Item" Field="@nameof(Item.MaxDurability)" Caption="Maximum durability" />
<DataGridColumn TItem="Item" Field="@nameof(Item.EnchantCategories)" Caption="Enchant categories">
<DisplayTemplate>
@(string.Join(", ", ((Item)context).EnchantCategories))
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="Item" Field="@nameof(Item.RepairWith)" Caption="Repair with">
<DisplayTemplate>
@(string.Join(", ", ((Item)context).RepairWith))
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="Item" Field="@nameof(Item.CreatedDate)" Caption="Created date" DisplayFormat="{0:d}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo("fr-FR")" />
</DataGrid>
```
## Add paging code
Open the `Pages/List.razor.cs` file and modify the code as follows:
```csharp title="Pages/List.razor.cs"
public partial class List
{
private List<Item> items;
private int totalItem;
[Inject]
public HttpClient Http { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
private async Task OnReadData(DataGridReadDataEventArgs<Item> e)
{
if (e.CancellationToken.IsCancellationRequested)
{
return;
}
// When you use a real API, we use this follow code
//var response = await Http.GetJsonAsync<Item[]>( $"http://my-api/api/data?page={e.Page}&pageSize={e.PageSize}" );
var response = (await Http.GetFromJsonAsync<Item[]>($"{NavigationManager.BaseUri}fake-data.json")).Skip((e.Page - 1) * e.PageSize).Take(e.PageSize).ToList();
if (!e.CancellationToken.IsCancellationRequested)
{
totalItem = (await Http.GetFromJsonAsync<List<Item>>($"{NavigationManager.BaseUri}fake-data.json")).Count;
items = new List<Item>(response); // an actual data for the current page
}
}
}
```

@ -0,0 +1,173 @@
---
sidebar_position: 6
title: Blazorise DataGrid
---
## Install Blazorise DataGrid
Select your project, then right click and choose `Manage NuGet Packages...`:
![Install Blazorise DataGrid](/img/affichage-donnees/blazorise-01.png)
Select the `Browse` tab and search for `blazorise datagrid`:
![Install Blazorise DataGrid](/img/affichage-donnees/blazorise-02.png)
Select the package then right click on `Install`:
![Install Blazorise DataGrid](/img/affichage-donnees/blazorise-03.png)
:::tip
It's normal that the version you install is different, Blazorise nugets evolve, always use the latest version.
:::
Accept changes by clicking `OK`:
![Install Blazorise DataGrid](/img/affichage-donnees/blazorise-04.png)
Blazorise Datagrid is now installed on your project.
Do the same for the `Blazorise.Bootstrap` & `Blazorise.Icons.FontAwesome` package.
It is also possible to install packages directly with the `Package Manager Console` window (Click in the `View => Other Windows => Package Manager Console` menu to display it).
To install, just use the `Install-Package` command, example:
```shell
Install-Package Blazorise.Bootstrap
Install-Package Blazorise.Icons.FontAwesome
```
## Register Blasorize in the app
Open the `Program.cs` file and add the highlighted changes:
```csharp title="Program.cs"
...
builder.Services.AddHttpClient();
// highlight-start
builder.Services
.AddBlazorise()
.AddBootstrapProviders()
.AddFontAwesomeIcons();
// highlight-end
var app = builder.Build();
...
```
## Add Blazorise Sources
Open the `Pages/_Layout.cshtml` file and add the highlighted changes:
```cshtml title="Pages/_Layout.cshtml"
...
<script src="_framework/blazor.server.js"></script>
// highlight-start
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.4/css/all.css">
<link href="_content/Blazorise/blazorise.css" rel="stylesheet" />
<link href="_content/Blazorise.Bootstrap/blazorise.bootstrap.css" rel="stylesheet" />
// highlight-end
</body>
</html>
```
## Import Blazorise DataGrid
In order for Blazor to recognize the new component, you must add in the global import file `_Imports.razor` the following import at the end of the file:
```cshtml title="_Imports.razor"
@using Blazorise.DataGrid
```
## Declare the grid
Open the `Pages/List.razor` file and edit the following file:
```cshtml title="Pages/List.razor"
@page "/list"
@using Minecraft.Crafting.Models
<h3>List</h3>
<DataGrid TItem="Item"
Data="@items"
PageSize="int.MaxValue"
Responsive>
<DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="#" />
<DataGridColumn TItem="Item" Field="@nameof(Item.DisplayName)" Caption="Display name" />
<DataGridColumn TItem="Item" Field="@nameof(Item.StackSize)" Caption="Stack size" />
<DataGridColumn TItem="Item" Field="@nameof(Item.MaxDurability)" Caption="Maximum durability" />
<DataGridColumn TItem="Item" Field="@nameof(Item.EnchantCategories)" Caption="Enchant categories">
<DisplayTemplate>
@(string.Join(", ", ((Item)context).EnchantCategories))
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="Item" Field="@nameof(Item.RepairWith)" Caption="Repair with">
<DisplayTemplate>
@(string.Join(", ", ((Item)context).RepairWith))
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="Item" Field="@nameof(Item.CreatedDate)" Caption="Created date" DisplayFormat="{0:d}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo("fr-FR")" />
</DataGrid>
```
Your page now displays a dynamic grid with all of your data.
## Blazorise Documentation
You will find at this address the documentation of [Blazorise DataGrid](https://blazorise.com/docs/extensions/datagrid/getting-started).
## Concept: NuGet
It is essential for any modern development platform to provide a mechanism for developers to create, share and exploit the code that is useful to them. Often, this code is grouped into "packages" containing the compiled code (in the form of DLLs) as well as all the content necessary for the projects that use them.
In the case of .NET (including .NET Core), Microsoft's supported code sharing mechanism is NuGet. It defines how .NET packages are created, hosted, and used, and provides tools for each of these roles.
Simply put, a NuGet package is a ZIP file with the .nupkg extension, which contains compiled code (DLLs), other files related to that code, and a descriptive manifest that includes information like the version number of the package. Developers who have code to share create packages and publish them to a public or private host. Consumers retrieve these packages from the corresponding hosts, add them to their projects, and then call their functionality in their project code. NuGet then handles all the intermediate details itself.
Since NuGet supports both private hosts and the public nuget.org host, it is possible to use NuGet packages to share code that is exclusive to an organization or workgroup. They are also a convenient way to embed your own code for use only in your own projects. In short, a NuGet package is a shareable unit of code, which does not require or imply any particular mode of sharing.
### Flow of packages between creators, hosts and consumers
In its role as a public host, NuGet itself manages the central repository of over 100,000 unique packages at [nuget.org](https://www.nuget.org/).
These packages are used by millions of .NET/.NET Core developers every day.
NuGet also lets you host packages privately in the cloud (such as on Azure DevOps), on a private network, or even just on your local file system.
These packages are thus reserved for developers who have access to the host, which makes it possible to make them available to a specific group of consumers.
Whatever its nature, a host serves as a connection point between creators and consumers of packages.
Creators produce convenient NuGet packages and publish them to a host.
Consumers search for convenient and compatible packages on accessible hosts, download them, and include these packages in their projects.
Once installed in a project, package APIs are available to the rest of the code in the project.
![Flow of packages](/img/affichage-donnees/nuget-roles.png)
### Package targeting compatibility
A "compatible" package is a package that contains assemblies created for at least one target version of .NET Framework/Core that is compatible with that of the project using the package.
Developers can either build version-specific packages, like with UWP controls, or support a wider range of target versions.
To maximize package compatibility, they target .NET Standard, which all .NET and .NET Core projects can leverage.
This is the most efficient way for both makers and consumers because a single package (which typically contains a single assembly) works for all projects.
Package developers who need APIs outside of .NET Standard, meanwhile, create a separate assembly for each of the target .NET Framework versions they want to support, and wrap them all in the same package ( “multiple targeting”).
When a consumer installs such a package, NuGet extracts only the assemblies needed by the project.
This reduces the package footprint in the final application and/or in the assemblies produced by this project.
A multi-targeted package is, of course, more difficult for its creator to manage.
### Dependency management
The ease of building on the work of others is one of the most powerful aspects of a package management system.
As a result, most of the work NuGet does is maintaining this dependency tree or "graph" for each project.
In other words, you only need to worry about packages that you use directly in a project.
If any of them use other packages themselves (and so on), NuGet takes care of all those lower-level dependencies.
The following illustration shows a project that depends on five packages, which in turn depend on several others.
![Gestion des dépendances](/img/affichage-donnees/dependency-graph.png)

@ -0,0 +1,142 @@
---
sidebar_position: 3
title: Fake data
---
## Generation of fake data
In order to be able to start using your site, we will generate fake data to simulate a connection to an API.
To do this, create a new `fake-data.json` file at the root of the `wwwroot` directory:
![Generation of fake data](/img/affichage-donnees/fausse-donnees-01.png)
## Data representation class
Your data must match the output format of your future API.
Create a `Models` folder of your project then create the `Item.cs` class:
![Data representation class](/img/affichage-donnees/fausse-donnees-02.png)
This class will represent the object version of your data.
Class content:
```csharp title="Models/Item.cs"
public class Item
{
public int Id { get; set; }
public string DisplayName { get; set; }
public string Name { get; set; }
public int StackSize { get; set; }
public int MaxDurability { get; set; }
public List<string> EnchantCategories { get; set; }
public List<string> RepairWith { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime? UpdatedDate { get; set; }
}
```
## How to Generate Fake Data Easily
In order to generate random data we can use the following site [https://json-generator.com/](https://json-generator.com/).
The template to use for the class is:
```json
[
'{{repeat(15, 30)}}',
{
id: '{{index(1)}}',
displayname: '{{company()}}',
name: function () {
return this.displayname.toLowerCase();
},
stacksize: '{{integer(1, 64)}}',
maxdurability: '{{integer(1, 125)}}',
enchantcategories: [
'{{repeat(0, 3)}}',
'{{random("armor", "armor_head", "armor_chest", "weapon", "digger", "breakable", "vanishable")}}'
],
repairwith: [
'{{repeat(0, 2)}}',
'{{random("oak_planks", "spruce_planks", "birch_planks", "jungle_planks", "acacia_planks", "dark_oak_planks", "crimson_planks", "warped_planks")}}'
],
createddate: '{{date(new Date(2014, 0, 1), new Date(), "YYYY-MM-dd")}}',
updateddate: '{{random(date(new Date(2014, 0, 1), new Date(), "YYYY-MM-dd"), null)}}'
}
]
```
Copy the result of the generation into the `fake-data.json` file.
:::caution
Your data must start with `[` and end with `]`.
Example:
```json
[
{
"id": 1,
"displayname": "Furnafix",
"name": "furnafix",
"stacksize": 36,
"maxdurability": 32,
"enchantcategories": [],
"repairwith": [],
"createddate": "2019-02-16",
"updateddate": null
},
{
"id": 2,
"displayname": "Deminimum",
"name": "deminimum",
"stacksize": 49,
"maxdurability": 46,
"enchantcategories": [],
"repairwith": [],
"createddate": "2020-06-14",
"updateddate": null
},
...
]
```
:::
## Concept: Serialization
Serialization is the process of converting an object into a stream of bytes to store the object or transmit it to memory, a database, or a file.
Its main purpose is to save the state of an object so that it can be recreated if necessary. The reverse process is called deserialization.
### How serialization works
This illustration shows the overall serialization process:
![How serialization works](/img/affichage-donnees/serialization-process.png)
The object is serialized into a stream that contains the data. The stream can also contain information about the type of the object, such as its version, culture, and assembly name.
From this stream, the object can be stored in a database, file or memory.
### Uses of serialization
Serialization allows the developer to save the state of an object and recreate it as needed, providing object storage as well as data exchange.
Through serialization, a developer can perform actions such as:
* Sending the object to a remote application using a web service
* Passing an object from one domain to another
* Passing an object through a firewall as a JSON or XML string
* Manage security or user information between applications
### JSON serialization with C#
The `System.Text.Json` namespace contains classes for JavaScript Object Notation (JSON) serialization and deserialization.
JSON is an open standard that is commonly used to share data on the web.
JSON serialization serializes an object's public properties into a string, byte array, or stream that conforms to the [JSON RFC 8259](https://tools.ietf.org/html/rfc8259) specification.
To control how JsonSerializer serializes or deserializes an instance of the class:
* Use a `JsonSerializerOptions` object
* Apply attributes from the `System.Text.Json.Serialization` namespace to classes or properties
* Implement custom converters

@ -0,0 +1,51 @@
---
sidebar_position: 5
title: HTML table
---
## Use an HTML table
Now that we have displayed one of the fields of our object, we are going to display all the information.
Open the `List.razor` file and fill it in as follows:
```cshtml title="Pages/List.razor"
@page "/list"
<h3>List</h3>
@if (items != null)
{
// highlight-start
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>Display Name</th>
<th>Stack Size</th>
<th>Maximum Durability</th>
<th>Enchant Categories</th>
<th>Repair With</th>
<th>Created Date</th>
</tr>
</thead>
<tbody>
@foreach (var item in items)
{
<tr>
<td>@item.Id</td>
<td>@item.DisplayName</td>
<td>@item.StackSize</td>
<td>@item.MaxDurability</td>
<td>@(string.Join(", ", item.EnchantCategories))</td>
<td>@(string.Join(", ", item.RepairWith))</td>
<td>@item.CreatedDate.ToShortDateString()</td>
</tr>
}
</tbody>
</table>
// highlight-end
}
```
You have now displayed all of your data, it is simple but lacks functionality, impossible to sort the data or filter it or have pagination.

@ -0,0 +1,26 @@
---
sidebar_position: 1
title: Introduction
---
## View data with Blazor
This lab will allow you to display data on your blazor site.
We will start from the default site created with Visual Studio.
### List of steps
* Creation of a new page
* Generation of fake data
* Use data
* Use an HTML table
* Use Blazorise
* Use paging with Blazorise
### List of concepts
* Layout
* Serialization
* Lifecycle events
* NuGet

@ -0,0 +1,274 @@
---
sidebar_position: 2
title: Page creation
---
## Creation of a new page
In order to display your data, we will create a new page.
To do this, nothing could be simpler, select the `Pages` folder then right click and select `Add > Razor Component...`
![Creation of a new page](/img/affichage-donnees/creation-page-01.png)
We will name our page `List.razor` then click on `Add`
![Creation of a new page](/img/affichage-donnees/creation-page-02.png)
## Separation View & Model
In order to separate the View and Model part, we will therefore create a partial class for our view.
Select the `Pages` folder then right click and select `Add > Class...`
![Separation View & Model](/img/affichage-donnees/creation-page-03.png)
We will name our class `List.razor.cs` then click on `Add`
![Separation View & Model](/img/affichage-donnees/creation-page-04.png)
Remember to make your class partial with the `partial` keyword as follows:
```csharp title="Pages/List.razor.cs"
public partial class List
{
}
```
You can now remove the code from your view, for that remove the following part:
```cshtml title="Pages/List.razor"
<h3>List</h3>
// highlight-start
@code {
}
// highlight-end
```
## Declare the url of our new page
In order to make our page accessible, let's declare the url of our page as follows:
```cshtml title="Pages/List.razor"
// highlight-next-line
@page "/list"
<h3>List</h3>
```
From now on our page will be available from the `/list` address of our site.
## Add our new page to the menu
Open the `Shared/NavMenu.razor` file and add the highlighted lines:
```cshtml title="Shared/NavMenu.razor"
<div class="top-row pl-4 navbar navbar-dark">
<a class="navbar-brand" href="">MyBeautifulAdmin</a>
<button class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<ul class="nav flex-column">
// highlight-start
<li class="nav-item px-3">
<NavLink class="nav-link" href="list">
<span class="oi oi-list-rich" aria-hidden="true"></span> List
</NavLink>
</li>
// highlight-end
<li class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="oi oi-plus" aria-hidden="true"></span> Counter
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="fetchdata">
<span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data
</NavLink>
</li>
</ul>
</div>
@code {
private bool collapseNavMenu = true;
private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
}
```
You can now navigate to your new page.
## Concept: Layout
Some app elements, such as menus, copyright messages, and company logos, are usually part of the overall app layout.
Placing a copy of the markup for these elements in all components of an application is not efficient.
Whenever one of these elements is updated, every component that uses the element must be updated.
This approach is expensive to maintain and can lead to inconsistent content if an update is missed.
Layouts solve these problems.
### Create a layout
To create a layout:
* Create a new Razor component. Layouts use the `.razor` extension like regular Razor components. Since layouts are shared between components of an application, they are usually placed in the `Shared` application folder. However, layouts can be placed anywhere accessible to components that use it.
* Inherits the component from `LayoutComponentBase`, it defines a `Body` property (type `RenderFragment`) so that the content is rendered inside the layout.
* Use the Razor `@Body` syntax to specify the location in the layout tag where the content is displayed.
* The following `DoctorWhoLayout` layout shows a sample layout. The layout inherits from `LayoutComponentBase` and defines the `@Body` between the navbar ( &lt;nav&gt;...&lt;/nav&gt; ) and the footer ( &lt;footer&gt;...&lt;/footer&gt; ; ).
```cshtml title="Shared/DoctorWhoLayout.razor"
// highlight-next-line
@inherits LayoutComponentBase
<header>
<h1>Doctor Who&trade; Episode Database</h1>
</header>
<nav>
<a href="masterlist">Master Episode List</a>
<a href="search">Search</a>
<a href="new">Add Episode</a>
</nav>
// highlight-next-line
@Body
<footer>
@TrademarkMessage
</footer>
@code {
public string TrademarkMessage { get; set; } =
"Doctor Who is a registered trademark of the BBC. " +
"https://www.doctorwho.tv/";
}
```
### Apply a layout
Use the Razor `@layout` directive to apply a layout to a Razor routable component that has an `@page` directive.
The compiler converts `@layout` to a `LayoutAttribute` and applies the attribute to the component class.
The content of the next `Episodes` page is inserted into the `DoctorWhoLayout` at the position of `@Body`.
```cshtml title="Pages/Episodes.razor"
@page "/episodes"
// highlight-next-line
@layout DoctorWhoLayout
<h2>Episodes</h2>
<ul>
<li>
<a href="https://www.bbc.co.uk/programmes/p00vfknq">
<em>The Ribos Operation</em>
</a>
</li>
<li>
<a href="https://www.bbc.co.uk/programmes/p00vfdsb">
<em>The Sun Makers</em>
</a>
</li>
<li>
<a href="https://www.bbc.co.uk/programmes/p00vhc26">
<em>Nightmare of Eden</em>
</a>
</li>
</ul>
```
The following rendered HTML markup is produced by the `DoctorWhoLayout` layout and the `Episodes` page. Superfluous markup does not appear in order to focus on the content provided by the two components involved:
* The Doctor Who episode database ™ header ( &lt;h1&gt;...&lt;/h1&gt; ) of the header ( &lt;header&gt;...&lt;/header&gt; ), of the navigation bar ( &lt;nav&gt;...&lt;/nav&gt; ) and the trademark information element ( &lt;div&gt;...&lt;/div&gt; ) of the footer ( &lt; footer&gt;...&lt;/footer&gt; ) comes from the `DoctorWhoLayout` component.
* The episodes header ( &lt;h2&gt;...&lt;/h2&gt; ) and episode list ( &lt;ul&gt;...&lt;/ul&gt; ) come from the `Episodes` page.
```cshtml
<body>
<div id="app">
<header>
<h1>Doctor Who&trade; Episode Database</h1>
</header>
<nav>
<a href="main-list">Main Episode List</a>
<a href="search">Search</a>
<a href="new">Add Episode</a>
</nav>
<h2>Episodes</h2>
<ul>
<li>...</li>
<li>...</li>
<li>...</li>
</ul>
<footer>
Doctor Who is a registered trademark of the BBC.
https://www.doctorwho.tv/
</footer>
</div>
</body>
```
### Apply a layout to a component folder
Each application folder can optionally contain a template file named `_Imports.razor`.
The compiler includes the directives specified in the `Imports` file in all Razor templates in the same folder and recursively in all its subfolders.
Therefore, a `_Imports.razor` file containing `@layout DoctorWhoLayout` ensures that all components in a folder use the `DoctorWhoLayout` layout.
There is no need to repeatedly add `@layout DoctorWhoLayout` to all Razor Components ( .razor ) in the folder and subfolders.
:::caution
Do not add a Razor `@layout` directive to the root `_Imports.razor` file, which results in an infinite layout loop.
To control the default application layout, specify the layout in the `Router` component.
:::
:::note
The Razor `@layout` directive only applies a layout to routable components with a `@page` directive.
:::
Example :
My application contains its pages in the `Pages` folder, I want to add an administration part to my application so I will create an `Admin` directory in the `Pages` folder:
![Apply a layout to a component folder](/img/affichage-donnees/layout-01.png)
I created a layout for my admin part located `Shared/AdminLayout.razor` and I want my admin pages to use it, two choices:
* Add at the top of each page `@layout AdminLayout`
* This choice is not the most relevant because in the event of a change of layout, you will have to go back to all the pages
* Create a `_Imports.razor` file and specify `@layout AdminLayout`, all pages in my `Admin` folder will now use this layout.
### Apply a default layout to an app
Specify the default application layout in the `App` component of the `Router` component.
The following example from an app based on a Blazor project template sets the default layout to the `MainLayout` layout.
```cshtml title="App.razor"
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<p>Sorry, there's nothing at this address.</p>
</NotFound>
</Router>
```
### Nested Layouts
A component can refer to a layout which, in turn, refers to another layout. For example, nested layouts are used to create multilevel menu structures.

@ -0,0 +1,114 @@
---
sidebar_position: 4
title: Use data
---
## Retrieve data
In order to use the data in the `fake-data.json` file, we will use the `GetFromJsonAsync` method of the `HttpClient` object.
This method allows to obtain and to convert (Deserialization) the data of the file in a list containing our data.
## Add default http client
Open the `Program.cs` file and add the highlighted line.
```csharp title="Program.cs"
builder.Services.AddSingleton<WeatherForecastService>();
// highlight-next-line
builder.Services.AddHttpClient();
var app = builder.Build();
```
This addition allows to define an `HttpClient` for the Blazor application.
## Page code
Open the `List.razor.cs` class and fill it as follows:
```csharp title="Pages/List.razor.cs"
public partial class List
{
// highlight-start
private Item[] items;
[Inject]
public HttpClient Http { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
protected override async Task OnInitializedAsync()
{
items = await Http.GetFromJsonAsync<Item[]>($"{NavigationManager.BaseUri}fake-data.json");
}
// highlight-end
}
```
The `OnInitializedAsync` method fires when the page is displayed, see the concept "Lifecycle events" further down this page.
## Show data
First we will perform a simple display of our data.
To do this, open the `List.razor` file and fill it in as follows:
```cshtml title="Pages/List.razor"
@page "/list"
<h3>List</h3>
// highlight-start
@if (items != null)
{
foreach (var item in items)
{
<div>@item.Id</div>
}
}
// highlight-end
```
You have now displayed the list of the value of the `Id` field in your page.
:::caution
Be careful, remember to check if your list is not null before doing your `foreach`. Otherwise an error will be thrown.
:::
## Concept: Life cycle events
The following diagrams illustrate the Razor component lifecycle events.
Component lifecycle events:
* If the component is rendered for the first time on a request:
* Create the component instance.
* Perform property injection. Run `SetParametersAsync`.
* Call OnInitialized{Async}. If an Incomplete Task is returned, the Task is awaited, then the component is re-rendered.
* Call OnParametersSet{Async}. If an Incomplete Task is returned, the Task is awaited, then the component is re-rendered.
* Render for all synchronous jobs and all Tasks.
![Événements de cycle de vie](/img/affichage-donnees/lifecycle1.png)
Document Object Model (DOM) event processing:
* The event handler is executed.
* If an Incomplete Task is returned, the Task is waited for, then the component is re-rendered.
* Render for all synchronous jobs and all Tasks.
![Événements de cycle de vie](/img/affichage-donnees/lifecycle2.png)
The Render lifecycle:
* Avoid additional rendering operations on the component:
* After the first rendering.
* When ShouldRender is false .
* Generate render tree (difference) and component render.
* Wait for the DOM to update.
* Call OnAfterRender{Async} .
![Événements de cycle de vie](/img/affichage-donnees/lifecycle3.png)

@ -0,0 +1,48 @@
---
sidebar_position: 17
title: Your next steps
---
It is now your turn, we are going to give you a description as well as a Mockup, a bit like in a request for development in a company.
It's up to you to make the necessary developments!
## The description
A new inventory page must be created.
This is made up of two parts:
- On the left a grid representing the elements already present in my inventory
- On the right a grid/list of available elements
Here are the page specs:
* It must be accessible from the "inventory" URL of the website.
* It should also display images and alt text for each item.
* It should record all user actions.
* The text of the page must be in English/French at least.
### Inventory Grid
Here are the specifications of the inventory grid:
* This grid should contain items that are already in my inventory, as well as free slots.
* Adding to the grid is available by dragging/dropping the list on the right.
* When saving the position of the dropped element must be taken into account.
* It must not be possible to replace an element by another unless this one (the target element) becomes the element to be dropped in another position.
* Moving an item out of inventory will be &equivalent to deletion.
For the bravest (**optional**):
* Respect the `stackSize` data, management of the number of elements for a case.
* Manage right click for drag/drop (cuts number of items in half).
* Manage inventory size in config file.
### Item List
Here are the item list specifications:
* The list must have pagination.
* The name and image of the elements must be present.
* A search field will filter the elements, the filtering must be done on the server side and not by the list.
* It must be possible to sort items by their names.
## Mockup
![Mockup](/img/your-next-steps.png)

@ -0,0 +1,96 @@
// @ts-check
// Note: type annotations allow type checking and IDEs autocompletion
const lightCodeTheme = require('prism-react-renderer/themes/github');
const darkCodeTheme = require('prism-react-renderer/themes/dracula');
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'Blazor',
tagline: 'Blazor',
url: 'https://iut.jeeo-web.com/',
baseUrl: '/',
onBrokenLinks: 'throw',
onBrokenMarkdownLinks: 'warn',
favicon: 'img/favicon.ico',
organizationName: 'IUT Clermont-Ferrand', // Usually your GitHub org/user name.
projectName: 'Blazor', // Usually your repo name.
// Even if you don't use internalization, you can use this field to set useful
// metadata like html lang. For example, if your site is Chinese, you may want
// to replace "en" with "zh-Hans".
i18n: {
defaultLocale: 'en',
locales: ['en'],
},
presets: [
[
'classic',
/** @type {import('@docusaurus/preset-classic').Options} */
({
// Disable the blog plugin
blog: false,
docs: {
sidebarPath: require.resolve('./sidebars.js'),
remarkPlugins: [require('mdx-mermaid')],
routeBasePath: '/',
},
theme: {
customCss: require.resolve('./src/css/custom.css'),
},
}),
],
],
themeConfig:
/** @type {import('@docusaurus/preset-classic').ThemeConfig} */
({
navbar: {
title: 'Blazor',
logo: {
alt: 'Blazor',
src: 'img/logo.png',
},
items: [
{
type: 'localeDropdown',
position: 'right',
},
],
},
footer: {
style: 'dark',
links: [],
copyright: `Copyright © ${new Date().getFullYear()} Blazor, Inc. Built with Docusaurus.`,
},
prism: {
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
additionalLanguages: ['csharp', "cshtml"],
},
}),
plugins: [
[
require.resolve("@easyops-cn/docusaurus-search-local"),
{
hashed: true,
docsRouteBasePath: '/',
highlightSearchTermsOnTargetPage: true
},
],
],
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
localeConfigs: {
en: {
htmlLang: 'en-US',
},
},
},
};
module.exports = config;

@ -0,0 +1,32 @@
{
"sidebar.tutorialSidebar.category.Add an item": {
"message": "Ajouter un item"
},
"sidebar.tutorialSidebar.category.API": {
"message": "API"
},
"sidebar.tutorialSidebar.category.Delete an item": {
"message": "Supprimer un item"
},
"sidebar.tutorialSidebar.category.DI & IOC": {
"message": "DI & IOC"
},
"sidebar.tutorialSidebar.category.Edit an item": {
"message": "Editer un item"
},
"sidebar.tutorialSidebar.category.Globalization & Localization": {
"message": "Globalisation & Localisation"
},
"sidebar.tutorialSidebar.category.Razor Component": {
"message": "Composant Razor"
},
"sidebar.tutorialSidebar.category.View data": {
"message": "Afficher des données"
},
"sidebar.tutorialSidebar.category.Deploy Apps": {
"message": "Déployer l'application"
},
"sidebar.tutorialSidebar.category.Bonus": {
"message": "Bonus"
}
}

@ -0,0 +1,85 @@
---
sidebar_position: 4
title: Ajouter bouton
---
Afin d'accèder à notre page d'ajout, nous allons ajouter un bouton au dessus de notre grille.
Ouvrez le fichier `Pages/List.razor` et ajouter les modification en surbrillance :
```cshtml title="Pages/List.razor"
@page "/list"
@using MyBeautifulAdmin.Models
<h3>List</h3>
// highlight-start
<div>
<NavLink class="btn btn-primary" href="Add" Match="NavLinkMatch.All">
<i class="fa fa-plus"></i> Ajouter
</NavLink>
</div>
// highlight-end
<DataGrid TItem="Data"
Data="@data"
ReadData="@OnReadData"
TotalItems="@totalData"
PageSize="10"
ShowPager
Responsive>
<DataGridColumn TItem="Data" Field="@nameof(Data.Id)" Caption="#" />
<DataGridColumn TItem="Data" Field="@nameof(Data.FirstName)" Caption="First Name" />
<DataGridColumn TItem="Data" Field="@nameof(Data.LastName)" Caption="Last Name" />
<DataGridColumn TItem="Data" Field="@nameof(Data.DateOfBirth)" Caption="Date Of Birth" DisplayFormat="{0:d}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo("fr-FR")" />
<DataGridColumn TItem="Data" Field="@nameof(Data.Roles)" Caption="Roles">
<DisplayTemplate>
@(string.Join(", ", ((Data)context).Roles))
</DisplayTemplate>
</DataGridColumn>
</DataGrid>
```
## Notion : Routage
### Modèles de routage
Le composant `Router` permet le routage vers des composants Razor dans une application Blazor. Le composant `Router` est utilisé dans le composant `App` des applications Blazor.
```cshtml title="App.razor"
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<p>Sorry, there's nothing at this address.</p>
</NotFound>
</Router>
```
Quand un composant Razor (`.razor`) avec une directive `@page` est compilé, la classe de composant générée est fournie en RouteAttribute spécifiant le modèle de routage du composant.
Lorsque lapplication démarre, lassembly spécifié en tant que routeur `AppAssembly` est analysé pour collecter des informations de routage pour les composants de lapplication qui ont un RouteAttribute .
Au moment de lexécution, le RouteView composant :
* Reçoit du avec RouteData Router tous les paramètres ditinéraire.
* Restitue le composant spécifié avec sa disposition, y compris les dispositions imbriquées supplémentaires.
Les composants prennent en charge plusieurs modèles de routage à laide de plusieurs directives `@page`.
Lexemple de composant suivant se charge sur les demandes pour `/BlazorRoute` et `/DifferentBlazorRoute`.
```cshtml title="Pages/BlazorRoute.razor"
// highlight-start
@page "/BlazorRoute"
@page "/DifferentBlazorRoute"
// highlight-end
<h1>Blazor routing</h1>
```
:::info
Pour que les URL soient correctement résolues, lapplication doit inclure une balise &lt;base&gt; dans son fichier `wwwroot/index.html` ( Blazor WebAssembly ) ou fichier `Pages/_Layout.cshtml` ( Blazor Server ) avec le chemin daccès de base de lapplication spécifié dans l'attribut `href`.
:::

@ -0,0 +1,161 @@
---
sidebar_position: 5
title: Création du modèle d'ajout
---
## Modèle d'ajout
Afin d'ajouter un element nous allons créer un objet représentant notre item.
Pour cela dans créer une nouvelle classe `Models/ItemModel.cs`, cette classe comprendra l'ensemble des informations de notre objet de l'API.
Nous allons aussi ajouter une propriété afin que l'utilisation accepte les conditions d'ajout.
De même des annotations seront présentent afin de valider les champs de notre formulaires.
```csharp title="Models/ItemModel.cs"
public class ItemModel
{
public int Id { get; set; }
[Required]
[StringLength(50, ErrorMessage = "Le nom affiché ne doit pas dépasser 50 caractères.")]
public string DisplayName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "Le nom ne doit pas dépasser 50 caractères.")]
[RegularExpression(@"^[a-z''-'\s]{1,40}$", ErrorMessage = "Seulement les caractères en minuscule sont acceptées.")]
public string Name { get; set; }
[Required]
[Range(1, 64)]
public int StackSize { get; set; }
[Required]
[Range(1, 125)]
public int MaxDurability { get; set; }
public List<string> EnchantCategories { get; set; }
public List<string> RepairWith { get; set; }
[Required]
[Range(typeof(bool), "true", "true", ErrorMessage = "Vous devez accepter les conditions.")]
public bool AcceptCondition { get; set; }
[Required(ErrorMessage = "L'image de l'item est obligatoire !")]
public byte[] ImageContent { get; set; }
}
```
## Notion : Data Annotation
### Attributs de validation
Les attributs de validation vous permettent de spécifier des règles de validation pour des propriétés de modèle.
Lexemple suivant montre une classe de modèle qui est annotée avec des attributs de validation.
Lattribut `[ClassicMovie]` est un attribut de validation personnalisé, et les autres sont prédéfinis.
```csharp
public class Movie
{
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Title { get; set; }
[ClassicMovie(1960)]
[DataType(DataType.Date)]
[Display(Name = "Release Date")]
public DateTime ReleaseDate { get; set; }
[Required]
[StringLength(1000)]
public string Description { get; set; }
[Range(0, 999.99)]
public decimal Price { get; set; }
public Genre Genre { get; set; }
public bool Preorder { get; set; }
}
```
### Attributs prédéfinis
Voici certains des attributs de validation prédéfinis :
* `[ValidateNever]`: ValidateNeverAttribute Indique quune propriété ou un paramètre doit être exclu de la validation.
* `[CreditCard]`: Vérifie que la propriété a un format de carte de crédit. Requiert des méthodes supplémentaires de validation jQuery.
* `[Compare]`: Valide que deux propriétés dun modèle correspondent.
* `[EmailAddress]`: Vérifie que la propriété a un format de-mail.
* `[Phone]`: Vérifie que la propriété a un format de numéro de téléphone.
* `[Range]`: Vérifie que la valeur de la propriété est comprise dans une plage spécifiée.
* `[RegularExpression]`: Valide le fait que la valeur de propriété corresponde à une expression régulière spécifiée.
* `[Required]`: Vérifie que le champ na pas la valeur null. Pour plus dinformations sur le comportement de cet attribut, consultez [Required] attribut .
* `[StringLength]`: Valide le fait quune valeur de propriété de type chaîne ne dépasse pas une limite de longueur spécifiée.
* `[Url]`: Vérifie que la propriété a un format dURL.
* `[Remote]`: Valide lentrée sur le client en appelant une méthode daction sur le serveur. Pour plus dinformations sur le comportement de cet attribut, consultez [Remote] attribut .
Vous trouverez la liste complète des attributs de validation dans lespace de noms [System.ComponentModel.DataAnnotations](https://docs.microsoft.com/fr-fr/dotnet/api/system.componentmodel.dataannotations).
### Messages derreur
Les attributs de validation vous permettent de spécifier le message derreur à afficher pour lentrée non valide. Par exemple :
```csharp
[StringLength(8, ErrorMessage = "Name length can't be more than 8.")]
```
En interne, les attributs appellent `String.Format` avec un espace réservé pour le nom de champ et parfois dautres espaces réservés. Par exemple :
```csharp
[StringLength(8, ErrorMessage = "{0} length must be between {2} and {1}.", MinimumLength = 6)]
```
Appliqué à une propriété `Name`, le message derreur créé par le code précédent serait « Name length must be between 6 and 8 ».
Pour savoir quels paramètres sont passés à `String.Format` pour le message derreur dun attribut particulier, consultez le [code source de DataAnnotations](https://github.com/dotnet/runtime/tree/main/src/libraries/System.ComponentModel.Annotations/src/System/ComponentModel/DataAnnotations).
### Attributs personnalisés
Pour les scénarios non gérés par les attributs de validation prédéfinis, vous pouvez créer des attributs de validation personnalisés. Créez une classe qui hérite de ValidationAttribute et substituez la méthode IsValid.
La méthode `IsValid` accepte un objet nommé value, qui est lentrée à valider. Une surcharge accepte également un objet `ValidationContext`, qui fournit des informations supplémentaires telles que linstance de modèle créée par la liaison de modèle.
Lexemple suivant vérifie que la date de sortie dun film appartenant au genre Classic nest pas ultérieure à une année spécifiée. L'attribut `[ClassicMovie]`:
* Sexécute uniquement sur le serveur.
* Pour les films classiques, valide la date de publication :
```csharp
public class ClassicMovieAttribute : ValidationAttribute
{
public ClassicMovieAttribute(int year)
{
Year = year;
}
public int Year { get; }
public string GetErrorMessage() =>
$"Classic movies must have a release year no later than {Year}.";
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
var movie = (Movie)validationContext.ObjectInstance;
var releaseYear = ((DateTime)value).Year;
if (movie.Genre == Genre.Classic && releaseYear > Year)
{
return new ValidationResult(GetErrorMessage());
}
return ValidationResult.Success;
}
}
```
La variable `movie` de lexemple précédent représente un objet `Movie` qui contient les données de lenvoi du formulaire. Quand la validation échoue, un `ValidationResult` avec un message derreur est retourné.

@ -0,0 +1,23 @@
---
sidebar_position: 3
title: Création de la page d'ajout
---
## Création de la page
Comme précédemment, créer une nouvelle page qui se nommera `Add.razor` ansi que la classe partiel `Add.razor.cs`.
## Code source final
```cshtml title="Pages/Add.razor"
@page "/add"
<h3>Add</h3>
```
```csharp title="Pages/Add.razor.cs"
public partial class Add
{
}
```

@ -0,0 +1,239 @@
---
sidebar_position: 6
title: Création du formulaire
---
Ouvrez le fichier `Pages/Add.razor` et modifier le fichier suite :
```cshtml title="Pages/Add.razor"
@page "/add"
<h3>Add</h3>
<EditForm Model="@itemModel" OnValidSubmit="@HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<p>
<label for="display-name">
Display name:
<InputText id="display-name" @bind-Value="itemModel.DisplayName" />
</label>
</p>
<p>
<label for="name">
Name:
<InputText id="name" @bind-Value="itemModel.Name" />
</label>
</p>
<p>
<label for="stack-size">
Stack size:
<InputNumber id="stack-size" @bind-Value="itemModel.StackSize" />
</label>
</p>
<p>
<label for="max-durability">
Max durability:
<InputNumber id="max-durability" @bind-Value="itemModel.MaxDurability" />
</label>
</p>
<p>
Enchant categories:
<div>
@foreach (var item in enchantCategories)
{
<label>
<input type="checkbox" @onchange="@(e => OnEnchantCategoriesChange(item, e.Value))" />@item
</label>
}
</div>
</p>
<p>
Repair with:
<div>
@foreach (var item in repairWith)
{
<label>
<input type="checkbox" @onchange="@(e => OnRepairWithChange(item, e.Value))" />@item
</label>
}
</div>
</p>
<p>
<label>
Item image:
<InputFile OnChange="@LoadImage" accept=".png" />
</label>
</p>
<p>
<label>
Accept Condition:
<InputCheckbox @bind-Value="itemModel.AcceptCondition" />
</label>
</p>
<button type="submit">Submit</button>
</EditForm>
```
* Le composant `EditForm` est rendu à lemplacement où l'élément &lt;EditForm&gt; apparaît.
* Le modèle est créé dans le code du composant et conservé dans un champ privé ( itemModel ). Le champ est assigné à l'attribut `Model` de l'élément &lt;EditForm&gt; .
* Le composant `InputText` ( id="display-name" ) est un composant dentrée pour la modification des valeurs de chaîne. L'attribut directive `@bind-Value` lie la propriété `itemModel.DisplayName` de modèle à la propriété `Value` du composant `InputText`.
* La méthode `HandleValidSubmit` est assignée à `OnValidSubmit`. Le gestionnaire est appelé si le formulaire réussit la validation.
* Le validateur des annotations de données (`DataAnnotationsValidator`) attache la prise en charge de la validation à laide des annotations de données :
* Si le champ de formulaire &lt;input&gt; nest pas renseigné lorsque le bouton Submit est sélectionné, une erreur saffiche dans le résumé des validations (`ValidationSummary`) (« The DisplayName field is required. ») et `HandleValidSubmit` nest pas appelée.
* Si le champ de formulaire &lt;input&gt; contient plus de cinquante caractères lorsque le bouton Submit est sélectionné, une erreur saffiche dans le résumé des validations (« Le nom affiché ne doit pas dépasser 50 caractères. ») et `HandleValidSubmit` nest pas appelée.
* Si le champ de formulaire &lt;input&gt; contient une valeur valide lorsque le bouton Submit est sélectionné, `HandleValidSubmit` est appelé.
## Code du formulaire
Ouvrez le fichier `Pages/Add.razor.cs` et modifier le fichier suite :
```csharp title="Pages/Add.razor.cs"
public partial class Add
{
[Inject]
public ILocalStorageService LocalStorage { get; set; }
[Inject]
public IWebHostEnvironment WebHostEnvironment { get; set; }
/// <summary>
/// The default enchant categories.
/// </summary>
private List<string> enchantCategories = new List<string>() { "armor", "armor_head", "armor_chest", "weapon", "digger", "breakable", "vanishable" };
/// <summary>
/// The default repair with.
/// </summary>
private List<string> repairWith = new List<string>() { "oak_planks", "spruce_planks", "birch_planks", "jungle_planks", "acacia_planks", "dark_oak_planks", "crimson_planks", "warped_planks" };
/// <summary>
/// The current item model
/// </summary>
private ItemModel itemModel = new()
{
EnchantCategories = new List<string>(),
RepairWith = new List<string>()
};
private async void HandleValidSubmit()
{
// Get the current data
var currentData = await LocalStorage.GetItemAsync<List<Item>>("data");
// Simulate the Id
itemModel.Id = currentData.Max(s => s.Id) + 1;
// Add the item to the current data
currentData.Add(new Item
{
Id = itemModel.Id,
DisplayName = itemModel.DisplayName,
Name = itemModel.Name,
RepairWith = itemModel.RepairWith,
EnchantCategories = itemModel.EnchantCategories,
MaxDurability = itemModel.MaxDurability,
StackSize = itemModel.StackSize,
CreatedDate = DateTime.Now
});
// Save the image
var imagePathInfo = new DirectoryInfo($"{WebHostEnvironment.WebRootPath}/images");
// Check if the folder "images" exist
if (!imagePathInfo.Exists)
{
imagePathInfo.Create();
}
// Determine the image name
var fileName = new FileInfo($"{imagePathInfo}/{itemModel.Name}.png");
// Write the file content
await File.WriteAllBytesAsync(fileName.FullName, itemModel.ImageContent);
// Save the data
await LocalStorage.SetItemAsync("data", currentData);
}
private async Task LoadImage(InputFileChangeEventArgs e)
{
// Set the content of the image to the model
using (var memoryStream = new MemoryStream())
{
await e.File.OpenReadStream().CopyToAsync(memoryStream);
itemModel.ImageContent = memoryStream.ToArray();
}
}
private void OnEnchantCategoriesChange(string item, object checkedValue)
{
if ((bool)checkedValue)
{
if (!itemModel.EnchantCategories.Contains(item))
{
itemModel.EnchantCategories.Add(item);
}
return;
}
if (itemModel.EnchantCategories.Contains(item))
{
itemModel.EnchantCategories.Remove(item);
}
}
private void OnRepairWithChange(string item, object checkedValue)
{
if ((bool)checkedValue)
{
if (!itemModel.RepairWith.Contains(item))
{
itemModel.RepairWith.Add(item);
}
return;
}
if (itemModel.RepairWith.Contains(item))
{
itemModel.RepairWith.Remove(item);
}
}
}
```
Vous pouvez désormais ajouter un nouvel item, si vous retourner sur la liste le votre nouvel élement est présent.
## Notion : Formulaire et validation
### Composants de formulaire intégrés
L'infrastructure Blazor fournit des composants de formulaire intégrés pour recevoir et valider les entrées utilisateur.
Les entrées sont validées lorsquelles sont modifiées et lorsquun formulaire est envoyé.
Les composants dentrée disponibles sont répertoriés dans le tableau suivant.
| Composant dentrée | Rendu comme… |
| ----------- | ----------- |
| InputCheckbox | `<input type="checkbox">` |
| InputDate&lt;TValue&gt; | `<input type="date">` |
| InputFile | `<input type="file">` |
| InputNumber&lt;TValue&gt; | `<input type="number">` |
| InputRadio&lt;TValue&gt; | `<input type="radio">` |
| InputRadioGroup&lt;TValue&gt; | Groupe denfants InputRadio&lt;TValue&gt; |
| InputSelect&lt;TValue&gt; | `<select>` |
| InputText | `<input>` |
| InputTextArea | `<textarea>` |
Pour plus dinformations sur le InputFile composant, consultez [ASP.NET Core Blazor chargements de fichiers](https://docs.microsoft.com/fr-fr/aspnet/core/blazor/file-uploads).
Tous les composants dentrée, y compris EditForm , prennent en charge les attributs arbitraires. Tout attribut qui ne correspond pas à un paramètre de composant est ajouté à lélément HTML rendu.
Les composants dentrée fournissent le comportement par défaut pour valider quand un champ est modifié, y compris la mise à jour de la classe CSS Field pour refléter létat du champ comme valide ou non valide.
Certains composants incluent une logique danalyse utile.
Par exemple, InputDate&lt;TValue&gt; et InputNumber&lt;TValue&gt; gèrent correctement les valeurs non analysables en inscrivant des valeurs non analysables en tant querreurs de validation.
Les types qui peuvent accepter des valeurs NULL prennent également en charge la possibilité de valeur null du champ cible (par exemple, int? pour un entier Nullable).

@ -0,0 +1,112 @@
---
sidebar_position: 8
title: Affichage de l'image
---
Nous avons maintenant ajouter un nouvel item avec son image, il est alors temps de gérer celle-ci dans notre tableau.
Ouvrez le fichier `Pages/List.razor` et ajouter les lignes en surbrillances :
```cshtml title="Pages/List.razor"
<DataGrid TItem="Item"
Data="@items"
ReadData="@OnReadData"
TotalItems="@totalItem"
PageSize="10"
ShowPager
Responsive>
<DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="#" />
// highlight-start
<DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="Image">
<DisplayTemplate>
<img src="images/@(context.Name).png" class="img-thumbnail" title="@context.DisplayName" alt="@context.DisplayName" style="max-width: 150px" />
</DisplayTemplate>
</DataGridColumn>
// highlight-end
<DataGridColumn TItem="Item" Field="@nameof(Item.DisplayName)" Caption="Display name" />
<DataGridColumn TItem="Item" Field="@nameof(Item.StackSize)" Caption="Stack size" />
<DataGridColumn TItem="Item" Field="@nameof(Item.MaxDurability)" Caption="Maximum durability" />
<DataGridColumn TItem="Item" Field="@nameof(Item.EnchantCategories)" Caption="Enchant categories">
<DisplayTemplate>
@(string.Join(", ", ((Item)context).EnchantCategories))
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="Item" Field="@nameof(Item.RepairWith)" Caption="Repair with">
<DisplayTemplate>
@(string.Join(", ", ((Item)context).RepairWith))
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="Item" Field="@nameof(Item.CreatedDate)" Caption="Created date" DisplayFormat="{0:d}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo("fr-FR")" />
</DataGrid>
```
Notre image est bien afficher mais par contre les images de nos fausses données elle ne sont présente.
Modifions le code afin de prendre en compte l'image suivante en image par défaut.
![Image par défaut](/img/ajouter-item/default.png)
Télécharger l'image et placer la dans le dossier `wwwroot/images/default.png`.
![Image par défaut](/img/ajouter-item/default-image.png)
Ouvrez le fichier `Pages/List.razor.cs` et ajouter les lignes en surbrillances :
```csharp title="Pages/List.razor.cs"
...
[Inject]
public ILocalStorageService LocalStorage { get; set; }
// highlight-start
[Inject]
public IWebHostEnvironment WebHostEnvironment { get; set; }
// highlight-end
[Inject]
public NavigationManager NavigationManager { get; set; }
...
```
Ouvrez le fichier `Pages/List.razor` et modifier les lignes en surbrillances :
```cshtml title="Pages/List.razor"
<DataGrid TItem="Item"
Data="@items"
ReadData="@OnReadData"
TotalItems="@totalItem"
PageSize="10"
ShowPager
Responsive>
<DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="#" />
// highlight-start
<DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="Image">
<DisplayTemplate>
@if (File.Exists($"{WebHostEnvironment.WebRootPath}/images/{context.Name}.png"))
{
<img src="images/@(context.Name).png" class="img-thumbnail" title="@context.DisplayName" alt="@context.DisplayName" style="max-width: 150px"/>
}
else
{
<img src="images/default.png" class="img-thumbnail" title="@context.DisplayName" alt="@context.DisplayName" style="max-width: 150px"/>
}
</DisplayTemplate>
</DataGridColumn>
// highlight-end
<DataGridColumn TItem="Item" Field="@nameof(Item.DisplayName)" Caption="Display name" />
<DataGridColumn TItem="Item" Field="@nameof(Item.StackSize)" Caption="Stack size" />
<DataGridColumn TItem="Item" Field="@nameof(Item.MaxDurability)" Caption="Maximum durability" />
<DataGridColumn TItem="Item" Field="@nameof(Item.EnchantCategories)" Caption="Enchant categories">
<DisplayTemplate>
@(string.Join(", ", ((Item)context).EnchantCategories))
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="Item" Field="@nameof(Item.RepairWith)" Caption="Repair with">
<DisplayTemplate>
@(string.Join(", ", ((Item)context).RepairWith))
</DisplayTemplate>
</DataGridColumn>
<DataGridColumn TItem="Item" Field="@nameof(Item.CreatedDate)" Caption="Created date" DisplayFormat="{0:d}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo("fr-FR")" />
</DataGrid>
```

@ -0,0 +1,26 @@
---
sidebar_position: 1
title: Introduction
---
## Ajouter un nouvel item
Ce TP va vous permettre d'ajouter un nouvel item.
### Liste des étapes
* Stocker nos données dans le LocalStorage
* Création d'une nouvelle page
* Ajouter un bouton `Ajouter`
* Création du model d'ajout
* Création du formulaire
* Redirection vers la page de liste
* Affichage de l'image téléchargé
### Liste des notions
* Blazor Storage
* Routage
* Data Annotation
* Formulaire et validation
* Uri et assistance détat de navigation

@ -0,0 +1,155 @@
---
sidebar_position: 2
title: Stocker nos données
---
## Utiliser le stockage des données en local
Nous avons utiliser notre fichier `fake-data.json` pour remplir notre grille, mais dans les étapes suivantes nous allons devoir ajouter / modifier les données.
Le plus simple serait de modifier directement le fichier `fake-data.json` mais cette solution est trop simple 😁.
Pour cela nous allons utiliser le `LocalStorage` du navigateur.
## Utilisation de Blazored.LocalStorage
Afin de faciliter sont utilisation nous allons utiliser le nuget `Blazored.LocalStorage`.
Installer le package avec la commande `Install-Package Blazored.LocalStorage`
## Parametrer Blazored.LocalStorage
Ouvrer le fichier `Program.cs` et ajouter la modification en surbrillance :
```csharp title="Program.cs"
...
builder.Services
.AddBlazorise()
.AddBootstrapProviders()
.AddFontAwesomeIcons();
// highlight-next-line
builder.Services.AddBlazoredLocalStorage();
var app = builder.Build();
...
```
## Enregistrer les données dans le LocalStorage
Ouvrez le fichier `Pages/List.razor.cs` et le modifier comme suite :
```csharp title="Pages/List.razor.cs"
public partial class List
{
private List<Item> items;
private int totalItem;
[Inject]
public HttpClient Http { get; set; }
[Inject]
public ILocalStorageService LocalStorage { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// Do not treat this action if is not the first render
if (!firstRender)
{
return;
}
var currentData = await LocalStorage.GetItemAsync<Item[]>("data");
// Check if data exist in the local storage
if (currentData == null)
{
// this code add in the local storage the fake data (we load the data sync for initialize the data before load the OnReadData method)
var originalData = Http.GetFromJsonAsync<Item[]>($"{NavigationManager.BaseUri}fake-data.json").Result;
await LocalStorage.SetItemAsync("data", originalData);
}
}
private async Task OnReadData(DataGridReadDataEventArgs<Item> e)
{
if (e.CancellationToken.IsCancellationRequested)
{
return;
}
// When you use a real API, we use this follow code
//var response = await Http.GetJsonAsync<Data[]>( $"http://my-api/api/data?page={e.Page}&pageSize={e.PageSize}" );
var response = (await LocalStorage.GetItemAsync<Item[]>("data")).Skip((e.Page - 1) * e.PageSize).Take(e.PageSize).ToList();
if (!e.CancellationToken.IsCancellationRequested)
{
totalItem = (await LocalStorage.GetItemAsync<List<Item>>("data")).Count;
items = new List<Item>(response); // an actual data for the current page
}
}
}
```
## Notion : Blazor Storage
### Conserver létat entre les sessions de navigateur
En règle générale, conservez létat entre les sessions de navigateur où les utilisateurs créent activement des données, et non simplement des données qui existent déjà.
Pour conserver létat entre les sessions de navigateur, lapplication doit conserver les données dans un autre emplacement de stockage que la mémoire du navigateur.
La persistance de lÉtat nest pas automatique. Vous devez prendre des mesures lors du développement de lapplication pour implémenter la persistance des données avec état.
La persistance des données est généralement requise uniquement pour létat de valeur élevée que les utilisateurs ont consacrés à la création.
Dans les exemples suivants, létat persistant fait gagner du temps ou contribue à des activités commerciales :
* Web Forms à plusieurs étapes : il est fastidieux pour un utilisateur de saisir à nouveau des données pour plusieurs étapes terminées dun formulaire Web à plusieurs étapes si leur état est perdu. Un utilisateur perd lÉtat dans ce scénario sil quitte le formulaire et le retourne plus tard.
* Paniers dachat : tout composant commercialement important dune application qui représente un chiffre daffaires potentiel peut être maintenu. Un utilisateur qui perd son état et, par conséquent, son panier, peut acheter moins de produits ou de services lorsquils reviennent sur le site ultérieurement.
Une application peut conserver uniquement létat de l' application. Les interfaces utilisateur ne peuvent pas être rendues persistantes, telles que les instances de composant et leurs arborescences de rendu. Les composants et les arborescences de rendu ne sont généralement pas sérialisables. Pour conserver létat de linterface utilisateur, tel que les nœuds développés dun contrôle Tree View, lapplication doit utiliser du code personnalisé pour modéliser le comportement de létat de linterface utilisateur en tant quÉtat de lapplication sérialisable.
### Emplacement de conservation de lÉtat
Les emplacements communs existent pour létat de persistance :
* Stockage côté serveur
* URL
* Stockage du navigateur
* Service de conteneur dÉtat en mémoire
### Stockage du navigateur
Pour les données temporaires que lutilisateur crée activement, un emplacement de stockage couramment utilisé est celui des `localStorage` & `sessionStorage` regroupements et du navigateur :
* `localStorage` est limité à la fenêtre du navigateur. Si lutilisateur recharge la page ou ferme et ouvre à nouveau le navigateur, létat persiste. Si lutilisateur ouvre plusieurs onglets de navigateur, lÉtat est partagé à travers les onglets. Les données sont conservées dans `localStorage` jusquà ce quelles soient explicitement effacées.
* `sessionStorage` est étendu à longlet navigateur. Si lutilisateur recharge longlet, létat persiste. Si lutilisateur ferme longlet ou le navigateur, lÉtat est perdu. Si lutilisateur ouvre plusieurs onglets de navigateur, chaque onglet possède sa propre version indépendante des données.
:::info
`localStorage` et `sessionStorage` peuvent être utilisés dans les applications Blazor WebAssembly, mais uniquement en écrivant du code personnalisé ou à laide dun package tiers.
:::
:::caution
Les utilisateurs peuvent afficher ou altérer les données stockées dans `localStorage` et `sessionStorage`.
:::
### Ou trouver les données de session locales
Tout les navigateurs récents permettent de visualiser les données de session locales.
Pour cela, dans votre navigateur, appuyer sur la touche `F12`, dans la nouvelle fenêtre :
* Sélectionner l'onglet `Application`
* Dans `Storage` :
* Sélectionner `Local Storage`
* Sélectionner l'url de votre application
Vous avez désormais accès à toutes les données stocké, vous pouvez les modifier ansi que les supprimer.
![Ou trouver les données de session locales](/img/ajouter-item/local-session-location.png)

@ -0,0 +1,94 @@
---
sidebar_position: 7
title: Redirection
---
## Effectuer une redirection
Maintenant que vous pouvez ajouter un élement, il serait interressant de rediriger l'utilisateur vers la liste lors de son ajout.
Ouvrez le fichier `Pages/Add.razor.cs` et ajouter les lignes en surbrillances :
```csharp title="Pages/Add.razor.cs"
public partial class Add
{
[Inject]
public ILocalStorageService LocalStorage { get; set; }
// highlight-start
[Inject]
public NavigationManager NavigationManager { get; set; }
// highlight-end
/// <summary>
/// The default roles.
/// </summary>
private List<string> roles = new List<string>() { "admin", "writter", "reader", "member" };
/// <summary>
/// The current data model
/// </summary>
private DataModel dataModel = new()
{
DateOfBirth = DateTime.Now,
Roles = new List<string>()
};
private async void HandleValidSubmit()
{
// Get the current data
var currentData = await LocalStorage.GetItemAsync<List<Data>>("data");
// Simulate the Id
dataModel.Id = currentData.Max(s => s.Id) + 1;
// Add the item to the current data
currentData.Add(new Data
{
LastName = dataModel.LastName,
DateOfBirth = dataModel.DateOfBirth,
FirstName = dataModel.FirstName,
Id = dataModel.Id,
Roles = dataModel.Roles
});
// Save the data
await LocalStorage.SetItemAsync("data", currentData);
// highlight-next-line
NavigationManager.NavigateTo("list");
}
private void OnRoleChange(string item, object checkedValue)
{
if ((bool)checkedValue)
{
if (!dataModel.Roles.Contains(item))
{
dataModel.Roles.Add(item);
}
return;
}
if (dataModel.Roles.Contains(item))
{
dataModel.Roles.Remove(item);
}
}
}
```
## Notion : Uri et assistance détat de navigation
Le `NavigationManager` permet de gérer les URI et la navigation dans le code C#. Le `NavigationManager` fournit lévénement et les méthodes indiqués dans le tableau suivant.
| Membre | Description |
| ---- | ---- |
| Uri | Obtient lURI absolu actuel. |
| BaseUri | Obtient lURI de base (avec une barre oblique de fin) qui peut être prépendée aux chemins dURI relatifs pour produire un URI absolu. En règle générale, `BaseUri` correspond à lattribut `href` de lélément `<base>` du document dans `wwwroot/index.html` (Blazor WebAssembly) ou `Pages/_Layout.cshtml` (Blazor Server). |
| NavigateTo | Accède à lURI spécifié. Si `forceLoad` est à la valeur `true`: <ul><li>Le routage côté client est contourné.</li><li>Le navigateur est forcé de charger la nouvelle page à partir du serveur, que lURI soit normalement géré par le routeur côté client.</li></ul>Si `replace` est à la valeur `true`, lURI actuel de lhistorique du navigateur est remplacé au lieu denvoyer un nouvel URI sur la pile dhistoriques. |
| LocationChanged | Événement qui se déclenche lorsque lemplacement de navigation a changé. |
| ToAbsoluteUri | Convertit un URI relatif en URI absolu. |
| ToBaseRelativePath | Étant donné un URI de base (par exemple, un URI précédemment retourné par BaseUri), convertit un URI absolu en URI par rapport au préfixe dURI de base. |
| GetUriWithQueryParameter | Retourne un URI construit en mettant à jour avec un seul paramètre ajouté, mis à jour `NavigationManager.Uri` ou supprimé. |

@ -0,0 +1,314 @@
---
sidebar_position: 2
title: Affichage des images
---
## Utiliser des images Base64
Les fichiers image ne sont pas la meilleure façon d'enregistrer l'image, dans de nombreuses architectures, l'image est enregistrée dans la base de données.
On modifie le code pour enregistrer l'image dans la base de données ;)
### Mettre à jour le modèle
Ouvrez le modèle d'élément et ajoutez une propriété qui contiendra l'image base64.
```csharp title="Models/Item.cs"
public class Item
{
public int Id { get; set; }
public string DisplayName { get; set; }
public string Name { get; set; }
public int StackSize { get; set; }
public int MaxDurability { get; set; }
public List<string> EnchantCategories { get; set; }
public List<string> RepairWith { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime? UpdatedDate { get; set; }
// highlight-next-line
public string ImageBase64 { get; set; }
}
```
```csharp title="Models/ItemModel.cs"
public class ItemModel
{
...
// highlight-next-line
public string ImageBase64 { get; set; }
}
```
### Mettre à jour le service local
Nous allons également modifier le service local pour enregistrer l'image en base64, nous supprimons toute référence pour enregistrer l'image.
Supprimez le code de surbrillance !
```csharp title="Services/DataLocalService.cs"
public async Task Add(ItemModel model)
{
// Get the current data
var currentData = await _localStorage.GetItemAsync<List<Item>>("data");
// Simulate the Id
model.Id = currentData.Max(s => s.Id) + 1;
// Add the item to the current data
currentData.Add(ItemFactory.Create(model));
// highlight-start
// Save the image
var imagePathInfo = new DirectoryInfo($"{_webHostEnvironment.WebRootPath}/images");
// Check if the folder "images" exist
if (!imagePathInfo.Exists)
{
imagePathInfo.Create();
}
// Determine the image name
var fileName = new FileInfo($"{imagePathInfo}/{model.Name}.png");
// Write the file content
await File.WriteAllBytesAsync(fileName.FullName, model.ImageContent);
// highlight-end
// Save the data
await _localStorage.SetItemAsync("data", currentData);
}
public async Task Update(int id, ItemModel model)
{
// Get the current data
var currentData = await _localStorage.GetItemAsync<List<Item>>("data");
// Get the item int the list
var item = currentData.FirstOrDefault(w => w.Id == id);
// Check if item exist
if (item == null)
{
throw new Exception($"Unable to found the item with ID: {id}");
}
// highlight-start
// Save the image
var imagePathInfo = new DirectoryInfo($"{_webHostEnvironment.WebRootPath}/images");
// Check if the folder "images" exist
if (!imagePathInfo.Exists)
{
imagePathInfo.Create();
}
// Delete the previous image
if (item.Name != model.Name)
{
var oldFileName = new FileInfo($"{imagePathInfo}/{item.Name}.png");
if (oldFileName.Exists)
{
File.Delete(oldFileName.FullName);
}
}
// Determine the image name
var fileName = new FileInfo($"{imagePathInfo}/{model.Name}.png");
// Write the file content
await File.WriteAllBytesAsync(fileName.FullName, model.ImageContent);
// highlight-end
// Modify the content of the item
ItemFactory.Update(item, model);
// Save the data
await _localStorage.SetItemAsync("data", currentData);
}
public async Task Delete(int id)
{
// Get the current data
var currentData = await _localStorage.GetItemAsync<List<Item>>("data");
// Get the item int the list
var item = currentData.FirstOrDefault(w => w.Id == id);
// Delete item in
currentData.Remove(item);
// highlight-start
// Delete the image
var imagePathInfo = new DirectoryInfo($"{_webHostEnvironment.WebRootPath}/images");
var fileName = new FileInfo($"{imagePathInfo}/{item.Name}.png");
if (fileName.Exists)
{
File.Delete(fileName.FullName);
}
// highlight-end
// Save the data
await _localStorage.SetItemAsync("data", currentData);
}
```
### Mettre à jour de la factory
C'est dans la factory que l'on transforme l'image en base64.
Changer le code de la factory :
```csharp title="Factories/ItemFactory.cs"
public static ItemModel ToModel(Item item, byte[] imageContent)
{
return new ItemModel
{
Id = item.Id,
DisplayName = item.DisplayName,
Name = item.Name,
RepairWith = item.RepairWith,
EnchantCategories = item.EnchantCategories,
MaxDurability = item.MaxDurability,
StackSize = item.StackSize,
ImageContent = imageContent,
// highlight-next-line
ImageBase64 = string.IsNullOrWhiteSpace(item.ImageBase64) ? Convert.ToBase64String(imageContent) : item.ImageBase64
};
}
public static Item Create(ItemModel model)
{
return new Item
{
Id = model.Id,
DisplayName = model.DisplayName,
Name = model.Name,
RepairWith = model.RepairWith,
EnchantCategories = model.EnchantCategories,
MaxDurability = model.MaxDurability,
StackSize = model.StackSize,
CreatedDate = DateTime.Now,
// highlight-next-line
ImageBase64 = Convert.ToBase64String(model.ImageContent)
};
}
public static void Update(Item item, ItemModel model)
{
item.DisplayName = model.DisplayName;
item.Name = model.Name;
item.RepairWith = model.RepairWith;
item.EnchantCategories = model.EnchantCategories;
item.MaxDurability = model.MaxDurability;
item.StackSize = model.StackSize;
item.UpdatedDate = DateTime.Now;
// highlight-next-line
item.ImageBase64 = Convert.ToBase64String(model.ImageContent);
}
```
### Update the views
Supprimez le code de surbrillance !
```csharp title="Pages/Edit.razor.cs"
...
protected override async Task OnInitializedAsync()
{
var item = await DataService.GetById(Id);
var fileContent = await File.ReadAllBytesAsync($"{WebHostEnvironment.WebRootPath}/images/default.png");
// highlight-start
if (File.Exists($"{WebHostEnvironment.WebRootPath}/images/{itemModel.Name}.png"))
{
fileContent = await File.ReadAllBytesAsync($"{WebHostEnvironment.WebRootPath}/images/{item.Name}.png");
}
// highlight-end
// Set the model with the item
itemModel = ItemFactory.ToModel(item, fileContent);
}
...
```
```cshtml title="Pages/Edit.razor"
...
<p>
<label>
Current Item image:
<img src="data:image/png;base64, @(itemModel.ImageBase64)" class="img-thumbnail" title="@itemModel.DisplayName" alt="@itemModel.DisplayName" style="min-width: 50px; max-width: 150px"/>
</label>
</p>
...
```
```cshtml title="Pages/List.razor"
...
<DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="Image">
<DisplayTemplate>
@if (!string.IsNullOrWhiteSpace(context.ImageBase64))
{
<img src="data:image/png;base64, @(context.ImageBase64)" class="img-thumbnail" title="@context.DisplayName" alt="@context.DisplayName" style="min-width: 50px; max-width: 150px" />
}
else
{
<img src="images/default.png" class="img-thumbnail" title="@context.DisplayName" alt="@context.DisplayName" style="max-width: 150px"/>
}
</DisplayTemplate>
</DataGridColumn>
...
```
## Notion: Base64 Images
Pour comprendre les images base64, commençons par comprendre pourquoi nous en avons besoin.
Les développeurs Web du monde entier essaient actuellement de minimiser les données inhabituelles pour maximiser les performances de leur site Web.
Les performances du site Web peuvent être optimisées en réduisant la taille de la page, le nombre de requêtes, la position des scripts et des feuilles de style et de nombreux autres facteurs affectent la vitesse de la page Web.
La méthode Base64 est utilisée pour minimiser les requêtes du serveur en intégrant des données d'image dans le code HTML.
La méthode Base64 utilise l'URI de données et sa syntaxe est la suivante :
```html title="Base64 Syntax"
data:[<MIMETYPE>][;charset=<CHARSET>][;base64],<DATA>
```
Pour l'intégrer en HTML, vous devez écrire le code de la balise html img comme ceci :
```html title="Base64 image in img TagXHTML"
<img alt="coderiddles" src="data:image/jpg;base64,S)OWE902WEIWUOLKASJODIIWJ9878978JKKJKIWEURU4954590EJ09JT9T99TIR32EQ2EKJDKSDFDNXZCNBAC3SASDASD45ASD5ASD5A4SDASD54DS56DB67V6VBN78M90687LKJKSDFJSDF76F7S7D78F9SDF78S6FSDF9SDFSFSDGFSDFSDLFJSDF7SD86FSDFSDFSDFS8F89SDIFOSDFSFJHKJL" />
```
Nous pouvons facilement convertir des images base64 à l'aide de [outil en ligne](https://www.base64-image.de/).
Nous pouvons également intégrer des images base64 dans la feuille de style (propriété background-image).
```css title="Base64 image inside StylesheetCSS"
.imagediv
{
background: url(data:image/jpg;base64,S)OWE902WEIWUOLKASJODIIWJ9878978JKKJKIWEURU4954590EJ09JT9T99TIR32EQ2EKJDKSDFDNXZCNBAC3SASDASD45ASD5ASD5A4SDASD54DS56DB67V6VBN78M90687LKJKSDFJSDF76F7S7D78F9SDF78S6FSDF9SDFSFSDGFSDFSDLFJSDF7SD86FSDFSDFSDFS8F89SDIFOSDFSFJHKJL);
}
```
### Les images Base64 peuvent-elles améliorer les performances ?
Si nous encodons toutes les petites images de nos sites Web, encapsulez-les dans du code css ou html. Cela réduira le nombre de requêtes HTTP car le client (navigateur) n'a pas à émettre de requête distincte pour les images.
Cela augmentera certainement les performances du site Web. Parce que la majeure partie du temps de chargement des pages est consacrée au transfert de données sur Internet. Ne pas le traiter sur le serveur.
**Avantages des images Base64 :**
* Supprime les requêtes HTTP séparées pour le chargement d'images en enveloppant le code d'image codé dans CSS ou HTML.
* Les données encodées d'image peuvent être enregistrées dans la base de données et peuvent générer un fichier image. Juste au cas où nous perdrions la copie du fichier image.
**Inconvénients des images Base64 :**
* Bien que Base64 augmente les performances, mais soyez prudent. Cela augmentera la taille de l'image d'environ 20 à 25 %. que ce qu'il est réellement sous sa forme binaire. Donc, plus de données doivent être transférées sur Internet. Pour les appareils mobiles, c'est un petit inconvénient.
* Même si nous appliquons la compression gzip, cela ne fera que réduire la taille du fichier css à environ 10-12 %.
* IE6 et IE7 ne prennent pas en charge l'URI de données, ce qui signifie que les images base64 ne seront pas chargées dans les navigateurs ie6 et ie7.
* Si vous appliquez l'encodage base64 à de nombreuses images de taille moyenne, cela augmentera la taille du contenu HTML ou la taille du contenu CSS. Le navigateur doit donc faire un aller-retour pour obtenir le contenu complet.

@ -0,0 +1,18 @@
---
sidebar_position: 1
title: Introduction
---
## API
Nous allons voir le fonctionnement des appels des API.
### Liste des étapes
* Affichage de l'image
* Effectuer des requêtes HTTP
### Liste des notions
* Base64 Images
* IHttpClientFactory

@ -0,0 +1,450 @@
---
sidebar_position: 1
title: Effectuer des requêtes HTTP
---
## Format des données
Deux fonctionnement existent afin de récupérer des données d'une API, la première et de récupérer les données brut au format JSON, la deuxième est d'utiliser la sérialisation / désérialisation.
Dans notre cas, la librairie utilisée implémente par défaut la sérialisation / désérialisation.
Afin de pouvoir manipuler nos données nous allons utiliser notre classe de composant `Item` en local, les données reçues elles proviennent de la sérialisation de la classe `Item` du serveur.
```csharp title="Models/Item.cs"
public class Item
{
public int Id { get; set; }
public string DisplayName { get; set; }
public string Name { get; set; }
public int StackSize { get; set; }
public int MaxDurability { get; set; }
public List<string> EnchantCategories { get; set; }
public List<string> RepairWith { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime? UpdatedDate { get; set; }
public string ImageBase64 { get; set; }
}
```
:::info
Ajouter ou supprimer des champs dans notre modèle ne génére pas d'erreur lors de la récupération des données.
:::
## Utilisation de l'IOC
Grâçe à notre IOC nous allons donc appeler notre API dans un service spécifique implémentant l'interface `IDataService`.
Créer la classe `DataApiService`:
```csharp title="Services/DataApiService.cs"
public class DataApiService : IDataService
{
private readonly HttpClient _http;
public DataApiService(
HttpClient http)
{
_http = http;
}
public async Task Add(ItemModel model)
{
// Get the item
var item = ItemFactory.Create(model);
// Save the data
await _http.PostAsJsonAsync("https://localhost:7234/api/Crafting/", item);
}
public async Task<int> Count()
{
return await _http.GetFromJsonAsync<int>("https://localhost:7234/api/Crafting/count");
}
public async Task<List<Item>> List(int currentPage, int pageSize)
{
return await _http.GetFromJsonAsync<List<Item>>($"https://localhost:7234/api/Crafting/?currentPage={currentPage}&pageSize={pageSize}");
}
public async Task<Item> GetById(int id)
{
return await _http.GetFromJsonAsync<Item>($"https://localhost:7234/api/Crafting/{id}");
}
public async Task Update(int id, ItemModel model)
{
// Get the item
var item = ItemFactory.Create(model);
await _http.PutAsJsonAsync($"https://localhost:7234/api/Crafting/{id}", item);
}
public async Task Delete(int id)
{
await _http.DeleteAsync($"https://localhost:7234/api/Crafting/{id}");
}
public async Task<List<CraftingRecipe>> GetRecipes()
{
return await _http.GetFromJsonAsync<List<CraftingRecipe>>("https://localhost:7234/api/Crafting/recipe");
}
}
```
Nous avons désormais une classe permettant de passer l'ensemble de nos appels par une API.
## Enregistrez le service de données
Ouvrez le fichier "Program.cs" et modifiez la ligne suivante :
```csharp title="Program.cs"
...
builder.Services.AddScoped<IDataService, DataApiService>();
...
```
## Ajouter l'exemple d'API à votre projet
Téléchargez ce [projet](/Minecraft.Crafting.Api.zip).
Décompressez le fichier dans le répertoire de votre projet, au même endroit que le répertoire du projet Blazor.
Exemple:
![Sample Api Location](/img/api/sample-api-location.png)
Sur Visual Studio, cliquez droit sur la solution et choisissez `Add => Existing Project...`
![Add Existing Project](/img/api/add-existing-project.png)
Sélectionnez le fichier `Minecraft.Crafting.Api.csproj` dans le répertoire `Minecraft.Crafting.Api`.
Votre solution contient maintenant deux projets, votre client et l'exemple d'API.
## Comment démarrer deux projets en même temps dans Visual Studio
Pour tester votre client avec l'API, vous devez démarrer les deux projets en même temps.
Pour cela, sur Visual Studio, cliquez droit sur la solution et choisissez `Properties`
![Solution Properties](/img/api/solution-properties.png)
Sur le nouvel écran, sélectionnez "Multiple startup projects" et sélectionnez "Start" pour les deux projets.
![Multiple startup projects](/img/api/multiple-startup-projects.png)
Lorsque vous démarrez le mode débogage, les deux projets sont démarrés.
## Notion : IHttpClientFactory
Une `IHttpClientFactory` peut être inscrite et utilisée pour configurer et créer des instances de HttpClient dans une application.
`IHttpClientFactory` offre les avantages suivants :
* Fournit un emplacement central pour le nommage et la configuration dinstance de `HttpClient` logiques. Par exemple, un client nommé github peut être inscrit et configuré pour accéder à GitHub. Un client par défaut peut être inscrit pour un accès général.
* Codifie le concept de middleware sortant par le biais de la délégation de gestionnaires dans `HttpClient`. Fournit des extensions pour le middleware basé sur Polly pour tirer parti de la délégation de gestionnaires dans `HttpClient`.
* Gère le regroupement et la durée de vie des instances sous-jacentes `HttpClientMessageHandler`. La gestion automatique évite les problèmes DNS courants (Domain Name System) qui se produisent lors de la gestion `HttpClient` manuelle des durées de vie.
* Ajoute une expérience de journalisation configurable (via `ILogger`) pour toutes les requêtes envoyées via des clients créés par la fabrique.
### Modèles de consommation
Vous pouvez utiliser IHttpClientFactory dans une application de plusieurs façons :
* Utilisation de base
* Clients nommés
* Clients typés
* Clients générés
La meilleure approche dépend des exigences de lapplication.
#### Utilisation de base
Inscrivez `IHttpClientFactory` dans `Program.cs` en appelant `AddHttpClient` :
```csharp
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
// highlight-next-line
builder.Services.AddHttpClient();
```
Une `IHttpClientFactory` demande peut être demandée à laide de linjection de dépendances (DI). Le code suivant utilise `IHttpClientFactory` pour créer une instance `HttpClient` :
```csharp
public class BasicModel : PageModel
{
private readonly IHttpClientFactory _httpClientFactory;
// highlight-next-line
public BasicModel(IHttpClientFactory httpClientFactory) => _httpClientFactory = httpClientFactory;
public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }
public async Task OnGet()
{
var httpRequestMessage = new HttpRequestMessage(
HttpMethod.Get,
"https://api.github.com/repos/dotnet/AspNetCore.Docs/branches")
{
Headers =
{
{ HeaderNames.Accept, "application/vnd.github.v3+json" },
{ HeaderNames.UserAgent, "HttpRequestsSample" }
}
};
// highlight-next-line
var httpClient = _httpClientFactory.CreateClient();
var httpResponseMessage = await httpClient.SendAsync(httpRequestMessage);
if (httpResponseMessage.IsSuccessStatusCode)
{
using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync();
GitHubBranches = await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubBranch>>(contentStream);
}
}
}
```
Lutilisation de `IHttpClientFactory` comme dans lexemple précédent est un bon moyen de refactoriser une application existante. Il na aucun impact sur lutilisation `HttpClient`.
Dans les endroits où des instances `HttpClient` sont créées dans une application existante, remplacez ces occurrences par des appels à `CreateClient`.
#### Clients nommés
Les clients nommés sont un bon choix quand :
* Lapplication nécessite de nombreuses utilisations distinctes de `HttpClient`.
* Beaucoup de `HttpClient` entre eux ont une configuration différente.
Spécifiez la configuration dun nom `HttpClient` lors de son inscription dans `Program.cs`:
```csharp
builder.Services.AddHttpClient("GitHub", httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");
// using Microsoft.Net.Http.Headers;
// The GitHub API requires two headers.
httpClient.DefaultRequestHeaders.Add(HeaderNames.Accept, "application/vnd.github.v3+json");
httpClient.DefaultRequestHeaders.Add(HeaderNames.UserAgent, "HttpRequestsSample");
});
```
Dans le code précédent, le client est configuré avec :
* Adresse de base `https://api.github.com/de`.
* Deux en-têtes requis pour travailler avec lAPI GitHub.
##### CreateClient
Chaque fois que `CreateClient` est appelée :
* Une nouvelle instance est `HttpClient` créée.
* Laction de configuration est appelée.
Pour créer un client nommé, passez son nom à `CreateClient` :
```csharp
public class NamedClientModel : PageModel
{
private readonly IHttpClientFactory _httpClientFactory;
public NamedClientModel(IHttpClientFactory httpClientFactory) =>
_httpClientFactory = httpClientFactory;
public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }
public async Task OnGet()
{
// highlight-next-line
var httpClient = _httpClientFactory.CreateClient("GitHub");
var httpResponseMessage = await httpClient.GetAsync(
"repos/dotnet/AspNetCore.Docs/branches");
if (httpResponseMessage.IsSuccessStatusCode)
{
using var contentStream =
await httpResponseMessage.Content.ReadAsStreamAsync();
GitHubBranches = await JsonSerializer.DeserializeAsync
<IEnumerable<GitHubBranch>>(contentStream);
}
}
}
```
Dans le code précédent, la requête na pas besoin de spécifier un nom dhôte. Le code peut passer uniquement le chemin daccès, car ladresse de base configurée pour le client est utilisée.
#### Clients typés
Clients typés :
* Fournissent les mêmes fonctionnalités que les clients nommés, sans quil soit nécessaire dutiliser des chaînes comme clés.
* Bénéficie de laide dIntelliSense et du compilateur lors de lutilisation des clients.
* Fournissent un emplacement unique pour configurer et interagir avec un `HttpClient` particulier. Par exemple, un client typé unique peut être utilisé :
* Pour un point de terminaison principal unique.
* Pour encapsuler toute logique traitant du point de terminaison.
* Collaborez avec la di et pouvez être injecté si nécessaire dans lapplication.
Un client typé accepte un `HttpClient` paramètre dans son constructeur :
```csharp
public class GitHubService
{
private readonly HttpClient _httpClient;
// highlight-next-line
public GitHubService(HttpClient httpClient)
{
_httpClient = httpClient;
_httpClient.BaseAddress = new Uri("https://api.github.com/");
// using Microsoft.Net.Http.Headers;
// The GitHub API requires two headers.
_httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
_httpClient.DefaultRequestHeaders.Add(
HeaderNames.UserAgent, "HttpRequestsSample");
}
public async Task<IEnumerable<GitHubBranch>?> GetAspNetCoreDocsBranchesAsync() =>
await _httpClient.GetFromJsonAsync<IEnumerable<GitHubBranch>>(
"repos/dotnet/AspNetCore.Docs/branches");
}
```
Dans le code précédent :
* La configuration est déplacée dans le client typé.
* Linstance fournie `HttpClient` est stockée en tant que champ privé.
Les méthodes spécifiques à lAPI peuvent être créées qui exposent `HttpClient` des fonctionnalités.
Par exemple, la méthode encapsule du `GetAspNetCoreDocsBranches` code pour récupérer les branches GitHub de docs.
Le code suivant appelle `AddHttpClient` dans `Program.cs` pour inscrire la `GitHubService` classe cliente typée :
```csharp
builder.Services.AddHttpClient<GitHubService>();
```
Le client typé est inscrit comme étant transitoire avec injection de dépendances. Dans le code précédent, `AddHttpClient` sinscrit `GitHubService` en tant que service temporaire. Cette inscription utilise une méthode de fabrique pour :
* Créez une instance de `HttpClient`.
* Créez une instance de `GitHubService`, en passant linstance de `HttpClient` son constructeur.
Le client typé peut être injecté et utilisé directement :
```csharp
public class TypedClientModel : PageModel
{
private readonly GitHubService _gitHubService;
// highlight-next-line
public TypedClientModel(GitHubService gitHubService) => _gitHubService = gitHubService;
public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }
public async Task OnGet()
{
try
{
// highlight-next-line
GitHubBranches = await _gitHubService.GetAspNetCoreDocsBranchesAsync();
}
catch (HttpRequestException)
{
// ...
}
}
}
```
La configuration dun client typé peut également être spécifiée lors de son inscription `Program.cs`, plutôt que dans le constructeur du client typé :
```csharp
builder.Services.AddHttpClient<GitHubService>(httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");
// ...
});
```
#### Clients générés
`IHttpClientFactory` peut être utilisé en combinaison avec des bibliothèques tierces telles que Refit.
Refit est une REST bibliothèque pour .NET.
Il convertit les REST API en interfaces actives. Appel `AddRefitClient` pour générer une implémentation dynamique dune interface, qui utilise `HttpClient` pour effectuer les appels HTTP externes.
Une interface personnalisée représente lAPI externe :
```csharp
public interface IGitHubClient
{
[Get("/repos/dotnet/AspNetCore.Docs/branches")]
Task<IEnumerable<GitHubBranch>> GetAspNetCoreDocsBranchesAsync();
}
```
Appel `AddRefitClient` pour générer limplémentation dynamique, puis appeler `ConfigureHttpClient` pour configurer le sous-jacent `HttpClient` :
```csharp
// highlight-next-line
builder.Services.AddRefitClient<IGitHubClient>()
// highlight-next-line
.ConfigureHttpClient(httpClient =>
{
httpClient.BaseAddress = new Uri("https://api.github.com/");
// using Microsoft.Net.Http.Headers;
// The GitHub API requires two headers.
httpClient.DefaultRequestHeaders.Add(
HeaderNames.Accept, "application/vnd.github.v3+json");
httpClient.DefaultRequestHeaders.Add(
HeaderNames.UserAgent, "HttpRequestsSample");
});
```
Utilisez DI pour accéder à limplémentation dynamique de `IGitHubClient`:
```csharp
public class RefitModel : PageModel
{
private readonly IGitHubClient _gitHubClient;
// highlight-next-line
public RefitModel(IGitHubClient gitHubClient) => _gitHubClient = gitHubClient;
public IEnumerable<GitHubBranch>? GitHubBranches { get; set; }
public async Task OnGet()
{
try
{
// highlight-next-line
GitHubBranches = await _gitHubClient.GetAspNetCoreDocsBranchesAsync();
}
catch (ApiException)
{
// ...
}
}
}
```
### Type de requête d'HttpClient
HttpClient prend en charge dautres verbes HTTP :
| Propriétés | Verbe |
| ---- | ---- |
| Delete | Représente une méthode de protocole HTTP DELETE. |
| Get | Représente une méthode de protocole HTTP GET. |
| Head | Représente une méthode de protocole HTTP HEAD. La méthode HEAD est identique à GET, mais le serveur retourne uniquement des en-têtes de message dans la réponse, sans corps du message. |
| Method | Méthode &HTTP. |
| Options | Représente une méthode de protocole HTTP OPTIONS. |
| Patch | Obtient la méthode de protocole HTTP PATCH. |
| Post | Représente une méthode de protocole HTTP POST utilisée pour publier une nouvelle entité en plus d'un URI. |
| Put | Représente une méthode de protocole HTTP PUT utilisée pour remplacer une entité identifiée par un URI. |
| Trace | Représente une méthode de protocole HTTP TRACE. |

@ -0,0 +1,655 @@
---
sidebar_position: 3
title: Authentification
---
ASP.NET Core prend en charge la configuration et la gestion de la sécurité dans les Blazor applications.
Les scénarios de sécurité diffèrent entre les applications Blazor Server et Blazor WebAssembly.
Étant donné que les applications Blazor Server sexécutent sur le serveur, les contrôles dautorisation peuvent déterminer les éléments suivants :
* Les options de linterface utilisateur présentées à un utilisateur (par exemple, les entrées de menu disponibles pour un utilisateur).
* Les règles daccès pour les zones de lapplication et les composants.
Les applications Blazor WebAssembly sexécutent sur le client.
Lautorisation est uniquement utilisée pour déterminer les options de linterface utilisateur à afficher.
Étant donné que les contrôles côté client peuvent être modifiés ou ignorés par un utilisateur, une application Blazor WebAssembly ne peut pas appliquer les règles daccès dautorisation.
Pour nos exemples nous allons utiliser un petit projet d'exemple disponible [ici](/DemoAuthentication.zip).
## Création d'une application client
Nous allons créer une application client permettant de gérer l'authentification locale.
Créer une nouvelle application Blazor WASM.
Installer le package `Microsoft.AspNetCore.Components.Authorization` dans la version 5.0.13.
![required library](/img/authentication/nuget-Microsoft.AspNetCore.Components.Authorization.png)
Ou en utilisant la console Package Manager : `PM> Install-Package Microsoft.AspNetCore.Components.Authorization -Version 5.0.13`
:::caution
Dans les applications Blazor WebAssembly, les vérifications dauthentification peuvent être ignorées, car tout le code côté client peut être modifié par les utilisateurs.
Cela vaut également pour toutes les technologies dapplication côté client, y compris les infrastructures dapplication JavaScript SPA ou les applications natives pour nimporte quel système dexploitation.
:::
## Modèles
Comme d'habitude, nous devons créer les classes de modèle qui prendraient divers paramètres d'authentification pour la connexion et l'enregistrement de nouveaux utilisateurs.
Nous allons créer ces classes dans le dossier `Models`.
```csharp title="Models/RegisterRequest.cs"
public class RegisterRequest
{
[Required]
public string Password { get; set; }
[Required]
[Compare(nameof(Password), ErrorMessage = "Passwords do not match!")]
public string PasswordConfirm { get; set; }
[Required]
public string UserName { get; set; }
}
```
```csharp title="Models/LoginRequest.cs"
public class LoginRequest
{
[Required]
public string Password { get; set; }
[Required]
public string UserName { get; set; }
}
```
```csharp title="Models/CurrentUser.cs"
public class CurrentUser
{
public Dictionary<string, string> Claims { get; set; }
public bool IsAuthenticated { get; set; }
public string UserName { get; set; }
}
```
```csharp title="Models/AppUser.cs"
public class AppUser
{
public string Password { get; set; }
public List<string> Roles { get; set; }
public string UserName { get; set; }
}
```
Nous avons maintenant des classes pour aider à conserver les paramètres d'authentification.
## Création du service d'authentification
Nous allons maintenant créer notre service d'authentification, celui ci utilisera une liste d'utilisateur en mémoire, seul l'utilisateur `Admin` sera définie par défaut.
Ajoutez une nouvelle interface dans le projet.
```csharp title="Services/IAuthService.cs"
public interface IAuthService
{
CurrentUser GetUser(string userName);
void Login(LoginRequest loginRequest);
void Register(RegisterRequest registerRequest);
}
```
Ajoutons une classe concrète et implémentons l'interface précédente.
```csharp title="Services/AuthService.cs"
public class AuthService : IAuthService
{
private static readonly List<AppUser> CurrentUser;
static AuthService()
{
CurrentUser = new List<AppUser>
{
new AppUser { UserName = "Admin", Password = "123456", Roles = new List<string> { "admin" } }
};
}
public CurrentUser GetUser(string userName)
{
var user = CurrentUser.FirstOrDefault(w => w.UserName == userName);
if (user == null)
{
throw new Exception("User name or password invalid !");
}
var claims = new List<Claim>();
claims.AddRange(user.Roles.Select(s => new Claim(ClaimTypes.Role, s)));
return new CurrentUser
{
IsAuthenticated = true,
UserName = user.UserName,
Claims = claims.ToDictionary(c => c.Type, c => c.Value)
};
}
public void Login(LoginRequest loginRequest)
{
var user = CurrentUser.FirstOrDefault(w => w.UserName == loginRequest.UserName && w.Password == loginRequest.Password);
if (user == null)
{
throw new Exception("User name or password invalid !");
}
}
public void Register(RegisterRequest registerRequest)
{
CurrentUser.Add(new AppUser { UserName = registerRequest.UserName, Password = registerRequest.Password, Roles = new List<string> { "guest" } });
}
}
```
## Authentication State Provider
Comme son nom l'indique, cette classe fournit l'état d'authentification de l'utilisateur dans les Applications Blazor.
`AuthenticationStateProvider` est une classe abstraite dans l'espace de noms `Autorisation`.
Blazor utilise cette classe qui sera héritée et remplacée par nous avec une implémentation personnalisée pour obtenir l'état de l'utilisateur.
Cet état peut provenir du stockage de session, des cookies ou du stockage local comme dans notre cas.
Commençons par ajouter la classe Provider dans le dossier `Services`. Appelons-le `CustomStateProvider`. Comme mentionné, cette classe héritera de la classe `AuthenticationStateProvider`.
```csharp title="Services/CustomStateProvider.cs"
public class CustomStateProvider : AuthenticationStateProvider
{
private readonly IAuthService _authService;
private CurrentUser _currentUser;
public CustomStateProvider(IAuthService authService)
{
this._authService = authService;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var identity = new ClaimsIdentity();
try
{
var userInfo = GetCurrentUser();
if (userInfo.IsAuthenticated)
{
var claims = new[] { new Claim(ClaimTypes.Name, _currentUser.UserName) }.Concat(_currentUser.Claims.Select(c => new Claim(c.Key, c.Value)));
identity = new ClaimsIdentity(claims, "Server authentication");
}
}
catch (HttpRequestException ex)
{
Console.WriteLine("Request failed:" + ex);
}
return new AuthenticationState(new ClaimsPrincipal(identity));
}
public async Task Login(LoginRequest loginParameters)
{
_authService.Login(loginParameters);
// No error - Login the user
var user = _authService.GetUser(loginParameters.UserName);
_currentUser = user;
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task Logout()
{
_currentUser = null;
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
public async Task Register(RegisterRequest registerParameters)
{
_authService.Register(registerParameters);
// No error - Login the user
var user = _authService.GetUser(registerParameters.UserName);
_currentUser = user;
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
private CurrentUser GetCurrentUser()
{
if (_currentUser != null && _currentUser.IsAuthenticated)
{
return _currentUser;
}
return new CurrentUser();
}
}
```
Vous pouvez voir que, par défaut, nous arrivons à implémenter une fonction `GetAuthenticationStateAsync`.
Maintenant, cette fonction est assez importante car Blazor l'appelle très souvent pour vérifier l'état d'authentification de l'utilisateur dans l'application.
De plus, nous injecterons le service `AuthService` et le modèle `CurrentUser` au constructeur de cette classe.
L'idée derrière cela est que nous n'utiliserons pas directement l'instance de service dans la vue (composants Razor), nous injecterons plutôt l'instance de `CustomStateProvider` dans nos vues qui accéderont à leur tour aux services.
**Explication**
`GetAuthenticationStateAsync` - Récupère l'utilisateur actuel à partir de l'objet de service. si l'utilisateur est authentifié, nous ajouterons ses claims dans une liste et créerons une identité de claims. Après cela, nous renverrons un état d'authentification avec les données requises.
Les 3 autres méthodes sont assez simples. Nous appellerons simplement les méthodes de service requises. Mais voici une chose supplémentaire que je voudrais expliquer.
Désormais, à chaque connexion, enregistrement, déconnexion, il y a techniquement un changement d'état dans l'authentification.
Nous devons informer l'ensemble de l'application que l'état de l'utilisateur a changé.
Par conséquent, nous utilisons une méthode de notification et transmettons l'état d'authentification actuel en appelant `GetAuthenticationStateAsync`. Assez logique, hein ?
Maintenant, pour activer ces services et dépendances dans le projet, nous devons les enregistrer dans l'IOC, n'est-ce pas ?
Pour cela, accédez au `Program.cs` du projet et effectuez les ajouts suivants.
```csharp {6-10} title="Program.cs"
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();
builder.Services.AddScoped<CustomStateProvider>();
builder.Services.AddScoped<AuthenticationStateProvider>(s => s.GetRequiredService<CustomStateProvider>());
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
await builder.Build().RunAsync();
}
```
Travaillons maintenant sur l'ajout des composants Razor pour l'interface utilisateur.
Ici, nous allons ajouter 2 composants, c'est-à-dire le composant Login et le composant Register.
Nous protégerons l'intégralité de l'application en la sécurisant.
Cela signifie que seul un utilisateur authentifié sera autorisé à afficher les données de la page.
Ainsi, nous aurons un composant de mise en page séparé pour les composants de connexion/inscription.
Accédez au dossier `Shared` du projet.
Voici où vous devriez idéalement ajouter tous les composants Razor partagés.
Dans notre cas, ajoutons un nouveau composant Razor et appelons-le, `AuthLayout.razor`.
```html title="Shared/AuthLayout.razor"
@inherits LayoutComponentBase
<div class="main">
<div class="content px-4">
@Body
</div>
</div>
```
Vous pouvez voir qu'il s'agit d'un fichier html assez basique avec une balise d'héritage.
Nous ne nous concentrerons pas sur css/html.
Cependant, ce composant de mise en page va agir comme un conteneur qui contiendra le composant de connexion et d'enregistrement en lui-même.
## Login Component
```html title="Pages/Authentication/Login.razor"
@page "/login"
@layout AuthLayout
<h1 class="h2 font-weight-normal login-title">
Login
</h1>
<EditForm class="form-signin" OnValidSubmit="OnSubmit" Model="loginRequest">
<DataAnnotationsValidator />
<label for="inputUsername" class="sr-only">User Name</label>
<InputText id="inputUsername" class="form-control" @bind-Value="loginRequest.UserName" autofocus placeholder="Username" />
<ValidationMessage For="@(() => loginRequest.UserName)" />
<label for="inputPassword" class="sr-only">Password</label>
<InputText type="password" id="inputPassword" class="form-control" placeholder="Password" @bind-Value="loginRequest.Password" />
<ValidationMessage For="@(() => loginRequest.Password)" />
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
<label class="text-danger">@error</label>
<NavLink href="register">
<h6 class="font-weight-normal text-center">Create account</h6>
</NavLink>
</EditForm>
```
```csharp title="Pages/Authentication/Login.razor.cs"
public partial class Login
{
[Inject]
public CustomStateProvider AuthStateProvider { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
private string error { get; set; }
private LoginRequest loginRequest { get; set; } = new LoginRequest();
private async Task OnSubmit()
{
error = null;
try
{
await AuthStateProvider.Login(loginRequest);
NavigationManager.NavigateTo("");
}
catch (Exception ex)
{
error = ex.Message;
}
}
}
```
## Register Component
```html title="Pages/Authentication/Register.razor"
@page "/register"
@layout AuthLayout
<h1 class="h2 font-weight-normal login-title">
Register
</h1>
<EditForm class="form-signin" OnValidSubmit="OnSubmit" Model="registerRequest">
<DataAnnotationsValidator />
<label for="inputUsername" class="sr-only">User Name</label>
<InputText id="inputUsername" class="form-control" placeholder="Username" autofocus @bind-Value="@registerRequest.UserName" />
<ValidationMessage For="@(() => registerRequest.UserName)" />
<label for="inputPassword" class="sr-only">Password</label>
<InputText type="password" id="inputPassword" class="form-control" placeholder="Password" @bind-Value="@registerRequest.Password" />
<ValidationMessage For="@(() => registerRequest.Password)" />
<label for="inputPasswordConfirm" class="sr-only">Password Confirmation</label>
<InputText type="password" id="inputPasswordConfirm" class="form-control" placeholder="Password Confirmation" @bind-Value="@registerRequest.PasswordConfirm" />
<ValidationMessage For="@(() => registerRequest.PasswordConfirm)" />
<button class="btn btn-lg btn-primary btn-block" type="submit">Create account</button>
<label class="text-danger">@error</label>
<NavLink href="login">
<h6 class="font-weight-normal text-center">Already have an account? Click here to login</h6>
</NavLink>
</EditForm>
```
```csharp title="Pages/Authentication/Register.razor.cs"
public partial class Register
{
[Inject]
public CustomStateProvider AuthStateProvider { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
private string error { get; set; }
private RegisterRequest registerRequest { get; set; } = new RegisterRequest();
private async Task OnSubmit()
{
error = null;
try
{
await AuthStateProvider.Register(registerRequest);
NavigationManager.NavigateTo("");
}
catch (Exception ex)
{
error = ex.Message;
}
}
}
```
## Activée l'authentification
Maintenant, nous devons faire savoir à Blazor que l'authentification est activée et que nous devons transmettre l'attribut `Authorize` à toute l'application.
Pour cela, modifiez le composant principal, c'est-à-dire le composant `App.razor`.
```html title="App.razor"
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<CascadingAuthenticationState>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</CascadingAuthenticationState>
</NotFound>
</Router>
```
### Qu'est-ce qu'`AuthorizeRoteView` ?
C'est une combinaison de `AuthorizeView` et `RouteView`, de sorte qu'il affiche la page spécifique mais uniquement aux utilisateurs autorisés.
### Qu'est-ce que `CascadingAuthenticationState` ?
Cela fournit un paramètre en cascade à tous les composants descendants.
Ajoutons un bouton de déconnexion à la barre de navigation supérieure de l'application pour permettre à l'utilisateur de se déconnecter ainsi que de forcer la redirection vers la page `Login` si l'utilisateur n'est pas connecté.
Nous devrons apporter des modifications au composant `MainLayout.razor` pour cela.
```html {11} title="Shared/MainLayout.razor"
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<div class="top-row px-4">
<a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
<button type="button" class="btn btn-link ml-md-auto" @onclick="@LogoutClick">Logout</button>
</div>
<div class="content px-4">
@Body
</div>
</div>
</div>
```
```csharp title="Shared/MainLayout.razor.cs"
public partial class MainLayout
{
[Inject]
public CustomStateProvider AuthStateProvider { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
[CascadingParameter]
private Task<AuthenticationState> AuthenticationState { get; set; }
protected override async Task OnParametersSetAsync()
{
if (!(await AuthenticationState).User.Identity.IsAuthenticated)
{
NavigationManager.NavigateTo("/login");
}
}
private async Task LogoutClick()
{
await AuthStateProvider.Logout();
NavigationManager.NavigateTo("/login");
}
}
```
Nous voulons afficher notre nom d'utilisateur ainsi que ces roles sur le page index peu de temps après notre connexion.
Accédez à `Pages/Index.razor` et apportez les modifications suivantes.
```html title="Pages/Index.razor"
@page "/"
<AuthorizeView>
<Authorized>
<h1>Hello @context.User.Identity.Name !!</h1>
<p>Welcome to Blazor Learner.</p>
<ul>
@foreach (var claim in context.User.Claims)
{
<li>@claim.Type: @claim.Value</li>
}
</ul>
</Authorized>
<Authorizing>
<h1>Loading ...</h1>
</Authorizing>
<NotAuthorized>
<h1>Authentication Failure!</h1>
<p>You're not signed in.</p>
</NotAuthorized>
</AuthorizeView>
```
Vous pouvez voir qu'il y a un tas d'erreurs insolubles qui indiquent probablement que peu d'éléments ne sont pas définis dans l'espace de noms ou quelque chose du genre.
C'est parce que nous n'avons pas importé les nouveaux espaces de noms.
Pour ce faire, accédez à `_Import.razor` et ajoutez ces lignes en bas.
```html title="_Imports.razor"
@using Microsoft.AspNetCore.Components.Authorization
```
De même nous voulons créer une page d'administration accessible seulement par les utilisateur ayant le role `admin`.
Créer la page `Pages/Admin.razor`:
```html title="Pages/Admin.razor"
@page "/admin"
<h3>Admin Page</h3>
```
Nous allons mettre dans notre menu notre nouvelle page:
```html {10-16} title="Shared/NavMenu.razor"
...
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<ul class="nav flex-column">
<li class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Home
</NavLink>
</li>
<AuthorizeView Roles="admin">
<li class="nav-item px-3">
<NavLink class="nav-link" href="admin" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span> Admin page
</NavLink>
</li>
</AuthorizeView>
</ul>
</div>
...
```
## Notion : Composant AuthorizeView
Le composant `AuthorizeView` affiche le contenu de linterface utilisateur de manière sélective, selon que lutilisateur est autorisé ou non.
Cette approche est utile lorsque vous devez uniquement afficher les données de lutilisateur et que vous navez pas besoin dutiliser lidentité de lutilisateur dans la logique procédurale.
Le composant expose une variable context de type `AuthenticationState`, que vous pouvez utiliser pour accéder aux informations relatives à lutilisateur connecté :
```html
<AuthorizeView>
<h1>Hello, @context.User.Identity.Name!</h1>
<p>You can only see this content if you're authenticated.</p>
</AuthorizeView>
```
Vous pouvez également fournir un contenu différent pour laffichage si lutilisateur nest pas autorisé :
```html
<AuthorizeView>
<Authorized>
<h1>Hello, @context.User.Identity.Name!</h1>
<p>You can only see this content if you're authorized.</p>
<button @onclick="SecureMethod">Authorized Only Button</button>
</Authorized>
<NotAuthorized>
<h1>Authentication Failure!</h1>
<p>You're not signed in.</p>
</NotAuthorized>
</AuthorizeView>
@code {
private void SecureMethod() { ... }
}
```
Le contenu des balises `<Authorized>` et `<NotAuthorized>` peut inclure des éléments arbitraires, tels que dautres composants interactifs.
Un gestionnaire dévénements par défaut pour un élément autorisé, tel que la méthode `SecureMethod` de l'élément `<button>` dans lexemple précédent, peut être appelé uniquement par un utilisateur autorisé.
Les conditions dautorisation, comme les rôles ou les stratégies qui contrôlent les options dinterface utilisateur ou daccès, sont traitées dans la section `Autorisation`.
Si les conditions dautorisation ne sont pas spécifiées, `AuthorizeView` utilise une stratégie par défaut et traite :
* Les utilisateurs authentifiés (connectés) comme étant autorisés.
* Les utilisateurs non authentifiés (déconnectés) comme étant non autorisés.
## Notion : Autorisation en fonction du rôle et de la stratégie
Le composant `AuthorizeView` prend en charge lautorisation basée sur le rôle et basée sur les stratégies.
Pour lautorisation en fonction du rôle, utilisez le paramètre `Roles` :
```html
<AuthorizeView Roles="admin, superuser">
<p>You can only see this if you're an admin or superuser.</p>
</AuthorizeView>
```
## Notion : Attribut [Authorize]
L'attribut [Authorize] peut être utilisé dans les Razor composants :
```html
@page "/"
@attribute [Authorize]
You can only see this if you're signed in.
```
:::caution
Utilisez uniquement `[Authorize]` sur les composants `@page` atteints via le Blazor routeur.
Lautorisation est effectuée uniquement en tant quaspect du routage et pas pour les composants enfants rendus dans une page.
Pour autoriser laffichage déléments spécifiques dans une page, utilisez `AuthorizeView` à la place.
:::
L'attribut [Authorize] prend également en charge lautorisation basée sur les rôles ou la stratégie. Pour lautorisation en fonction du rôle, utilisez le paramètre `Roles` :
```html
@page "/"
@attribute [Authorize(Roles = "admin, superuser")]
<p>You can only see this if you're in the 'admin' or 'superuser' role.</p>
```

@ -0,0 +1,333 @@
---
sidebar_position: 2
title: Graphql
---
Pour nos exemples nous allons utiliser un petit projet d'exemple basé sur une base de données SQLite.
Cette exemple est disponible [ici](/DemoGraphQL.zip).
## Création d'une application client
Nous allons créer une application client permettant de consommer GraphQL.
Créer une nouvelle application Blazor WASM.
Modifier le fichier `appsettings.json`, en ajoutant une adresse vers l'application GraphQL :
```csharp title="wwwroot/appsettings.json"
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"GraphQLURI": "https://localhost:44371/graphql",
"AllowedHosts": "*"
}
```
Installer le package `GraphQL.Client` dans sa dernière version.
![required library](/img/graphql/29-GraphQL.CLient-new-version.png)
Ou en utilisant la console Package Manager : `PM> Install-Package GraphQL.Client`
Installer le nuget de serialisation de GraphQL `GraphQL.Client.Serializer.Newtonsoft` :
![required library](/img/graphql/30-GraphQL-Serializer-Newtonsoft.png)
`PM> Install-Package GraphQL.Client.Serializer.Newtonsoft`
Après l'installation, nous allons l'enregistrer dans la classe `Program` :
```csharp {8} title="Program.cs"
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<IGraphQLClient>(s => new GraphQLHttpClient(builder.Configuration["GraphQLURI"], new NewtonsoftJsonSerializer()));
await builder.Build().RunAsync();
}
```
L'étape suivante consiste à créer la classe `OwnerConsumer`, qui stockera toutes les requêtes et mutations :
```csharp title="OwnerConsumer.cs"
public class OwnerConsumer
{
private readonly IGraphQLClient _client;
public OwnerConsumer(IGraphQLClient client)
{
_client = client;
}
}
```
Maintenant, enregistrons cette classe :
```csharp {9} title="Program.cs"
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<IGraphQLClient>(s => new GraphQLHttpClient(builder.Configuration["GraphQLURI"], new NewtonsoftJsonSerializer()));
builder.Services.AddScoped<OwnerConsumer>();
await builder.Build().RunAsync();
}
```
Nous avons tout terminé concernant la configuration.
## Création des classes de modèle
Nous allons créer les classes de modèle afin de pouvoir utiliser les données de nos requêtes :
```csharp title="Models/TypeOfAccount.cs"
public enum TypeOfAccount
{
Cash,
Savings,
Expense,
Income
}
```
```csharp title="Models/Account.cs"
public class Account
{
public Guid Id { get; set; }
public TypeOfAccount Type { get; set; }
public string Description { get; set; }
}
```
```csharp title="Models/Owner.cs"
public class Owner
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public ICollection<Account> Accounts { get; set; }
}
```
Maintenant nous allons créer la classe `Input` pour les actions de mutation.
```csharp title="Models/OwnerInput.cs"
public class OwnerInput
{
public string Name { get; set; }
public string Address { get; set; }
}
```
Nous avons maintenant tout préparé et sommes prêts à commencer à créer des requêtes et des mutations.
## Création de requêtes et de mutations pour consommer l'API GraphQL
Ouvrer la classe `OwnerConsumer` et ajoutons la méthode `GetAllOwners` :
```csharp title="OwnerConsumer.cs"
public async Task<List<Owner>> GetAllOwners()
{
var query = new GraphQLRequest
{
Query = @"
query ownersQuery{
owners {
id
name
address
accounts {
id
type
description
}
}
}"
};
var response = await _client.SendQueryAsync<ResponseOwnerCollectionType>(query);
return response.Data.Owners;
}
```
Comme vous pouvez le voir, nous créons un nouvel objet `GraphQLRequest` qui contient une propriété `Query` pour la requête que nous voulons envoyer à l'API GraphQL.
Cette requête est la même que celle que vous pouvez utiliser avec l'outil `UI.Playground`.
Pour exécuter la requête, appeler la méthode `SenQueryAsync` qui accepte un type de réponse (comme paramètre générique) et la requête.
Enfin, le code renvoie la liste des propriétaires à partir de cette réponse.
Nous n'avons pas la classe `ResponseOwnerCollectionType`, créons donc un nouveau dossier `ResponseTypes` et à l'intérieur créer les deux nouvelles classes :
```csharp title="ResponseTypes/ResponseOwnerCollectionType.cs"
public class ResponseOwnerCollectionType
{
public List<Owner> Owners { get; set; }
}
```
```csharp title="ResponseTypes/ResponseOwnerType.cs"
public class ResponseOwnerType
{
public Owner Owner { get; set; }
}
```
### Get Query
```csharp title="OwnerConsumer.cs"
public async Task<Owner> GetOwner(Guid id)
{
var query = new GraphQLRequest
{
Query = @"
query ownerQuery($ownerID: ID!) {
owner(ownerId: $ownerID) {
id
name
address
accounts {
id
type
description
}
}
}",
Variables = new { ownerID = id }
};
var response = await _client.SendQueryAsync<ResponseOwnerType>(query);
return response.Data.Owner;
}
```
### Create Mutation
```csharp title="OwnerConsumer.cs"
public async Task<Owner> CreateOwner(OwnerInput ownerToCreate)
{
var query = new GraphQLRequest
{
Query = @"
mutation($owner: ownerInput!){
createOwner(owner: $owner){
id,
name,
address
}
}",
Variables = new {owner = ownerToCreate}
};
var response = await _client.SendMutationAsync<ResponseOwnerType>(query);
return response.Data.Owner;
}
```
### Update Mutation
```csharp title="OwnerConsumer.cs"
public async Task<Owner> UpdateOwner(Guid id, OwnerInput ownerToUpdate)
{
var query = new GraphQLRequest
{
Query = @"
mutation($owner: ownerInput!, $ownerId: ID!){
updateOwner(owner: $owner, ownerId: $ownerId){
id,
name,
address
}
}",
Variables = new { owner = ownerToUpdate, ownerId = id }
};
var response = await _client.SendMutationAsync<ResponseOwnerType>(query);
return response.Data.Owner;
}
```
### Delete Mutation
```csharp title="OwnerConsumer.cs"
public async Task<Owner> DeleteOwner(Guid id)
{
var query = new GraphQLRequest
{
Query = @"
mutation($ownerId: ID!){
deleteOwner(ownerId: $ownerId)
}",
Variables = new { ownerId = id }
};
var response = await _client.SendMutationAsync<ResponseOwnerType>(query);
return response.Data.Owner;
}
```
## Utilisation dans une page Blazor
Créer une nouvelle page `Consume.razor` pour tester notre code :
```html title="Pages/Consume.razor"
@page "/consume"
<h3>Consume</h3>
@if (Owner != null)
{
<table class="table">
<thead>
<tr>
<th>Id</th>
<th>Name</th>
<th>Address</th>
</tr>
</thead>
<tbody>
@foreach (var item in Owner)
{
<tr>
<td>@item.Id</td>
<td>@item.Name</td>
<td>@item.Address</td>
</tr>
}
</tbody>
</table>
}
```
Créer le code de la page :
```csharp title="Pages/Consume.razor.cs"
public partial class Consume
{
private List<Owner> Owner;
[Inject]
public OwnerConsumer Consumer { get; set; }
protected override async Task OnInitializedAsync()
{
this.Owner = await Consumer.GetAllOwners();
}
}
```

@ -0,0 +1,100 @@
---
sidebar_position: 1
title: Websocket
---
Ce cours explique comment commencer avec les WebSockets dans ASP.NET Core.
[WebSocket](https://wikipedia.org/wiki/WebSocket) ([RFC 6455](https://tools.ietf.org/html/rfc6455)) est un protocole qui autorise des canaux de communication persistants bidirectionnels sur les connexions TCP.
Son utilisation profite aux applications qui tirent parti dune communication rapide et en temps réel, par exemple les applications de conversation, de tableau de bord et de jeu.
Pour nos exemples nous allons utiliser un petit projet d'exemple disponible [ici](/DemoWebsocket.zip).
## Définition WebSocket
Dans le paradigme traditionnel du Web, le client était responsable de l'initiation de la communication avec un serveur, et le serveur ne pouvait pas renvoyer de données à moins qu'elles n'aient été préalablement demandées par le client.
Avec WebSockets, vous pouvez envoyer des données entre le serveur et le client via une seule connexion TCP, et généralement les WebSockets sont utilisés pour fournir des fonctionnalités en temps réel aux applications modernes.
![Définition WebSocket](/img/websocket/sockets.png)
## Quest-ce quun SignalR Hub ?
L'API SignalR hubs vous permet dappeler des méthodes sur des clients connectés à partir du serveur.
* Dans le code serveur, vous définissez des méthodes qui sont appelées par le client.
* Dans le code client, vous définissez des méthodes qui sont appelées à partir du serveur.
SignalR soccupe de tout en arrière-plan qui rend possible les communications de client à serveur et de serveur à client en temps réel.
## Installez le package client SignalR .NET
Le package `Microsoft.AspNetCore.SignalR.Client` est requis pour que les clients .NET se connectent aux hubs SignalR.
Installer le package `Microsoft.AspNetCore.SignalR.Client` dans sa dernière version.
![required library](/img/websocket/nuget-Microsoft.AspNetCore.SignalR.Client.png)
Ou en utilisant la console Package Manager : `PM> Install-Package Microsoft.AspNetCore.SignalR.Client`
## Utiliser SignalR
Pour établir une connexion, créez un `HubConnectionBuilder` et appelez la méthode `Build`.
L'URL du concentrateur, le protocole, le type de transport, le niveau de journalisation, les en-têtes et d'autres options peuvent être configurés lors de la création d'une connexion.
Configurez toutes les options requises en insérant l'une des méthodes `HubConnectionBuilder` dans la méthode `Build`.
Démarrez la connexion avec `StartAsync`.
```csharp title="DemoSignalR.razor.cs"
public partial class DemoSignalR
{
private HubConnection connection;
private string connectionUrl = "https://localhost:44391/ChatHub";
private List<Chat> logs = new List<Chat>();
private string message = "";
private string userName = "UserName";
public DemoSignalR()
{
// Create the new SignalR Hub
connection = new HubConnectionBuilder()
.WithUrl(connectionUrl)
.Build();
}
public void Dispose()
{
OnClose();
}
private async void OnClose()
{
// Send message for user disconnect
await connection.InvokeAsync("SendMessage", new Chat { Type = "disconnect", Name = userName });
// Stop the connection
await connection.StopAsync();
}
private async void OnConnect()
{
// Handler to treat the receive message
connection.On<Chat>("ReceiveMessage", chat =>
{
logs.Add(chat);
StateHasChanged();
});
// Start the connection
await connection.StartAsync();
// Send message for user connect
await connection.InvokeAsync("SendMessage", new Chat { Type = "connect", Name = userName });
}
private async Task SendMessageAsync()
{
// Send the user message
await connection.InvokeAsync("SendMessage", new Chat { Type = "message", Name = userName, Message = message });
}
}
```

@ -0,0 +1,293 @@
---
sidebar_position: 13
title: Configuration
---
La configuration dans ASP.NET Core est effectuée à laide dun ou de plusieurs fournisseurs de configuration.
Les fournisseurs de configuration lisent les données de configuration des paires clé-valeur à laide dune variété de sources de configuration :
* Paramètres des fichiers, tels que `appsettings.json`
* Variables d'environnement
* Azure Key Vault
* Azure App Configuration
* Arguments de ligne de commande
* Fournisseurs personnalisés, installés ou créés
* Fichiers de répertoire
* Objets .NET en mémoire
:::caution
Les fichiers de configuration et de paramètres dans une application Blazor Webassembly sont visibles par les utilisateurs. Ne stockez pas de secrets dapplication, dinformations didentification ou dautres données sensibles dans la configuration ou les fichiers dune application Webassembly.
:::
## Configuration par défaut
Les applications web ASP.NET Core créées avec dotnet new ou Visual Studio générer le code suivant :
```csharp
public static async Task Main(string[] args)
{
// highlight-next-line
var builder = WebApplication.CreateBuilder(args);
...
```
`CreateBuilder` fournit la configuration par défaut de lapplication dans lordre suivant :
* `ChainedConfigurationProvider` : ajoute un `IConfiguration` existant en tant que source. Dans le cas de configuration par défaut, ajoute la configuration d'hôte et la définit en tant que première source de la configuration de l'application.
* `appsettings.json` utilisant le provider par défaut JSON.
* appsettings.`Environment`.json tilisant le provider par défaut JSON. Par exemple, appsettings.**Production**.json et appsettings.**Development**.json.
* Secrets dapplication lorsque lapplication sexécute dans l' environnement `Development`.
* Variables denvironnement à laide du fournisseur de configuration des variables denvironnement.
* Arguments de ligne de commande à laide du fournisseur de configuration de ligne de commande.
Les fournisseurs de configuration ajoutés ultérieurement remplacent les paramètres de clé précédents.
Par exemple, si `MyKey` est défini à la fois dans `appsettings.json` et dans lenvironnement, la valeur denvironnement est utilisée.
À laide des fournisseurs de configuration par défaut, le fournisseur de configuration de ligne de commande remplace tous les autres fournisseurs.
## appsettings.json
Prenons le `appsettings.json` fichier suivant :
```json title="appsettings.json"
{
"Position": {
"Title": "Editor",
"Name": "Joe Smith"
},
"MyKey": "My appsettings.json Value",
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
```
Le code suivant affiche plusieurs des paramètres de configuration précédents :
```html title="Config.razor"
@page "/config"
<h3>Config</h3>
<div>
<div>MyKey: @Configuration["MyKey"]</div>
<div>Position:Title: @Configuration["Position:Title"]</div>
<div>Position:Name: @Configuration["Position:Name"]</div>
<div>Logging:LogLevel:Default: @Configuration["Logging:LogLevel:Default"]</div>
</div>
```
```csharp title="Config.razor.cs"
using Microsoft.Extensions.Configuration;
public partial class Config
{
[Inject]
public IConfiguration Configuration { get; set; }
}
```
La configuration par défaut `JsonConfigurationProvider` charge dans lordre suivant :
* appsettings.json
* appsettings.`Environment`.json : par exemple, appsettings.**Production**.json et appsettings.**Development**.json. La version de lenvironnement du fichier est chargée à partir de `IHostingEnvironment.EnvironmentName`.
Les valeurs de appsettings.`Environment`.json remplacent les clés dans appsettings.json. Par exemple, par défaut :
* En développement, le fichier appsettings.**Development**.json remplace les valeurs trouvées dans appsettings.json.
* En production, le fichier appsettings.**Production**.json remplace les valeurs trouvées dans appsettings.json. Par exemple, lors du déploiement de lapplication sur Azure.
Si une valeur de configuration doit être garantie, utiliser `GetValue`. Lexemple précédent lit uniquement des chaînes et ne prend pas en charge de valeur par défaut.
À laide de la configuration par défaut de appsettings.json et de appsettings.`Environment`.json. les fichiers sont activés avec `reloadOnChange: true`.
Les modifications apportées aux fichiers appsettings.json et appsettings.`Environment`.json après le démarrage de lapplication est lu par le fournisseur de configuration JSON.
## Lier des données de configuration hiérarchiques à laide du modèle options
La méthode recommandée pour lire les valeurs de configuration associées utilise le modèle doptions. Par exemple, pour lire les valeurs de configuration suivantes :
```json title="appsettings.json"
"Position": {
"Title": "Editor",
"Name": "Joe Smith"
}
```
Créez la classe suivante `PositionOptions` :
```csharp title="PositionOptions.cs"
public class PositionOptions
{
public const string Position = "Position";
public string Title { get; set; }
public string Name { get; set; }
}
```
Une classe doptions :
* Doit être non abstract avec un constructeur sans paramètre public.
* Toutes les propriétés publiques en lecture/écriture du type sont liées.
* Les champs ne sont pas liés. Dans le code précédent, `Position` nest pas lié. La propriété `Position` est utilisée afin que la chaîne "Position" ne doive pas être codée en dur dans lapplication lors de la liaison de la classe à un fournisseur de configuration.
Le code suivant :
* Appelle `ConfigurationBinder.Bind` pour lier la classe `PositionOptions` à la section `Position`.
* Affiche les données de configuration `Position`.
```html title="Config.razor"
@page "/config"
<h3>Config</h3>
@if (positionOptions != null)
{
<div>
<div>Title: @positionOptions.Title</div>
<div>Name: @positionOptions.Name</div>
</div>
}
```
```csharp title="Config.razor.cs"
private PositionOptions positionOptions;
public partial class Config
{
[Inject]
public IConfiguration Configuration { get; set; }
private PositionOptions positionOptions;
protected override void OnInitialized()
{
base.OnInitialized();
positionOptions = new PositionOptions();
Configuration.GetSection(PositionOptions.Position).Bind(positionOptions);
}
}
```
Dans le code précédent, par défaut, les modifications apportées au fichier de configuration JSON après le démarrage de lapplication sont lues.
`ConfigurationBinder.Get<T>` lie et retourne le type spécifié. `ConfigurationBinder.Get<T>` peut être plus pratique que lutilisation de `ConfigurationBinder.Bind`.
Le code suivant montre comment utiliser `ConfigurationBinder.Get<T>` avec la classe `PositionOptions` :
```csharp title="Config.razor.cs"
public partial class Config
{
[Inject]
public IConfiguration Configuration { get; set; }
private PositionOptions positionOptions;
protected override void OnInitialized()
{
base.OnInitialized();
positionOptions = Configuration.GetSection(PositionOptions.Position).Get<PositionOptions>();
}
}
```
Une autre approche de lutilisation du modèle options consiste à lier la section et à lajouter au conteneur du service dinjection de dépendances.
Dans le code suivant, `PositionOptions` est ajouté au conteneur de services avec `Configure` et lié à la configuration :
```csharp title="Program.cs"
...
builder.Services.Configure<PositionOptions>(option =>
{
var positionOptions = builder.Configuration.GetSection(PositionOptions.Position).Get<PositionOptions>();
option.Name = positionOptions.Name;
option.Title = positionOptions.Title;
});
...
```
À laide du code précédent, le code suivant lit les options de position :
```csharp title="Config.razor.cs"
public partial class Config
{
[Inject]
public IConfiguration Configuration { get; set; }
[Inject]
public IOptions<PositionOptions> OptionsPositionOptions { get; set; }
private PositionOptions positionOptions;
protected override void OnInitialized()
{
base.OnInitialized();
positionOptions = OptionsPositionOptions.Value;
}
}
```
## Sécurité et secrets de lutilisateur
Instructions relatives aux données de configuration :
* Ne stockez jamais des mots de passe ou dautres données sensibles dans le code du fournisseur de configuration ou dans les fichiers de configuration en texte clair. Loutil Gestionnaire de secret peut être utilisé pour stocker les secrets en développement.
* Nutilisez aucun secret de production dans les environnements de développement ou de test.
* Spécifiez les secrets en dehors du projet afin quils ne puissent pas être validés par inadvertance dans un référentiel de code source.
Par défaut, la source de configuration des secrets de lutilisateur est inscrite après les sources de configuration JSON.
Par conséquent, les clés de secrets dutilisateur sont prioritaires sur les clés dans appsettings.json et appsettings.`Environment`.json.
## Variables d'environnement
À laide de la configuration par défaut, le `EnvironmentVariablesConfigurationProvider` charge la configuration à partir des paires clé-valeur de variable denvironnement après la lecture du ficher `appsettings.json`, appsettings.`Environment`.json et les secrets de lutilisateur.
Par conséquent, les valeurs de clés lues à partir de lenvironnement remplacent les valeurs lues à partir de `appsettings.json` appsettings.`Environment`.json et les secrets de lutilisateur.
Le séparateur `:` ne fonctionne pas avec les clés hiérarchiques de variable denvironnement sur toutes les plateformes. `__`, le double trait de soulignement, est :
* Pris en charge par toutes les plateformes. Par exemple, le séparateur `:` nest pas pris en charge par `Bash`, mais `__` l'est.
* Automatiquement remplacé par un `:`
Les commandes `set` suivantes :
* Définissez les clés denvironnement et les valeurs de l' exemple précédent sur Windows.
* Testez les paramètres lors de lutilisation. La dotnet run commande doit être exécutée dans le répertoire du projet.
```shell
set MyKey="My key from Environment"
set Position__Title=Environment_Editor
set Position__Name=Environment_Rick
dotnet run
```
Paramètres denvironnement précédents :
* Sont uniquement définies dans les processus lancés à partir de la fenêtre de commande dans laquelle ils ont été définis.
* Ne seront pas lues par les navigateurs lancés avec Visual Studio.
Les commandes `setx` suivantes peuvent être utilisées pour définir les clés et les valeurs denvironnement sur Windows.
Contrairement à `set`, les paramètres `setx` sont conservés. `/M` définit la variable dans lenvironnement système.
Si le commutateur `/M` nest pas utilisé, une variable denvironnement utilisateur est définie.
```shell
setx MyKey "My key from setx Environment" /M
setx Position__Title Environment_Editor /M
setx Position__Name Environment_Rick /M
```
Pour vérifier que les commandes précédentes remplacent `appsettings.json` et appsettings.`Environment`.json:
* avec Visual Studio : quittez et redémarrez Visual Studio.
* Avec linterface CLI : démarrez une nouvelle fenêtre de commande et entrez `dotnet run`.
Un des grand intéret des variables d'environnement et l'utilisation de celle-ci avec Docker.

@ -0,0 +1,24 @@
---
sidebar_position: 4
title: Création du projet
---
## Créer un nouveau site Blazor
Ouvrer Visual Studio et sélectionner `Create a new Project`
![Créer un nouveau site Blazor](/img/creation-projet/creation-projet-01.png)
Rechercher dans la liste des templates `Blazor Server`, sélectionner le type de projet et cliquer sur `Next`.
![Créer un nouveau site Blazor](/img/creation-projet/creation-projet-02.png)
Renseigner les informations ainsi que l'emplacement de votre projet et cliquer sur `Next`.
![Créer un nouveau site Blazor](/img/creation-projet/creation-projet-03.png)
Laisser les options par défaut et cliquer sur `Create`.
![Créer un nouveau site Blazor](/img/creation-projet/creation-projet-04.png)
Bravo, votre site est désormais disponible.

@ -0,0 +1,37 @@
---
sidebar_position: 3
title: Ajouter l'action Supprimer
---
## Ajouter l'action Supprimer
Afin de supprimer un élément nous allons donc modifier la grille afin d'ajouter une action permettant la supression de notre élement.
Pour cela nous allons modifier la colonne contenant déjà notre lien de navigation vers la modification.
Ouvrer le fichier `Pages/List.razor` et ajouter les modification en surbrillance commme suite :
```cshtml title="Pages/List.razor"
...
<DataGridColumn TItem="Item" Field="@nameof(Item.CreatedDate)" Caption="Created date" DisplayFormat="{0:d}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo("fr-FR")" />
<DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="Action">
<DisplayTemplate>
<a href="Edit/@(context.Id)" class="btn btn-primary"><i class="fa fa-edit"></i> Editer</a>
// highlight-next-line
<button type="button" class="btn btn-primary" @onclick="() => OnDelete(context.Id)"><i class="fa fa-trash"></i> Supprimer</button>
</DisplayTemplate>
</DataGridColumn>
</DataGrid>
```
Dans le code de notre page `Pages/List.razor.cs` ajouter la méthode :
```csharp title="Pages/List.razor.cs"
...
private void OnDelete(int id)
{
}
```

@ -0,0 +1,58 @@
---
sidebar_position: 4
title: Installer Blazored.Modal
---
## Utilisation de Blazored Modal
Ajouter le nuget `Blazored.Modal`.
### Ajouter le service
```csharp title="Program.cs"
// highlight-next-line
using Blazored.Modal;
public static async Task Main(string[] args)
{
...
// highlight-next-line
builder.Services.AddBlazoredModal();
...
}
```
### Ajouter les Imports
```cshtml title="_Imports.razor"
@using Blazored.Modal
@using Blazored.Modal.Services
```
### Ajouter le composant CascadingBlazoredModal autour du composant Router existant
```cshtml title="App.razor"
// highlight-next-line
<CascadingBlazoredModal>
<Router AppAssembly="typeof(Program).Assembly">
...
</Router>
// highlight-next-line
</CascadingBlazoredModal>
```
### Ajouter les références Javascript & CSS
```cshtml title="Pages/_Layout.cshtml"
<link href="_content/Blazored.Modal/blazored-modal.css" rel="stylesheet" />
```
```html
<script src="_content/Blazored.Modal/blazored.modal.js"></script>
```
## Documentation
Lien vers la documentation [Blazored.Modal](https://github.com/Blazored/Modal).

@ -0,0 +1,358 @@
---
sidebar_position: 5
title: Création d'une popup de confirmation
---
## Création d'une popup de confirmation
Afin de créer notre popup, créer le dossier `Modals` à la racine du site.
Créer le composant razor `DeleteConfirmation.razor` et sa classe `DeleteConfirmation.razor.cs`.
```cshtml title="Modals/DeleteConfirmation.razor"
<div class="simple-form">
<p>
Are you sure you want to delete @item.DisplayName ?
</p>
<button @onclick="ConfirmDelete" class="btn btn-primary">Delete</button>
<button @onclick="Cancel" class="btn btn-secondary">Cancel</button>
</div>
```
```csharp title="Modals/DeleteConfirmation.razor.cs"
public partial class DeleteConfirmation
{
[CascadingParameter]
public BlazoredModalInstance ModalInstance { get; set; }
[Inject]
public IDataService DataService { get; set; }
[Parameter]
public int Id { get; set; }
private Item item = new Item();
protected override async Task OnInitializedAsync()
{
// Get the item
item = await DataService.GetById(Id);
}
void ConfirmDelete()
{
ModalInstance.CloseAsync(ModalResult.Ok(true));
}
void Cancel()
{
ModalInstance.CancelAsync();
}
}
```
Ouvrer le fichier `Pages/List.razor.cs` et ajouter les modifications :
```csharp title="Pages/List.razor.cs"
...
[Inject]
public NavigationManager NavigationManager { get; set; }
[CascadingParameter]
public IModalService Modal { get; set; }
...
private async void OnDelete(int id)
{
var parameters = new ModalParameters();
parameters.Add(nameof(Item.Id), id);
var modal = Modal.Show<DeleteConfirmation>("Delete Confirmation", parameters);
var result = await modal.Result;
if (result.Cancelled)
{
return;
}
await DataService.Delete(id);
// Reload the page
NavigationManager.NavigateTo("list", true);
}
```
## Notion : Paramètres en cascade
### Composant `CascadingValue`
Un composant ancêtre fournit une valeur en cascade à laide du composant Blazor de linfrastructure `CascadingValue`, qui encapsule un sous-arbre dune hiérarchie de composants et fournit une valeur unique à tous les composants de sa sous-arborescence.
Lexemple suivant montre le déroulement des informations de thème dans la hiérarchie des composants dun composant de disposition pour fournir une classe de style CSS aux boutons des composants enfants.
La classe C# `ThemeInfo` suivante est placée dans un dossier nommé `UIThemeClasses` et spécifie les informations de thème.
:::info
Pour les exemples de cette section, lespace de noms de lapplication est BlazorSample . Quand vous expérimentez le code dans votre propre exemple dapplication, remplacez lespace de noms de lapplication par lespace de noms de votre application dexemple.
:::
```csharp title="UIThemeClasses/ThemeInfo.cs"
namespace BlazorSample.UIThemeClasses
{
public class ThemeInfo
{
public string? ButtonClass { get; set; }
}
}
```
Le layout suivant spécifie les informations de thème ( `ThemeInfo` ) comme valeur en cascade pour tous les composants qui composent le corps du layout de la propriété `Body`.
La valeur `ButtonClass` est affectée à `btn-success`, qui est un style de bouton de démarrage. Tout composant descendant dans la hiérarchie des composants peut utiliser la propriété `ButtonClass` à travers la valeur `ThemeInfo` en cascade.
```cshtml title="Shared/MainLayout.razor"
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<CascadingValue Value="theme">
<div class="content px-4">
@Body
</div>
</CascadingValue>
</div>
</div>
@code {
private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
```
### Attribut `[CascadingParameter]`
Pour utiliser des valeurs en cascade, les composants descendants déclarent des paramètres en cascade à laide de l'attribut `[CascadingParameter]`.
Les valeurs en cascade sont liées aux paramètres en cascade par type.
Le composant suivant lie la valeur `ThemeInfo` en cascade à un paramètre en cascade, en utilisant éventuellement le même nom `ThemeInfo`. Le paramètre est utilisé pour définir la classe CSS pour le bouton `Increment Counter (Themed)`.
```cshtml title="Pages/ThemedCounter.razor"
@page "/themed-counter"
@using BlazorSample.UIThemeClasses
<h1>Themed Counter</h1>
<p>Current count: @currentCount</p>
<p>
<button class="btn" @onclick="IncrementCount">
Increment Counter (Unthemed)
</button>
</p>
<p>
<button
class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)"
@onclick="IncrementCount">
Increment Counter (Themed)
</button>
</p>
@code {
private int currentCount = 0;
[CascadingParameter]
protected ThemeInfo? ThemeInfo { get; set; }
private void IncrementCount()
{
currentCount++;
}
}
```
### Plusieurs valeurs en cascade
Pour mettre en cascade plusieurs valeurs du même type dans la même sous-arborescence, fournissez une chaîne unique `Name` à chaque composant `CascadingValue` et à ses attributs `[CascadingParameter]` correspondants.
Dans lexemple suivant, deux composants `CascadingValue` montent en cascade différentes instances de `CascadingType` :
```cshtml
<CascadingValue Value="@parentCascadeParameter1" Name="CascadeParam1">
<CascadingValue Value="@ParentCascadeParameter2" Name="CascadeParam2">
...
</CascadingValue>
</CascadingValue>
@code {
private CascadingType parentCascadeParameter1;
[Parameter]
public CascadingType ParentCascadeParameter2 { get; set; }
...
}
```
Dans un composant descendant, les paramètres en cascade reçoivent leurs valeurs en cascade du composant ancêtre avec l'attribut `Name` :
```cshtml
...
@code {
[CascadingParameter(Name = "CascadeParam1")]
protected CascadingType ChildCascadeParameter1 { get; set; }
[CascadingParameter(Name = "CascadeParam2")]
protected CascadingType ChildCascadeParameter2 { get; set; }
}
```
### Transmettre des données dans une hiérarchie de composants
Les paramètres en cascade permettent également aux composants de transmettre des données à travers une hiérarchie de composants.
Prenons lexemple de jeu donglets de linterface utilisateur suivant, où un composant de jeu donglets gère une série donglets individuels.
Créez une interface `ITab` que les onglets implémentent dans un dossier nommé `UIInterfaces`.
```csharp title="UIInterfaces/ITab.cs"
using Microsoft.AspNetCore.Components;
namespace BlazorSample.UIInterfaces
{
public interface ITab
{
RenderFragment ChildContent { get; }
}
}
```
Le composant `TabSet` suivant gère un ensemble donglets. Les composants du jeu donglets `Tab`, qui sont créés plus loin dans cette section, fournissent les éléments de liste ( `<li>...</li>` ) de la liste ( `<ul>...</ul>` ).
Les composants enfants `Tab` ne sont pas explicitement passés comme paramètres à `TabSet`.
Au lieu de cela, les composants enfants `Tab` font partie du contenu enfant de `TabSet`.
Toutefois, le `TabSet` nécessite toujours une référence `Tab` à chaque composant pour pouvoir afficher les en-têtes et longlet actif.
Pour activer cette coordination sans nécessiter de code supplémentaire, le composant `TabSet` peut se présenter comme une valeur en cascade qui est ensuite récupérée par les composants `Tab` descendants.
```cshtml title="Shared/TabSet.razor"
@using BlazorSample.UIInterfaces
<!-- Display the tab headers -->
<CascadingValue Value=this>
<ul class="nav nav-tabs">
@ChildContent
</ul>
</CascadingValue>
<!-- Display body for only the active tab -->
<div class="nav-tabs-body p-4">
@ActiveTab?.ChildContent
</div>
@code {
[Parameter]
public RenderFragment ChildContent { get; set; }
public ITab ActiveTab { get; private set; }
public void AddTab(ITab tab)
{
if (ActiveTab == null)
{
SetActiveTab(tab);
}
}
public void SetActiveTab(ITab tab)
{
if (ActiveTab != tab)
{
ActiveTab = tab;
StateHasChanged();
}
}
}
```
Les composants `Tab` descendants capturent le conteneur `TabSet` sous la forme dun paramètre en cascade. Les composants `Tab` sajoutent à la `TabSet` coordonnée et pour définir longlet actif.
```cshtml title="Shared/Tab.razor"
@using BlazorSample.UIInterfaces
@implements ITab
<li>
<a @onclick="ActivateTab" class="nav-link @TitleCssClass" role="button">
@Title
</a>
</li>
@code {
[CascadingParameter]
public TabSet ContainerTabSet { get; set; }
[Parameter]
public string Title { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
private string TitleCssClass =>
ContainerTabSet.ActiveTab == this ? "active" : null;
protected override void OnInitialized()
{
ContainerTabSet.AddTab(this);
}
private void ActivateTab()
{
ContainerTabSet.SetActiveTab(this);
}
}
```
Le composant `ExampleTabSet` suivant utilise le composant `TabSet`, qui contient trois composants `Tab`.
```cshtml title="Pages/ExampleTabSet.razor"
@page "/example-tab-set"
<TabSet>
<Tab Title="First tab">
<h4>Greetings from the first tab!</h4>
<label>
<input type="checkbox" @bind="showThirdTab" />
Toggle third tab
</label>
</Tab>
<Tab Title="Second tab">
<h4>Hello from the second tab!</h4>
</Tab>
@if (showThirdTab)
{
<Tab Title="Third tab">
<h4>Welcome to the disappearing third tab!</h4>
<p>Toggle this tab from the first tab.</p>
</Tab>
}
</TabSet>
@code {
private bool showThirdTab;
}
```

@ -0,0 +1,61 @@
---
sidebar_position: 2
title: Modification du service de données
---
Afin de prendre en compte la supression d'un élement, nous allons premier ajouter cette fonctionnalité à notre service de données.
## Ajouter la méthode à l'interface
Ouvrer le fichier `Services/IDataService.cs` et ajouter l'élement en surbrillance :
```csharp title="Services/IDataService.cs"
public interface IDataService
{
Task Add(ItemModel model);
Task<int> Count();
Task<List<Item>> List(int currentPage, int pageSize);
Task<Item> GetById(int id);
Task Update(int id, ItemModel model);
// highlight-start
Task Delete(int id);
// highlight-end
}
```
## Ajouter la méthode à l'implémentation
Ouvrer le fichier `Services/DataLocalService.cs` et ajouter la méthode suivante :
```csharp title="Services/DataLocalService.cs"
...
public async Task Delete(int id)
{
// Get the current data
var currentData = await _localStorage.GetItemAsync<List<Item>>("data");
// Get the item int the list
var item = currentData.FirstOrDefault(w => w.Id == id);
// Delete item in
currentData.Remove(item);
// Delete the image
var imagePathInfo = new DirectoryInfo($"{_webHostEnvironment.WebRootPath}/images");
var fileName = new FileInfo($"{imagePathInfo}/{item.Name}.png");
if (fileName.Exists)
{
File.Delete(fileName.FullName);
}
// Save the data
await _localStorage.SetItemAsync("data", currentData);
}
```

@ -0,0 +1,19 @@
---
sidebar_position: 1
title: Introduction
---
## Supprimer un item
Ce TP va vous permettre de supprimer un item.
### Liste des étapes
* Modification du service de données
* Ajouter l'action Supprimer
* Installer Blazored.Modal
* Création d'une popup de confirmation
### Liste des notions
* Paramètres en cascade

@ -0,0 +1,215 @@
---
sidebar_position: 2
sidebar_label: Créer fichier CI
title: Créer un fichier CI
---
Le CI de votre projet utilise Drone.
## Créer le fichier CI initial
A la racine de votre projet créez un nouveau fichier vide avec le nom `.drone.yml`.
Remplissez le fichier avec les données initiales d'un fichier CI :
```yml
kind: pipeline
type: docker
name: default
trigger:
event:
- push
steps:
```
Ce code permet :
* Définir le type de pipeline et son nom.
* Lancer le CI lorsqu'un push est validé sur le référentiel.
* Sous `steps`, le fichier yaml contient la liste des travaux.
### Ajouter la tâche de build
En premier pour un CI est la construction de votre projet, ajoutez le code ci-dessous dans le fichier CI.
```yml
- name: build
image: mcr.microsoft.com/dotnet/sdk:6.0
commands:
- cd Sources/
- dotnet restore MySolution.sln
- dotnet build MySolution.sln -c Release --no-restore
```
### Ajouter la tâche de test
La deuxième étape consiste à tester votre projet.
```yml
- name: tests
image: mcr.microsoft.com/dotnet/sdk:6.0
commands:
- cd Sources/
- dotnet restore MySolution.sln
- dotnet test MySolution.sln --no-restore
depends_on: [build]
```
### Ajouter la tâche d'analyse
Un CI est également présent pour analyser votre code !
```yml
- name: code-analysis
image: hub.codefirst.iut.uca.fr/thomas.bellembois/codefirst-dronesonarplugin-dotnet6
commands:
- cd Sources/
- dotnet restore MySolution.sln
- dotnet sonarscanner begin /k:$REPO_NAME /d:sonar.host.url=$$$${PLUGIN_SONAR_HOST} /d:sonar.coverageReportPaths="coveragereport/SonarQube.xml" /d:sonar.coverage.exclusions="Tests/**" /d:sonar.login=$$$${PLUGIN_SONAR_TOKEN}
- dotnet build MySolution.sln -c Release --no-restore
- dotnet test MySolution.sln --logger trx --no-restore /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura --collect "XPlat Code Coverage"
- reportgenerator -reports:"**/coverage.cobertura.xml" -reporttypes:SonarQube -targetdir:"coveragereport"
- dotnet publish MySolution.sln -c Release --no-restore -o $CI_PROJECT_DIR/build/release
- dotnet sonarscanner end /d:sonar.login=$$$${PLUGIN_SONAR_TOKEN}
secrets: [ SECRET_SONAR_LOGIN ]
settings:
# accessible en ligne de commande par $${PLUGIN_SONAR_HOST}
sonar_host: https://codefirst.iut.uca.fr/sonar/
# accessible en ligne de commande par $${PLUGIN_SONAR_TOKEN}
sonar_token:
from_secret: SECRET_SONAR_LOGIN
depends_on: [tests]
```
### Ajouter le travail de documentation
Votre projet contient également une documentation, créez un job pour la générer.
```yml
- name: generate-and-deploy-docs
image: hub.codefirst.iut.uca.fr/thomas.bellembois/codefirst-docdeployer
failure: ignore
volumes:
- name: docs
path: /docs
commands:
- /entrypoint.sh
when:
branch:
- master
depends_on: [ build ]
# The volumes declaration appear at the end of the file, after all steps
volumes:
- name: docs
temp: {}
```
## Ajouter la tâche de CD
Pour le CD, nous déployons une image docker de votre projet, ajoutons le travail pour construire et déployons votre image docker dans un registre.
```yaml
- name: docker-build
image: plugins/docker
settings:
dockerfile: Sources/Dockerfile
context: .
registry: hub.codefirst.iut.uca.fr
repo: hub.codefirst.iut.uca.fr/my-group/my-application-client
username:
from_secret: SECRET_REGISTRY_USERNAME
password:
from_secret: SECRET_REGISTRY_PASSWORD
when:
branch:
- master
```
## Exemple de fichier complet
Découvrez ici un dossier complet avec tous les tâches.
```yaml
kind: pipeline
type: docker
name: default
trigger:
event:
- push
steps:
- name: build
image: mcr.microsoft.com/dotnet/sdk:6.0
commands:
- cd Sources/
- dotnet restore MySolution.sln
- dotnet build MySolution.sln -c Release --no-restore
- name: tests
image: mcr.microsoft.com/dotnet/sdk:6.0
commands:
- cd Sources/
- dotnet restore MySolution.sln
- dotnet test MySolution.sln --no-restore
depends_on: [build]
- name: code-analysis
image: hub.codefirst.iut.uca.fr/thomas.bellembois/codefirst-dronesonarplugin-dotnet6
commands:
- cd Sources/
- dotnet restore MySolution.sln
- dotnet sonarscanner begin /k:$REPO_NAME /d:sonar.host.url=$$$${PLUGIN_SONAR_HOST} /d:sonar.coverageReportPaths="coveragereport/SonarQube.xml" /d:sonar.coverage.exclusions="Tests/**" /d:sonar.login=$$$${PLUGIN_SONAR_TOKEN}
- dotnet build MySolution.sln -c Release --no-restore
- dotnet test MySolution.sln --logger trx --no-restore /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura --collect "XPlat Code Coverage"
- reportgenerator -reports:"**/coverage.cobertura.xml" -reporttypes:SonarQube -targetdir:"coveragereport"
- dotnet publish MySolution.sln -c Release --no-restore -o $CI_PROJECT_DIR/build/release
- dotnet sonarscanner end /d:sonar.login=$$$${PLUGIN_SONAR_TOKEN}
secrets: [ SECRET_SONAR_LOGIN ]
settings:
# accessible en ligne de commande par $${PLUGIN_SONAR_HOST}
sonar_host: https://codefirst.iut.uca.fr/sonar/
# accessible en ligne de commande par $${PLUGIN_SONAR_TOKEN}
sonar_token:
from_secret: SECRET_SONAR_LOGIN
depends_on: [tests]
- name: generate-and-deploy-docs
image: hub.codefirst.iut.uca.fr/thomas.bellembois/codefirst-docdeployer
failure: ignore
volumes:
- name: docs
path: /docs
commands:
- /entrypoint.sh
when:
branch:
- master
depends_on: [ build ]
- name: docker-build
image: plugins/docker
settings:
dockerfile: Sources/Dockerfile
context: .
registry: hub.codefirst.iut.uca.fr
repo: hub.codefirst.iut.uca.fr/my-group/my-application-client
username:
from_secret: SECRET_REGISTRY_USERNAME
password:
from_secret: SECRET_REGISTRY_PASSWORD
when:
branch:
- master
volumes:
- name: docs
temp: {}
```

@ -0,0 +1,55 @@
---
sidebar_position: 1
sidebar_label: Générer fichier Docker
title: Générer un fichier Docker
---
## Comment générer mon fichier docker
Il est possible de générer un `Dockerfile` automatiquement avec Visual Studio.
Sur votre projet, cliquez droit et sélectionnez `Add` => `Docker Support...` :
![Prise en charge de Docker](/img/deploy/docker-support-visual-studio.png)
Sélectionnez la cible "Linux" :
![OS cible](/img/deploy/docker-file-options.png)
Un nouveau fichier `Dockerfile` est créé dans votre projet.
## La configuration du nuget
Si vous utilisez des référentiels spécifiques, vous devez utiliser un fichier `nuget.config`.
N'oubliez pas d'ajouter votre `nuget.config` à votre image de build, juste après `WORKDIR /src` ajouter `COPY ["nuget.config", "."]`
:::caution
Si vous n'ajoutez pas le fichier, restaurer le nuget ne fonctionne pas, par défaut restaurer la recherche sur nuget.org
:::
## Exemple de fichier Dockerfile
```txt
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
COPY ["nuget.config", "."]
COPY ["MyBeautifulApp/MyBeautifulApp.csproj", "MyBeautifulApp/"]
RUN dotnet restore "MyBeautifulApp/MyBeautifulApp.csproj"
COPY . .
WORKDIR "/src/MyBeautifulApp"
RUN dotnet build "MyBeautifulApp.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "MyBeautifulApp.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyBeautifulApp.dll"]
```

@ -0,0 +1,251 @@
---
sidebar_position: 3
title: Création d'un service de données
---
Comme nous avons ajouter précedemment l'ajout d'un item, nous avons maintenant deux endroits dans le code dans notre code ou nous accédons à notre storage local.
Afin de simplifier la gestion de notre code nous allons donc utiliser l'IOC & DI.
Dans notre cadre, nous allons utiliser l'IOC native de AspNetCore, nous allons utiliser la DI pour gérer nos données.
Nous avons déjà utilisé l'IOC d'AspNetCore avec les propriétés utilisant l'attribut `[Inject]`, ceux-ci étant gérés par le système ou des librairies externe.
## Création de l'interface du service de données
Nous allons donc créer un répertoire `Services` à la racine de notre site et créer notre interface.
```csharp title="Services/IDataService.cs"
public interface IDataService
{
Task Add(ItemModel model);
Task<int> Count();
Task<List<Item>> List(int currentPage, int pageSize);
}
```
Notre interface contient les méthodes pour gérer nos données.
## Création de l'implémentation du service de données
Nous allons désormais créer notre classe pour gérer nos données en local.
```csharp title="Services/DataLocalService.cs"
public class DataLocalService : IDataService
{
private readonly HttpClient _http;
private readonly ILocalStorageService _localStorage;
private readonly NavigationManager _navigationManager;
private readonly IWebHostEnvironment _webHostEnvironment;
public DataLocalService(
ILocalStorageService localStorage,
HttpClient http,
IWebHostEnvironment webHostEnvironment,
NavigationManager navigationManager)
{
_localStorage = localStorage;
_http = http;
_webHostEnvironment = webHostEnvironment;
_navigationManager = navigationManager;
}
public async Task Add(ItemModel model)
{
// Get the current data
var currentData = await _localStorage.GetItemAsync<List<Item>>("data");
// Simulate the Id
model.Id = currentData.Max(s => s.Id) + 1;
// Add the item to the current data
currentData.Add(new Item
{
Id = model.Id,
DisplayName = model.DisplayName,
Name = model.Name,
RepairWith = model.RepairWith,
EnchantCategories = model.EnchantCategories,
MaxDurability = model.MaxDurability,
StackSize = model.StackSize,
CreatedDate = DateTime.Now
});
// Save the image
var imagePathInfo = new DirectoryInfo($"{_webHostEnvironment.WebRootPath}/images");
// Check if the folder "images" exist
if (!imagePathInfo.Exists)
{
imagePathInfo.Create();
}
// Determine the image name
var fileName = new FileInfo($"{imagePathInfo}/{model.Name}.png");
// Write the file content
await File.WriteAllBytesAsync(fileName.FullName, model.ImageContent);
// Save the data
await _localStorage.SetItemAsync("data", currentData);
}
public async Task<int> Count()
{
return (await _localStorage.GetItemAsync<Item[]>("data")).Length;
}
public async Task<List<Item>> List(int currentPage, int pageSize)
{
// Load data from the local storage
var currentData = await _localStorage.GetItemAsync<Item[]>("data");
// Check if data exist in the local storage
if (currentData == null)
{
// this code add in the local storage the fake data
var originalData = await _http.GetFromJsonAsync<Item[]>($"{_navigationManager.BaseUri}fake-data.json");
await _localStorage.SetItemAsync("data", originalData);
}
return (await _localStorage.GetItemAsync<Item[]>("data")).Skip((currentPage - 1) * pageSize).Take(pageSize).ToList();
}
}
```
Vous remarquerez que nous utilisons une autre façon d'injecter des dépendances dans cette classe en passant par le constructeur.
`ILocalStorageService` & `HttpClient` & `IWebHostEnvironment` & `NavigationManager` sont automatiquement résolue par l'IOC.
## Modification des pages
Modifions nos pages pour prendre en compte notre nouveau service :
```csharp title="Pages/List.razor.cs"
public partial class List
{
private List<Item> items;
private int totalItem;
[Inject]
public IDataService DataService { get; set; }
[Inject]
public IWebHostEnvironment WebHostEnvironment { get; set; }
private async Task OnReadData(DataGridReadDataEventArgs<Item> e)
{
if (e.CancellationToken.IsCancellationRequested)
{
return;
}
if (!e.CancellationToken.IsCancellationRequested)
{
items = await DataService.List(e.Page, e.PageSize);
totalItem = await DataService.Count();
}
}
}
```
```csharp title="Pages/Add.razor.cs"
public partial class Add
{
/// <summary>
/// The default enchant categories.
/// </summary>
private List<string> enchantCategories = new List<string>() { "armor", "armor_head", "armor_chest", "weapon", "digger", "breakable", "vanishable" };
/// <summary>
/// The current item model
/// </summary>
private ItemModel itemModel = new()
{
EnchantCategories = new List<string>(),
RepairWith = new List<string>()
};
/// <summary>
/// The default repair with.
/// </summary>
private List<string> repairWith = new List<string>() { "oak_planks", "spruce_planks", "birch_planks", "jungle_planks", "acacia_planks", "dark_oak_planks", "crimson_planks", "warped_planks" };
[Inject]
public IDataService DataService { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
private async void HandleValidSubmit()
{
await DataService.Add(itemModel);
NavigationManager.NavigateTo("list");
}
private async Task LoadImage(InputFileChangeEventArgs e)
{
// Set the content of the image to the model
using (var memoryStream = new MemoryStream())
{
await e.File.OpenReadStream().CopyToAsync(memoryStream);
itemModel.ImageContent = memoryStream.ToArray();
}
}
private void OnEnchantCategoriesChange(string item, object checkedValue)
{
if ((bool)checkedValue)
{
if (!itemModel.EnchantCategories.Contains(item))
{
itemModel.EnchantCategories.Add(item);
}
return;
}
if (itemModel.EnchantCategories.Contains(item))
{
itemModel.EnchantCategories.Remove(item);
}
}
private void OnRepairWithChange(string item, object checkedValue)
{
if ((bool)checkedValue)
{
if (!itemModel.RepairWith.Contains(item))
{
itemModel.RepairWith.Add(item);
}
return;
}
if (itemModel.RepairWith.Contains(item))
{
itemModel.RepairWith.Remove(item);
}
}
}
```
## Définition du service de données
Désormais il faut que nous définitions dans l'IOC de notre application la résolution de notre interface / classe.
Ouvrez le fichier `Program.cs` et ajouter la ligne suivante :
```csharp title="Program.cs"
...
builder.Services.AddScoped<IDataService, DataLocalService>();
...
```
Plus tard nous implémenterons la gestion des données grâce à une API, il suffira simplement de créer une nouvelle classe `Services/DataApiService.cs` implémentant l'interface `IDataService` avec les appels API et de modifier l'IOC avec ce nouveau service.

@ -0,0 +1,17 @@
---
sidebar_position: 1
title: Introduction
---
## DI & IOC
Ce TP va vous permettre de comprendre l'injection de dépendances et l'inversion de contrôle.
### Liste des étapes
* Création d'un service de données
### Liste des notions
* Injection de dépendances (DI)
* Inversion de contrôle (IOC)

@ -0,0 +1,192 @@
---
sidebar_position: 2
title: DI & IOC
---
## Définition
Lorsque lon a compris les fondamentaux de la programmation orientée objet (POO), il faut rapidement comprendre et assimiler plusieurs patrons de conceptions.
En effet, cela permet de faire face à des problèmes connus et ainsi dimposer de bonnes pratiques qui sappuient sur lexpérience de concepteurs de logiciels.
Pour moi, et sûrement pour beaucoup dautres concepteurs de logiciels, lun des premiers objectifs de la conception est de garder une interface claire, facilement maintenable et surtout évolutive. Car tout développeur le sait, les technologies évoluent très très vite.
Cest pourquoi, nous allons voir linjection de dépendances (DI) et linversion de contrôle (Ioc).
### DI (Dependency Injection - injection de dépendances)
En C# il est simple dinstancier un nouvel objet via lopérateur «new ». Linstanciation dun nouvel objet dans une classe impose un couplage (cest-à-dire une connexion étroitement liée), voir même une référence à une assembly.
On pourrait sen sortir si le projet nétait pas trop complexe avec des objets simples et très peu de couplage. Mais, imaginons que vous vouliez créer un service, comment allez-vous faire pour limplémenter rapidement dans une nouvelle classe/assembly ? Et dans le pire des cas, le jour où vous voulez changer de type de service dans votre projet simple, qui au fil du temps est devenu complexe, comment allez-vous procéder ?
Vous lavez compris, ce patron est pour tout développeur qui se soucie de la qualité de son logiciel et cherche à rendre son application la plus maintenable possible.
En effet, linjection permet détablir de façon dynamique la relation entre plusieurs classes. Elle consiste à découper les responsabilités entres les différents modules, les différentes parties et facilite même la modification ultérieure des classes.
### IOC (Inversion Of Control - inversion de contrôle)
Une fois que lon a assimilé le principe de linjection de dépendances, nous pouvons nous attaquer à un autre concept de plus haut niveau : linversion de contrôle.
En effet, si lon a peu ou beaucoup de classes faisant appel à notre service, nous créerons autant de fois que nécessaire des instances `IMyService` en amont.
Nous nous retrouverons donc avec plusieurs instances, ce qui ne devait pas se produire. Aussi, le fait dinstancier notre interface à plusieurs endroits, nous nous retrouverons face à autant de duplication de code que dinstanciations, ce qui rendra notre application difficilement maintenable.
Par conséquent, la première idée qui nous vient à lesprit lorsque lon maîtrise mal linversion de dépendances est de rendre notre classe `MyService` statique.
Ce qui est une très mauvaise idée. On na plus de problème dinstances par contre, à présent, il nous faudra rendre notre classe `thread-safe` et bien gérer le `multi-threading`.
Sans parler des problèmes auxquels nous seront confrontés lorsquon voudra faire des tests unitaires.
Dautres aurons pensé au `singleton`. Il nen est rien, il y a bien souvent trop de problèmes de fuite de mémoire.
Ensuite, lorsque lon cherche dans ses fondamentaux, on pense tout naturellement à un des patrons de conception décrit par le « Gof » : la `Factory`.
En effet, une factory avec un singleton pourrait être la solution, malheureusement, il resterait toujours un couplage faible entre la classe et lappel à chaque factory.
Dans notre exemple, nous nen aurions quune, on nen voit donc pas lintérêt.
Mais, dans une application il y a largement plus de factory et il y aurait donc autant de couplages que de factory. Sans compter sur les instanciations qui peuvent être plus ou moins longues.
Or dans certaines conditions nous pourrions en voir immédiatement sans devoir attendre le temps dinstanciation.
Pour pouvoir encore plus externaliser du code, comme la création de notre objet, et ne pas ralentir notre programme sil y a des instanciations longues, nous allons devoir mapper les contrats avec leurs implémentations au démarrage de lapplication.
LIOC ce défini comme un conteneur qui détermine ce qui doit être instancié et retourné au client pour éviter que ce dernier appel explicitement le constructeur avec lopérateur « new ».
En résumé, cest un objet qui agit comme un cache pour les instances dont nous avons besoin dans diverses parties de nos applications.
## Ajouter des services à une application Blazor Server
Après avoir créé une application, examinez une partie du fichier `Program.cs` :
```csharp
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
```
La variable `builder` représente un objet `Microsoft.AspNetCore.Builder.WebApplicationBuilder` avec une propriété `IServiceCollection`, qui est une liste dobjets de descripteur de service.
Les services sont ajoutés en fournissant des descripteurs de service à la collection de services.
Lexemple suivant illustre le concept avec linterface `IDataAccess` et son implémentationconcrète `DataAccess` :
```csharp
builder.Services.AddSingleton<IDataAccess, DataAccess>();
```
## Durée de vie du service
Les services peuvent être configurés avec les durées de vie suivantes :
### `Scoped`
Les applications Blazor WebAssembly nont actuellement pas de concept détendues de DI. Les services `Scoped` inscrits se comportent comme des services `Singleton`.
Le modèle dhébergement Blazor Server prend en charge la durée de vie `Scoped` des requêtes HTTP, mais pas entre `SignalR` et les messages de connexion/circuit parmi les composants chargés sur le client.
La partie Pages Razor ou MVC de lapplication traite normalement les services délimités et recrée les services sur chaque requête HTTP lors de la navigation entre des pages ou des vues ou dune page ou dun composant.
Les services délimités ne sont pas reconstruits lors de la navigation entre les composants sur le client, où la communication vers le serveur se déroule sur la connexion `SignalR` du circuit de lutilisateur, et non via les requêtes HTTP.
Dans les scénarios de composant suivants sur le client, les services délimités sont reconstruits, car un nouveau circuit est créé pour lutilisateur :
* Lutilisateur ferme la fenêtre du navigateur. Lutilisateur ouvre une nouvelle fenêtre et retourne à lapplication.
* Lutilisateur ferme un onglet de lapplication dans une fenêtre de navigateur. Lutilisateur ouvre un nouvel onglet et retourne à lapplication.
* Lutilisateur sélectionne le bouton recharger/actualiser du navigateur.
### `Singleton`
La DI crée une instance unique du service. Tous les composants nécessitant un service `Singleton` reçoivent la même instance du service.
### `Transient`
Chaque fois quun composant obtient une instance dun service `Transient` à partir du conteneur de service, il reçoit une nouvelle instance du service.
## Demander un service dans un composant
Une fois que les services sont ajoutés à la collection de services, injectez les services dans les composants à laide de la directive Razor `@inject`, qui a deux paramètres :
* Type : type du service à injecter.
* Propriété : nom de la propriété qui reçoit le service dapplication injecté. La propriété ne nécessite pas de création manuelle. Le compilateur crée la propriété.
Utilisez plusieurs instructions `@inject` pour injecter différents services.
L'exemple suivant montre comment utiliser `@inject`.
Limplémentation du service `Services.IDataAccess` est injectée dans la propriété `DataRepository` du composant. Notez comment le code utilise uniquement labstraction `IDataAccess` :
```csharp
@page "/customer-list"
@inject IDataAccess DataRepository
@if (customers != null)
{
<ul>
@foreach (var customer in customers)
{
<li>@customer.FirstName @customer.LastName</li>
}
</ul>
}
@code {
private IReadOnlyList<Customer>? customers;
protected override async Task OnInitializedAsync()
{
customers = await DataRepository.GetAllCustomersAsync();
}
private class Customer
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
private interface IDataAccess
{
public Task<IReadOnlyList<Customer>> GetAllCustomersAsync();
}
}
```
En interne, la propriété générée (`DataRepository`) utilise lattribut `[Inject]`.
En règle générale, cet attribut nest pas utilisé directement.
Si une classe de base est requise pour les composants et les propriétés injectées sont également requises pour la classe de base, ajoutez manuellement lattribut `[Inject]` :
```csharp
using Microsoft.AspNetCore.Components;
public class ComponentBase : IComponent
{
[Inject]
protected IDataAccess DataRepository { get; set; }
...
}
```
## Utiliser linjection de dépendances dans les services
Les services complexes peuvent nécessiter des services supplémentaires.
Dans lexemple suivant, `DataAccess` nécessite le service `HttpClient` par défaut. `@inject` (ou lattribut `[Inject]`) nest pas disponible pour une utilisation dans les services.
Linjection de constructeur doit être utilisée à la place.
Les services requis sont ajoutés en ajoutant des paramètres au constructeur du service.
Lorsque lauthentification unique crée le service, elle reconnaît les services dont il a besoin dans le constructeur et les fournit en conséquence.
Dans lexemple suivant, le constructeur reçoit un `HttpClient` via DI. `HttpClient` est un service par défaut.
```csharp
using System.Net.Http;
public class DataAccess : IDataAccess
{
public DataAccess(HttpClient http)
{
...
}
}
```
Conditions préalables pour linjection de constructeur :
* Un constructeur doit exister dont les arguments peuvent tous être remplis par DI. Les paramètres supplémentaires non couverts par linjection de dépendances sont autorisés sils spécifient des valeurs par défaut.
* Le constructeur applicable doit être `public`.
* Un constructeur applicable doit exister. En cas dambiguïté, DI lève une exception.

@ -0,0 +1,24 @@
---
sidebar_position: 3
title: Ajouter l'action Editer
---
Afin d'atteindre la page d'édition nous allons donc modifier la grille afin d'ajouter un lien vers notre nouvelle page avec l'identifiant de notre élement.
Pour cela nous allons créer une nouvelle colonne `DataGridColumn` avec un `DisplayTemplate`.
Ouvrer le fichier `Pages/List.razor` et ajouter les modification en surbrillance commme suite :
```cshtml title="Pages/List.razor"
...
<DataGridColumn TItem="Item" Field="@nameof(Item.CreatedDate)" Caption="Created date" DisplayFormat="{0:d}" DisplayFormatProvider="@System.Globalization.CultureInfo.GetCultureInfo("fr-FR")" />
// highlight-start
<DataGridColumn TItem="Item" Field="@nameof(Item.Id)" Caption="Action">
<DisplayTemplate>
<a href="Edit/@(context.Id)" class="btn btn-primary"><i class="fa fa-edit"></i> Editer</a>
</DisplayTemplate>
</DataGridColumn>
// highlight-end
</DataGrid>
```

@ -0,0 +1,372 @@
---
sidebar_position: 4
title: Création page éditer
---
## Création de la page
Comme précédemment, créer une nouvelle page qui se nommera `Edit.razor` ansi que la classe partiel `Edit.razor.cs`.
## Code source final
```cshtml title="Pages/Edit.razor"
<h3>Edit</h3>
```
```csharp title="Pages/Edit.razor.cs"
public partial class Edit
{
}
```
## Définir l'url de la page
Nous allons définir l'url de notre page en spécifiant que nous souhaitons notre identifiant.
Ouvrer le fichier `Pages/Edit.razor` et ajouter les modification en surbrillance commme suite :
```cshtml title="Pages/Edit.razor"
// highlight-next-line
@page "/edit/{Id:int}"
<h3>Edit</h3>
```
## Passer un paramètre
Nous avons passer dans l'url l'id de notre item à editer, maintenant nous allons le récupérer dans un `Parameter` afin de pouvoir l'utiliser dans notre code.
Ouvrer le fichier `Pages/Edit.razor.cs` et ajouter les modification en surbrillance commme suite :
```cshtml title="Pages/Edit.razor.cs"
public partial class Edit
{
// highlight-start
[Parameter]
public int Id { get; set; }
// highlight-end
}
```
## Afficher le paramètre
Afin de verifier le passage de notre identifiant nous allons l'afficher.
Ouvrer le fichier `Pages/Edit.razor` et ajouter les modification en surbrillance commme suite :
```cshtml title="Pages/Edit.razor"
@page "/edit/{Id:int}"
<h3>Edit</h3>
// highlight-next-line
<div>Mon paramètre: @Id</div>
```
## Notion : Paramètres d'URL
### Paramètres ditinéraire
Le routeur utilise des paramètres de routage pour remplir les paramètres de composant correspondants portant le même nom.
Les noms de paramètres ditinéraire ne respectent pas la casse. Dans lexemple suivant, le paramètre `text` assigne la valeur du segment de route à la propriété du composant `Text`.
Quand une demande est effectuée pour `/RouteParameter/amazing`, le `<h1>` contenu de la balise est restitué sous la forme `Blazor is amazing!`.
```cshtml title="Pages/RouteParameter.razor"
@page "/RouteParameter/{text}"
<h1>Blazor is @Text!</h1>
@code {
[Parameter]
public string? Text { get; set; }
}
```
Les paramètres facultatifs sont pris en charge.
Dans lexemple suivant, le paramètre facultatif `text` assigne la valeur du segment de routage à la propriété du composant `Text`. Si le segment nest pas présent, la valeur de `Text` est définie sur `fantastic`.
```cshtml title="Pages/RouteParameter.razor"
@page "/RouteParameter/{text?}"
<h1>Blazor is @Text!</h1>
@code {
[Parameter]
public string? Text { get; set; }
protected override void OnInitialized()
{
Text = Text ?? "fantastic";
}
}
```
Utilisez `OnParametersSet OnInitialized{Async}` à la place pour autoriser la navigation vers le même composant avec une valeur de paramètre facultative différente.
En fonction de lexemple précédent, utilisez `OnParametersSet` lorsque lutilisateur doit naviguer de `/RouteParameter` vers `/RouteParameter/amazing` ou de `/RouteParameter/amazing` vers `/RouteParameter` :
```cshtml
protected override void OnParametersSet()
{
Text = Text ?? "fantastic";
}
```
### Contraintes ditinéraire
Une contrainte ditinéraire applique la correspondance de type sur un segment de routage à un composant.
Dans lexemple suivant, litinéraire vers le composant `User` correspond uniquement si :
* Un segment `Id` de routage est présent dans lURL de la demande.
* Le segment `Id` est un type entier ( `int` ).
```cshtml title="Pages/User.razor"
@page "/user/{Id:int}"
<h1>User Id: @Id</h1>
@code {
[Parameter]
public int Id { get; set; }
}
```
Les contraintes de routage indiquées dans le tableau suivant sont disponibles.
Pour plus dinformations sur les contraintes ditinéraire qui correspondent à la culture dite indifférente, consultez lavertissement sous le tableau.
| Contrainte | Exemple | Exemples de correspondances | Invariant culture correspondance
| ---- | ---- | ---- | ----
| `bool` | `{active:bool}` | `true`, FALSE | Non
| `datetime` | `{dob:datetime}` | `2016-12-31`, `2016-12-31 7:32pm` | Oui
| `decimal` | `{price:decimal}` | `49.99`, `-1,000.01` | Oui
| `double` | `{weight:double}` | `1.234`, `-1,001.01e8` | Oui
| `float` | `{weight:float}` | `1.234, -1`,`001.01e8` | Oui
| `guid` | `{id:guid}` | `CD2C1638-1638-72D5-1638-DEADBEEF1638`, `{CD2C1638-1638-72D5-1638-DEADBEEF1638}` | Non
| `int` | `{id:int}` | `123456789`, `-123456789` | Oui
| `long` | `{ticks:long}` | `123456789`, `-123456789` | Oui
:::caution
Les contraintes de routage qui vérifient que lURL peut être convertie en type CLR (comme `int` ou `DateTime`) utilisent toujours la culture invariant. ces contraintes partent du principe que lURL nest pas localisable.
:::
Les contraintes de route fonctionnent également avec les paramètres facultatifs. Dans lexemple suivant, `Id` est obligatoire, mais `Option` est un paramètre ditinéraire booléen facultatif.
```cshtml title="Pages/User.razor"
@page "/user/{Id:int}/{Option:bool?}"
<p>
Id: @Id
</p>
<p>
Option: @Option
</p>
@code {
[Parameter]
public int Id { get; set; }
[Parameter]
public bool Option { get; set; }
}
```
## Notion : Paramètres de composant
Les paramètres de composant passent des données aux composants et sont définis à laide de Propriétés C# publiques sur la classe de composant avec l'attribut `[Parameter]`.
Dans lexemple suivant, un type de référence intégré ( `System.String` ) et un type référence défini par lutilisateur ( `PanelBody` ) sont passés en tant que paramètres de composant.
```csharp title="PanelBody.cs"
public class PanelBody
{
public string? Text { get; set; }
public string? Style { get; set; }
}
```
```cshtml title="Shared/ParameterChild.razor"
<div class="card w-25" style="margin-bottom:15px">
<div class="card-header font-weight-bold">@Title</div>
<div class="card-body" style="font-style:@Body.Style">
@Body.Text
</div>
</div>
@code {
[Parameter]
public string Title { get; set; } = "Set By Child";
[Parameter]
public PanelBody Body { get; set; } =
new()
{
Text = "Set by child.",
Style = "normal"
};
}
```
:::caution
Le fait de fournir des valeurs initiales pour les paramètres de composant est pris en charge, mais ne crée pas un composant qui écrit dans ses propres paramètres une fois que le composant est rendu pour la première fois.
:::
Les paramètres `Title` & `Body` de composant et du composant `ParameterChild` sont définis par des arguments dans la balise HTML qui restitue linstance du composant.
Le composant `ParameterParent` suivant restitue deux composants `ParameterChild`:
* Le premier composant `ParameterChild` est rendu sans fournir darguments de paramètre.
* Le deuxième composant `ParameterChild` reçoit des valeurs pour `Title` et `Body` à partir du composant `ParameterParent`, qui utilise une expression C# explicite pour définir les valeurs des propriétés de `PanelBody`.
```cshtml title="Pages/ParameterParent.razor"
@page "/parameter-parent"
<h1>Child component (without attribute values)</h1>
<ParameterChild />
<h1>Child component (with attribute values)</h1>
<ParameterChild Title="Set by Parent" Body="@(new PanelBody() { Text = "Set by parent.", Style = "italic" })" />
```
Le balisage HTML de rendu suivant du composant `ParameterParent` montre les valeurs `ParameterChild` par défaut des composants lorsque le composant `ParameterParent` ne fournit pas de valeurs de paramètre de composant.
Lorsque le composant `ParameterParent` fournit des valeurs de paramètre de composant, elles remplacent les valeurs `ParameterChild` par défaut du composant.
:::note
Par souci de clarté, les classes de style CSS rendues ne sont pas affichées dans le balisage HTML rendu suivant.
:::
```html
<h1>Child component (without attribute values)</h1>
<div>
<div>Set By Child</div>
<div>Set by child.</div>
</div>
<h1>Child component (with attribute values)</h1>
<div>
<div>Set by Parent</div>
<div>Set by parent.</div>
</div>
```
Assigner un champ, une propriété ou un résultat C# dune méthode à un paramètre de composant en tant que valeur dattribut HTML à laide symbole `@`.
Le composant `ParameterParent2` suivant affiche quatre instances du composant précédent `ParameterChild` et définit leurs valeurs `Title` de paramètre sur :
* Valeur du champ `title`.
* Résultat de la méthode `GetTitle` C#.
* Date locale actuelle au format long avec `ToLongDateString`, qui utilise une expression C# implicite.
* `panelData` Propriété de lobjet `Title`.
```cshtml title="Pages/ParameterParent2.razor"
@page "/parameter-parent-2"
<ParameterChild Title="@title" />
<ParameterChild Title="@GetTitle()" />
<ParameterChild Title="@DateTime.Now.ToLongDateString()" />
<ParameterChild Title="@panelData.Title" />
@code {
private string title = "From Parent field";
private PanelData panelData = new();
private string GetTitle()
{
return "From Parent method";
}
private class PanelData
{
public string Title { get; set; } = "From Parent object";
}
}
```
:::info
Quand vous assignez un membre C# à un paramètre de composant, préfixez le membre avec le symbole `@` et ne faites jamais précéder lattribut HTML du paramètre.
Utilisation correcte :
```html
<ParameterChild Title="@title" />
```
Incorrect :
```html
<ParameterChild @Title="title" />
```
:::
Lutilisation dune expression Razor explicite pour concaténer du texte avec un résultat dexpression en vue dune assignation à un paramètre nest pas prise en charge.
Lexemple suivant cherche à concaténer le texte « Set by » avec la valeur de propriété dun objet. La syntaxe Razor suivante nest pas prise en charge :
```html
<ParameterChild Title="Set by @(panelData.Title)" />
```
Le code de lexemple précédent génère une Erreur du compilateur lors de la génération de lapplication :
```
Les attributs de composant ne prennent pas en charge le contenu complexe (mixte C# et balisage).
```
Pour prendre en charge lassignation dune valeur composée, utilisez une méthode, un champ ou une propriété. Lexemple suivant effectue la concaténation de « `Set by` » et de la valeur de propriété dun objet dans la méthode C# `GetTitle`:
```cshtml title="Pages/ParameterParent3.razor"
@page "/parameter-parent-3"
<ParameterChild Title="@GetTitle()" />
@code {
private PanelData panelData = new();
private string GetTitle() => $"Set by {panelData.Title}";
private class PanelData
{
public string Title { get; set; } = "Parent";
}
}
```
Les paramètres de composant doivent être déclarés en tant que Propriétés automatiques, ce qui signifie quils ne doivent pas contenir de logique personnalisée dans leurs accesseurs `get` ou `set`.
Par exemple, la `StartData` propriété suivante est une propriété automatique :
```csharp
[Parameter]
public DateTime StartData { get; set; }
```
Ne placez pas de logique personnalisée dans l'accesseur `get` ou `set`, car les paramètres de composant sont purement destinés à une utilisation en tant que canal dun composant parent pour transmettre des informations à un composant enfant.
Si un accesseur `set` dune propriété de composant enfant contient une logique qui provoque le rerendu du composant parent, les résultats dune boucle de rendu infinie.
Pour transformer une valeur de paramètre reçue :
* Laissez la propriété de paramètre en tant que propriété automatique pour représenter les données brutes fournies.
* Créez une autre propriété ou méthode pour fournir les données transformées en fonction de la propriété du paramètre.
Remplacez `OnParametersSetAsync` pour transformer un paramètre reçu chaque fois que de nouvelles données sont reçues.
Lécriture dune valeur initiale dans un paramètre de composant est prise en charge, car les assignations de valeur initiales ninterfèrent pas avec le rendu Blazor de composant automatique.
Lassignation suivante de la locale actuelle `DateTime` avec `DateTime.Now` à `StartData` est une syntaxe valide dans un composant :
```csharp
[Parameter]
public DateTime StartData { get; set; } = DateTime.Now;
```
Après lassignation initiale de `DateTime.Now`, nassignez pas de valeur à `StartData` dans le code.

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save