Writing an Orchard Webshop Module from scratch - Part 7

7. Rendering the ShoppingCart and ShoppingCartWidget

This post has been updated to be compatible for Orchard >= 1.4.

This is part 7 of a tutorial on writing a new Orchard module from scratch.
For an overview of the tutorial, please see the introduction.

In this part, we will:

  • Create a webpage (view) for the shoppingcart
  • Have a look at IContentManager.GetItemMetadata.
  • Enable our users to update the quantity and remove items from the shopping cart;
  • Create a widget that will be visible on every page and shows a link to the shopping cart page, as well as the total number of items in the shopping cart;
  • Show how to include (and expose) resources with our module using the IResourceManifestProvider;
  • Apply the MVVM pattern using KnockoutJS and jQuery on the client side to enable our users to update the quantities of the shoppingcart items, as well as update the shoppingcart "widget" in realtime.


So let's get started!


Creating the display of the shoppingcart

First of all, we'll add some markup to our "ShoppingCart.cshtml" template, which currently looks like this:

Views/ShoppingCart.cshtml:

TODO: display our shopping cart contents!

This view should render the complete contents of the shopping cart.
So our first step is to replace the contents of "ShoppingCart.cshtml" with the following markup:

Views/ShoppingCart.cshtml:

@{
    Style.Require("Skywalker.Webshop.ShoppingCart");
}
<article class="shoppingcart">
    <table>
        <thead>
            <tr>
                <td>Article</td>
                <td class="numeric">Quantity</td>
                <td class="numeric">Price</td>
                <td></td>
            </tr>
        </thead>
        <tbody>
            @for (var i = 0; i < 5; i++) {
                <tr>
                    <td>Product title</td>
                    <td class="numeric"><input type="number" value="1" /></td>
                    <td class="numeric">$9.99</td>
                    <td><a class="icon delete" href="#"></a></td>
                </tr>
            }
 
        </tbody>
        <tfoot>
            <tr class="separator"><td colspan="4">&nbsp;</td></tr>
            <tr>
                <td class="numeric label" colspan="2">VAT (19%):</td>
                <td class="numeric">$9.99</td>
                <td></td>
            </tr>
            <tr>
                <td class="numeric label" colspan="2">Total:</td>
                <td class="numeric">$9.99</td>
                <td></td>
            </tr>
        </tfoot>
    </table>
    <footer>
        <div class="group">
            <div class="align left"><a class="button" href="#">Continue shopping</a></div>
            <div class="align right"><a class="button next" href="#">Proceed to checkout</a></div>
        </div>
    </footer>
</article>

Managing Resources

The first interesting thing to notice is the second line:

Style.Require("Skywalker.Webshop.ShoppingCart");

What's this, precious? That, my love, is something quite useful: it's part of Orchard's resource management API that enables module developers to add stylesheets and scripts to the final output of the page being rendered, as well as making reusable styles and scripts available to other modules.

Style is a property on the Orchard.Mvc.ViewEngines.Razor.WebViewPage<T> class, which we can use to instruct Orchard to include a stylesheet when our view is being rendered.

The Require method takes the name of a resource, which we define using a resource manifest.
I should add that we could have  used the Include instead of the Require method: Include takes the relative path to the resource to be included and does not require us to create a resource manifest. However, using a resource manifest enables us to define dependencies between resources, which is really neat. For example, if you wrote a Javascript file that had a dependency on some jQuery UI script, you could define this dependency using the resource manifest. When you include you include your script in a page using the Require method, Orchard automatically includes the dependency resources as well.
However, the primary purpose of resource manifests is probably to expose resources from the module. However, in this tutorial we will demonstrate the resource manifest API. 

A resource manifest is implemented as a simple class that implements the IManifestResourceProvider interface.

Let's go ahead and create a new class in the root of our project named "ResourceManifest.cs":

ResourceManifest.cs:

using Orchard.UI.Resources;
 
namespace Skywalker.Webshop
{
    public class ResourceManifest : IResourceManifestProvider
    {
        public void BuildManifests(ResourceManifestBuilder builder)
        {
            // Create and add a new manifest
            var manifest = builder.Add();
 
            // Define a "common" style sheet
            manifest.DefineStyle("Skywalker.Webshop.Common").SetUrl("common.css");
 
            // Define the "shoppingcart" style sheet
            manifest.DefineStyle("Skywalker.Webshop.ShoppingCart").SetUrl("shoppingcart.css").SetDependencies("Skywalker.Webshop.Common");
        }
    }
}


IResourceManifestProvider defines just one method called BuildManifest, which receives a ResourceManifestBuilder argument.
We use this builder to create and add a new manifest, which is used to register our resources. The Add method creates a new instance of ResourceManifest and adds it to the manifest builder.

In our case, we register two resources: a "common.css" file and a "shoppingcart.css" file using the DefineStyle method. Although I am using a naming scheme here where the name of the resource starts with the namespace of the module, it is not required. It just makes sure that there won't be any conflict with other modules who may expose resources using the same name.
By convention, Orchard expects stylesheets to be stored in a folder called Styles, but the ResourceManifest class contains methods to change this if you wanted to.

Note that we set the "Skywalker.Webshop.ShoppingCart" resource to be dependent on the "Skywalker.Webshop.Common" resource.
This enables us to just reference "Skywalker.Webshop.ShoppingCart" in our "ShoppingCart.cshtml" template, without having to reference "Webshop.Common" as well. Orchard will automatically include dependent resources. Very nice.

Let's create a folder called Styles in the root of our project and add two .css files to it:

Styles/common.css:

.group .align.left {
     float: left;
}
 
.group .align.right {
     float: right;
}
 
.icon {
    display: inline-block;
    width: 16px;
    height: 16px;
    background: url("../images/sprites.png");
}
 
.icon.edit {
    background-position: -8px -40px;
}
 
.icon.edit:hover {
    background-position: -40px -40px;
}
 
.icon.delete {
    background-position: -8px -7px;
}
 
.icon.delete:hover {
    background-position: -39px -7px;
}

 

Styles/shoppingcart.css:

article.shoppingcart {
    width: 500px;
}
 
article.shoppingcart table {
    width: 100%;   
}
 
article.shoppingcart td {
    padding: 7px 3px 4px 4px;
}
 
article.shoppingcart table thead td {
    background: #f6f6f6;
    font-weight: bold;
}
 
article.shoppingcart table tfoot tr.separator td {
    border-bottom: 1px solid #ccc;
}
 
article.shoppingcart table tfoot td {
    font-weight: bold;
}
 
article.shoppingcart footer {
    margin-top: 20px;
}
 
article.shoppingcart td.numeric {
    width: 75px;
    text-align: right;
}
 
article.shoppingcart td.numeric input {
    width: 50px;
}


Notice that common.css references an image file called sprites.png, which is stored in a folder called Images.
You can download it right now and include it if you're coding along (Right click... save as into a new folder called Images):

sprites.png:

 

Let's see what we've accomplished so far:

 

Well, I might not be much of a designer, but this is definitely not what we'd expect after all that css coding.
Apparently the CSS is not being applied for some reason. Let's see if F12 Developer Tools can give us a hint:

Aha. Requesting the two .css files both returned a 404 error, even though the urls are correct.


Allowing resources to be downloaded

So what's up? Well, Orchard's installation comes with a web.config file that by default maps all requests to physical files to the System.Web.HttpNotFoundHandler. The reason, I assume, is to prevent access to files on disk that should not be (directly) accessible from the outside world. Obviously, we need to override this for our styles and images folders. The way to do this is easy: just create a web.config file in the Styles and Images folders and paste in the following configuration:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <appSettings>
    <add key="webpages:Enabled" value="false" />
  </appSettings>
  <system.web>
    <httpHandlers>
      <!-- iis6 - for any request in this location, return via managed static file handler -->
      <add path="*" verb="*" type="System.Web.StaticFileHandler" />
    </httpHandlers>
  </system.web>
  <system.webServer>
    <handlers accessPolicy="Script,Read">
      <!--
      iis7 - for any request to a file exists on disk, return it via native http module.
      accessPolicy 'Script' is to allow for a managed 404 page.
      -->
      <add name="StaticFile" path="*" verb="*" modules="StaticFileModule" preCondition="integratedMode" resourceType="File" requireAccess="Read" />
    </handlers>
  </system.webServer>
</configuration>


This will effectively map requests to physical files in the containing folder to the System.Web.StaticFileHandler.
Right now we had to do this ourselves, but Orchard comes with a command line tool to create a new Module project that creates a basic skeleton for us, including a "Styles" and "Scripts" folder that contain a web.config with the required configuration.
To learn more, check out the docs.

Now let's refresh the shopping cart page:


That's more like it!

 

Displaying shopping cart data instead of hardcoded values

The next step is to have the view display the actual contents of the shopping cart.
We could do this by adding the ShoppingCart instance to the ShoppingCart shape from the ShoppingCartController, but remember that each ShoppinCartItem only holds the ID of the product, not the product item itself.
That would mean we need to query the database from within our rendering code. Although technically not a problem, we should adhere to good practices and prepare our view data from our controller into easy consumable data to be used by our views.

Preparing view data from our controller can be done in different ways. One way is to write one or two view models and pass them to the ShoppingCart shape, and that would be perfectly fine.
However, with Orchard, it's very easy to use dynamic shapes, so let's see how that works.

Open ShoppinCartController.cs and modify it as follows:

[Themed]
        public ActionResult Index() {
 
            // Create a new shape using the "New" property of IOrchardServices.
            var shape = _services.New.ShoppingCart();
 
            // Create a LINQ query that projects all items in the shoppingcart into shapes
            var query = from item in _shoppingCart.Items
                        let product = _shoppingCart.GetProduct(item.ProductId)
                        select _services.New.ShoppingCartItem(
                            Product: product,
                            Quantity: item.Quantity
                        );
 
            // Execute the LINQ query and store the results on a property of the shape
            shape.Products = query.ToList();
 
            // Store the grand total, sub total and VAT of the shopping cart in a property on the shape
            shape.Total    = _shoppingCart.Total();
            shape.Subtotal = _shoppingCart.Subtotal();
            shape.Vat      = _shoppingCart.Vat();
 
            // Return a ShapeResult
            return new ShapeResult(this, shape);
        }

As you can see, the LINQ query projects each ShoppingCartItem into a shape with coincidentally the same name which receives the following properties:

  • Product (ProductPart)
  • Quantity (Int32)

We then store the results of the query on to a new property called Products on the shape that we will return from the action, as well as some other properties: Total, Subtotal and VAT.
This information will be used by our ShoppingCart view. 

A quick word of caution
Before we continue, I'd like to quickly mention that certain property names are reserved for shapes, one of which being Items.
In the previous version of this post, I demonstrated what happens when we used Items instead of Products as the property name for our shape: nothing showed up in the view.
Although I don't have a list of reserved names, just be aware of this fact when you're wondering why something isn't showing: try a different property name if you suspect the name is reserved.


Optimizing the query

Now, you can say a lot about the query we just saw, but one thing you can't say is that it's very efficient.
For one thing, it hits the database for each iteration to load the ProductPart. This is known as an N+1 query problem and is the cause of much grieve when it comes to performance.

Let's see if we can improve that query so that it only needs to hit the database once and return all products and content items.

Orchard provides an IContentManager service that we can access via IOrchardServices (which we take as a dependency via the constructor).
IContentManager has a method called GetMany<TContent>, where TContent is constrained to be a class that implements IContent. The argument is expecting an enumerable of ids.
As it so happens to be, all content parts implement IContent, including our own ProductPart class (by inheriting from ContentPart, which implements IContent).

Let's modify our query to take advantage of the GetMany method:

 // Get a list of all product IDs from the shopping cart
            var ids = _shoppingCart.Items.Select(x => x.ProductId).ToList();
 
            // Load all product parts by the list of IDs
            var productParts = _services.ContentManager.GetMany<ProductPart>(ids, VersionOptions.Latest, QueryHints.Empty).ToArray();
            
            // Create a LINQ query that projects all items in the shoppingcart into shapes
            var query = from item in _shoppingCart.Items
                        from productPart in productParts where productPart.Id == item.ProductId
                        select _services.New.ShoppingCartItem(
                            Product: productPart,
                            Quantity: item.Quantity
                        );

This is much better: we first project our shopping cart items into an enumerable of product IDs.
Next, we use the ContentManager to fetch all product parts in a single call by passing in a list of product IDs. We then project the results into a list.

The query is now a simple SelectMany operation (using Linq query syntax) on two in-memory lists, which performs much better.

Seperating domain logic from controller logic

The next thing we need to do is move this query out of the Controller. It is good practice to separate domain logic from controller logic and keep your controllers as thin as possible. This allows the query to be reused from other parts of the application as well.
However, the part where we project shoppingcart data into a shape is a concern of the controller, so we need to break these two things up.
What we want is the ShoppingCart class to return a list of ProductParts as well as the quantity for each product.

Let's introduce a new class for that: ProductQuantity:

Models/ProductQuantity.cs:

namespace Skywalker.Webshop.Models
{
    public sealed class ProductQuantity {
        public ProductPart ProductPart { getset; }
        public int Quantity { getset; }
    }
 
}

Next we'll modify IShoppingCart to include a method called GetProducts:

Services/IShoppingCart.cs:

using System.Collections.Generic;
using Orchard;
using Skywalker.Webshop.Models;
 
namespace Skywalker.Webshop.Services
{
    public interface IShoppingCart : IDependency {
        IEnumerable<ShoppingCartItem> Items { get; }
        void Add(int productId, int quantity = 1);
        void Remove(int productId);
        ProductPart GetProduct(int productId);
        IEnumerable<ProductQuantity> GetProducts();
        decimal Subtotal();
        decimal Vat();
        decimal Total();
        int ItemCount();
    }
}


We'll implement that method in ShoppingCart

Services/ShoppingCart.cs:

 public IEnumerable<ProductQuantity> GetProducts()
        {
            // Get a list of all product IDs from the shopping cart
            var ids = Items.Select(x => x.ProductId).ToList();
 
            // Load all product parts by the list of IDs
            var productParts = _contentManager.GetMany<ProductPart>(ids, VersionOptions.Latest, QueryHints.Empty).ToArray();
 
            // Create a LINQ query that projects all items in the shoppingcart into shapes
            var query = from item in Items
                        from productPart in productParts
                        where productPart.Id == item.ProductId
                        select new ProductQuantity {
                            ProductPart = productPart,
                            Quantity = item.Quantity
                        };
 
            return query;
        }

Next, we'll update ShoppingCartController to leverage the GetProducts method of IShoppingCart:

Controllers/ShoppingcartController.cs:

        [Themed]
        public ActionResult Index() {
 
            // Create a new shape using the "New" property of IOrchardServices.
            var shape = _services.New.ShoppingCart(
                Products: _shoppingCart.GetProducts().ToList(),
                Total: _shoppingCart.Total(),
                Subtotal: _shoppingCart.Subtotal(),
                Vat: _shoppingCart.Vat()
            );
 
            // Return a ShapeResult
            return new ShapeResult(this, shape);
        }


That's looking much better: we ask the ShoppingCart service to return a list of products. Note that we aren't projecting it into a list of shapes anymore, since the ProductQuantity type holds all the information that we need.


Updating the ShoppingCart.cshtml template

Now that the controller is updated to provide view data, let's update "ShoppingCart.cshtml" to make use of it:

Views/ShoppingCart.cshtml:

@using Orchard.ContentManagement
@using Orchard.Core.Title.Models
@using Skywalker.Webshop.Models
@{
     Style.Require("Skywalker.Webshop.ShoppingCart");
     var items = (IList<ProductQuantity>)Model.Products;
     var subtotal = (decimal) Model.Subtotal;
     var vat = (decimal) Model.Vat;
     var total = (decimal) Model.Total;
 }
<article class="shoppingcart">
    <table>
        <thead>
            <tr>
                <td>Article</td>
                <td class="numeric">Quantity</td>
                <td class="numeric">Price</td>
                <td></td>
            </tr>
        </thead>
        <tbody>
            @foreach (var item in items) {
                 var product = item.ProductPart;
                 var titlePart = product.As<TitlePart>();
                 var title = titlePart != null ? titlePart.Title : "(no TitlePart attached)";
                 var quantity = item.Quantity;
                <tr>
                    <td>@title</td>
                    <td class="numeric"><input type="number" value="@quantity" /></td>
                    <td class="numeric">@product.UnitPrice.ToString("c")</td>
                    <td class="action"><a class="icon delete" href="#"></a></td>
                </tr>
            }
        </tbody>
        <tfoot>
            <tr class="separator"><td colspan="4">&nbsp;</td></tr>
            <tr>
                <td class="numeric label" colspan="2">Subtotal:</td>
                <td class="numeric">@subtotal.ToString("c")</td>
                <td></td>
            </tr>
            <tr>
                <td class="numeric label" colspan="2">VAT (19%):</td>
                <td class="numeric">@vat.ToString("c")</td>
                <td></td>
            </tr>
            <tr>
                <td class="numeric label" colspan="2">Total:</td>
                <td class="numeric">@total.ToString("c")</td>
                <td></td>
            </tr>
        </tfoot>
    </table>
    <footer>
        <div class="group">
            <div class="align left"><a class="button" href="#">Continue shopping</a></div>
            <div class="align right"><a class="button next" href="#">Proceed to checkout</a></div>
        </div>
    </footer>
</article>


That was easy enough: simply render the properties of the Model (which is the shape we created in the action).

However, you may be wondering about the following lines:

var titlePart = product.As<TitlePart>();
var title = titlePart != null ? titlePart.Title : "(no TitlePart attached)";

The product variable is simply a ProductPart instance.
Although a ProductPart or its base type ContentPart do not have an As<T> method, there is an extension method defined for IContent (which ContentPart implements) that can cast a given content part into another given content part.
That's great, but what does that mean exactly?

Casting from one content part to another

Remember that all content items have a content type. Each content type contains zero or more content parts. The data of each content part is stored on the content item. In a sense, a content type is the "class" for a content item, where the content item is an "instance" of that class. So, given a content item, you may want to get the value of a given property on a given part of that content item. Since ContentPart provides access to the ContentItem it is attached to, we can easily retrieve access to other content parts by using the Get method of the ContentItem. And that's exactly what the As<T> extension method does for us: 

 public static T As<T>(this IContent content) where T : IContent {
            return content == null ? default(T) : (T)content.ContentItem.Get(typeof(T));
        }

Nice.

Casting from one content part to another using dynamic

What's also nice: when we cast a content item to dynamic, we can navigate directly to the property of any part attached to that content item. For example:

var product = (dynamic)item.ProductPart;
var title = product.TitlePart != null ? product.TitlePart.Title : "(no TitlePart attached)";

The way this works is that because ContentPart (the base class of ProductPart) implements a certain Clay behavior, this Clay behavior kicks in when we invoke methods and properties on the product variable (which is cast to dynamic).
To learn more about Clay and behaviors, be sure to check out these in-depth posts: http://downplay.co.uk/tutorials/clay/down-the-rabbit-hole

This also means that whenever the site administrator creates a new product type, he needs to make sure he adds both the ProductPart as well as the TitlePart if he wants to display a title.
But what if the administrator forgot to do so? To guard against forgetting to attach the TitlePart, we check for its presence before using the Title property like so:

var title = titlePart != null ? titlePart.Title : "(no TitlePart attached)";


If there is no TitlePart attached to the content item, we use a default string.

However, there is a better way to do this when dealing with content titles: by using the GetItemMetadata method of IContentManager.

ItemMetadata

GetItemMetadata returns an instance of ContentItemMetadata and contains quite some useful information about the specified content item.
When you invoke GetItemMetadata, an event is fired via the Event Bus. This event can be handled by implementing the IContentHandler interface or by deriving from a class that implements this interface.
Orchard comes with a whole set of content handlers by default, on of which being the TitlePartHandler. This handler implements the GetItemMetadata method like so:

protected override void GetItemMetadata(GetContentItemMetadataContext context) {
            var part = context.ContentItem.As<TitlePart>();
 
            if (part != null) {
                context.Metadata.DisplayText = part.Title;
            }
        }

What it does is what we were doing ourselves: casting the content part/item to TitlePart, and if that succeeded, we retrieved the value of the Title property of the TitlePart.
In this case, the TitlePartHandler sets the value of the Title property to the DisplayText of the Metadata, which is the object we get when we invoke IContentManager.GetItemMetdata.

Using GetItemMetadata is the recommended approach, as it is a more generic way to get the title for a content item. For example, consider having a product content type that wants to use a title that includes information from another field, e.g. Color.
To prevent the user from having to enter the color both in the title as well as in the Color field, you could choose to skip adding a TitlePart to your content type and instead write a content handler that sets the title dynamically.

Using GetItemMetadata
Let's modify our code to take make use of GetItemMetadata. Although we could invoke that method from within our view, I'd recommend to do so from the controller instead, because we need access to a ContentManager instance and potentially need to retrieve data from some data source. And these are the kind of things we don't want to do from a view.

The updated controller looks like this:

Controllers/ShoppingCartController.cs:

using System.Linq;
using System.Web.Mvc;
using Orchard;
using Orchard.Mvc;
using Orchard.Themes;
using Skywalker.Webshop.Services;
 
namespace Skywalker.Webshop.Controllers
{
    public class ShoppingCartController : Controller
    {
        private readonly IShoppingCart _shoppingCart;
        private readonly IOrchardServices _services;
 
        public ShoppingCartController(IShoppingCart shoppingCart, IOrchardServices services)
        {
            _shoppingCart = shoppingCart;
            _services = services;
        }
 
        [HttpPost]
        public ActionResult Add(int id) {
            
            // Add the specified content id to the shopping cart with a quantity of 1.
            _shoppingCart.Add(id, 1); 
 
            // Redirect the user to the Index action (yet to be created)
            return RedirectToAction("Index");
        }
 
        [Themed]
        public ActionResult Index() {
 
            // Create a new shape using the "New" property of IOrchardServices.
            var shape = _services.New.ShoppingCart(
                Products: _shoppingCart.GetProducts().Select(p => _services.New.ShoppingCartItem(
                    ProductPart: p.ProductPart, 
                    Quantity: p.Quantity,
                    Title: _services.ContentManager.GetItemMetadata(p.ProductPart).DisplayText)
                ).ToList(),
                Total: _shoppingCart.Total(),
                Subtotal: _shoppingCart.Subtotal(),
                Vat: _shoppingCart.Vat()
            );
 
            // Return a ShapeResult
            return new ShapeResult(this, shape);
        }
    }
}


Notice that we are back to using a shape again to represent each shopping cart item: this is because we need to hold an extra piece of information: the Title of the product, retrieved via a call to GetItemMetadata.
since our model definition has changed, we also need to update our view. The complete "ShoppingCart.cshtml" file should look like this:

Views/ShoppingCart.cshtml:

@{
     Style.Require("Skywalker.Webshop.ShoppingCart");
     var items = (IList<dynamic>)Model.Products;
     var subtotal = (decimal) Model.Subtotal;
     var vat = (decimal) Model.Vat;
     var total = (decimal) Model.Total;
 }
<article class="shoppingcart">
    <table>
        <thead>
            <tr>
                <td>Article</td>
                <td class="numeric">Quantity</td>
                <td class="numeric">Price</td>
                <td></td>
            </tr>
        </thead>
        <tbody>
            @foreach (var item in items) {
                var product = item.ProductPart;
                var title = item.Title;
                var quantity = item.Quantity;
                <tr>
                    <td>@title</td>
                    <td class="numeric"><input type="number" value="@quantity" /></td>
                    <td class="numeric">@product.UnitPrice.ToString("c")</td>
                    <td class="action"><a class="icon delete" href="#"></a></td>
                </tr>
            }
        </tbody>
        <tfoot>
            <tr class="separator"><td colspan="4">&nbsp;</td></tr>
            <tr>
                <td class="numeric label" colspan="2">Subtotal:</td>
                <td class="numeric">@subtotal.ToString("c")</td>
                <td></td>
            </tr>
            <tr>
                <td class="numeric label" colspan="2">VAT (19%):</td>
                <td class="numeric">@vat.ToString("c")</td>
                <td></td>
            </tr>
            <tr>
                <td class="numeric label" colspan="2">Total:</td>
                <td class="numeric">@total.ToString("c")</td>
                <td></td>
            </tr>
        </tfoot>
    </table>
    <footer>
        <div class="group">
            <div class="align left"><a class="button" href="#">Continue shopping</a></div>
            <div class="align right"><a class="button next" href="#">Proceed to checkout</a></div>
        </div>
    </footer>
</article>

Now, let's have a look at the shoppingcart when we add a few items to it:

(note that because we made a change to a sourcecode file, Orchard recompiles our module, which causes the application pool to recycle, which in turn causes our session to be lost. Therefore we need to add a product to our cart again using the product catalog. In the previous post of this tutorial, there was a discussion on whether to use the Session or not to store the shoppingcart. One of the disadvantages is that the session can be lost as we've just seen, which is obviously not ideal for our customers. In a future part, we will fix this by creating an abstract PersistenceProvider of some sorts and some default implementations, one of which storing the shoppingcart in the database instead of the session. The ShoppingCart will then be modified to make use of a PersistenceProvider).

"Making good progress" is what this is called.
 

Updating quantity and removing items

Now it's time to enable the user to update the quantity, as well as remove an item from the shoppingcart.
Whenever the user modifies the quantity to 0 or less, it should also cause the item to be removed.

First of all, we need to put an update button somewhere so that the user can apply the changes he made.
Later on, we will unobtrusively enhance that experience using KnockoutJS, jQuery and Ajax techniques. But it's good practice to have the shoppingcart functioning without requiring javascript support.

We'll start by modifying "ShoppingCart.cshtml" as follows:

Views/ShoppingCart.cshtml:

@using Skywalker.Webshop.Models
@{
     Style.Require("Skywalker.Webshop.ShoppingCart");
     var items = (IList<dynamic>)Model.Products;
     var subtotal = (decimal) Model.Subtotal;
     var vat = (decimal) Model.Vat;
     var total = (decimal) Model.Total;
 }
<article class="shoppingcart">
    @using (Html.BeginFormAntiForgeryPost(Url.Action("Update""ShoppingCart"new { area = "Skywalker.Webshop" }))) {
        <table>
            <thead>
                <tr>
                    <td>Article</td>
                    <td class="numeric">Unit Price</td>
                    <td class="numeric">Quantity</td>
                    <td class="numeric">Total Price</td>
                    <td class="action"></td>
                </tr>
            </thead>
            <tbody>
                @for (var i = 0; i < items.Count; i++) {
                    var item = items[i];
                    var product = (ProductPart)item.ProductPart;
                    var title = item.Title ?? "(no routepart attached)";
                    var quantity = (int)item.Quantity;
                    var unitPrice = product.UnitPrice;
                    var totalPrice = quantity * unitPrice;
                    <tr>
                        <td>@title</td>
                        <td class="numeric">@unitPrice.ToString("c")</td>
                        <td class="numeric">
                            <input name="@string.Format("items[{0}].ProductId"i)" type="hidden" value="@product.Id" />
                            <input name="@string.Format("items[{0}].IsRemoved"i)" type="hidden" value="false" />
                            <input name="@string.Format("items[{0}].Quantity"i)" type="number" value="@quantity" />
                        </td>
                        <td class="numeric">@totalPrice.ToString("c")</td>
                        <td class="action"><a class="icon delete" href="#"></a></td>
                    </tr>
                }
            
            </tbody>
            <tfoot>
                <tr><td colspan="5">&nbsp;</td></tr>
                <tr class="separator">
                    <td class="update" colspan="5"><button name="command" value="Update" type="submit">Update</button></td>
                </tr>
                <tr>
                    <td class="numeric label" colspan="3">Subtotal:</td>
                    <td class="numeric">@subtotal.ToString("c")</td>
                    <td></td>
                </tr>
                <tr>
                    <td class="numeric label" colspan="3">VAT (19%):</td>
                    <td class="numeric">@vat.ToString("c")</td>
                    <td></td>
                </tr>
                <tr>
                    <td class="numeric label" colspan="3">Total:</td>
                    <td class="numeric">@total.ToString("c")</td>
                    <td></td>
                </tr>
            </tfoot>
        </table>
        <footer>
            <div class="group">
                <div class="align left"><button type="submit" name="command" value="ContinueShopping">Continue shopping</button></div>
                <div class="align right"><button type="submit" name="command" value="Checkout">Proceed to checkout</button></div>
            </div>
        </footer>
    }
</article>


The most notable changes are as follows:

  • The entire shopping cart is wrapped inside a <form> element (using the Html.BeginFormAntiForgeryPost helper method), which submits to the "Update" action method (which we'll implement in a moment);
  • We replaced all <a> elements that represented buttons with a <button type="submit" /> element, so that we don't require javascript to submit the form. Furthermore, we named each button "command", so that we can accept a single "command" argument on our "Update" action method that indicates which button caused the form to submit;
  • We have given the quantity input field a special formatted name so that the modelbinder can map the posted values into an array of objects that we'll define in a moment. We also added 2 extra hidden fields; one for holding the product ID for which we want to update the quantity, and another one for storing a boolean value whether we want to delete this item from the shopping cart. We will use javascript to handle the click event of the "remove" button to set this hidden field's value to "true". Although we said before that we don't want to rely on javascript, I think the remove button in this case is not an essential feature. Users of browsers without javascript support can remove an item by setting the quantity to 0. However, we should hide the remove button by default, and make it visible using javascript, so that our users without javascript support won't get confused when they find that the remove button doesn't work. An alternative solution might be to use a <button> by default, and replace it using jQuery with a nicer version of the styled <a> element. But I'll leave that as an excericise to the reader.
  • We included an extra table column so that we can both display a unit price as well as the total price of an item.
  • We added an "update" button and changed a bit of the HTML to make it look good. If you're coding along, you should also add the following CSS rule to "shoppingcart.css" to make the Update button align to the right side of the table:

article.shoppingcart td.update {
    text-align: right;
}


Handling the form

To handle the form post, we need to implement the Update action method on our ShoppingCartController:

public ActionResult Update(string command, UpdateShoppingCartItemViewModel[] items) {
 
            // Loop through each posted item
            foreach (var item in items) {
 
                // Select the shopping cart item by posted product ID
                var shoppingCartItem = _shoppingCart.Items.SingleOrDefault(x => x.ProductId == item.ProductId);
 
                if (shoppingCartItem != null) {
                    // Update the quantity of the shoppingcart item. If IsRemoved == true, set the quantity to 0
                    shoppingCartItem.Quantity = item.IsRemoved ? 0 : item.Quantity < 0 ? 0 : item.Quantity;
                }
            }
 
            // Update the shopping cart so that items with 0 quantity will be removed
            _shoppingCart.UpdateItems();
 
            // Return an action result based on the specified command
            switch (command) {
                case "Checkout":
                    break;
                case "ContinueShopping":
                    break;
                case "Update":
                    break;
            }
 
            // Return to Index if no command was specified
            return RedirectToAction("Index");
        }

The method takes two arguments: command contains the value of the button that caused the postback, and an array of UpdateShoppingCartItemViewModels, which is a simple view model that contains three properties: ProductId, IsRemoved and Quantitiy.
Create a new folder called "ViewModels" and create a new class file called "UpdateShoppingCartViewModel.cs": 

namespace Skywalker.Webshop.ViewModels
{
    public class UpdateShoppingCartItemViewModel
    {
        public decimal ProductId { getset; }
        public bool IsRemoved { getset; }
        public int Quantity { getset; }
    }
}


Distinguising between view models and domain models help keep the intent of each type of model clear.

The first thing we do in the Update action is loop through each posted item, and select a corresponding ShoppingCartItem from the shoppingcart based on the product id.
If we find one, we update the quantity, ensuring that we don't update a quantity with a negative value (if a negative value is provided, we simply update with 0).
When we're done updating the quantities, we call the UpdateItems method of the shoppingcart (make sure that this method is added on the IShoppingCart interface as well; I forgot to do so in the previous post and have just recently fixed that), which will delete all entries where Quantity is 0 or less (although a negative quantity should never occur, since we are throwing an exception in the setter of the ShoppingCartItem.Quantity property when you pass a negative value):

Services/ShoppingCart.cs:

 public void UpdateItems() {
            ItemsInternal.RemoveAll(x => x.Quantity == 0);
        }


After updating the shoppingcart, we do a switch select on the specified command in our Update action of ShoppingCartController. Right now we don't do anything special, but later on we will come back here and redirect to the appropriate page.
Finally, we return a RedirectToRouteResult that takes the user back to the Index of the shopping cart in case no command was specified.

Let's continue and implement the remove buttons. We will write a little javascript to handle the click event and leverage jQuery to manipulate the DOM and submit the form.
The default Orchard installation includes a module called Orchard.jQuery, which all it does is defining a ResourceManifest:

public class ResourceManifest : IResourceManifestProvider {
        public void BuildManifests(ResourceManifestBuilder builder) {
            var manifest = builder.Add();
            manifest.DefineScript("jQuery").SetUrl("jquery-1.6.4.min.js""jquery-1.6.4.js").SetVersion("1.6.4");
 
            ... 
        }
    }

Let's put this module to good use for us!

Module Dependencies

Whenever you introduce a dependency on a Module, you should include the name of that module in your module's manifest.
When users enable your module, Orchard will automatically enable all dependency modules.

Let's go ahead and modify our Module.txt:

Module.txt:

Name: Skywalker.WebShop
AntiForgery: enabled
Author: Sipke Schoorstra
Website: http://skywalkersoftwaredevelopment.net
Version: 1.0
OrchardVersion: 1.4
Description: Orchard Webshop Module Demo
Category: Webshop
Dependencies: Orchard.Projector, Orchard.Forms, Orchard.jQuery


ShoppingCart.js

Next, we'll write our shoppingcart specific javascript and register it with our own ResourceManifest.

Let's go ahead and create a new "Scripts" folder, copy "web.config" from either "Styles" or "Images" to it, and add a new javascript file named "shoppingcart.js".
Enter the following code into "shoppingcart.js":

Scripts/shoppingcart.js:

(function ($) {
 
    $(".shoppingcart a.icon.delete").click(function (e) {
        var $button = $(this);
        var $tr = $button.parents("tr:first");
        var $isRemoved = $("input[name$='IsRemoved']", $tr).val("true");
        var $form = $button.parents("form");
 
        $form.submit();
        e.preventDefault();
    });
 
})(jQuery);


This code essentially attaches a click eventhandler to all <a> elements that have both an "icon" and "delete" class defined and are contained within any element that has a "shoppingcart" class defined.
When the click event fires, the handler selects the hidden input element with a name that ends with "IsRemoved" and sets its value to "true".
Finally, it submits the form and prevents the default action of the event (which for an <a> element would be to navigate to the specified href value, which we don't want in this case).

In order to include this script, we'll register it inside our ResourceManifest class:

ResourceManifest.cs:

using Orchard.UI.Resources;
 
namespace Skywalker.Webshop
{
    public class ResourceManifest : IResourceManifestProvider
    {
        public void BuildManifests(ResourceManifestBuilder builder) {
 
            // Create and add a new manifest
            var manifest = builder.Add();
 
            // Define a "common" style sheet
            manifest.DefineStyle("Skywalker.Webshop.Common").SetUrl("common.css");
 
            // Define the "shoppingcart" style sheet
            manifest.DefineStyle("Skywalker.Webshop.ShoppingCart").SetUrl("shoppingcart.css").SetDependencies("Skywalker.Webshop.Common");
 
            // Define the "shoppingcart" script and set a dependency on the "jQuery" resource
            manifest.DefineScript("Skywalker.Webshop.ShoppingCart").SetUrl("shoppingcart.js").SetDependencies("jQuery");
        }
    }
}


This time we are using the DefineScript method of the manifest. By convention, Orchard expects scripts to be stored in a folder named Scripts.
Note that we are setting a dependency on the "jQuery" resource, which is defined by the Orchard.jQuery module as we saw earlier.

Now we are ready to reference our javascript from within our shoppingcart template file:

Views/ShoppingCart.cshtml:

@using Skywalker.Webshop.Models
@{
    Style.Require("Skywalker.Webshop.ShoppingCart");
    Script.Require("Skywalker.Webshop.ShoppingCart");
  ...

This time, instead of using the Style property, we are using the Script property of the view to include our script resource in the final HTML output.
By default, it will be inserted just before the </body> element of the HTML document, but you could change it if you wanted to:

    Script.Require("Skywalker.Webshop.ShoppingCart").AtHead();

 

If we include it at the end of our document, a situation could arise where a user clicks the remove button without causing any effect. This happens when the browser is still downloading the script after the page has already been displayed, causing a small time window in which the user can click the remove button, even though the script has not yet been downloaded (and therefore not executed and attached any eventhandlers). So that's something to consider.
 

Next, let's display a message to the user when there are no items in the shopping cart. This requires a small modification in our shopping cart template:

Views/ShoppingCart.cshtml:

@using Skywalker.Webshop.Models 
@{     
Style.Require("Skywalker.Webshop.ShoppingCart");
    Script.Require("Skywalker.Webshop.ShoppingCart");
    var items = (IList<dynamic>)Model.Products;
    var subtotal = (decimal) Model.Subtotal;
    var vat = (decimal) Model.Vat;
    var total = (decimal) Model.Total;
}
@if (!items.Any()) {
    <p>You don't have any items in your shopping cart.</p>
    <a class="button" href="#">Continue shopping</a> }
else {
    <article class="shoppingcart">
        
...
    </article>
}


As a result:

 

Displaying a shopping cart "widget" on every page

Most webshops display some sort of widget that have a hyperlink that points directly to the shopping cart page, as well as shows the total item count and total order amount.
In order to achieve this, we could either create a Widget so that we can add this widget to any zone from within the admin, or we could create a shape from our module, which theme developers can then inject into any zone they like. Let's go with creating the Widget option.

A widget in orchard is just a content type which has at least the WidgetPart attached, as well as a setting called "Stereotype" with "Widget" as its value.
For an introduction on writing widgets, check out the docs.

We'll go ahead and start by creating a new content part named ShoppingCartWidgetPart:

Models/ShoppingCartWidgetPart.cs

using Orchard.ContentManagement;
 
namespace Skywalker.Webshop.Models
{
    public class ShoppingCartWidgetPart : ContentPart {
    }
}

 

The ShoppingCartWidgetPart has no implementation and derives from ContentPart instead of ContentPart<TRecord>, since it doesn't need to store any data.
The reason why we are creating a class even though it does nothing useful is so that't is easy to extend the part with properties and settings later on.

An alternative approach that does not involve creating an empty class is defining a content part named "ShoppingCartWidgetPart" using the Migrations class, and then create a class that implements IShapeTableProvider to create shapes.

Now that we have a content part class, let's continue by creating a driver for it. Since we currently don't support any configuration for this part, we will just implement the Display method that will create a shape:

Drivers/ShoppingCartWidgetPartDriver.cs:

using Orchard.ContentManagement.Drivers;
using Skywalker.Webshop.Models;
using Skywalker.Webshop.Services;
 
namespace Skywalker.Webshop.Drivers
{
    public class ShoppingCartWidgetPartDriver : ContentPartDriver<ShoppingCartWidgetPart> {
        private readonly IShoppingCart _shoppingCart;
 
        public ShoppingCartWidgetPartDriver(IShoppingCart shoppingCart) {
            _shoppingCart = shoppingCart;
        }
 
        protected override DriverResult Display(ShoppingCartWidgetPart part, string displayType, dynamic shapeHelper) {
            return ContentShape("Parts_ShoppingCartWidget", () => shapeHelper.Parts_ShoppingCartWidget(
                ItemCount: _shoppingCart.ItemCount(),
                TotalAmount: _shoppingCart.Total()
            ));
        }
    }
}


All this driver does is creating a shape called "ShoppingCartWidget", which will have two properties: ItemCount, which represents the total number of items currently in the shopping cart, and TotalAmount, which represents the total sum of the price of all items in the shopping cart.

Next, we 'll modify "Placement.info" to specify where the shape should be rendered within a content item.

Placement.info:

<Placement>
  <Place Parts_Product_Edit="Content:1" />
  <Place Parts_Product="Content:0" />
  <Place Parts_Product_AddButton="Content:after" />
  <Place Parts_ShoppingCartWidget="Content:0" />
</Placement>

 

Next, we'll modify our Migrations class to define a new content type called ShoppingCartWidget:


Migrations.cs:

public int UpdateFrom2() {
 
            // Define a new content type called "ShoppingCartWidget"
            ContentDefinitionManager.AlterTypeDefinition("ShoppingCartWidget", type => type
 
                // Attach the "ShoppingCartWidgetPart"
                .WithPart("ShoppingCartWidgetPart")
 
                // In order to turn this content type into a widget, it needs the WidgetPart
                .WithPart("WidgetPart")
 
                // It also needs a setting called "Stereotype" to be set to "Widget"
                .WithSetting("Stereotype""Widget")
                );
 
            return 3;
        }


Nothing much we haven't seen before, except that we are now adding a setting to our new content type: "Sterotype" and adding the WidgetPart.
The "Sterotype" setting tells Orchard that our content type is to be treated as a widget, so that it for example shows up in the "Widgets" menu when the user wants to add a widget to a zone.
Before we'll try it out, we'll first create a template for the shape being created by the ShoppingCartWidgetPartDriver:

Views/Parts/ShoppingCartWidget.cshtml:

@{
    Style.Require("Skywalker.Webshop.ShoppingCartWidget");
    
    var itemCount = (int) Model.ItemCount;
    var totalAmount = (decimal) Model.TotalAmount;
}
<article>
    <span class="label">Items:</span> <span class="value">@itemCount</span><br/>
    <span class="label">Amount:</span> <span class="value">@totalAmount.ToString("c")</span><br/>
    <div class="group">
        <div class="align right">
            <a href="@Url.Action("Index""ShoppingCart"new { area = "Skywalker.Webshop" })">View shoppingcart</a>
        </div>
    </div>
</article>

 

At the top, we are referencing a stylesheet resource that we will create now. In the "Styles" folder, create a new file named "shoppingcartwidget.css":

Styles/shoppingcartwidget.css:

article.widget-shopping-cart-widget header h1{
    background: #f6f6f6;
    font-weight: bold;
    line-height: 24px;
    margin: 0;
    padding: 0 5px 0 5px;
}
 
article.widget-shopping-cart-widget article {
    padding: 5px;
    border: 1px dotted #ccc;
    line-height: 20px;
}
 
article.widget-shopping-cart-widget article span.label{
    width: 60px;
    font-style: italic;
    color: #aaa;
    display: inline-block;
}


Next, update the ResourceManifest:

ResourceManifest.cs:

using Orchard.UI.Resources;
 
namespace Skywalker.Webshop
{
    public class ResourceManifest : IResourceManifestProvider
    {
        public void BuildManifests(ResourceManifestBuilder builder) {
 
            // Create and add a new manifest
            var manifest = builder.Add();
 
            // Define a "common" style sheet
            manifest.DefineStyle("Skywalker.Webshop.Common").SetUrl("common.css");
 
            // Define the "shoppingcart" style sheet
            manifest.DefineStyle("Skywalker.Webshop.ShoppingCart").SetUrl("shoppingcart.css").SetDependencies("Skywalker.Webshop.Common");
 
            // Define the "shoppingcartwidget" style sheet             manifest.DefineStyle("Skywalker.Webshop.ShoppingCartWidget").SetUrl("shoppingcartwidget.css").SetDependencies("Webshop.Common");
 
            // Define the "shoppingcart" script and set a dependency on the "jQuery" resource
            manifest.DefineScript("Skywalker.Webshop.ShoppingCart").SetUrl("shoppingcart.js").SetDependencies("jQuery");
        }
    }
}

 

Adding the widget to a zone


Allright! Let's start the Admin UI and see if we can drop a ShoppingCartWidget on the AsideSecond zone of the current theme.

Go to Widgets and click the Add button on the AsideSecond zone:

We now see a list of Widgets to choose from; click the widget we just created:

 What we see next is not what you may have expected:

Looking at the log files (which are created in the "~/App_Data/Logs" folder) shows us where the error occurs:

2012-01-12 05:43:19,012 [13] Orchard.Widgets.Controllers.AdminController - Creating widget failed: Object reference not set to an instance of an object.
System.NullReferenceException: Object reference not set to an instance of an object.
   at Orchard.Widgets.Models.WidgetPart.set_LayerPart(LayerPart value)
   at Orchard.Widgets.Controllers.AdminController.AddWidget(Int32 layerId, String widgetType, String zone, String returnUrl)

As it turns out, the LayerPart property setter of the WidgetPart is throwing an exception.

Let's have a look at its source:

/Orchard.Widgets/Models/WidgetPart.cs:

/// <summary>
        /// The layerPart where the widget belongs.
        /// </summary>
        public LayerPart LayerPart {
            get { return this.As<ICommonPart>().Container.As<LayerPart>(); }
            set { this.As<ICommonPart>().Container = value; }
        }


Aha! The WidgetPart expects that any content item it is attached to will have a part attached to it that implements the ICommonPart interface.

Since we didn't attach such a part to our ShoppingCartWidget content type, the As<> extension method returned a null reference, hence the NullReferenceException error.

That's easy to fix: let's add another migration method to our Migrations class that will attach the CommonPart to our widget:

Migrations.cs:

 public int UpdateFrom3()
        {
            // Update the ShoppingCartWidget so that it has a CommonPart attached, which is required for widgets (it's generally a good idea to have this part attached)
            ContentDefinitionManager.AlterTypeDefinition("ShoppingCartWidget", type => type
                .WithPart("CommonPart")
            );
 
            return 4;
        }


Now let's try adding the widget again to the AsideSecond zone.
When we now select the ShoppingCartWidget, we see success:

The input fields Zone, Layer, Position and Title are indirectly created by the WidgetPart driver (remember, the driver simply creates shapes, and it's the shape template that defines the look of that shape). The Owner field is created by the CommonPart driver.

Should we ever decide that we want our users to be able to configure our ShoppingCartWidget and create an edit shape, this is the screen where it will show up.

We'll go ahead and press the Save button. We'll see that our widget has succesfully been added to the AsideSecond zone:

Let's see how it looks on the front end:

 

Looks like a widget to me!

Enhancing the shoppingcart experience using KnockoutJS and jQuery

Although the shoppingcart is functioning quite well, it could be even better if the totals would update automatically as soon as the user updates a quantity or deletes any item from his cart.
This could be done using just javascript / jQuery, but the resulting script will quickly become a messy mixture of domain logic and UI logic.
Therefore we'll apply the MVVM pattern in javascript using a javascript library called KnockoutJS.

KnockoutJS allows us to code against view models without having to update the UI manually.
For example, if we update a property on a view model, the UI will immediately reflect that change. The other way around works as well: whenever the user changes the value of an input element, that value will be stored in the view model. What's more, all other values calculated from that property will be updated as well, which will also update the UI.

Let's see how this all works.

First of all we need to download the latest version of knockoutJS. As it turns out, therre is a Knockout module available for us to download, so we don't have to download KnockoutJS ourselves:

Hit Install and enable the Knockout feature.

We'll also make use of another javascript library called LinqJS. LinqJs is a nifty library that allows us to easily query and create projections from javascript arrays in the same way we can in C#.
You can download LinqJS from here: http://linqjs.codeplex.com/. The downloaded zip file contains multiple scripts and other files, but the only one we are going to use is "jquery.linq.min.js".
However, it also turns out that there is an Orchard module providing the LinqJS script library, so let's go ahead and download that one as well:

Install that module as well and enable just the "AIM LinqJS jQuery support" feature.

Because our javascript shoppingcart.js file will depend on both KnockoutJS and LinqJS, we'll update our ResourceManifest

ResourceManifest.cs:

// Define the "shoppingcart" script and set a dependency on the "jQuery" resource
manifest.DefineScript("Skywalker.Webshop.ShoppingCart").SetUrl("shoppingcart.js").SetDependencies("jQuery""jQuery_LinqJs""ko");

 

And of course we'll update our Module manifest with our new dependencies:

Module.txt:

Name: Skywalker.WebShop
AntiForgery: enabled
Author: Sipke Schoorstra
Website: http://skywalkersoftwaredevelopment.net
Version: 1.0
OrchardVersion: 1.4
Description: Orchard Webshop Module Demo
Category: Webshop
Dependencies: Orchard.Projector, Orchard.Forms, Orchard.jQuery, AIM.LinqJs, Orchard.Knockout

 

Since "KnockoutJS" and "LinqJS" are dependencies for "Skywalker.Webshop.ShoppingCart", we don't need to explicitly reference them from our "ShoppingCart.cshtml", as Orchard will automatically reference all dependency resources.

Defining the ViewModel

Open "shoppingcart.js" and replace its contents with the following code:

shoppingcart.js:

(function ($) {
 
    $(".shoppingcart a.icon.delete").live("click"function (e) {
        e.preventDefault();
 
        // Check if the clicked button is generated by KO. If so, we simply remove the item from the model and return.
        var shoppingCartItem = ko.dataFor(this);
 
        if (shoppingCartItem != null) {
            shoppingCartItem.remove();
            return;
        }
 
        // If we got here, the clicked button was not created by KO (which should only happen if we disabled KO).
        var $button = $(this);
        var $tr = $button.parents("tr:first");
        var $isRemoved = $("input[name$='IsRemoved']", $tr).val("true");
        var $form = $button.parents("form");
 
        $form.submit();
 
    });
 
    /*****************************************************     * ShoppingCartItem class     ******************************************************/
    var ShoppingCartItem = function (data) {
 
        this.id = data.id;
        this.title = data.title;
        this.unitPrice = data.unitPrice;
        this.quantity = ko.observable(data.quantity);
 
        this.total = ko.dependentObservable(function () {
            return this.unitPrice * parseInt(this.quantity());
        }, this);
 
        this.remove = function () {
            shoppingCart.items.remove(this);
            saveChanges();
        };
 
        this.quantity.subscribe(function (value) {
            saveChanges();
        });
 
        this.index = ko.dependentObservable(function () {
            return shoppingCart.items.indexOf(this);
        }, this);
    };
 
    /*****************************************************     * ShoppingCart (viewmodel)     ******************************************************/
    var shoppingCart = {
        items: ko.observableArray()
    };
 
    shoppingCart.calculateSubtotal = ko.dependentObservable(function () {
        return $.Enumerable.From(this.items()).Sum(function (x) { return x.total(); });
    }, shoppingCart);
 
    shoppingCart.itemCount = ko.dependentObservable(function () {
        return $.Enumerable.From(this.items()).Sum(function (x) { return parseInt(x.quantity()); });
    }, shoppingCart);
 
    shoppingCart.hasItems = ko.dependentObservable(function () { return this.items().length > 0; }, shoppingCart);
    shoppingCart.calculateVat = function () { return this.calculateSubtotal() * 0.19; };
    shoppingCart.calculateTotal = function () { return this.calculateSubtotal() + this.calculateVat(); };
 
    /*****************************************************     * SaveChanges     ******************************************************/
    var saveChanges = function () {
        var data = $.Enumerable.From(shoppingCart.items()).Select(function (x) { return { productId: x.id, quantity: x.quantity() }; }).ToArray();
        var url = $("article.shoppingcart").data("update-shoppingcart-url");
        var config = {
            url: url,
            type: "POST",
            data: data ? JSON.stringify(data) : null,
            dataType: "json",
            contentType: "application/json; charset=utf-8"
        };
        $.ajax(config);
    };
 
    /*****************************************************     * Initialization     ******************************************************/
    if ($("article.shoppingcart").length > 0) {
        $.ajaxSetup({ cache: false });
        ko.applyBindings(shoppingCart);
        var dataUrl = $("article.shoppingcart").data("load-shoppingcart-url");
 
        // Clear any existing table rows.
        $("article.shoppingcart tbody").empty();
 
        // Hide the "Update" button, as we will auto update the quantities using AJAX.
        $("button[value='Update']").hide();
 
        $.getJSON(dataUrl, function (data) {
            for (var i = 0; i < data.items.length; i++) {
                var item = data.items[i];
                shoppingCart.items.push(new ShoppingCartItem(item));
            }
        });
    }
 
})(jQuery);

We are basically defining a ShoppingCartItem class that has some properties and methods that apply to a single shoppingcart item representation on the client side.

Next, we defined a simple object called shoppingCart, which will hold an observable array where each item is an instance of ShoppingCartItem.
We also defined a function called saveChanges that will project all shoppingcart items into an array of JSON objects, suitable to be submitted using AJAX.

Finally, we load the initial shoppingcart items using an AJAX call once. The data we receive is a JSON object that will have an items property that will hold shoppingcart item data, from which we construct a new ShoppingCartItem and push that into the items property of the shoppingCart view model.

As you can see, we are using two urls: one url to post changes to, and another url to initially load the data from.
These urls will be stored using html5 data-* attributes on our shoppingcart wrapper div (in "ShoppingCart.cshtml").
The reason we are doing it like this is that we should not harcode urls in our javascript files; instead we want to use server generated urls.

Empowering ShoppingCart.cshtml with KnockoutJS data-binding

The next thing we need to do is modify "ShoppingCart.cshtml" to include knockoutJS bindings that make use of our clientside shoppingCart object (find the "data-bind" attributes sprinkled throughout the file; these are the links with our viewmodels):

ShoppingCart.cshtml:

@using Skywalker.Webshop.Models
@{
    Style.Require("Skywalker.Webshop.ShoppingCart");
    Script.Require("Skywalker.Webshop.ShoppingCart");
    var items = (IList<dynamic>)Model.Products;
    var subtotal = (decimal) Model.Subtotal;
    var vat = (decimal) Model.Vat;
    var total = (decimal) Model.Total;
}
@if (!items.Any())
{
    <p>You don't have any items in your shopping cart.</p>
    <a class="button" href="#">Continue shopping</a>
}
else { 
    <div data-bind="visible: !hasItems()">
        <p>You don't have any items in your shopping cart.</p>
        <a class="button" href="#">Continue shopping</a>
    </div>
    
    <div data-bind="visible: hasItems()">
        <article class="shoppingcart" data-load-shoppingcart-url="@Url.Action("GetItems""ShoppingCart"new { area = "Skywalker.WebShop" })" data-update-shoppingcart-url="@Url.Action("Update""ShoppingCart"new { area = "Skywalker.WebShop" })">
            @using(Html.BeginFormAntiForgeryPost(Url.Action("Update""ShoppingCart"new { area = "Skywalker.Webshop" })))
            {
                <table>
                    <thead>
                        <tr>
                            <td>Article</td>
                            <td class="numeric">Unit Price</td>
                            <td class="numeric">Quantity</td>
                            <td class="numeric">Total Price</td>
                            <td class="action"></td>
                        </tr>
                    </thead>
                    <tbody data-bind='template: {name: "itemTemplate", foreach: items}'>
                        @for (var i = 0; i < items.Count; i++) {
                            var item = items[i];
                            var product = (ProductPart) item.ProductPart;
                            var title = item.Title ?? "(no routepart attached)";
                            var quantity = (int) item.Quantity;
                            var unitPrice = product.UnitPrice;
                            var totalPrice = quantity*unitPrice;
                            <tr>
                                <td>@title</td>
                                <td class="numeric">@unitPrice.ToString("c")</td>
                                <td class="numeric">
                                    <input name="@string.Format("items[{0}].ProductId"i)" type="hidden" value="@product.Id" />
                                    <input name="@string.Format("items[{0}].IsRemoved"i)" type="hidden" value="false" />
                                    <input name="@string.Format("items[{0}].Quantity"i)" type="number" value="@quantity" />
                                </td>
                                <td class="numeric">@totalPrice.ToString("c")</td>
                                <td class="action"><a class="icon delete postback" href="#"></a></td>
                            </tr>
                        }
            
                    </tbody>
                    <tfoot>
                        <tr><td colspan="5">&nbsp;</td></tr>
                        <tr class="separator">
                            <td class="update" colspan="5"><button name="command" value="Update" type="submit">Update</button></td>
                        </tr>
                        <tr>
                            <td class="numeric label" colspan="3">Subtotal:</td>
                            <td class="numeric"><span data-bind="text: calculateSubtotal()">@subtotal.ToString("c")</span></td>
                            <td></td>
                        </tr>
                        <tr>
                            <td class="numeric label" colspan="3">VAT (19%):</td>
                            <td class="numeric"><span data-bind="text: calculateVat()">@vat.ToString("c")</span></td>
                            <td></td>
                        </tr>
                        <tr>
                            <td class="numeric label" colspan="3">Total:</td>
                            <td class="numeric"><span data-bind="text: calculateTotal()">@total.ToString("c")</span></td>
                            <td></td>
                        </tr>
                    </tfoot>
                </table>
                <footer>
                    <div class="group">
                        <div class="align left"><button type="submit" name="command" value="ContinueShopping">Continue shopping</button></div>
                        <div class="align right"><button type="submit" name="command" value="Checkout">Proceed to checkout</button></div>
                    </div>
                </footer>
            }
        </article>
        
        <script type="text/html" id="itemTemplate">
            <tr>
                <td><span data-bind="text: title"></span></td>
                <td class="numeric"><span data-bind="text: unitPrice"></span></td>
                <td class="numeric">
                    <input data-bind="attr: { name: 'items[' + index() + '].ProductId'}, value: id" type="hidden" />
                    <input data-bind="attr: { name: 'items[' + index() + '].Quantity'}, value: quantity" type="number" />
                </td>
                <td class="numeric"><span data-bind="text: total()"></span></td>
                <td><a class="icon delete" href="#"></a></td>
            </tr>
        </script>
    </div>
}


The most notable changes are that we added a "data-bind" attribute on certain elements. This lets Knockout know that the specified attribute value should be retrieved from the viewmodel (which is the shoppingCart object).
Another important change is the inclusion of the <script> element just below the end of the <article> element.
This script contains a template to be used by Knockout when binding the <tbody> element in our HTML table.

Also note that we included two html-* attributes on our <article> element:

data-load-shoppingcart-url="@Url.Action("GetItems", "ShoppingCart", new { area = "Skywalker.WebShop" })"
data-update-shoppingcart-url="@Url.Action("Update", "ShoppingCart", new { area = "Skywalker.WebShop" })"

Let's also update the "ShoppingCartWidget.cshtml" template so that it gets updated as well in realtime:

ShoppingCartWidget.cshtml:

@{
    Style.Require("Skywalker.Webshop.ShoppingCartWidget");
    
    var itemCount = (int) Model.ItemCount;
    var totalAmount = (decimal) Model.TotalAmount;
}
<article>
    <span class="label">Items:</span> <span class="value" data-bind="text: itemCount()">@itemCount</span><br/>
    <span class="label">Amount:</span> <span class="value" data-bind="text: calculateTotal()">@totalAmount.ToString("c")</span><br/>
    <div class="group">
        <div class="align right">
            <a href="@Url.Action("Index""ShoppingCart"new { area = "Skywalker.Webshop" })">View shoppingcart</a>
        </div>
    </div>
</article>


That's how easy it is once we are using KnockoutJS: all we had to do is add a data-bind attribute. Quite awesome if you ask me.

Next, we'll modify our ShoppingCartController by completely replacing its content with the following code:

Controllers/ShoppingCartController.cs:

using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using Orchard;
using Orchard.Mvc;
using Orchard.Themes;
using Skywalker.Webshop.Models;
using Skywalker.Webshop.Services;
using Skywalker.Webshop.ViewModels;
 
namespace Skywalker.Webshop.Controllers
{
    public class ShoppingCartController : Controller
    {
        private readonly IShoppingCart _shoppingCart;
        private readonly IOrchardServices _services;
 
        public ShoppingCartController(IShoppingCart shoppingCart, IOrchardServices services)
        {
            _shoppingCart = shoppingCart;
            _services = services;
        }
 
        [HttpPost]
        public ActionResult Add(int id) {
            
            // Add the specified content id to the shopping cart with a quantity of 1.
            _shoppingCart.Add(id, 1); 
 
            // Redirect the user to the Index action (yet to be created)
            return RedirectToAction("Index");
        }
 
        [Themed]
        public ActionResult Index() {
 
            // Create a new shape using the "New" property of IOrchardServices.
            var shape = _services.New.ShoppingCart(
                Products: _shoppingCart.GetProducts().Select(p => _services.New.ShoppingCartItem(
                    ProductPart: p.ProductPart, 
                    Quantity: p.Quantity,
                    Title: _services.ContentManager.GetItemMetadata(p.ProductPart).DisplayText)
                ).ToList(),
                Total: _shoppingCart.Total(),
                Subtotal: _shoppingCart.Subtotal(),
                Vat: _shoppingCart.Vat()
            );
 
            // Return a ShapeResult
            return new ShapeResult(this, shape);
        }
 
        [HttpPost]
        public ActionResult Update(string command, UpdateShoppingCartItemViewModel[] items)
        {
            UpdateShoppingCart(items);
 
            if (Request.IsAjaxRequest())
                return Json(true);
 
            switch (command)
            {
                case "Checkout":
                    break;
                case "ContinueShopping":
                    break;
                case "Update":
                    break;
            }
            return RedirectToAction("Index");
        }
 
        public ActionResult GetItems() {
            var products = _shoppingCart.GetProducts();
            
            var json = new {
                items = (from item in products
                         select new {
                             id = item.ProductPart.Id,
                             title = _services.ContentManager.GetItemMetadata(item.ProductPart).DisplayText ?? "(No TitlePart attached)",
                             unitPrice = item.ProductPart.UnitPrice,
                             quantity = item.Quantity
                         }).ToArray()
            };
 
            return Json(json, JsonRequestBehavior.AllowGet);
        }
 
        private void UpdateShoppingCart(IEnumerable<UpdateShoppingCartItemViewModel> items) {
            
            _shoppingCart.Clear();
 
            if (items == null)
                return;
 
            _shoppingCart.AddRange(items
                .Where(item => !item.IsRemoved)
                .Select(item => new ShoppingCartItem(item.ProductId, item.Quantity < 0 ? 0 : item.Quantity))
            );
 
            _shoppingCart.UpdateItems();
        }
    }
}

We added one action named GetItems (constrained by the AjaxOnlyAttribute) and updated the existing Update action to also handle AJAX requests.
We also improved readability a bit by moving the shoppingcart update logic into a private method called UpdateShoppingCart. Keeping methods as small as possible and having them responsible for one task is considered good practice, as it makes code more readable and maintainable.


Note that we also simplified the Update Shoppingcart logic a bit by simply clearing the entire cart, and then re-populate it with the specified list. The reason for this is that since we implemented KnockoutJs, the user is able to remove items on the client side which will cause the item to be removed from the <form>.
Right after the item is deleted, we issue an AJAX POST that submits the entire form. The posted data will not contain the removed item, so on the serverside we would have to compare the posted items with the items currently in the cart to see what items to update and what items to remove.

Or we take the easy way and just clear the entire cart, and then we re-populate it with the received items. It's generally considered good practice to use the simplest option as possible.

The Update action will be invoked from our "shoppingcart.js" script as soon as the user either updates a quantity field or removes an item.
The GetItems action is invoked once when the DOM is loaded, to initially setup the clientside viewmodels.

Let's go ahead and build our module. Then refresh the front end:

Globalization

However, you'll soon notice that all the financial values lost the $ sign.
The problem is that we are binding the values directly with the elements, without formatting the values as we are doing server side. Let's have a look at one of those lines:

<span data-bind="text: calculateTotal()">@total.ToString("c")</span>

The serverside code @total.ToString("c") will format the amount as we have seen.
However, as soon as KnockoutJS kicks in on the client, it will rebind the value of the <span> element using the expression "text: calculateTotal()".

We could solve this the easy way like this: "text: '$' + calculateTotal()". However, we can do better than that. Let's use the Globalization plugin.
The advantage we gain is that our webshop module will be able to easily support different cultures, including different currencies.

What we need to do is download the Globalization plugin, update our ResourceManifest, and update the templates to use the format method that is provided by the Globalization plugin.
We also need to tell the Globalization plugin which culture to use based on the culture in which the website is running.
Let's do all that. 

Go ahead and download the Globalization files: https://github.com/jquery/globalize/zipball/master.
Extract the zip file and copy the file "globalize.js" and some or all of the cultures to the "Scripts" folder of our module (keep the culture files inside the "cultures" folder, so that they will be stored in "Scripts/cultures".

Now let's update the ResourceManifest:

ResourceManifest.cs:

using Orchard.UI.Resources;
 
namespace Skywalker.Webshop
{
    public class ResourceManifest : IResourceManifestProvider
    {
        public void BuildManifests(ResourceManifestBuilder builder) {
 
            // Create and add a new manifest
            var manifest = builder.Add();
 
            // Define a "common" style sheet
            manifest.DefineStyle("Skywalker.Webshop.Common").SetUrl("common.css");
 
            // Define the "shoppingcart" style sheet
            manifest.DefineStyle("Skywalker.Webshop.ShoppingCart").SetUrl("shoppingcart.css").SetDependencies("Skywalker.Webshop.Common");
 
            // Define the "shoppingcartwidget" style sheet
            manifest.DefineStyle("Skywalker.Webshop.ShoppingCartWidget").SetUrl("shoppingcartwidget.css").SetDependencies("Webshop.Common");
 
            // Define Globalization resources
            manifest.DefineScript("Globalize").SetUrl("globalize.js").SetDependencies("jQuery");
            manifest.DefineScript("Globalize.Cultures").SetBasePath(manifest.BasePath + "scripts/cultures/").SetUrl("globalize.culture.js").SetCultures("en-US""nl-NL").SetDependencies("Globalize""jQuery");
            manifest.DefineScript("Globalize.SetCulture").SetUrl("~/Skywalker.Webshop/Resource/SetCultureScript").SetDependencies("Globalize.Cultures");
 
 
            // Define the "shoppingcart" script and set a dependencies
            manifest.DefineScript("Skywalker.Webshop.ShoppingCart").SetUrl("shoppingcart.js").SetDependencies("jQuery""jQuery_LinqJs""ko""Globalize.SetCulture");
        }
    }
}


Now this is getting more interesting. What we did is define three extra resources: "Globalize", "Globalize.Cultures" and "Globalize.SetCulture".

"Globalize" points to the core Globalization file.
"Globalize.Cultures" points to "/scripts/cultures/globalize.culture.js" (even tough the specified file does not exists; we'll explain in a moment).
"Globalize.SetCulture" is setup so that it points to the url: "~/Orchard.Webshop/Resource/SetCultureScript" (we'll explain in a moment).


Note that we're setting the url of the "Globalize.Cultures" resource to a non-existing file: "globalize.culture.js".
What is happening here is that Orchard will dynamically change that filename based on the current culture in which the site is running. We pass a list of available cultures using the SetCultures method.
So, if the site is running in the en-US culture, the filename will become "globalize.culture.en-US.js". Quite thoughtful of Orchard, because this is exactly what we need.

Also note that the "Globalize.SetCulture" resource sets its url to "~/Orchard.Webshop/Resource/SetCultureUrl".
As we'll see next, this will resolve into a call on a new controller named ResourceController. The purpose of this controller is to generate some javascript that initializes the clientside Globalize object with the culture as is set serverside by Orchard.

We'll go ahead create a new controller named ResourceController in te Controllers folder:

ResourceController.cs:

using System.Web.Mvc;
using Orchard;
 
namespace Skywalker.Webshop.Controllers
{
    public class ResourceController : Controller {
 
        private readonly IWorkContextAccessor _workContextAccessor;
 
        public ResourceController(IWorkContextAccessor workContextAccessor) {
            _workContextAccessor = workContextAccessor;
        }
 
        public string SetCultureScript() {
            return string.Format("Globalize.culture(\"{0}\");", _workContextAccessor.GetContext().CurrentCulture);
        }
    }
}


The controller has one action method called SetCultureScript, and i will simply generate some javascript that tells the clientside Globalize object to use the culture which is being rendered serverside.

For example, if the site was running in the "fr-FR" culture, the resulting javascript would become:

Globalize.culture("fr-FR");

Now that we have installed the Globalization plugin, we'll go ahead and update both "ShoppingCart.cshtml" and "ShoppingCartWidget.cshtml" to format the financial values using the Globalization plugin:

ShoppingCart.cshtml:

                        ...
                        <tr>
                            <td class="numeric label" colspan="3">Subtotal:</td>
                            <td class="numeric"><span data-bind="text: Globalize.format(calculateSubtotal(), 'c')">@subtotal.ToString("c")</span></td>
                            <td></td>
                        </tr>
                        <tr>
                            <td class="numeric label" colspan="3">VAT (19%):</td>
                            <td class="numeric"><span data-bind="text: Globalize.format(calculateVat(), 'c')">@vat.ToString("c")</span></td>
                            <td></td>
                        </tr>
                        <tr>
                            <td class="numeric label" colspan="3">Total:</td>
                            <td class="numeric"><span data-bind="text: Globalize.format(calculateTotal(), 'c')">@total.ToString("c")</span></td>
                            <td></td>
                        </tr>
                ...
            
        <script type="text/html" id="itemTemplate">
                ...
                <td class="numeric"><span data-bind="text: Globalize.format(unitPrice, 'c')"></span></td>
                ...
                <td class="numeric"><span data-bind="text: Globalize.format(total(), 'c')"></span></td>
...         </script>  ...


ShoppingCartWidget.cshtml:

...

<span class="label">Amount:</span> <span class="value" data-bind="text: Globalize.format(calculateTotal(), 'c')">@totalAmount.ToString("c")</span><br/>
...

Let's see what's happening now:

And there you have it: a functioning shopping cart that gracefully falls back when javascript is disabled, but unobtrusively enhances the user experience when javascript is enabled.

In this part, we saw how to:

  • Include resources with our module, such as CSS, scripts and images;
  • Create a Widget;
  • Make use of javascript frameworks such as KnockoutJS and jQuery and communciate with our module via AJAX requests;
  • Optimize queries using the ContentManager;
  • Enable client side globalization support;
We also saw a few plain ASP.NET MVC, Javascript & CSS techniques at work, which had nothing to do with Orchard.
The reason I wanted to show you that is to see that building an Orchard module is very much like building a plain old MVC area. 
And that's one of the nice things about the way Orchard was built: you can build anything you like using ASP.NET MVC and include it as a Module.
Download the source: Part07.zip

In the next part, we'll implement the "proceed to checkout" button, which will take the user to a page where he can either create an account or log in with an existing account.

Part 8 - Registering new Customers with the site

Tags: orchard, module development, shoppingcart, webshop, knockoutjs, jQuery, linqjs, globalization

blog comments powered by Disqus