Olá Habr! Eu continuo a fazer loja online no Blazor. Nesta parte, falarei sobre como acrescentei a capacidade de exibir uma cesta de mercadorias e o trabalho organizado com o estado. Para detalhes, bem-vindo ao gato.Conteúdo
Referências
→ Fontes→ Imagens no registro do DockerStateful
Não gostei que o estado se perca ao alternar entre páginas. Por exemplo, os campos pelos quais filtrei produtos. Para resolver esse problema, mudei para serviços singleton com estado e registrei o ViewModel da página no contêiner DI como um singleton. Essencialmente, usei o contêiner de DI como um armazenamento de estado e o ViewModel começou a injetar no View como um serviço.O 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) Serviços
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();
}
}
Aqui é necessário contar sobrepublic event EventHandler OnBasketItemsCountChanged;
Eu queria exibir o número atual de produtos na cesta no cabeçalho da página. O problema é que o título não é filho da página do carrinho de compras e, portanto, ignora a atualização de seu estado. Para que ele redesenhe eu e adicione este evento, e nele eu pendurei aqui um manipulador:@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 em um contêiner DI
services.AddTransient<IApiRepository, ApiRepository>();
services.AddSingleton<IBasketService, BasketService>();
services.AddSingleton<BasketViewModel>();
Opção no Angular 9
Até agora, o desenvolvimento no Blazor me traz mais prazer do que o Angular.