Boutique en ligne côté client Blazor: Partie 5 - Afficher la corbeille et travailler avec Stateful



Bonjour, Habr! Je continue de faire de la boutique en ligne sur Blazor. Dans cette partie, je vais parler de la façon dont j'ai ajouté la possibilité de voir un panier de marchandises et d'organiser le travail avec l'État. Pour plus de détails, bienvenue au chat.

Contenu



Références


→  Sources
Images sur le registre Docker

Avec état


Je n'ai pas aimé que l'état soit perdu lors du basculement entre les pages. Par exemple, les champs par lesquels j'ai filtré les produits. Pour résoudre ce problème, je suis passé aux services singleton avec état et j'ai enregistré le ViewModel de la page dans le conteneur DI en tant que singleton. Essentiellement, j'ai utilisé le conteneur DI comme magasin d'état et ViewModel a commencé à injecter dans View en tant que service.

Le code


1) Modèles


    public sealed class ProductModel
    {
        public Guid Id { get; set; }
        public string Version { get; set; }
        public string Title { get; set; }
        public decimal Price { get; set; }
    }

    public class BasketLineModel
    {
        public uint Quantity { get; set; }
        public ProductModel Product { get; set; }
    }

    public class BasketModel
    {
        public List<BasketLineModel> Lines { get; set; } = new List<BasketLineModel>();
    }

2) Services


    public class BasketService : IBasketService
    {
        private readonly IApiRepository _repository;
        private BasketModel _basket;

        public BasketService(IApiRepository repository)
        {
            _repository = repository;
            _basket = new BasketModel();
        }

        public string Error { get; private set; }
        public IReadOnlyList<BasketLineModel> Model => _basket.Lines.AsReadOnly();
        public event EventHandler OnBasketItemsCountChanged;
        public long ItemsCount => _basket?.Lines?.Sum(l => l.Quantity) ?? 0;

        public async Task Load()
        {
            var count = ItemsCount;
            var (r, e) = await _repository.GetBasket();
            _basket = r;
            Error = e;
            if (string.IsNullOrWhiteSpace(Error) && count != ItemsCount)
                OnBasketItemsCountChanged?.Invoke(null, null);
        }

        public async Task Add(ProductModel product)
        {
            var (_, e) = await _repository.AddToBasket(product);
            Error = e;
            if (!string.IsNullOrWhiteSpace(e))
                return;
            await Load();
        }

        public async Task Remove(ProductModel product)
        {
            var (_, e) = await _repository.Remove(product.Id);
            Error = e;
            if (!string.IsNullOrWhiteSpace(e))
                return;
            await Load();
        }
    }

Ici, il faut parler de

public event EventHandler OnBasketItemsCountChanged;

Je voulais afficher le nombre actuel de produits dans le panier dans l'en-tête de la page. Le problème est que le titre n'est pas un enfant de la page du panier, il ignore donc la mise à jour de son état. Pour qu'il me redessine et ajoute cet événement, et que j'y ai accroché un tel gestionnaire:

@using BlazorEShop.Spa.BlazorWasm.Client.Core.Services
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

@inject NavigationManager Navigation
@inject SignOutSessionStateManager SignOutManager
@inject IBasketService Basket

@implements IDisposable

<AuthorizeView>
    <Authorized>
        <span class="text-success">Total Items In Basket: @TotalItemsCount </span>  
        Hello, @context?.User?.Identity?.Name!
        <button class="nav-link btn btn-link" @onclick="BeginSignOut">Log out</button>
    </Authorized>
    <NotAuthorized>
        <a href="authentication/login">Log in</a>
    </NotAuthorized>
</AuthorizeView>

@code
{
    public long TotalItemsCount { get; set; }

    protected override void OnInitialized()
    {
        Basket.OnBasketItemsCountChanged += Bind;
        TotalItemsCount = Basket.ItemsCount;
        base.OnInitialized();
    }

    public void Dispose()
    {
        Basket.OnBasketItemsCountChanged -= Bind;
    }

    public void Bind(object s, EventArgs e)
    {
        if (TotalItemsCount == Basket.ItemsCount)
            return;
        TotalItemsCount = Basket.ItemsCount;
        this.StateHasChanged();
    }

    private async Task BeginSignOut(MouseEventArgs args)
    {
        await SignOutManager.SetSignOutState();
        Navigation.NavigateTo("authentication/logout");
    }
}

3) ViewModel


    public class BasketViewModel
    {
        private bool _isInitialized;
        private readonly IBasketService _service;

        public BasketViewModel(IBasketService service)
        {
            _service = service;
        }

        public string Error => _service.Error;
        public IReadOnlyList<BasketLineModel> Model => _service.Model;

        public async Task OnInitializedAsync()
        {
            if (_isInitialized)
                return;
            Console.WriteLine("BASKET INIT!");
            await _service.Load();
            _isInitialized = true;
        }

        public Task Add(ProductModel product) => _service.Add(product);

        public Task Remove(ProductModel product) => _service.Remove(product);
    }

4) Voir


@page "/basket"
@attribute [Authorize]
@inject BasketViewModel ViewModel

<h3>Basket</h3>
<Error Model="@ViewModel.Error" />
<input type="button" class="btn btn-primary my-3" value="Create Order" /> <!--TODO:   -->
<div class="table-responsive">
    <table class="table">
        <thead>
            <tr>
                <th>Title</th>
                <th>Price</th>
                <th>Quantity</th>
                <th></th>
            </tr>
        </thead>
        <tbody>

            @if (ViewModel.Model == null)
            {
                <tr>
                    <td>
                        <em>Loading...</em>
                    </td>
                </tr>
            }
            else
            {
                foreach (var line in ViewModel.Model)
                {

                    <tr>
                        <td>@line.Product.Title</td>
                        <td>@line.Product.Price</td>
                        <td>@line.Quantity</td>
                        <td>
                            <input type="button"
                                   class="btn btn-success"
                                   @onclick="@(async x=>await ViewModel.Add(line.Product))"
                                   value="+" />
                            <input class="btn btn-warning"
                                   value="-"
                                   type="button"
                                   @onclick="@(async x=>await ViewModel.Remove(line.Product))" />
                        </td>
                    </tr>
                }
            }
        </tbody>
    </table>
</div>

@code
{
    protected override async Task OnInitializedAsync()
    {
        await ViewModel.OnInitializedAsync();
    }
}

5) Enregistrement dans un conteneur DI


services.AddTransient<IApiRepository, ApiRepository>();
services.AddSingleton<IBasketService, BasketService>();
services.AddSingleton<BasketViewModel>();

Option sur Angular 9


Jusqu'à présent, le développement sur Blazor m'apporte plus de plaisir qu'Angular.


All Articles