« Blazor .NET Core 3.1 » : différence entre les versions

De Banane Atomic
Aller à la navigationAller à la recherche
Ligne 433 : Ligne 433 :
</kode>
</kode>


== Error InputSelect`1[System.Int32] does not support the type 'System.Int32' ==
=== Error InputSelect`1[System.Int32] does not support the type 'System.Int32' ===
Use {{boxx|select}} instead of {{boxx|InputSelect}}:
Use {{boxx|select}} instead of {{boxx|InputSelect}}:
<kode lang='razor'>
<kode lang='razor'>

Version du 3 mai 2020 à 16:09

Liens

Additional libraries

Blazorise components, bootstrap, font-awesome
Radzen Blazor Components components
Blazored components
BlazorStrap, Bootstrap 4 Components for Blazor Framework bootstrap
Blazor Extensions
Awesome Blazor projects

Description

  • Développement frontend avec C#
  • Intégration des bibliothèques .NET existantes (nuget)

WebAssembly

Permet d'exécuter du bytecode (code intermediaire) dans le navigateur grâce à la javascript runtime sandbox.
WebAssembly est nativement présent dans les navigateurs moderne.
WebAssembly possède un runtime .NET (mono.wasm), ce qui permet d'exécuter des assemblies .NET dans le navigateur.

Créer une application

Bash.svg
# Blazor WebAssembly
dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.1.0-preview4.19579.2
dotnet new blazorwasm -o blazorwasm
cd blazorwasm
dotnet run

# Blazor server
dotnet new blazorserver -o blazorserver
cd blazorserver
dotnet run

Hosting models

Blazor WebAssembly

L'application est téléchargée et exécutée dans le navigateur.

  • Excellentes performances
  • Pas besoin d'un serveur web ASP.NET Core pour héberger l'application
  • Permet une utilisation offline une fois chargée
  • Le temps de chargement est long, il faut télécharger l'application, ses dépendances et le .NET runtime
  • Les identifiants pour les accès aux bdd et web API sont chargés par chaque client

Blazor Server

L'application est exécutée sur le serveur. UI updates, event handling et les appels JavaScript sont pris en charge grâce à une connexion SignalR.

  • Pas besoin de WebAssembly
  • Chaque interaction utilisateur nécessite un appel au serveur
  • Un serveur web ASP.NET Core est nécessaire pour héberger l'application

Blazor server application

C'est une ASP.NET Core application.

Arborescence de fichiers

  • Program.cs: point d'entrée, appelle Startup.cs
  • Startup.cs: configuration (DI)
  • Pages: Components
  • Shared: shared Components comme les layouts (main, menu)
  • App.razor: routing
  • _Imports.razor: import namespaces
  • wwwroot: static files (css, js)
  • Data: service to access data

Debug

Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // avoir des erreurs détaillées
    services.AddServerSideBlazor()
            .AddCircuitOptions(options => { options.DetailedErrors = true; });

Code in the same page vs Code-behind

Code in the same page

MyPage.razor
@page "/mypage"

@inject IDataService DataService

<h1>Title</h1>

@code {
    private Data data;
    // some C# code
}

Code-behind

MyPage.razor
@page "/mypage"
@inherits MyPageBase
<h1>Title</h1>
MyPageBase.cs
public class MyPageBase : ComponentBase
{
    [Inject]
    private IDataService DataService { get; set; }

    protected Data data;
    // some C# code
}

Pages

Pages/Items.razor
@* url *@
@page "/items"
@* url avec un parameter *@
@page "/item/{Id:int}"

@* attendre que la valeur soit chargée *@
@if (Items == null)
{
    <p><em>Loading...</em></p>
}
else
{ /* ... */ }

@code {
    [Parameter]
    public int ItemId { get; set; }
}

Components

Components/MyComponent.razor

Components/MyComponentBase.cs
[Parameter]
public EventCallback<bool> MyEventCallback { get; set; }

// invoke asynchronously callback
await MyEventCallback.InvokeAsync(true);
_Imports.razor
@* Make components available *@
@using BlazorServerApp.Components
Pages/MyPage.razor
<MyComponent @ref="MyComponent"
             MyEventCallback="@MyComponent_OnSomethingHappened"></MyComponent>
Pages/MyPageBase.cs
protected MyComponentBase MyComponent { get; set; }

public async void MyComponent_OnSomethingHappened()
{
    // refresh the DOM if elements have changed
    StateHasChanged();
}

Lifecycle methods

  1. OnInitialized OnInitializedAsync
  2. OnParametersSet OnParametersSetAsync
  3. OnAfterRender OnAfterRenderAsync

Layout

App.razor
<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        @* définit le layout par défaut *@
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>
Shared/MainLayout.razor
@inherits LayoutComponentBase

<div class="sidebar">
    <NavMenu />
</div>

<div class="main">
    <div class="content px-4">
        @Body
    </div>
</div>

Data Service

Dependency Injection

Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    // configuration du container de DI
    services.AddHttpClient<IItemDataService, ItemDataService>(
        client => client.BaseAddress = new Uri("http://localhost:5002")
    );

Data Service

Services/IItemDataService.cs
public interface IItemDataService
{
    Task<IEnumerable<Item>> GetAllItemsAsync();
    Task<Item> GetItemAsync(int itemId);
    Task UpdateItemAsync(Item item);
    Task DeleteItemAsync(int itemId);
    Task<Item> AddItemAsync(Item item);
}
Services/ItemDataService.cs
public class ItemDataService : IItemDataService
{
    private readonly HttpClient _httpClient;

    // injection de HttpClient
    public ItemDataService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<IEnumerable<Item>> GetAllItemsAsync()
    {
        return await JsonSerializer.DeserializeAsync<IEnumerable<Item>>(
            await _httpClient.GetStreamAsync($"api/item"),
            new JsonSerializerOptions()
            {
                PropertyNameCaseInsensitive = true
            });
    }

    public async Task<Item> GetItemAsync(int itemId)
    {
        return await JsonSerializer.DeserializeAsync<Item>(
            await _httpClient.GetStreamAsync($"api/item/{itemId}"),
            new JsonSerializerOptions()
            {
                PropertyNameCaseInsensitive = true
            });
    }

    public async Task UpdateItemAsync(Item item)
    {
        var itemJson = new StringContent(
            JsonSerializer.Serialize(item),
            Encoding.UTF8,
            "application/json");

        await _httpClient.PutAsync("api/item", itemJson);
    }

    public async Task DeleteItemAsync(int itemId)
    {
        await _httpClient.DeleteAsync($"item/{itemId}");
    }

    public async Task<Item> AddItemAsync(Item item)
    {
        var itemJson = new StringContent(
            JsonSerializer.Serialize(item),
            Encoding.UTF8,
            "application/json");

        var response = await _httpClient.PostAsync("item", itemJson);

        if (response.IsSuccessStatusCode)
        {
            return await JsonSerializer.DeserializeAsync<Item>(
                await response.Content.ReadAsStreamAsync());
        }

        return null;
    }

Component

Pages/MyComponentBase.cs
public class MyComponentBase : ComponentBase
{
    // injection de IItemDataService (cette syntaxe n'est possible que dans les components)
    [Inject]
    public IItemDataService ItemDataService { get; set; }

    public IEnumerable<Item> Items { get; set; }

    protected override async Task OnInitializedAsync()
    {
        Items = await ItemDataService.GetAllItemsAsync();
    }

Form

EditItem.razor
@page "/edititem/{ItemId:int}"
@* to access to Model.Item *@
@using Model

@inherits EditItemBase

<EditForm Model="@Item"
          OnValidSubmit="@HandleValidSubmitAsync"
          OnInvalidSubmit="@HandleInvalidSubmitAsync">

    <button type="submit">Save</button> 
    <button @onclick="Cancel">Cancel</button>           
</EditForm>
EditItemBase.cs
[Parameter]
public int ItemId { get; set; }

[Inject]
public IItemDataService ItemDataService { get; set; }

public Item Item { get; set; } = new Item();

protected override async Task OnInitializedAsync()
{
    Item = await ItemDataService.GetItemAsync(ItemId);
}

// when the submit button is clicked and the model is valid
protected async Task HandleValidSubmitAsync()
{
    await ItemDataService.UpdateItemAsync(Item);
}

// when the submit button is clicked and the model is not valid
protected async Task HandleInvalidSubmitAsync()
{

}

Input Text

Razor.svg
<div class="form-group row">

    <label for="name"
           class="col-sm-3">
        Item: 
    </label>

    <InputText id="name"
               @bind-Value="@Item.Name"
               class="form-control col-sm-8"
               placeholder="Enter name">
    </InputText>
</div>

Input Text Area

Razor.svg
<div class="form-group row">
    <label for="comment"
           class="col-sm-3">
        Comment: 
    </label>
    <InputTextArea id="comment"
                   class="form-control col-sm-8"
                   @bind-Value="@Item.Comment"
                   placeholder="Enter comment">
    </InputTextArea>
</div>

Input Number

Razor.svg
<div class="form-group row">

    <label for="price"
           class="col-sm-3">Price: </label>

    <InputNumber id="price"
                 @bind-Value="@Item.Price"
                 class="form-control col-sm-8">
    </InputNumber>
</div>

Input Date

Razor.svg
<div class="form-group row">

    <label for="date"
           class="col-sm-3">
        Date: 
    </label>

    <InputDate id="date"
               class="form-control col-sm-8"
               @bind-Value="@Item.Date"
               placeholder="Enter date">
    </InputDate>
</div>

Input Select

Razor.svg
<div class="form-group row">
    <label for="country" class="col-sm-3">Color: </label>
    <InputSelect id="color"
                 @bind-Value="@CountryId"
                 class="form-control col-sm-8">
        @foreach (var color in Enum.GetValues(typeof(RgbColor)))
        {
            <option value="@color">@color</option>
        }
        <option value="@RgbColor.Red">@RgbColor.Red</option>
        <option value="@RgbColor.Green">@RgbColor.Green</option>
        <option value="@RgbColor.Blue">@RgbColor.Blue</option>
    </InputSelect>
</div>

Error InputSelect`1[System.Int32] does not support the type 'System.Int32'

Use select instead of InputSelect:

Razor.svg
<div class="form-group row">
    <label for="group" class="col-sm-3">Color: </label>
    <select id="group"
            @bind="@User.Group.Id"
            class="form-control col-sm-8">
        @foreach (var availableGroup in AvailableGroups)
        {
            <option value="@availableGroup.Id">@availableGroup.Name</option>
        }
    </select>
</div>

Input CheckBox

Razor.svg
<div class="form-group row">
    <label for="smoker"
           class=" offset-sm-3">
        <InputCheckbox id="smoker"
                       @bind-Value="@Employee.Smoker">
        </InputCheckbox>
        &nbsp;Smoker
    </label>
</div>

Form validation

Item.cs
// nécessite le package nuget System.ComponentModel.Annotations
[Required]
[StringLength(50, ErrorMessage = "Name too long (max 50 char)")]
public string Name { get; set; }
Pages/ItemEdit.razor
<EditForm Model="@Item">
    <DataAnnotationsValidator />
    @* affiche la liste des messages d'erreur de validation *@
    <ValidationSummary />

    <div class="form-group row">
        <label for="name" class="col-sm-3">Name: </label>
        <InputText id="name" @bind-value="@Item.Name" class="form-control col-sm-8" placeholder="Enter name"></InputText>>
        @* affiche le message d'erreur de validation *@
        <ValidationMessage class="offset-sm-3 col-sm-8" For="@(() => Item.Name)" />
    </div>

    <button type="submit">Save</button>
    <button type="button" @onclick="Cancel">Cancel</button>

Fluent validation

Bash.svg
dotnet add package Blazored.FluentValidation
_Imports.razor
@using Blazored.FluentValidation
Pages/EditItem.razor
<EditForm Model="@Item">
    <FluentValidationValidator />
    <ValidationSummary />

Navigation

MyPage.razor
<a @onclick="@NavigateToHome">Home</a>
<a @onclick="@(() => NavigateToEdit(item.Id)">Home</a>
MyPageBase.cs
[Inject]
public NavigationManager NavigationManager { get; set; }

protected void NavigateToHome()
{
    NavigationManager.NavigateTo("home");
}

protected void NavigateToEdit(int itemId)
{
    NavigationManager.NavigateTo($"edit/{itemId}");
}

Static files

Pages/_Host.cshtml
<head>
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link href="css/site.css" rel="stylesheet" />
wwwroot/css/site.css
/* custom css */

/* import Iconic font */
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
  • wwwroot/favicon.ico

Font-Awesome

  • wwwroot/css/font-awesome
    • css/all.min.css
    • webfonts/*
    • LICENSE.txt
wwwroot/css/site.css
@import url('font-awesome/css/all.min.css');

Toast message

Bash.svg
dotnet add package Blazored.Toast
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddBlazoredToast();
_Imports.razor
@using Blazored.Toast
@using Blazored.Toast.Services
Pages/_Host.cshtml
<head>
    <link href="_content/Blazored.Toast/blazored-toast.css" rel="stylesheet" />
Shared/MainLayout.razor
@using Blazored.Toast.Configuration

@*
Position (Default: ToastPosition.TopRight)
Timeout (Default: 5)
SuccessClass: add css class to success toast
SuccessIconClass: add css class to success toast icon
ErrorIconClass: add css class to error toast icon
*@
<BlazoredToasts Position="ToastPosition.BottomRight"
                Timeout="10"
                SuccessClass="success-toast-override"
                SuccessIconClass="fa fa-thumbs-up"
                ErrorIconClass="fa fa-bug" />
Pages/MyPageBase.cs
[Inject]
public IToastService ToastService { get; set; }

this.ToastService.ShowSuccess("Success message", "Success title");
this.ToastService.ShowError("Error message", "Error title");

Javascript interop

  • .NET code calls into JS
  • JS calls into .NET code
Pages/MyPageBase.cs
[Inject]
public IJSRuntime JSRuntime { get; set; }

var result = await JSRuntime.InvokeAsync<object>("MyJsMethod", "");
wwwroot/js/myscript.js
(function() {

    window.MyJsMethod = {
Pages/_Host.cshtml
<head>
    <link href="_content/AnotherProject/css/style.css" rel="stylesheet" />
</head>

<body>
    <!-- à la fin de body -->
    <script src="_content/AnotherProject/js/myscript.js"></script>
</body>
_Imports.razor
@using AnotherProject