Blazor Client Side Интернет Магазин: Часть 5 — Просмотр корзины и работа с Stateful

от автора

Привет, Хабр! Продолжаю делать интернет магазин на Blazor. В этой части расскажу о том как добавил в него возможность просмотра корзины товаров и организовал работу с состоянием. За подробностями добро пожаловать под кат.

Содержание

Ссылки

→ Исходники
Образы на Docker Registry

Stateful

Мне не понравилось что при переходе между страницами теряется состояние. Например те поля по которым я отфильтровал товары. Чтобы решить эту проблему я перешел к stateful сервисам-синглтонам и зарегистрировал ViewModel страницы в DI контейнере как синглтон. По сути я использовал DI контейнер как хранилище состояния, а ViewModel начал инжектить в View как сервис.

Код

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

Тут надо рассказать про

public event EventHandler OnBasketItemsCountChanged; 

Я хотел в заголовке страницы отображать текущее количество товаров в корзине. Проблема в том что заголовок не является дочерним элементом по отношению к странице корзины покупок поэтому обновлении ее стояния он игнорирует. Чтобы он перерисовывался я и добавил это событие, а в нем повесил вот такой обработчик:

@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) Регистрация в DI контейнере

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

Вариант на Angular 9

Пока что разработка на Blazor мне больше удовольствия приносит чем Ангуляр.

ссылка на оригинал статьи https://habr.com/ru/post/495812/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *