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 мне больше удовольствия приносит чем Ангуляр.

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 3

      0
      почему не github? не удобно
        0
        Когда я регал Гитлаб на Гитхабе не было удобного CI/CD. Сейчас уже получше стало. Да и Гитхаб у меня есть. Просто я забыл учетные данные к нему.
        0

        Only users with full accounts can post comments. Log in, please.