Hello, Habr! I continue to do online store on Blazor. In this part Iβll talk about how I added the ability to view a basket of goods to it and organized work with the state. For details, welcome to cat.Content
References
β Sourcesβ Images on the Docker RegistryStateful
I did not like that the state is lost when switching between pages. For example, those fields by which I filtered products. To solve this problem, I switched to stateful singleton services and registered the ViewModel of the page in the DI container as a singleton. Essentially, I used the DI container as a state store, and ViewModel started injecting into View as a service.The code
1) Models
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();
}
}
Here it is necessary to tell aboutpublic event EventHandler OnBasketItemsCountChanged;
I wanted to display the current number of products in the basket in the page header. The problem is that the title is not a child of the shopping cart page, therefore, it ignores updating its state. So that he redraws I and adds this event, and in it I hung here such a handler:@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) View
@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) Registration in a DI container
services.AddTransient<IApiRepository, ApiRepository>();
services.AddSingleton<IBasketService, BasketService>();
services.AddSingleton<BasketViewModel>();
Option on Angular 9
So far, development on Blazor brings me more pleasure than Angular.