GRATUIT

Vos offres d'emploi informatique

Développeurs, chefs de projets, ingénieurs, informaticiens
Postez gratuitement vos offres d'emploi ici visibles par 4 000 000 de visiteurs uniques par mois

emploi.developpez.com

Developpez.com - Microsoft DotNET
X

Choisissez d'abord la catégorieensuite la rubrique :


Traduction du MVC Music Store Partie 8 : Panier avec utilisation d'Ajax pour les mises à jour

Date de publication : 10/01/2012. Date de mise à jour : 10/01/2012.

Par Jon Galloway (Blog)
 Jean-Michel Ormes (Traduction) (Home) (Blog)
 

Le MVC Music Store est une application d'étude qui introduit et explique étape par étape comment utiliser ASP.NET MVC et Visual Web Developer pour faire du développement web. Le MVC Music Store est un exemple simplifié d'application de magasin qui vend des albums de musique en ligne, implémente l'administration du site, l'authentification d'utilisateurs et la fonctionnalité d'achat sous forme de panier. Ce tutoriel détaille toutes les étapes à suivre pour construire le MVC Music Store. La partie 8 du tutoriel s'intitule "Panier avec utilisation d'Ajax pour les mises à jour".
Retrouvez l'ensemble des articles de la série sur cette page : MVC Music Store
N'hésitez pas à laisser votre avis sur le contenu de l'article directement via le forum : 5 commentaires Donner une note à l'article (5)

       Version PDF (Miroir)   Version hors-ligne (Miroir)
Viadeo Twitter Facebook Share on Google+        



Traduction
1. Introduction
2. Ajout des classes modèles Cart, Order et OrderDetail
3. Gestion de la logique métier du Panier
4. ViewModels
5. Le ShoppingCartController
6. Utilisation d'Ajax et jQuery pour les mises à jour
Remerciements


Traduction

Cet article est la traduction non officielle de l'article original de en Jon Galloway,en Part 8: Shopping Cart with Ajax Updates

Le code source ainsi que le support technique sont disponibles à cette adresse : en http://mvcmusicstore.codeplex.com


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.


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

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

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

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

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 :

  1. Enlève les albums supprimés de la liste
  2. Met à jour le compteur du panier dans l'entête
  3. Affiche un message de mise à jour à l'utilisateur
  4. 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 en http://mvcmusicstore.codeplex.com pour toutes questions ou tous commentaires.


Remerciements

Je tiens à remercier en Jon Galloway pour son aimable autorisation de traduire l'article, ainsi que Thomas Levesque et jpcheck pour la relecture orthographique.



               Version PDF (Miroir)   Version hors-ligne (Miroir)

Valid XHTML 1.0 TransitionalValid CSS!