VIII.1. Introduction▲
Nous allons permettre aux utilisateurs de placer des albums dans leurs paniers sans s'identifier, mais ils auront besoin de s'identifier en tant qu'invités pour compléter la commande. Les processus d'achat et de paiement seront séparés en deux contrôleurs : un ShoppingCartController qui permet d'ajouter anonymement des objets au panier, et un CheckoutController qui gère le processus de paiement. Nous allons commencer avec le panier dans cette section, puis construire le processus de paiement dans la section suivante.
VIII.2. Ajout des classes modèles Cart, Order et OrderDetail▲
Nos processus de paiement et de panier feront usage de nouvelles classes. Faites un clic-droit sur le dossier Models et ajoutez une classe Cart (Cart.cs) avec le code suivant.
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
;
}
}
}
Cette classe est assez similaire à d'autres classes que nous avons déjà utilisées jusqu'à présent, à l'exception de l'attribut [Key] pour la propriété RecordId. Nos éléments du panier auront un identifiant string appelé CartID qui permet les achats anonymes, mais la table inclut un entier en clé primaire appelé RecordId. Par convention, Entity Framework Code-First s'attend à ce que la clé primaire pour une table appelée Cart soit CartId ou ID, mais nous pouvons facilement changer cela par des annotations ou par code si on veut. Ceci est un exemple de la façon dont on peut utiliser les conventions simples d'Entity Framework Code-First quand elles nous conviennent, mais nous ne sommes pas contraints par elles quand ce n'est pas le cas.
Ensuite, ajoutez une classe Order (Order.cs) avec le code suivant.
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
;
}
}
}
Cette classe trace le résumé et les informations de livraison pour une commande. Elle ne compile pas encore, car elle contient une propriété de navigation OrderDetails qui dépend d'une classe que nous n'avons pas encore créée. Corrigeons ceci maintenant en ajoutant une classe appelée OrderDetail.cs, en ajoutant le code suivant.
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
;
}
}
}
Nous allons faire une dernière mise à jour de notre classe MusicStoreEntities afin d'inclure des DbSets qui exposent ces nouvelles classes modèle, et en incluant également un DbSet<Artist>. La classe MusicStoreEntities mise à jour apparaît ci-dessous.
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
;
}
}
}
VIII.3. Gestion de la logique métier du Panier▲
Ensuite, nous allons créer la classe ShoppingCart dans le dossier Models. Le modèle ShoppingCart gère l'accès aux données de la table Panier. De plus, cela va gérer la logique métier à l'ajout et la suppression des éléments du panier.
Puisque nous ne voulons pas obliger les utilisateurs à s'inscrire juste pour ajouter des éléments à leurs paniers, nous allons leur assigner un identifiant unique temporaire (en utilisant un GUID, ou un identifiant globalement unique) lorsqu'ils accèdent au panier. Nous allons stocker cet ID en utilisant la classe Session d'ASP.NET.
Note : La Session d'ASP.NET est un endroit pratique pour stocker des informations spécifiques à un utilisateur qui expireront après avoir quitté le site. Tout abus de l'état de session peut avoir des conséquences sur les performances de sites plus importants, l'utilisation que nous en ferons conviendra parfaitement à des fins de démonstrations.
La classe ShoppingCart expose les méthodes suivantes :
AddToCart prend un album en paramètre et l'ajoute au panier de l'utilisateur. Puisque la table Panier recense la quantité de chaque album, cela inclut une logique de création d'une nouvelle ligne si nécessaire ou juste l'incrémentation de la quantité si l'utilisateur a déjà commandé une copie de l'album.
RemoveFromCart prend un ID d'Album et l'enlève du panier de l'utilisateur. Si l'utilisateur n'a qu'une copie de l'album dans son panier, la ligne est supprimée.
EmptyCart enlève tous les éléments du panier de l'utilisateur.
GetCartItems récupère une liste des CartItems pour l'affichage ou le traitement.
GetCount récupère le nombre total d'albums qu'un utilisateur a dans son panier.
GetTotal calcule le coût total de tous les éléments présents dans le panier.
CreateOrder convertit le panier en une commande pendant la phase de paiement.
GetCart méthode statique qui permet à nos contrôleurs d'obtenir un objet panier. Elle utilise la méthode GetCartId pour gérer la lecture du CartId depuis la session utilisateur. La méthode GetCartId nécessite le HttpContextBase afin qu'elle puisse lire le CartId de l'utilisateur depuis sa session.
Voici la classe ShoppingCart complète :
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
(
);
}
}
}
VIII.4. ViewModels▲
Notre ShoppingCartController aura besoin de communiquer quelques informations complexes à ses vues qui ne correspondent pas proprement à nos objets modèles. Nous ne voulons pas modifier nos modèles pour que cela convienne à nos vues ; les classes modèles devraient représenter notre domaine, et non l'interface utilisateur. Une solution serait de passer l'information à nos vues en utilisant la classe ViewBag, comme nous l'avons fait avec les informations des dropdownlists du Store Manager, mais passer beaucoup d'informations via le ViewBag est difficile à gérer.
Une solution serait d'utiliser le pattern ViewModel. Lorsque l'on utilise ce pattern, nous créons des classes fortement typées qui sont optimisées pour les scénarios de notre vue spécifique, et qui exposent les propriétés pour les valeurs/contenus dynamiques dont la vue à besoin. Nos classes contrôleur peuvent remplir et transmettre ces classes optimisées à nos vues afin de les utiliser. Cela permet un type sécurisé, lors de la compilation, et la présence de l'IntelliSense dans les vues.
Nous allons créer deux Vues Modèles pour les utiliser dans notre contrôleur Shopping Cart : le ShoppingCartViewModel va s'occuper du contenu du panier de l'utilisateur, et le ShoppingCartRemoveViewModel sera utilisé pour afficher les informations de confirmation lorsqu'un utilisateur enlève quelque chose de son panier.
Nous allons créer un dossier ViewModels à la racine du projet pour garder les choses organisées. Faites un clic droit sur le projet, sélectionnez Add / New Folder.
Appelez le dossier ViewModels.

Ensuite, ajoutez la classe modèle ShoppingCartViewModel dans le dossier ViewModels. Elle a deux propriétés : une liste d'éléments Cart, et une valeur décimale qui contient le prix total de tous les éléments du panier.
using
System.
Collections.
Generic;
using
MvcMusicStore.
Models;
namespace
MvcMusicStore.
ViewModels
{
public
class
ShoppingCartViewModel
{
public
List<
Cart>
CartItems {
get
;
set
;
}
public
decimal
CartTotal {
get
;
set
;
}
}
}
Maintenant, ajoutez le ShoppingCartRemoveViewModel au dossier ViewModels, avec les quatre propriétés suivantes.
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
;
}
}
}
VIII.5. Le ShoppingCartController▲
Le ShoppingCartController a trois buts principaux : ajouter des éléments au panier, enlever des éléments du panier, et visualiser les éléments du panier. Il fera usage des trois classes que nous venons juste de créer : ShoppingCartViewModel, ShoppingCartRemoveViewModel, et ShoppingCart. Comme dans StoreController et StoreManagerController, nous allons ajouter un champ qui sera une instance de MusicStoreEntities.
Ajoutez le ShoppingCartController en utilisant un template de contrôleur vide.
Voici le contrôleur ShoppingCartController complet. Les actions Index et Add devraient être très familières. Les actions Remove et CartSummary gèrent deux cas spéciaux, dont nous discuterons dans la section suivante.
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"
);
}
}
}
VIII.6. Utilisation d'Ajax et jQuery pour les mises à jour▲
Nous allons ensuite créer une page Index du Panier qui est fortement typée au ShoppingCartViewModel et qui utilise le template List utilisant la même méthode que précédemment.
Cependant, au lieu d'utiliser un Html.ActionLink pour enlever les éléments du panier, nous utiliserons jQuery pour "lier" l'événement click pour tous les liens dans cette vue qui ont la classe HTML RemoveLink. Plutôt que de poster le formulaire, ce gestionnaire d'événements va juste faire un appel AJAX à notre action RemoveFromCart. Le RemoveFromCart renvoie un résultat JSON sérialisé, qui est automatiquement transmis à la méthode JavaScript spécifiée dans notre paramètre OnSuccess- handleUpdate dans ce cas. La fonction Javascript handleUpdate analyse les résultats JSON et effectue quatre mises à jour rapides de la page en utilisant jQuery :
- Enlève les albums supprimés de la liste
- Met à jour le compteur du panier dans l'entête
- Affiche un message de mise à jour à l'utilisateur
- Met à jour le prix total du panier
Puisque le scénario de suppression est géré par un appel Ajax dans la vue Index, nous n'avons pas besoin d'une vue supplémentaire pour l'action RemoveFromCar. Voic le code complet de la vue /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 +
')'
);
}
);
}
}
);
}
);
function handleUpdate
(
) {
// Load and deserialize the returned JSON data
var
json =
context.
get_data
(
);
var
data =
Sys.
Serialization.
JavaScriptSerializer.
deserialize
(
json);
// 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>
Afin de tester ceci, nous devons être en mesure d'ajouter des éléments de notre panier. Nous allons mettre à jour notre vue Store Details afin d'inclure un bouton "Add to cart". Tant qu'on y est, nous pouvons inclure certaines informations additionnelles de l'album que nous avons ajoutées depuis que nous avons fait la dernière mise à jour de cette vue : Genre, Artist, Price, et Album Art. Le code mis à jour de la vue Details du Store devrait apparaître comme suit.
@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>
Maintenant nous pouvons cliquer sur le store et essayer d'ajouter et enlever des albums de notre panier. Exécutez l'application et allez sur l'Index du Store.
Puis, cliquez sur un genre pour voir la liste des albums.
Maintenant, le fait de cliquer sur le titre d'un album nous montre la vue Details de l'album mise à jour, incluant le bouton "Add to cart".
Cliquer sur le bouton "Add to cart" montre la vue Index de notre panier avec la liste des éléments.
Après le chargement de votre panier, vous pouvez cliquer sur le lien Remove du panier pour voir Ajax faire ses mises à jour.
Nous avons construit un panier qui fonctionne et qui permet aux utilisateurs non enregistrés d'ajouter des éléments à leurs paniers. Dans la section suivante, nous allons leur permettre de s'inscrire et de compléter le processus de paiement.
Vous pouvez utiliser les discussions disponibles sur http://mvcmusicstore.codeplex.com pour toutes questions ou tous commentaires.