Udostępnij za pośrednictwem


Część 8. Koszyk z aktualizacjami AJAX

Autor: Jon Galloway

Sklep MVC Music Store to aplikacja samouczka, która wprowadza i wyjaśnia krok po kroku, jak używać ASP.NET MVC i Visual Studio na potrzeby tworzenia aplikacji internetowych.

MVC Music Store to uproszczona przykładowa implementacja sklepu, która sprzedaje albumy muzyczne online i implementuje podstawową administrację witryną, logowanie użytkowników i funkcjonalność koszyka zakupów.

W tej serii samouczków szczegółowo przedstawiono wszystkie kroki, które należy wykonać w celu utworzenia przykładowej aplikacji ASP.NET MVC Music Store. Część 8 obejmuje koszyk z Aktualizacje Ajax.

Pozwolimy użytkownikom umieszczać albumy w koszyku bez rejestracji, ale będą musieli zarejestrować się jako goście, aby ukończyć wyewidencjonowanie. Proces zakupów i wyewidencjonowania zostanie rozdzielony na dwa kontrolery: kontroler ShoppingCart, który pozwala anonimowo dodawać elementy do koszyka, oraz kontroler wyewidencjonowania, który obsługuje proces wyewidencjonowania. Zaczniemy od koszyka zakupów w tej sekcji, a następnie skompilujemy proces wyewidencjonowania w poniższej sekcji.

Dodawanie klas modelu Cart, Order i OrderDetail

Nasze procesy koszyka i finalizacji zakupu będą korzystać z niektórych nowych klas. Kliknij prawym przyciskiem myszy folder Models i dodaj klasę Cart (Cart.cs) z następującym kodem.

using System.ComponentModel.DataAnnotations;
 
namespace MvcMusicStore.Models
{
    public class Cart
    {
        [Key]
        public int      RecordId    { get; set; }
        public string   CartId      { get; set; }
        public int      AlbumId     { get; set; }
        public int      Count       { get; set; }
        public System.DateTime DateCreated { get; set; }
        public virtual Album Album  { get; set; }
    }
}

Ta klasa jest bardzo podobna do innych, których używaliśmy do tej pory, z wyjątkiem atrybutu [Key] dla właściwości RecordId. Nasze elementy koszyka będą miały identyfikator ciągu o nazwie CartID, aby umożliwić anonimowe zakupy, ale tabela zawiera klucz podstawowy o nazwie RecordId. Zgodnie z konwencją program Entity Framework Code-First oczekuje, że kluczem podstawowym dla tabeli o nazwie CartId będzie CartId lub ID, ale możemy łatwo zastąpić to adnotacjami lub kodem, jeśli chcemy. Jest to przykład używania prostych konwencji w programie Entity Framework Code-First, gdy są one dla nas odpowiednie, ale nie jesteśmy przez nie ograniczeni, gdy nie są one ograniczone.

Następnie dodaj klasę Order (Order.cs) z następującym kodem.

using System.Collections.Generic;
 
namespace MvcMusicStore.Models
{
    public partial class Order
    {
        public int    OrderId    { get; set; }
        public string Username   { get; set; }
        public string FirstName  { get; set; }
        public string LastName   { get; set; }
        public string Address    { get; set; }
        public string City       { get; set; }
        public string State      { get; set; }
        public string PostalCode { get; set; }
        public string Country    { get; set; }
        public string Phone      { get; set; }
        public string Email      { get; set; }
        public decimal Total     { get; set; }
        public System.DateTime OrderDate      { get; set; }
        public List<OrderDetail> OrderDetails { get; set; }
    }
}

Ta klasa śledzi informacje o podsumowaniu i dostarczaniu zamówienia. Nie zostanie jeszcze skompilowany, ponieważ ma właściwość nawigacji OrderDetails, która zależy od klasy, której jeszcze nie utworzyliśmy. Naprawmy to teraz, dodając klasę o nazwie OrderDetail.cs, dodając następujący kod.

namespace MvcMusicStore.Models
{
    public class OrderDetail
    {
        public int OrderDetailId { get; set; }
        public int OrderId { get; set; }
        public int AlbumId { get; set; }
        public int Quantity { get; set; }
        public decimal UnitPrice { get; set; }
        public virtual Album Album { get; set; }
        public virtual Order Order { get; set; }
    }
}

Wprowadzimy jedną ostatnią aktualizację do klasy MusicStoreEntities, aby uwzględnić zestawy DbSet, które uwidacznią te nowe klasy modelu, w tym również artystę> DbSet<. Zaktualizowana klasa MusicStoreEntities jest wyświetlana jak poniżej.

using System.Data.Entity;
 
namespace MvcMusicStore.Models
{
    public class MusicStoreEntities : DbContext
    {
        public DbSet<Album>     Albums  { get; set; }
        public DbSet<Genre>     Genres  { get; set; }
        public DbSet<Artist>    Artists {
get; set; }
        public DbSet<Cart>     
Carts { get; set; }
        public DbSet<Order>     Orders
{ get; set; }
        public DbSet<OrderDetail>
OrderDetails { get; set; }
    }
}

Zarządzanie logiką biznesową koszyka zakupów

Następnie utworzymy klasę ShoppingCart w folderze Models. Model ShoppingCart obsługuje dostęp do danych do tabeli Cart. Ponadto będzie obsługiwać logikę biznesową w celu dodawania i usuwania elementów z koszyka.

Ponieważ nie chcemy wymagać od użytkowników zarejestrowania się na koncie tylko w celu dodania elementów do koszyka zakupów, przypiszemy użytkownikom tymczasowy unikatowy identyfikator (przy użyciu identyfikatora GUID lub globalnie unikatowego identyfikatora) podczas uzyskiwania dostępu do koszyka zakupów. Zapiszemy ten identyfikator przy użyciu klasy ASP.NET Session.

Uwaga: Sesja ASP.NET to wygodne miejsce do przechowywania informacji specyficznych dla użytkownika, które wygasną po opuszczeniu witryny. Chociaż niewłaściwe użycie stanu sesji może mieć wpływ na wydajność większych witryn, nasze światło będzie działać dobrze w celach demonstracyjnych.

Klasa ShoppingCart uwidacznia następujące metody:

Funkcja AddToCart przyjmuje album jako parametr i dodaje go do koszyka użytkownika. Ponieważ tabela Cart śledzi ilość dla każdego albumu, zawiera logikę tworzenia nowego wiersza w razie potrzeby lub po prostu zwiększa ilość, jeśli użytkownik zamówił już jedną kopię albumu.

RemoveFromCart pobiera identyfikator albumu i usuwa go z koszyka użytkownika. Jeśli użytkownik miał tylko jedną kopię albumu w koszyku, wiersz zostanie usunięty.

Funkcja EmptyCart usuwa wszystkie elementy z koszyka użytkownika.

Polecenie GetCartItems pobiera listę elementów CartItems do wyświetlania lub przetwarzania.

Funkcja GetCount pobiera łączną liczbę albumów, które użytkownik ma w koszyku.

Polecenie GetTotal oblicza całkowity koszt wszystkich elementów w koszyku.

Funkcja CreateOrder konwertuje koszyk na zamówienie w fazie wyewidencjonowania.

GetCart to metoda statyczna, która umożliwia naszym kontrolerom uzyskanie obiektu koszyka. Używa metody GetCartId do obsługi odczytywania identyfikatora CartId z sesji użytkownika. Metoda GetCartId wymaga metody HttpContextBase, aby można było odczytać identyfikator CartId użytkownika z sesji użytkownika.

Oto kompletna klasa ShoppingCart:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
 
namespace MvcMusicStore.Models
{
    public partial class ShoppingCart
    {
        MusicStoreEntities storeDB = new MusicStoreEntities();
        string ShoppingCartId { get; set; }
        public const string CartSessionKey = "CartId";
        public static ShoppingCart GetCart(HttpContextBase context)
        {
            var cart = new ShoppingCart();
            cart.ShoppingCartId = cart.GetCartId(context);
            return cart;
        }
        // Helper method to simplify shopping cart calls
        public static ShoppingCart GetCart(Controller controller)
        {
            return GetCart(controller.HttpContext);
        }
        public void AddToCart(Album album)
        {
            // Get the matching cart and album instances
            var cartItem = storeDB.Carts.SingleOrDefault(
                c => c.CartId == ShoppingCartId 
                && c.AlbumId == album.AlbumId);
 
            if (cartItem == null)
            {
                // Create a new cart item if no cart item exists
                cartItem = new Cart
                {
                    AlbumId = album.AlbumId,
                    CartId = ShoppingCartId,
                    Count = 1,
                    DateCreated = DateTime.Now
                };
                storeDB.Carts.Add(cartItem);
            }
            else
            {
                // If the item does exist in the cart, 
                // then add one to the quantity
                cartItem.Count++;
            }
            // Save changes
            storeDB.SaveChanges();
        }
        public int RemoveFromCart(int id)
        {
            // Get the cart
            var cartItem = storeDB.Carts.Single(
                cart => cart.CartId == ShoppingCartId 
                && cart.RecordId == id);
 
            int itemCount = 0;
 
            if (cartItem != null)
            {
                if (cartItem.Count > 1)
                {
                    cartItem.Count--;
                    itemCount = cartItem.Count;
                }
                else
                {
                    storeDB.Carts.Remove(cartItem);
                }
                // Save changes
                storeDB.SaveChanges();
            }
            return itemCount;
        }
        public void EmptyCart()
        {
            var cartItems = storeDB.Carts.Where(
                cart => cart.CartId == ShoppingCartId);
 
            foreach (var cartItem in cartItems)
            {
                storeDB.Carts.Remove(cartItem);
            }
            // Save changes
            storeDB.SaveChanges();
        }
        public List<Cart> GetCartItems()
        {
            return storeDB.Carts.Where(
                cart => cart.CartId == ShoppingCartId).ToList();
        }
        public int GetCount()
        {
            // Get the count of each item in the cart and sum them up
            int? count = (from cartItems in storeDB.Carts
                          where cartItems.CartId == ShoppingCartId
                          select (int?)cartItems.Count).Sum();
            // Return 0 if all entries are null
            return count ?? 0;
        }
        public decimal GetTotal()
        {
            // Multiply album price by count of that album to get 
            // the current price for each of those albums in the cart
            // sum all album price totals to get the cart total
            decimal? total = (from cartItems in storeDB.Carts
                              where cartItems.CartId == ShoppingCartId
                              select (int?)cartItems.Count *
                              cartItems.Album.Price).Sum();

            return total ?? decimal.Zero;
        }
        public int CreateOrder(Order order)
        {
            decimal orderTotal = 0;
 
            var cartItems = GetCartItems();
            // Iterate over the items in the cart, 
            // adding the order details for each
            foreach (var item in cartItems)
            {
                var orderDetail = new OrderDetail
                {
                    AlbumId = item.AlbumId,
                    OrderId = order.OrderId,
                    UnitPrice = item.Album.Price,
                    Quantity = item.Count
                };
                // Set the order total of the shopping cart
                orderTotal += (item.Count * item.Album.Price);
 
                storeDB.OrderDetails.Add(orderDetail);
 
            }
            // Set the order's total to the orderTotal count
            order.Total = orderTotal;
 
            // Save the order
            storeDB.SaveChanges();
            // Empty the shopping cart
            EmptyCart();
            // Return the OrderId as the confirmation number
            return order.OrderId;
        }
        // We're using HttpContextBase to allow access to cookies.
        public string GetCartId(HttpContextBase context)
        {
            if (context.Session[CartSessionKey] == null)
            {
                if (!string.IsNullOrWhiteSpace(context.User.Identity.Name))
                {
                    context.Session[CartSessionKey] =
                        context.User.Identity.Name;
                }
                else
                {
                    // Generate a new random GUID using System.Guid class
                    Guid tempCartId = Guid.NewGuid();
                    // Send tempCartId back to client as a cookie
                    context.Session[CartSessionKey] = tempCartId.ToString();
                }
            }
            return context.Session[CartSessionKey].ToString();
        }
        // When a user has logged in, migrate their shopping cart to
        // be associated with their username
        public void MigrateCart(string userName)
        {
            var shoppingCart = storeDB.Carts.Where(
                c => c.CartId == ShoppingCartId);
 
            foreach (Cart item in shoppingCart)
            {
                item.CartId = userName;
            }
            storeDB.SaveChanges();
        }
    }
}

Modele widoków

Nasz kontroler koszyka zakupów będzie musiał przekazać do swoich widoków pewne złożone informacje, które nie są czystą mapą na obiekty modelu. Nie chcemy modyfikować naszych modeli, aby odpowiadały naszym poglądom; Klasy modeli powinny reprezentować naszą domenę, a nie interfejs użytkownika. Jednym z rozwiązań byłoby przekazanie informacji do naszych widoków przy użyciu klasy ViewBag, tak jak w przypadku informacji rozwijanych Menedżer sklepu, ale przekazywanie wielu informacji za pośrednictwem funkcji ViewBag staje się trudne do zarządzania.

Rozwiązaniem tego problemu jest użycie wzorca ViewModel . W przypadku korzystania z tego wzorca tworzymy silnie typizowane klasy zoptymalizowane pod kątem naszych konkretnych scenariuszy wyświetlania oraz uwidaczniają właściwości wartości dynamicznych/zawartości wymaganej przez nasze szablony widoków. Nasze klasy kontrolerów mogą następnie wypełniać i przekazywać te klasy zoptymalizowane pod kątem widoku do naszego szablonu widoku do użycia. Dzięki temu funkcja IntelliSense w szablonach widoków umożliwia sprawdzanie bezpieczeństwa typów, sprawdzanie czasu kompilacji i edytora.

Utworzymy dwa modele wyświetlania do użycia w kontrolerze koszyka zakupów: Model ShoppingCartViewModel będzie przechowywać zawartość koszyka użytkownika, a model ShoppingCartRemoveViewModel będzie używany do wyświetlania informacji potwierdzenia, gdy użytkownik usunie coś z koszyka.

Utwórzmy nowy folder ViewModels w katalogu głównym naszego projektu, aby zachować elementy uporządkowane. Kliknij prawym przyciskiem myszy projekt, wybierz polecenie Dodaj/Nowy folder.

Zrzut ekranu przedstawiający okno projektu z menu prawym przyciskiem myszy z opcjami Dodaj i Nowy folder wyróżnionymi kolorem żółtym.

Nadaj folderowi nazwę ViewModels.

Zrzut ekranu przedstawiający Eksplorator rozwiązań przedstawiający nowo utworzony i nowo nazwany folder View Models (Wyświetl modele) wyróżniony czarnym polem.

Następnie dodaj klasę ShoppingCartViewModel w folderze ViewModels. Ma dwie właściwości: listę elementów koszyka i wartość dziesiętną do przechowywania całkowitej ceny wszystkich elementów w koszyku.

using System.Collections.Generic;
using MvcMusicStore.Models;
 
namespace MvcMusicStore.ViewModels
{
    public class ShoppingCartViewModel
    {
        public List<Cart> CartItems { get; set; }
        public decimal CartTotal { get; set; }
    }
}

Teraz dodaj model ShoppingCartRemoveViewModel do folderu ViewModels z następującymi czterema właściwościami.

namespace MvcMusicStore.ViewModels
{
    public class ShoppingCartRemoveViewModel
    {
        public string Message { get; set; }
        public decimal CartTotal { get; set; }
        public int CartCount { get; set; }
        public int ItemCount { get; set; }
        public int DeleteId { get; set; }
    }
}

Kontroler koszyka

Kontroler koszyka zakupów ma trzy główne cele: dodawanie elementów do koszyka, usuwanie elementów z koszyka i wyświetlanie elementów w koszyku. Użyjemy trzech właśnie utworzonych klas: ShoppingCartViewModel, ShoppingCartRemoveViewModel i ShoppingCart. Podobnie jak w przypadku kontrolek StoreController i StoreManagerController, dodamy pole do przechowywania wystąpienia klasy MusicStoreEntities.

Dodaj nowy kontroler koszyka zakupów do projektu przy użyciu szablonu Pusty kontroler.

Zrzut ekranu przedstawiający okno Dodawanie kontrolera z kontrolerem koszyka w polu Nazwa kontrolera i wyróżnione kolorem niebieskim.

Oto kompletny kontroler ShoppingCart. Akcje Indeks i Dodaj kontroler powinny wyglądać bardzo znajomo. Akcje kontrolera Usuń i CartSummary obsługują dwa specjalne przypadki, które omówimy w poniższej sekcji.

using System.Linq;
using System.Web.Mvc;
using MvcMusicStore.Models;
using MvcMusicStore.ViewModels;
 
namespace MvcMusicStore.Controllers
{
    public class ShoppingCartController : Controller
    {
        MusicStoreEntities storeDB = new MusicStoreEntities();
        //
        // GET: /ShoppingCart/
        public ActionResult Index()
        {
            var cart = ShoppingCart.GetCart(this.HttpContext);
 
            // Set up our ViewModel
            var viewModel = new ShoppingCartViewModel
            {
                CartItems = cart.GetCartItems(),
                CartTotal = cart.GetTotal()
            };
            // Return the view
            return View(viewModel);
        }
        //
        // GET: /Store/AddToCart/5
        public ActionResult AddToCart(int id)
        {
            // Retrieve the album from the database
            var addedAlbum = storeDB.Albums
                .Single(album => album.AlbumId == id);
 
            // Add it to the shopping cart
            var cart = ShoppingCart.GetCart(this.HttpContext);
 
            cart.AddToCart(addedAlbum);
 
            // Go back to the main store page for more shopping
            return RedirectToAction("Index");
        }
        //
        // AJAX: /ShoppingCart/RemoveFromCart/5
        [HttpPost]
        public ActionResult RemoveFromCart(int id)
        {
            // Remove the item from the cart
            var cart = ShoppingCart.GetCart(this.HttpContext);
 
            // Get the name of the album to display confirmation
            string albumName = storeDB.Carts
                .Single(item => item.RecordId == id).Album.Title;
 
            // Remove from cart
            int itemCount = cart.RemoveFromCart(id);
 
            // Display the confirmation message
            var results = new ShoppingCartRemoveViewModel
            {
                Message = Server.HtmlEncode(albumName) +
                    " has been removed from your shopping cart.",
                CartTotal = cart.GetTotal(),
                CartCount = cart.GetCount(),
                ItemCount = itemCount,
                DeleteId = id
            };
            return Json(results);
        }
        //
        // GET: /ShoppingCart/CartSummary
        [ChildActionOnly]
        public ActionResult CartSummary()
        {
            var cart = ShoppingCart.GetCart(this.HttpContext);
 
            ViewData["CartCount"] = cart.GetCount();
            return PartialView("CartSummary");
        }
    }
}

Aktualizacje Ajax z zestawem jQuery

Następnie utworzymy stronę Indeks koszyka zakupów, która jest silnie typizowana do modelu ShoppingCartViewModel i używa szablonu Widok listy przy użyciu tej samej metody, co poprzednio.

Zrzut ekranu przedstawiający okno Dodawanie widoku z polem Nazwa widoku, aparatem widoków, klasą modelu i szkieletami oraz selektorem użyj selektora plików układu.

Jednak zamiast używać elementu Html.ActionLink do usuwania elementów z koszyka, użyjemy jQuery do "podłączania" zdarzenia kliknięcia dla wszystkich linków w tym widoku, które mają klasę HTML RemoveLink. Zamiast publikować formularz, ta procedura obsługi zdarzeń kliknięcia spowoduje tylko wywołanie zwrotne AJAX do naszej akcji kontrolera RemoveFromCart. Funkcja RemoveFromCart zwraca wynik serializacji JSON, który wywołanie zwrotne jQuery następnie analizuje i wykonuje cztery szybkie aktualizacje strony przy użyciu zapytania jQuery:

    1. Usuwa usunięty album z listy
    1. Aktualizacje liczbę koszyków w nagłówku
    1. Wyświetla komunikat o aktualizacji dla użytkownika
    1. Aktualizacje łączną cenę koszyka

Ponieważ scenariusz usuwania jest obsługiwany przez wywołanie zwrotne Ajax w widoku indeksu, nie potrzebujemy dodatkowego widoku dla akcji RemoveFromCart. Oto kompletny kod widoku /ShoppingCart/Index:

@model MvcMusicStore.ViewModels.ShoppingCartViewModel
@{
    ViewBag.Title = "Shopping Cart";
}
<script src="/Scripts/jquery-1.4.4.min.js"
type="text/javascript"></script>
<script type="text/javascript">
    $(function () {
        // Document.ready -> link up remove event handler
        $(".RemoveLink").click(function () {
            // Get the id from the link
            var recordToDelete = $(this).attr("data-id");
            if (recordToDelete != '') {
                // Perform the ajax post
                $.post("/ShoppingCart/RemoveFromCart", {"id": recordToDelete },
                    function (data) {
                        // Successful requests get here
                        // Update the page elements
                        if (data.ItemCount == 0) {
                            $('#row-' + data.DeleteId).fadeOut('slow');
                        } else {
                            $('#item-count-' + data.DeleteId).text(data.ItemCount);
                        }
                        $('#cart-total').text(data.CartTotal);
                        $('#update-message').text(data.Message);
                        $('#cart-status').text('Cart (' + data.CartCount + ')');
                    });
            }
        });
    });
</script>
<h3>
    <em>Review</em> your cart:
 </h3>
<p class="button">
    @Html.ActionLink("Checkout
>>", "AddressAndPayment", "Checkout")
</p>
<div id="update-message">
</div>
<table>
    <tr>
        <th>
            Album Name
        </th>
        <th>
            Price (each)
        </th>
        <th>
            Quantity
        </th>
        <th></th>
    </tr>
    @foreach (var item in
Model.CartItems)
    {
        <tr id="row-@item.RecordId">
            <td>
                @Html.ActionLink(item.Album.Title,
"Details", "Store", new { id = item.AlbumId }, null)
            </td>
            <td>
                @item.Album.Price
            </td>
            <td id="item-count-@item.RecordId">
                @item.Count
            </td>
            <td>
                <a href="#" class="RemoveLink"
data-id="@item.RecordId">Remove
from cart</a>
            </td>
        </tr>
    }
    <tr>
        <td>
            Total
        </td>
        <td>
        </td>
        <td>
        </td>
        <td id="cart-total">
            @Model.CartTotal
        </td>
    </tr>
</table>

Aby to przetestować, musimy mieć możliwość dodawania elementów do koszyka. Zaktualizujemy widok Szczegóły sklepu , aby zawierał przycisk "Dodaj do koszyka". Chociaż jesteśmy na to, możemy dołączyć niektóre z albumu dodatkowe informacje, które dodaliśmy od czasu ostatniej aktualizacji tego widoku: Gatunek, Artysta, Cena i Album Art. Zaktualizowany kod widoku Szczegóły sklepu jest wyświetlany, jak pokazano poniżej.

@model MvcMusicStore.Models.Album
@{
    ViewBag.Title = "Album - " + Model.Title;
 }
<h2>@Model.Title</h2>
<p>
    <img alt="@Model.Title"
src="@Model.AlbumArtUrl" />
</p>
<div id="album-details">
    <p>
        <em>Genre:</em>
        @Model.Genre.Name
    </p>
    <p>
        <em>Artist:</em>
        @Model.Artist.Name
    </p>
    <p>
        <em>Price:</em>
        @String.Format("{0:F}",
Model.Price)
    </p>
    <p class="button">
        @Html.ActionLink("Add to
cart", "AddToCart", 
        "ShoppingCart", new { id = Model.AlbumId }, "")
    </p>
</div>

Teraz możemy kliknąć sklep i przetestować dodawanie i usuwanie albumów do i z koszyka zakupów. Uruchom aplikację i przejdź do indeksu sklepu.

Zrzut ekranu przedstawiający okno Sklep muzyczny ze szczegółami gatunku zdefiniowanymi na podstawie wszystkich danych albumu wprowadzonych do bazy danych.

Następnie kliknij gatunek, aby wyświetlić listę albumów.

Zrzut ekranu przedstawiający okno Sklep muzyczny z listą albumów skojarzonych z gatunkiem Disco w bazie danych albumów.

Kliknięcie tytułu albumu spowoduje wyświetlenie zaktualizowanego widoku Szczegóły albumu, w tym przycisku "Dodaj do koszyka".

Zrzut ekranu przedstawiający okno Sklep muzyczny z zaktualizowanym widokiem Szczegóły albumu i przyciskiem Dodaj do koszyka.

Kliknięcie przycisku "Dodaj do koszyka" spowoduje wyświetlenie widoku Indeks koszyka zakupów z listą podsumowania koszyka.

Zrzut ekranu przedstawiający okno Sklep muzyczny z widokiem Koszyk z listą podsumowania wszystkich elementów w koszyku.

Po załadowaniu koszyka możesz kliknąć link Usuń z koszyka, aby wyświetlić aktualizację Ajax koszyka.

Zrzut ekranu przedstawiający okno Sklep muzyczny z widokiem Koszyk zakupów z albumem usuniętym z listy podsumowań.

Utworzyliśmy działający koszyk, który umożliwia wyrejestrowanym użytkownikom dodawanie przedmiotów do koszyka. W poniższej sekcji umożliwimy im zarejestrowanie się i ukończenie procesu wyewidencjonowania.