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

65 Comments

  • hgh review said

    I just could not depart your web site prior to suggesting that I really loved the usual info an individual provide to your guests? Is going to be back steadily to inspect new posts. http://www.hghreleaserreview.com

  • prom dress said

    {
    {I have|I've} been {surfing|browsing} online more than {three|3|2|4} hours today, yet I never found any interesting article like yours. {It's|It is}
    pretty worth enough for me. {In my opinion|Personally|In my view}, if all {webmasters|site owners|website owners|web owners} and bloggers made good content
    as you did, the {internet|net|web} will be {much more|a
    lot more} useful than ever before.|
    I {couldn't|could not} {resist|refrain from} commenting. {Very well|Perfectly|Well|Exceptionally well} written!|
    {I will|I'll} {right away|immediately} {take hold of|grab|clutch|grasp|seize|snatch} your {rss|rss feed}
    as I {can not|can't} {in finding|find|to find} your {email|e-mail} subscription {link|hyperlink} or {newsletter|e-newsletter} service. Do {you have|you've} any?
    {Please|Kindly} {allow|permit|let} me {realize|recognize|understand|recognise|know} {so that|in order that}
    I {may just|may|could} subscribe. Thanks.|
    {It is|It's} {appropriate|perfect|the best} time to make some plans for the future and {it is|it's}
    time to be happy. {I have|I've} read this post and if I could I {want to|wish to|desire to} suggest you {few|some} interesting things or {advice|suggestions|tips}. {Perhaps|Maybe} you {could|can} write next articles referring to this article. I {want to|wish to|desire to} read {more|even more} things about it!|
    {It is|It's} {appropriate|perfect|the best} time to make
    {a few|some} plans for {the future|the longer term|the
    long run} and {it is|it's} time to be happy. {I have|I've}
    {read|learn} this {post|submit|publish|put up} and if I {may just|may|could} I {want to|wish to|desire to} {suggest|recommend|counsel} you {few|some} {interesting|fascinating|attention-grabbing} {things|issues}
    or {advice|suggestions|tips}. {Perhaps|Maybe}
    you {could|can} write {next|subsequent} articles {relating to|referring to|regarding}
    this article. I {want to|wish to|desire to} {read|learn} {more|even more} {things|issues} {approximately|about} it!
    |
    {I have|I've} been {surfing|browsing} {online|on-line} {more than|greater than} {three|3} hours {these days|nowadays|today|lately|as of late}, {yet|but} I {never|by no means} {found|discovered} any {interesting|fascinating|attention-grabbing} article like yours. {It's|It
    is} {lovely|pretty|beautiful} {worth|value|price}
    {enough|sufficient} for me. {In my opinion|Personally|In my view}, if all {webmasters|site owners|website owners|web owners} and bloggers made {just right|good|excellent} {content|content material} as {you did|you probably did}, the {internet|net|web} {will be|shall be|might be|will probably be|can be|will likely be}
    {much more|a lot more} {useful|helpful} than ever before.
    |
    Ahaa, its {nice|pleasant|good|fastidious} {discussion|conversation|dialogue}
    {regarding|concerning|about|on the topic of} this {article|post|piece of writing|paragraph} {here|at this place} at this {blog|weblog|webpage|website|web site},
    I have read all that, so {now|at this time} me also commenting {here|at this place}.
    |
    I am sure this {article|post|piece of writing|paragraph} has touched all
    the internet {users|people|viewers|visitors}, its really really {nice|pleasant|good|fastidious} {article|post|piece of writing|paragraph} on building up new {blog|weblog|webpage|website|web
    site}.|
    Wow, this {article|post|piece of writing|paragraph} is {nice|pleasant|good|fastidious}, my {sister|younger sister}
    is analyzing {such|these|these kinds of} things, {so|thus|therefore} I am going to {tell|inform|let know|convey} her.
    |
    {Saved as a favorite|bookmarked!!}, {I really like|I like|I love} {your blog|your site|your web site|your
    website}!|
    Way cool! Some {very|extremely} valid points! I appreciate you {writing
    this|penning this} {article|post|write-up} {and the|and also the|plus the} rest of the {site is|website is} {also very|extremely|very|also
    really|really} good.|
    Hi, {I do believe|I do think} {this is an excellent|this
    is a great} {blog|website|web site|site}. I stumbledupon
    it ;) {I will|I am going to|I'm going to|I may} {come back|return|revisit} {once again|yet again} {since I|since i have} {bookmarked|book marked|book-marked|saved as a favorite} it. Money and freedom {is the best|is the greatest} way to change, may you be rich and continue to {help|guide} {other people|others}.|
    Woah! I'm really {loving|enjoying|digging} the template/theme of this {site|website|blog}.

    It's simple, yet effective. A lot of times it's {very hard|very
    difficult|challenging|tough|difficult|hard} to get that "perfect balance" between {superb usability|user friendliness|usability} and {visual
    appearance|visual appeal|appearance}. I must say {that you've|you have|you've}
    done a {awesome|amazing|very good|superb|fantastic|excellent|great}
    job with this. {In addition|Additionally|Also}, the blog loads {very|extremely|super} {fast|quick} for me on {Safari|Internet explorer|Chrome|Opera|Firefox}.
    {Superb|Exceptional|Outstanding|Excellent} Blog!
    |
    These are {really|actually|in fact|truly|genuinely} {great|enormous|impressive|wonderful|fantastic} ideas in
    {regarding|concerning|about|on the topic of} blogging. You have touched some {nice|pleasant|good|fastidious}
    {points|factors|things} here. Any way keep up wrinting.
    |
    {I love|I really like|I enjoy|I like|Everyone loves} what you guys {are|are usually|tend to be} up too.
    {This sort of|This type of|Such|This kind of} clever
    work and {exposure|coverage|reporting}! Keep up the {superb|terrific|very good|great|good|awesome|fantastic|excellent|amazing|wonderful} works
    guys I've {incorporated||added|included} you guys to {|my|our||my personal|my own} blogroll.|
    {Howdy|Hi there|Hey there|Hi|Hello|Hey}! Someone in my {Myspace|Facebook} group shared this {site|website} with us so I came to {give it a look|look it over|take a look|check it out}. I'm definitely
    {enjoying|loving} the information. I'm {book-marking|bookmarking} and will be tweeting this to my followers! {Terrific|Wonderful|Great|Fantastic|Outstanding|Exceptional|Superb|Excellent} blog and {wonderful|terrific|brilliant|amazing|great|excellent|fantastic|outstanding|superb} {style and design|design and style|design}.|
    {I love|I really like|I enjoy|I like|Everyone loves} what you guys {are|are usually|tend to be} up too. {This sort of|This type of|Such|This kind of} clever work and {exposure|coverage|reporting}! Keep up the {superb|terrific|very good|great|good|awesome|fantastic|excellent|amazing|wonderful} works guys I've {incorporated|added|included} you guys to {|my|our|my personal|my own} blogroll.
    |
    {Howdy|Hi there|Hey there|Hi|Hello|Hey} would you mind {stating|sharing} which blog
    platform you're {working with|using}? I'm {looking|planning|going} to start my own blog
    {in the near future|soon} but I'm having a {tough|difficult|hard} time {making a decision|selecting|choosing|deciding} between BlogEngine/Wordpress/B2evolution and Drupal. The reason I ask is because your {design and style|design|layout} seems different then most blogs and I'm looking for something {completely unique|unique}.
    P.S {My apologies|Apologies|Sorry} for {getting|being} off-topic but I had to ask!
    |
    {Howdy|Hi there|Hi|Hey there|Hello|Hey} would you mind letting me know which
    {webhost|hosting company|web host} you're {utilizing|working with|using}? I've loaded your blog in 3 {completely different|different} {internet
    browsers|web browsers|browsers} and I must say
    this blog loads a lot {quicker|faster} then most.
    Can you {suggest|recommend} a good {internet hosting|web hosting|hosting} provider at a {honest|reasonable|fair}
    price? {Thanks a lot|Kudos|Cheers|Thank you|Many thanks|Thanks}, I
    appreciate it!|
    {I love|I really like|I like|Everyone loves} it {when people|when individuals|when folks|whenever people} {come together|get together} and share {opinions|thoughts|views|ideas}.
    Great {blog|website|site}, {keep it up|continue the good work|stick with it}!
    |
    Thank you for the {auspicious|good} writeup. It in fact
    was a amusement account it. Look advanced to {far|more} added agreeable from you!
    {By the way|However}, how {can|could} we communicate?
    |
    {Howdy|Hi there|Hey there|Hello|Hey} just wanted to give you a quick heads up.
    The {text|words} in your {content|post|article} seem to be running off the screen in {Ie|Internet explorer|Chrome|Firefox|Safari|Opera}.
    I'm not sure if this is a {format|formatting} issue or something to do with {web browser|internet browser|browser} compatibility but I {thought|figured} I'd post to let you know.
    The {style and design|design and style|layout|design} look great though!
    Hope you get the {problem|issue} {solved|resolved|fixed} soon.
    {Kudos|Cheers|Many thanks|Thanks}|
    This is a topic {that is|that's|which is} {close to|near to} my heart... {Cheers|Many thanks|Best wishes|Take care|Thank you}! {Where|Exactly where} are your contact details though?|
    It's very {easy|simple|trouble-free|straightforward|effortless} to find
    out any {topic|matter} on {net|web} as compared to {books|textbooks},
    as I found this {article|post|piece of writing|paragraph} at this {website|web site|site|web page}.
    |
    Does your {site|website|blog} have a contact page? I'm having {a tough time|problems|trouble} locating it but, I'd like
    to {send|shoot} you an {e-mail|email}. I've got some {creative ideas|recommendations|suggestions|ideas} for your blog you might be interested in hearing. Either way, great {site|website|blog} and I look forward to seeing it {develop|improve|expand|grow} over time.|
    {Hola|Hey there|Hi|Hello|Greetings}! I've been {following|reading} your {site|web
    site|website|weblog|blog} for {a long time|a while|some time} now and finally got the
    {bravery|courage} to go ahead and give you a shout out
    from {New Caney|Kingwood|Huffman|Porter|Houston|Dallas|Austin|Lubbock|Humble|Atascocita} {Tx|Texas}!

    Just wanted to {tell you|mention|say} keep up the {fantastic|excellent|great|good} {job|work}!
    |
    Greetings from {Idaho|Carolina|Ohio|Colorado|Florida|Los angeles|California}!
    I'm {bored to tears|bored to death|bored} at work so I decided to {check out|browse} your {site|website|blog} on my iphone during lunch break. I {enjoy|really like|love} the {knowledge|info|information} you {present|provide} here and can't wait to take a look when I get home.

    I'm {shocked|amazed|surprised} at how {quick|fast} your blog loaded on my {mobile|cell phone|phone} .. I'm not
    even using WIFI, just 3G .. {Anyhow|Anyways}, {awesome|amazing|very good|superb|good|wonderful|fantastic|excellent|great} {site|blog}!
    |
    Its {like you|such as you} {read|learn} my {mind|thoughts}!
    You {seem|appear} {to understand|to know|to grasp} {so much|a lot} {approximately|about}
    this, {like you|such as you} wrote the {book|e-book|guide|ebook|e book}
    in it or something. {I think|I feel|I believe} {that you|that you
    simply|that you just} {could|can} do with {some|a few} {%|p.c.|percent} to {force|pressure|drive|power} the message {house|home} {a bit|a little bit}, {however|but} {other than|instead of} that, {this is|that is} {great|wonderful|fantastic|magnificent|excellent} blog. {A great|An excellent|A fantastic} read. {I'll|I will} {definitely|certainly} be back.|
    I visited {multiple|many|several|various} {websites|sites|web sites|web pages|blogs} {but|except|however} the audio {quality|feature} for audio songs {current|present|existing} at this {website|web site|site|web page} is {really|actually|in fact|truly|genuinely} {marvelous|wonderful|excellent|fabulous|superb}.|
    {Howdy|Hi there|Hi|Hello}, i read your blog {occasionally|from time to time} and i own a similar one and i was just {wondering|curious} if you get a lot of spam {comments|responses|feedback|remarks}? If so how do you {prevent|reduce|stop|protect against} it, any plugin or anything you can {advise|suggest|recommend}? I get so much lately it's driving me {mad|insane|crazy} so any {assistance|help|support} is very much appreciated.|
    Greetings! {Very helpful|Very useful} advice {within this|in this particular} {article|post}! {It is the|It's the} little changes {that make|which will make|that produce|that will make} {the biggest|the largest|the greatest|the most important|the most significant} changes. {Thanks a lot|Thanks|Many thanks} for sharing!|
    {I really|I truly|I seriously|I absolutely} love {your blog|your site|your website}.. {Very nice|Excellent|Pleasant|Great} colors & theme. Did you {create|develop|make|build} {this website|this site|this web site|this amazing site} yourself? Please reply back as I'm {looking to|trying to|planning to|wanting to|hoping to|attempting to} create {my own|my very own|my own personal} {blog|website|site} and {would like to|want to|would love to} {know|learn|find out} where you got this from or {what the|exactly what the|just what the} theme {is called|is named}. {Thanks|Many thanks|Thank you|Cheers|Appreciate it|Kudos}!|
    {Hi there|Hello there|Howdy}! This {post|article|blog post} {couldn't|could not} be written {any better|much better}! {Reading through|Looking at|Going through|Looking through} this {post|article} reminds me of my previous roommate! He {always|constantly|continually} kept {talking about|preaching about} this. {I will|I'll|I am going to|I most certainly will} {forward|send} {this article|this information|this post} to him. {Pretty sure|Fairly certain} {he will|he'll|he's going to} {have a good|have a very good|have a great} read. {Thank you for|Thanks for|Many thanks for|I appreciate you for} sharing!|
    {Wow|Whoa|Incredible|Amazing}! This blog looks {exactly|just} like my old one! It's on a {completely|entirely|totally} different {topic|subject} but it has pretty much the same {layout|page layout} and design. {Excellent|Wonderful|Great|Outstanding|Superb} choice of colors!|
    {There is|There's} {definately|certainly} {a lot to|a great deal to} {know about|learn about|find out about} this {subject|topic|issue}. {I like|I love|I really like} {all the|all of the} points {you made|you've made|you have made}.|
    {You made|You've made|You have made} some {decent|good|really good} points there. I {looked|checked} {on the internet|on the web|on the net} {for more info|for more information|to find out more|to learn more|for additional information} about the issue and found {most individuals|most people} will go along with your views on {this website|this site|this web site}.|
    {Hi|Hello|Hi there|What's up}, I {log on to|check|read} your {new stuff|blogs|blog} {regularly|like every week|daily|on a regular basis}. Your {story-telling|writing|humoristic} style is {awesome|witty}, keep {doing what you're doing|up the good work|it up}!|
    I {simply|just} {could not|couldn't} {leave|depart|go away} your {site|web site|website} {prior to|before} suggesting that I {really|extremely|actually} {enjoyed|loved} {the standard|the usual} {information|info} {a person|an individual} {supply|provide} {for your|on your|in your|to your} {visitors|guests}? Is {going to|gonna} be {back|again} {frequently|regularly|incessantly|steadily|ceaselessly|often|continuously} {in order to|to} {check up on|check out|inspect|investigate cross-check} new posts|
    {I wanted|I needed|I want to|I need to} to thank you for this {great|excellent|fantastic|wonderful|good|very good} read!! I {definitely|certainly|absolutely} {enjoyed|loved} every {little bit of|bit of} it. {I have|I've got|I have got} you {bookmarked|book marked|book-marked|saved as a favorite} {to check out|to look at} new {stuff you|things you} post…|
    {Hi|Hello|Hi there|What's up}, just wanted to {mention|say|tell you}, I {enjoyed|liked|loved} this {article|post|blog post}. It was {inspiring|funny|practical|helpful}. Keep on posting!|
    I {{leave|drop|{write|create}} a {comment|leave a response}|drop a {comment|leave a response}|{comment|leave a response}} {each time|when|whenever} I {appreciate|like|especially enjoy} a {post|article} on a {site|{blog|website}|site|website} or {I have|if I have} something to {add|contribute|valuable to contribute} {to the discussion|to the conversation}. {It is|Usually it is|Usually it's|It's} {a result of|triggered by|caused by} the {passion|fire|sincerness} {communicated|displayed} in the {post|article} I {read|looked at|browsed}. And {on|after} this {post|article} Skywalker Software Development - Writing an Orchard Webshop Module from scratch - Part 7. I {{was|was actually} moved|{was|was actually} excited} enough to {drop|{leave|drop|{write|create}}|post} a {thought|{comment|{comment|leave a response}a response}} {:-P|:)|;)|;-)|:-)} I {do have|actually do have} {{some|a few} questions|a couple of questions|2 questions} for you {if you {don't|do not|usually do not|tend not to} mind|if it's {allright|okay}}. {Is it|Could it be} {just|only|simply} me or {do|does it {seem|appear|give the impression|look|look as if|look like} like} {some|a few} of {the|these} {comments|responses|remarks} {look|appear|come across} {like they are|as if they are|like} {coming from|written by|left by} brain dead {people|visitors|folks|individuals}? :-P And, if you are {posting|writing} {on|at} {other|additional} {sites|social sites|online sites|online social sites|places}, {I'd|I would} like to {follow|keep up with} {you|{anything|everything} {new|fresh} you have to post}. {Could|Would} you {list|make a list} {all|every one|the complete urls} of {your|all your} {social|communal|community|public|shared} {pages|sites} like your {twitter feed, Facebook page or linkedin profile|linkedin profile, Facebook page or twitter feed|Facebook page, twitter feed, or linkedin profile}?|
    {Hi there|Hello}, I enjoy reading {all of|through} your {article|post|article post}. I {like|wanted} to write a little comment to support you.|
    I {always|constantly|every time} spent my half an hour to read this {blog|weblog|webpage|website|web site}'s {articles|posts|articles or reviews|content} {everyday|daily|every day|all the time} along with a {cup|mug} of coffee.|
    I {always|for all time|all the time|constantly|every time} emailed this {blog|weblog|webpage|website|web site} post page to all my {friends|associates|contacts}, {because|since|as|for the reason that} if like to read it {then|after that|next|afterward} my {friends|links|contacts} will too.|
    My {coder|programmer|developer} is trying to {persuade|convince} me to move to .net from PHP. I have always disliked the idea because of the {expenses|costs}. But he's tryiong none the less. I've been using {Movable-type|WordPress} on {a number of|a variety of|numerous|several|various} websites for about a year and am {nervous|anxious|worried|concerned} about switching to another platform. I have heard {fantastic|very good|excellent|great|good} things about blogengine.net. Is there a way I can {transfer|import} all my wordpress {content|posts} into it? {Any kind of|Any} help would be {really|greatly} appreciated!|
    {Hello|Hi|Hello there|Hi there|Howdy|Good day}! I could have sworn I've {been to|visited} {this blog|this web site|this website|this site|your blog} before but after {browsing through|going through|looking at} {some of the|a few of the|many of the} {posts|articles} I realized it's new to me. {Anyways|Anyhow|Nonetheless|Regardless}, I'm {definitely|certainly} {happy|pleased|delighted} {I found|I discovered|I came across|I stumbled upon} it and I'll be {bookmarking|book-marking} it and checking back {frequently|regularly|often}!|
    {Terrific|Great|Wonderful} {article|work}! {This is|That is} {the type of|the kind of} {information|info} {that are meant to|that are supposed to|that should} be shared {around the|across the} {web|internet|net}. {Disgrace|Shame} on {the {seek|search} engines|Google} for {now not|not|no longer} positioning this {post|submit|publish|put up} {upper|higher}! Come on over and {talk over with|discuss with|seek advice from|visit|consult with} my {site|web site|website} . {Thank you|Thanks} =)|
    Heya {i'm|i am} for the first time here. I {came across|found} this board and I find It {truly|really} useful & it helped me out {a lot|much}. I hope to give something back and {help|aid} others like you {helped|aided} me.|
    {Hi|Hello|Hi there|Hello there|Howdy|Greetings}, {I think|I believe|I do believe|I do think|There's no doubt that} {your site|your website|your web site|your blog} {might be|may be|could be|could possibly be} having {browser|internet browser|web browser} compatibility {issues|problems}. {When I|Whenever I} {look at your|take a look at your} {website|web site|site|blog} in Safari, it looks fine {but when|however when|however, if|however, when} opening in {Internet Explorer|IE|I.E.}, {it has|it's got} some overlapping issues. {I just|I simply|I merely} wanted to {give you a|provide you with a} quick heads up! {Other than that|Apart from that|Besides that|Aside from that}, {fantastic|wonderful|great|excellent} {blog|website|site}!|
    {A person|Someone|Somebody} {necessarily|essentially} {lend a hand|help|assist} to make {seriously|critically|significantly|severely} {articles|posts} {I would|I might|I'd} state. {This is|That is} the {first|very first} time I frequented your {web page|website page} and {to this point|so far|thus far|up to now}? I {amazed|surprised} with the {research|analysis} you made to {create|make} {this actual|this particular} {post|submit|publish|put up} {incredible|amazing|extraordinary}. {Great|Wonderful|Fantastic|Magnificent|Excellent} {task|process|activity|job}!|
    Heya {i'm|i am} for {the primary|the first} time here. I {came across|found} this board and I {in finding|find|to find} It {truly|really} {useful|helpful} & it helped me out {a lot|much}. {I am hoping|I hope|I'm hoping} {to give|to offer|to provide|to present} {something|one thing} {back|again} and {help|aid} others {like you|such as you} {helped|aided} me.|
    {Hello|Hi|Hello there|Hi there|Howdy|Good day|Hey there}! {I just|I simply} {would like to|want to|wish to} {give you a|offer you a} {huge|big} thumbs up {for the|for your} {great|excellent} {info|information} {you have|you've got|you have got} {here|right here} on this post. {I will be|I'll be|I am} {coming back to|returning to} {your blog|your site|your website|your web site} for more soon.|
    I {always|all the time|every time} used to {read|study} {article|post|piece of writing|paragraph} in news papers but now as I am a user of {internet|web|net} {so|thus|therefore} from now I am using net for {articles|posts|articles or reviews|content}, thanks to web.|
    Your {way|method|means|mode} of {describing|explaining|telling} {everything|all|the whole thing} in this {article|post|piece of writing|paragraph} is {really|actually|in fact|truly|genuinely} {nice|pleasant|good|fastidious}, {all|every one} {can|be able to|be capable of} {easily|without difficulty|effortlessly|simply} {understand|know|be aware of} it, Thanks a lot.|
    {Hi|Hello} there, {I found|I discovered} your {blog|website|web site|site} {by means of|via|by the use of|by way of} Google {at the same time as|whilst|even as|while} {searching for|looking for} a {similar|comparable|related} {topic|matter|subject}, your {site|web site|website} {got here|came} up, it {looks|appears|seems|seems to be|appears to be like} {good|great}. {I have|I've} bookmarked it in my google bookmarks.
    {Hello|Hi} there, {simply|just} {turned into|became|was|become|changed into} {aware of|alert to} your {blog|weblog} {thru|through|via} Google, {and found|and located} that {it is|it's} {really|truly} informative. {I'm|I am} {gonna|going to} {watch out|be careful} for brussels. {I will|I'll} {appreciate|be grateful} {if you|should you|when you|in the event you|in case you|for those who|if you happen to} {continue|proceed} this {in future}. {A lot of|Lots of|Many|Numerous} {other folks|folks|other people|people} {will be|shall be|might be|will probably be|can be|will likely be} benefited {from your|out of your} writing. Cheers!|
    {I am|I'm} curious to find out what blog {system|platform} {you have been|you happen to be|you are|you're} {working with|utilizing|using}? I'm {experiencing|having} some {minor|small} security {problems|issues} with my latest {site|website|blog} and {I would|I'd} like to find something more {safe|risk-free|safeguarded|secure}. Do you have any {solutions|suggestions|recommendations}?|
    {I am|I'm} {extremely|really} impressed with your writing skills {and also|as well as} with the layout on your {blog|weblog}. Is this a paid theme or did you {customize|modify} it yourself? {Either way|Anyway} keep up the {nice|excellent} quality writing, {it's|it is} rare to see a {nice|great} blog like this one {these days|nowadays|today}.|
    {I am|I'm} {extremely|really} {inspired|impressed} {with your|together with your|along with your} writing {talents|skills|abilities} {and also|as {smartly|well|neatly} as} with the {layout|format|structure} {for your|on your|in your|to your} {blog|weblog}. {Is this|Is that this} a paid {subject|topic|subject matter|theme} or did you {customize|modify} it {yourself|your self}? {Either way|Anyway} {stay|keep} up the {nice|excellent} {quality|high quality} writing, {it's|it is} {rare|uncommon} {to peer|to see|to look} a {nice|great} {blog|weblog} like this one {these days|nowadays|today}..|
    {Hi|Hello}, Neat post. {There is|There's} {a problem|an issue} {with your|together with your|along with your} {site|web site|website} in {internet|web} explorer, {may|might|could|would} {check|test} this? IE {still|nonetheless} is the {marketplace|market} {leader|chief} and {a large|a good|a big|a huge} {part of|section of|component to|portion of|component of|element of} {other folks|folks|other people|people} will {leave out|omit|miss|pass over} your {great|wonderful|fantastic|magnificent|excellent} writing {due to|because of} this problem.|
    {I'm|I am} not sure where {you are|you're} getting your {info|information}, but {good|great} topic. I needs to spend some time learning {more|much more} or understanding more. Thanks for {great|wonderful|fantastic|magnificent|excellent} {information|info} I was looking for this {information|info} for my mission.|
    {Hi|Hello}, i think that i saw you visited my {blog|weblog|website|web site|site} {so|thus} i came to “return the favor”.{I am|I'm} {trying to|attempting to} find things to {improve|enhance} my {website|site|web site}!I suppose its ok to use {some of|a few of} your ideas!!\

  • miumiu 長財布 said

    seiko 腕時計 <a href="http://www.watches2013jp.cc/">seiko 腕時計</a> http://www.watches2013jp.cc/ グッチ財布メンズ <a href="http://www.guccibagsjp.cc/">グッチ財布メンズ</a> http://www.guccibagsjp.cc/ ゴルフコース <a href="http://www.goruhujp.cc/">ゴルフコース</a> http://www.goruhujp.cc/ オークリー メガネ <a href="http://www.raybanjpsale.cc/">オークリー メガネ</a> http://www.raybanjpsale.cc/ オークリー メガネ <a href="http://www.oakleyglassessale.cc/">オークリー メガネ</a> http://www.oakleyglassessale.cc/

  • http://etasakura.exblog.jp said

    腕時計、財布、ブログ、最新の情報。私の家をご光臨賜ることを歓迎します。 http://etasakura.exblog.jp http://etasakura.exblog.jp [url=http://etasakura.exblog.jp]http://etasakura.exblog.jp[/url]

  • nike free run 2 said

    Butikk For <a href=http://www.freerunnorge2014.com/>nike free run norge</a><a href=http://www.scarpehogan2014.com/>hogan interactive uomo</a> con il 60% OFF

  • tyyty said

    This excellent website definitely has all of the info I wanted about this subject and didn't
    know who to ask.

  • ナイキ air said

    I know this if off topic but I'm looking into starting my own blog and was wondering what all is required to get setup? I'm assuming having a blog like yours would cost a pretty penny? I'm not very internet smart so I'm not 100% sure. Any recommendations or advice would be greatly appreciated. Thanks

  • ロレックス エクスプローラー said

    バック セリーヌ CELINE セリーヌ 2013-14年秋冬新作 ラゲージ NANO ショッパー トートバッグ ブライトオレンジ(BRIGHT ORANGE)は定番モデルの”ブギーバッグ”が代表格ですが、こちらのモデルも人気です。

  • 2013 buy authentic cheap Youth Nike Baltimore Ravens #81 Anquan Boldin Game Purple Team Color NFL Jersey from China shop said

    The Baltimore Ravens jersey store monogam is stamped a ove the eathe, bt it is not jst a topica appication. The sun literally scorches the much-needed microorganisms that exist within the Baltimore Ravens jerseys shop. "It's as close to something like the Earth that we've found so far. ) Miss Leslie's vintage--but vibrant--sound makes me believe in country Baltimore Ravens jerseys shop. The fact that he is sober now and trying in his way to show love, is enough for me.
    2013 buy authentic cheap Youth Nike Baltimore Ravens #81 Anquan Boldin Game Purple Team Color NFL Jersey from China shop http://www.tigard-or.gov/ravens.asp?id=40

  • セリーヌ マイクロショッパー said

    セリーヌ マイクロショッパー,セリーヌ ミニバッグ,セリーヌ ラゲージトート等セリーヌ コピー激安通販しています。全部実物撮影しております。無事に到着できるの絶対保障です!安心で買い物ください。よろしくお願いします。是非ご覧ください。

  • Reebok New York Giants #26 Antrel Rolle Red 2012 Super Bowl XLVI Premier EQT NFL Jersey said

    In ode to in the maket to that yo wi need to cheap Baltimore Ravens jerseys Nevef cash. What Is The Advantage Of Opting For A Silk Flower BouquetIn case you do not know, choosing the silk buy Baltimore Ravens jerseys bouquet among the other bridal bouquets available in the market is a very wise decision. wholesale Baltimore Ravens jerseys or rain may have seeped through between the cracks during the winter season, causing some bricks or stone to come loose. Everyday allow your body to soak up fresh air and direct sunlight for at least 15 minutes. They more expensive then Suit Supply suits but at the same time if you can find them discounted, the price difference is very slim.
    Reebok New York Giants #26 Antrel Rolle Red 2012 Super Bowl XLVI Premier EQT NFL Jersey http://www.boutiques-theophile.com/blog/index.asp?twid=k_244_nfl2_us-p-tagus2

  • Find Authentic Womens Game James Jones Jersey Nike Green Bay Packers 89 Green Team Color NFL Jerseys with cheap price said

    Require "over 40%" Institutional Ownership. The Semantic Web and Search Engine OptimizationFor SEO looking to gain a better understanding of the semantic web in order to give their clients a jump on the upcoming buy Baltimore Ravens jerseys, the best advice is to be patient. This is just the beginning of the 4th quarter, they have the best months still ahead of them. Even the lightest nike Baltimore Ravens jerseys, after an accumulation like this, will be easier to pack after being buried beneath cheap Baltimore Ravens jerseys layers. is another pharmaceutical nfl Baltimore Ravens jersey that engages in the discovery and Baltimore Ravens jersey store of medicines for cancer patients.

  • navigate to this site said

    For a great deal of testers, all from the queries from Sentence Correction are based for the policies of Standard English. Read each question carefully so you are aware exactly what to perform, even if the situation looks basic and familiar.
    navigate to this site http://www.klippl.com/update?id=83862

  • ray ban sale said

    Not a single thing ever previously skillful within the Medusa manufacturer. This manufacturer, established through artist Gianni Ray ban sunglasses inside 1937, is going engaging, engaging combined with glamorous European-style, It then what's more suggests any kind of unquestionable irreverent rock-star mindsets.

  • Alisha said

    Oh my goodness! Incredible article dude! Thank you, However I am encountering troubles woth your RSS.
    I don't know why I cannot join it. Is there anybody getting
    identical RSS problems? Anybody who knows the answer can you
    kindly respond? Thanx!!

  • Krista said

    Hello, its fastidious post on the topic of media print, we all
    be familiar with media is a enormous source of facts.

  • sunglasses ray ban said

    It is appropriate time to make a few plans for the long run and it's
    time to be happy. I have read this submit and if I may just I want to recommend you some fascinating things or tips.
    Maybe you could write subsequent articles regarding this article.

    I want to read even more things approximately it!

  • Isabel Marant Betty Sneakers said

    Lotta su guance di maiale. Senza l'aiuto di una rete metallica, sarà impossibile rimuovere suini dalla fossa,
    come il tessuto connettivo dei suini sarà sciolta.
    Style Isabel Marant Sneakers Statisticamente, questa è la frenatura assistita più popolare.
    Consiglierei a chiunque di utilizzare questo dispositivo a un altro dispositivo, sicuro e facile
    da usare. Isabel Marant Bobby Sneakers Amministrazioni comunali riluttanti ad
    aumentare le tasse di proprietà per i progetti che i rifiuti
    in una risorsa rinnovabile possono essere finanziati. Migliaia di città
    scavenger ritirare i loro posti di lavoro, mentre gli occhi taglienti, straccivendoli motivati, forse la
    risorsa più preziosa per l'ordinamento secondo le unità di compostaggio okhla come selfemployed e rimanere affamati.

    Isabel Marant Sneakers Bleu Blush è costantemente guidato da abbondante adulazione magia.
    Possa il Signore vostro candidato, wellcoordinated e sono l'ambizione inconfondibile signore di buon gusto.
    Isabel Marant Chaussures Soldes Dopo il casino
    che è stato sentito in tutto il mondo, Louis Vuitton Tribute Patchwork
    Bag, LVprezhnemu cerca di riscattarsi con un paio di borse in edizione limitata, che sperano stare meglio con
    il mondo. Questo è dove la linea Louis Vuitton Dentelle entra
    in gioco, la linea si presenta come una linea di biancheria intima di pizzo..
    Sneakers Isabel Marant Soldes Coltivazione della vite nel retro del mio cortile,
    l'altro giorno, ho scoperto che in realtà ha un vero e uva, naturalmente, penso
    che ci siano 30 o così buio grappolo d'uva viola autorizzati a decidere varietà Concord criminalità,
    le mangiatoie per uccelli. E 'stato incombono su di me per trovare un modo di
    usarli.. Sneakers Isabel Marant été 2014 E 'Chucky 3 3/4 di pollice tallone e
    piattaforma con una bella pelle scamosciata. C'è un avvio può essere indossato con i jeans
    o un vestito con l'ultima gonna a strati in seta. Sneakers Isabel Marant Soldes
    Segretario alla Sicurezza Nazionale Janet Napolitano ha detto
    ieri che la levata miei desert boots potrebbe abbastanza presto
    essere un ricordo del passato. Per i viaggiatori abituali che si
    iscrivono per un programma speciale, almeno, e per il resto di noi
    alla fine. Bottes Isabel Marant Femmes

  • Get The Girl Code said

    We are now giving free samples of premium candy bars.
    To become qualified, just answer our comment along with
    your address and we will send it out within the next day.

  • my Website said

    Being qualified to obtain cash loans no credit check within your favour is
    simple if you are employed permanently with fixed monthly income and hold an engaged bank account
    my Website however, not everybody is readily given a credit
    repair loan.

  • beats by dre pro cheap said

    By means of increasing possibly even tripling through precisely the same resource, feel together, and so described with the reveal area, Spector might generate a enormous,
    symphonic shade on a cds the man turned out.
    Doing exclusively good onto jukeboxes also Experience phone, component was
    already implemented and therefore used by sillybandz with certain great success, for example Beach front Toughness is a characteristic as well as Beatles.

  • monster legends free hack said

    Hello there I am so happy I found үoսr web site, ӏ reаlly found
    ƴoս by mistake, whіle I աɑs browsing on Digg fοr somеthіng else, Ɍegardless ӏ
    am here now and would ϳust liƙе to say thɑnk yoս foг а incredibe post аnd a all round enjoyable blog (Ι аlso love the theme/design), ӏ don’t hɑve time to read it all at
    the moment bսt I hаve bookmarked it and also added in your RSS feeds, ѕо when I havе time I will
    be bаck to read a lot moге, Please ɗo keep սp the excellent work.

  • England Information said

    You actually make it seem so easy with your presentation but I find this matter to be really something which I think I would never understand.
    It seems ttoo complicated and very broad for me. I am lookng forward ffor your next
    post, I'll try to get thhe hang of it!

  • easy-reglement.com said

    The web has truly changed the way we communicate and made it far easier to stay informed about the lives of our loved ones.

    A VPN creates an encrypted connection to a third-party server,
    and all your Internet traffic is routed through that server.
    Efficiency: Many carriers provide multiple receive emails, so your fax messages are
    automatically delivered to whomever you intend.

  • gogreengellibrand.com said

    The backend part of your company supports these profit centers.
    In addition, many of the words used started to
    take on slightly different meanings, depending on the context in which they are used.
    Daily, even hourly, businesses can see how many individuals clicked on their ads and the traffic that is being driven to their website.

  • lobstafest.com said

    This will get you traffic to your site and make money. In addition, many
    of the words used started to take on slightly different
    meanings, depending on the context in which they are used.
    To report stolen email addresses at Yahoo, click Yahoo.

  • http://drusselljuiceplus.com said

    However, this scene changed when more programmers became aware of virus programming and started building viruses that manipulated and destroyed data on infected computers.
    Then consider yourself one of the few, true internet marketers.
    It's probably some mix of the two, so I have to give him props for not going too far in either direction.

  • okuharajuiceplus said

    Although maximum site owners are familiar with these concepts, but they may
    lack the expertise to do it the correctly. Then consider yourself one of the few, true internet marketers.
    By ranking your website on the very first page of search engine results, you
    are tapping into a huge number of people looking for what exactly you offer.

  • ertebatpooya.com said

    The web has truly changed the way we communicate and made it
    far easier to stay informed about the lives of our loved ones.

    A VPN creates an encrypted connection to a third-party server,
    and all your Internet traffic is routed through that server.
    You can download Avast Free Antivirus for free from the Avast website.

  • jeffbrandtagency.com said

    The backend part of your company supports these profit centers.

    The says of free proxies and tunnelling services are over, and as the Chinese government begins
    to tighten its grip on what comes in and out of the country informationally
    speaking, the need for a vpn to bypass internet censorship in China grows every day.
    For more information visit: security gives you the protection from all
    kinds of worms, viruses and other problems.

  • penniesforkidshelpphone.com said

    Also, they may ask you for a small fee for unlocking
    your phone. Now whether it's AOL Instant Messenger, Google
    Talk, ICQ Lite or MSN Messenger, it may be AC3Filter, Any - DVD or BSplayer; you can locate any desired old version of softwares that too
    under one website only. A keen internet user will always remain on the look
    out of a good quality new software to upgrade his system.

  • www.eduangel.net said

    Wanneer dit gebeurt, zal het visitekaartje meestal eerder verslechteren, wat leidt tot een kortere levensduur .U kunt leren om gemakkelijk te tekenen zijn dat uw vriendje is te identificeren bedriegen, dus erachter
    te komen wat ze zijn . U komt in vele sits waar je kunt inschrijven voor gratis tijdschriften en catalogi
    komen . Als je jezelf de vraag hoe kan ik zien of mijn vriendje bedriegt me
    - je moet precies weten wat te zoeken . u zult dan kies de beste sites die u
    wilt en start de online dating reis .

  • Maxwell said

    De beste manier om een aanbeveling die je niet wilt gaan is gewoon om te vragen om te kunnen worden gewijzigd .
    Met de gratis online dating services die beschikbaar zijn voor iedereen, ik voel de wereld is veranderd in een betere plaats om iin te wonen. Zo moet
    je om voortdurend aandacht te besteden elke willekeurige gevoelens en gedachten die plotseling racen over je geest.
    Als je jezelf dde vraag hoe kan ik zien of mijn vriendje bedriegt
    me - je moet precies weten wat te zoeken . Voel je vrij om de tarieven te
    vergelijken met een andere dienst, en je zult zien dat de vraag of het skype vs yello
    beltarieven aan Europa of Skype vs Yello beltarieven naar Azië, Yello is
    zo veel goedkoper.

  • Maximum Shred said

    Hi! I know this is kinda off topic but I'd figured I'd ask.
    Would you be interested in exchanging links or maybe guest writing
    a blog article or vice-versa? My site discusses a lot of the same topics
    as yours and I feel we could greatly benefit from each other.
    If you happen to be interested feel free to shoot me an e-mail.
    I look forward to hearing from you! Awesome blog by the way!

  • bestelauto verzekering said

    in plaats van wist dat ze nodig was, vraagt ​​ze zich
    af of misschien was ze helemaal niet nodig alle.
    Toch hebben mensen de neiging om geld, ongeacht wat de
    situatie van de economie is te besteden . Goedkope huisdier verzekering, plannen en premies variëren afhankelijk van het soort
    huisdier bedekt . Ga naar uw favoriete zoekmachine en type in "swicki door Eurekster" .
    Je kunnen eenvoudig selecteren en toevoegen aan uw project .

  • Ton De Vos Kapper said

    in plaats van wist dat ze nodig was, vraagt ​​ze zich af of misschien was ze helemaal niet
    nodig alle. Ons doel is om singles rond te brengen om te
    chatten en plezier 24 uur van de dag . U komt in vele sites waar je kunt inschrijven voor gratis tijdschriften en catalogi komen .
    Draag geen iets te bloot en onthullende en gebruijk nooit een vloek woorden -
    dus hij weet dat hij kan u kennismaken met
    zijn moeder . Als u denkt over het kopen van goedkope elektrische gitaren, Hopelijk vindt u deze informatie nuttig .

  • auto inruilen said

    coolvetica is de bekendste gratis Helvetica look alike .
    Alles bij elkaar zal dit eruit als een kunstwerk muurschildering .
    Zodra alle 4 zijden zijjn ingesteld die u kunt tooevoegen Stabilisator over
    het paneel . Als je zijn lijden met deze huidaandoening voor een lange tijd en hebben enig
    succes met de behandeling opties die je hebt geprobeerd niet gehad, dan stel ik u proberen natuurlijke
    en zelfgemaakte remedies . elke productie-merk, zoals Nokia,
    Samsung, LG, HTC, Blackberry, Sony Ericsson en dergelijke bieden klanten goedkope apparaten te maken van .

  • consumer telemarketing lists said

    I'd like too thank you for the efforts you've put in writing this blog.
    I am hoping to checxk outt the same high-grade content by you in the
    future aas well. In fact, your creative writing abilities hhas encouraged me to
    get my very own blog now ;)

Add a Comment