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 <base> 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,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,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,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,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 ( <nav>...</nav> ) and the footer ( <footer>...</footer> ; ).
|
||||
|
||||
```cshtml title="Shared/DoctorWhoLayout.razor"
|
||||
// highlight-next-line
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<header>
|
||||
<h1>Doctor Who™ 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 ( <h1>...</h1> ) of the header ( <header>...</header> ), of the navigation bar ( <nav>...</nav> ) and the trademark information element ( <div>...</div> ) of the footer ( < footer>...</footer> ) comes from the `DoctorWhoLayout` component.
|
||||
* The episodes header ( <h2>...</h2> ) and episode list ( <ul>...</ul> ) come from the `Episodes` page.
|
||||
|
||||
```cshtml
|
||||
<body>
|
||||
<div id="app">
|
||||
<header>
|
||||
<h1>Doctor Who™ 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,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,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,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,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,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,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,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>
|
||||
```
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue