Blazor Client Side Online Store: Parte 5 - Ver la papelera de reciclaje y trabajar con Stateful



Hola Habr! Sigo haciendo tienda en línea en Blazor. En esta parte, hablaré sobre cómo le agregué la capacidad de ver una canasta de productos y cómo organicé el trabajo con el estado. Para más detalles, bienvenido a cat.

Contenido



Referencias


→  Fuentes
Imágenes en el Registro Docker

Stateful


No me gustó que el estado se pierda al cambiar de página. Por ejemplo, esos campos por los cuales filtré productos. Para resolver este problema, cambié a servicios singleton con estado y registré el ViewModel de la página en el contenedor DI como singleton. Esencialmente, utilicé el contenedor DI como una tienda de estado, y ViewModel comenzó a inyectar en View como un servicio.

El código


1) Modelos


    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) servicios


    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();
        }
    }

Aquí es necesario contar sobre

public event EventHandler OnBasketItemsCountChanged;

Quería mostrar el número actual de productos en la cesta en el encabezado de la página. El problema es que el título no es un elemento secundario de la página del carrito de compras, por lo tanto, ignora la actualización de su estado. Para que vuelva a dibujar I y agregue este evento, y en él colgué aquí un controlador:

@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) Ver


@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) Registro en un contenedor DI


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

Opción en Angular 9


Hasta ahora, el desarrollo en Blazor me trae más placer que Angular.


All Articles