Jedną z podstawowych funkcjonalności prawie każdej aplikacji e-commerce jest koszyk. Jak możemy przechowywać jego stan? Jak najłatwiej umieścić go w kontrolerze? Jeśli jeszcze tego nie wiecie, to dowiecie się za chwilę.
Kluczową decyzją przy tworzeniu koszyka nie jest jego budowa czy metody jakie będzie zawierał. Te są stosunkowo proste, bo i nie ma w nim miejsca na skomplikowane elementy. Miejsce na produkty, ceny, ilości, dodawanie i wywalanie zakupów to raczej typowy boilerplate. O wiele ważniejszą decyzją do podjęcia jest przechowywanie stanu koszyka w określony sposób.
Stan koszyka można przechowywać na kilka różnych sposobów, ale mi osobiście jedynymi sensownymi opcjami wydały się:
-
Przechowywanie stanu koszyka w bazie danych, co umożliwia dużą żywotność danych i daje możliwość kompletowania zamówień nawet przez kilka dni, a przy powiązaniu koszyka z zarejestrowanym kontem nawet dostęp z kilku różnych urządzeń.
-
Przechowywanie w słowniku Session, co ogranicza życie koszyka do jednej sesji, ale jest o wiele prostsze do zrealizowania.
I przy tej decyzji mamy do czynienia z tym, o czym pisałem jakiś czas temu. Mowa o kontekście, w jakim będzie funkcjonowała cała aplikacja. O ile przechowywanie koszyka w bazie danych i dokładanie do niego zakupów przez kilka dni jest całkiem fajne i może być użyteczne, to ile osób robiłoby coś takiego zamawiając jedzenie? Nie wiem jak Wy, ale ja zamawiam jedzenie raczej pod wpływem impulsu (i pewnie temu powinienem zacząć biegać). Nie zdarzyło mi się nigdy napełniać koszyka pizzą i zamówić ją kilka godzin później, umarłbym z głodu.
W związku z tym uznałem, że przechowywanie koszyka w danych sesji będzie najlepszym rozwiązaniem dla mojego projektu. Na dodatek jest ono banalnie proste do wdrożenia, bo jedyne co musimy zrobić to utworzyć Model Binder(łącznik modelu), który będzie tworzył instancję koszyka lub dostarczał nam instancję, która jest przechowywana w sesji za każdym razem, gdy metoda będzie potrzebowała koszyka na wejściu.
Tworzenie łącznika jest stosunkowo łatwe. Wystarczy stworzyć nową klasę, która będzie implementowała interfejs IModelBinder z przestrzeni nazw System.Web.Mvc. Metoda BindModel, którą musimy stworzyć implementując interfejs ma jedno zadanie – dostarczenie instancji obiektu Cart. Jeśli jest ona przechowywana w danych sesji, to właśnie ona zostanie nam zwrócona. W innym wypadku tworzona jest od nowa, zapisywana w sesji i zwracana. Cały Model Binder w moim wypadku wygląda tak:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class CartModelBinder : IModelBinder { //Session key private const string key = "Cart"; public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { Cart cart = null; //If session dict is not null - get cart from session dict if (controllerContext.HttpContext.Session != null) { cart = (Cart)controllerContext.HttpContext.Session[key]; } //If there is no active cart instance, create new one if (cart == null && controllerContext.HttpContext.Session != null) { cart = new Cart(); controllerContext.HttpContext.Session[key] = cart; } return cart; } } |
Aby aplikacja korzystała z naszego bindera musimy jeszcze dodać jeszcze jedną, krótką linijkę kodu w metodzie Application_Start w pliku global.asax.cs. Oto i ona:
1 |
ModelBinders.Binders.Add(typeof(Cart), new CartModelBinder()); |
Od teraz jeśli metoda kontrolera przyjmuje na wejściu obiekt Cart i nie dostarczymy go do niej w inny sposób, to będzie pozyskiwać referencję do koszyka na zasadach określonych w łączniku. Bardzo prosty przykład tego jak wygląda wywołanie metody operującej na koszyku możecie zobaczyć poniżej.
1 2 3 4 5 6 |
public ActionResult AddProductToCart(Cart cart, int id, int quantity = 1) { Product item = uow.Repository<Product>().Select(p => p.ID == id); cart.AddItem(item, quantity); return RedirectToAction("Index"); } |
Aby zakończyć koszyk musimy jeszcze napisać trochę rutynowego kodu do dodawania/usuwania poszczególnych pozycji i … to tyle. To naprawdę jest tak proste.