Blazor ASP.NET Core 7.0

De Banane Atomic
Aller à la navigationAller à la recherche

Pages

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

<PageTitle>Items</PageTitle>

@if (Items == null)  @* attendre que la valeur soit chargée *@
{
    <p><em>Loading...</em></p>
}
else
{ /* ... */ }
Pages/Items.razor.cs
public partial class Table : ComponentBase
{
    private IReadOnlyCollection<ItemDto> items;

    [Parameter]
    public int Id { get; set; }

    protected override async Task OnInitializedAsync()
    {
        items = await this.ItemClient.GetAsync();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender) { }

CSS isolation

Pages/Items.razor.css
/* scoped css only for this page */
/* @import is not allowed in scoped css */

JS isolation

Pages/MyPage.razor.js
export function myFunction() { }

export function myFunctionWithArg(arg) {
    return 'ok';
}
Pages/MyPage.razor.cs
public partial class MyPage : ComponentBase, IAsyncDisposable
{
    [Inject]
    private IJSRuntime JS { get; set; } = default!;

    private IJSObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import", "./Pages/MyPage.razor.js");
        }
    }

    private async Task MyJsFunction()
    {
        if (module is not null)
            await module.InvokeVoidAsync("myFunction");
    }

    private async ValueTask<string?> MyJsFunctionWithArg(string arg)
    {
        string result = null;
        if (module is not null) 
            await module.InvokeAsync<string>("myFunctionWithArg", arg);
        return result;
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
            await module.DisposeAsync();
    }

Form

EditItem.razor
@page "/edititem/{ItemId:int}"

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

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

[Inject]
private IItemService ItemService { get; set; }

private Item item = null;

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

// when the submit button is clicked and the model is valid
private async Task HandleValidSubmitAsync()
{
    await ItemService.UpdateItemAsync(item);
}

// when the submit button is clicked and the model is not valid
private 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>

Form validation

Data Annotation

Item.cs
[Required, StringLength(50, MinimumLength = 3, ErrorMessage = "Name must be between 3 and 50 characters")]
public string Name { get; set; }
Pages/ItemEdit.razor
<EditForm Model="@item">
    <DataAnnotationsValidator />
    <ValidationSummary />  @* affiche la liste des messages d'erreur de validation *@

    <InputText @bind-value="item.Name" 
               placeholder="Enter name" />
    <ValidationMessage class="offset-sm-3 col-sm-8" For="@(() => Item.Name)" />  @* affiche le message d'erreur de validation *@

Data binding

One way

Changes made in the UI doesn't change the binded property.
The onchange event can be used to manually update the property.

Fichier:Blazor.svg
<input type="text" value="@Description"
       @onchange=@((ChangeEventArgs __e) => Description = __e.Value.ToString())
       @onchange=OnInputChanged />

@code {
    string Description { get; set; } = "Text";

    void OnInputChanged(ChangeEventArgs e) => Description = e.Value.ToString();
}

Two way

Fichier:Blazor.svg
<input type="text" @bind="Description" />    @* by default uses the 'on change' event (focus lost) *@
<input type="text" @bind="Description"
                   @bind:event="oninput" />  @* trigger the binding 'on input' event *@

@code {
    string Description { get; set; } = "Text";
}

Form exemples

Deactivate the form while submitting

MyPage.razor
<EditForm Model="@search"
          OnValidSubmit="@HandleValidSubmitAsync">
    <DataAnnotationsValidator />

    <fieldset disabled="@isSearching">
        <div class="row justify-content-center">
            <InputText @bind-Value="search.Text"
                        class="col-sm-6"
                        placeholder="Nom de table" />
            <button type="submit" class="btn btn-primary col-auto">
                @if (!isSearching)
                {
                    <span class="oi oi-magnifying-glass" aria-hidden="true"></span>
                }
                else
                {
                    <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
                }
            </button>
        </div>

        <div class="row justify-content-center">
            <ValidationMessage class="text-danger col-sm-6" For="@(() => search.Text)" />
        </div>
    </fieldset>

</EditForm>

@if (isSearching)
{
    <h5 class="text-center mt-5 mb-2">Loading 
        <span class="spinner-border spinner-border-sm" role="status"></span>
    </h5>
}
else if (tables.Count > 0)
{
    <h5 class="text-center mt-5 mb-2">Résultat de la recherche</h5>
}
else if (validSearchHasBeenStarted)
{
    <h5 class="text-center mt-5 mb-2">Pas de résultat pour cette recherche</h5>
}
MyPage.razor.cs
private bool isSearching { get; set; } = false;
private bool validSearchHasBeenStarted { get; set; } = false;

private async Task HandleValidSubmitAsync()
{
    validSearchHasBeenStarted = true;
    isSearching = true;

    var data = await myClient.FetchDataAsync(search.Text);

    isSearching = false;
}

Dependency injection

Program.cs
builder.Services.AddTransient<IMyService, MyService>();
Pages/MyPage.razor.cs
[Inject]
private IMyService myService { get; set; }
Lifetime Description
Singleton creates a single instance of the service.

All components requiring a Singleton service receive the same instance of the service.

Transient creates a new instance of the service on each component request.
Scoped creates a new instance of the service on each HTTP request but not across SignalR connection/circuit messages.

Which means only when:

  • The user closes the browser's window, then opens a new window and navigates back to the app.
  • The user closes a tab of the app in a browser window, then opens a new tab and navigates back to the app.
  • The user selects the browser's reload/refresh button.

Entity Framework Core

Using a DbContext factory

DbContext isn't thread safe and isn't designed for concurrent use.
To handle multi-threads scenarios, use a AddDbContextFactory to create a DbContext for each query.

Program.cs
builder.Services.AddDbContextFactory<MyDbContext>();
MyRepository.cs
private readonly IDbContextFactory<MyDbContext> contextFactory;

public DataSummaryRepository(IDbContextFactory<MyDbContext> contextFactory)
{
    this.contextFactory = contextFactory;
}

public async Task<Data> FetchDataAsync()
{
    // create a DbContext for this query, then dispose it once the query has been executed
    using var context = contextFactory.CreateDbContext();
}

External JS libraries

LibMan library manager

Bash.svg
# install the LibMan CLI
dotnet tool install -g Microsoft.Web.LibraryManager.Cli

# create a libman.js file
libman init
# available providers: cdnjs, jsdelivr, unpkg, filesystem

libman install bootswatch@5.0.1 --files dist/darkly/bootstrap.min.css

# uninstall bootstrap
libman uninstall bootstrap@5.0.1

Bootstrap

Bash.svg
# install only bootstrap.min.css and bootstrap.bundle.min.js (with popper)
libman install bootstrap@latest \
--files dist/css/bootstrap.min.css \
--files dist/css/bootstrap.min.css.map \
--files dist/js/bootstrap.bundle.min.js \
--files dist/js/bootstrap.bundle.min.js.map
# default destination path: wwwroot/lib/bootstrap
Pages/_Host.cshtml
<head>
    <!-- REPLACE <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" /> BY -->
    <link rel="stylesheet" href="lib/bootstrap/css/bootstrap.min.css" />
    <link href="css/site.css" rel="stylesheet" />

    <script src="lib/bootstrap/js/bootstrap.bundle.min.js"></script>
    <script src="_framework/blazor.server.js"></script>
</body>
Delete folder wwwroot/css/bootstrap

Bootstrap icons

Bash.svg
# install the css and fonts only
libman install bootstrap-icons@latest \
--files font/bootstrap-icons.min.css \
--files font/fonts/bootstrap-icons.woff \
--files font/fonts/bootstrap-icons.woff2
# default destination path: wwwroot/lib/bootstrap-icons
Pages/_Host.cshtml
<head>
    <link rel="stylesheet" href="lib/bootstrap-icons/font/bootstrap-icons.min.css" />
    <link href="css/site.css" rel="stylesheet" />
wwroot/css/site.css
/* remove this line */
@import url('open-iconic/font/css/open-iconic-bootstrap.min.css');
Remove the folder wwwroot/css/open-iconic
Shared/NavMenu.razor
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
    <i class="bi bi-house"></i> Home
</NavLink>
Shared/NavMenu.razor.css
/* replace .oi by .bi */
.bi {
    width: 2rem;
    font-size: 1.1rem;
    vertical-align: text-top;
    top: -2px;
}

Turn on detailed errors

Program.cs
builder.Services.AddServerSideBlazor()
                .AddCircuitOptions(o =>
    {
        o.DetailedErrors = true;
        o.DetailedErrors = builder.Environment.IsDevelopment();  // only add details when debugging
    });