Original address: Http://skywalkersoftwaredevelopment.net/blog/writing-an-orchard-webshop-module-from-scratch-part-6
Create shopping cart services and controllers
This is the 6th chapter of the tutorial on writing a new orchard module from scratch.
For an overview of this tutorial, please see the introduction.
In this article, we will enable our users to add items to their shopping cart.
To create such a feature, we need:
- A "Add to cart" button to be added to our product catalog, add products to the shopping cart
- Some kind of shopping cart service to store added items
- Overview of our ShoppingCart, as well as the "continue checkout" button
- A link to our ShoppingCart page is displayed on each page, as well as widgets currently available in stock
Let's start with the first item: Display the "Add to Cart" button on our product catalog.
Render "Add to Cart" button
As you can see in previous posts, a product catalog is basically a list of content items.
In turn, each individual item in the catalog is rendered, from the collection of polygon component content. In this tutorial, the catalog contains a list of content items for the book, each of which is made up of part content, which we've already covered in the 4th article – creating Productparts.
When a orchard renders a content item, it invokes the part driver (Driver) for each part of the content item. In turn, each part of the driver creates a new shape and then renders it with the razor template.
We've got our "parts_product" shape template, created by the Productpart driver, and let's modify it to add a button.
In Visual Studio, open the Parts.Product.cshtml of your view folder:
Code to modify Tags:
@{
var price = (decimal) model.price;
var sku = (string) Model.sku;
}
<article>
Price: @price <br/>
Sku: @sku
<footer>
<button>add to Shoppingcart</button>
</footer>
</article>
Now our product catalog looks like this:
It's so easy. Now, in real-world themes, you might want to customize the appearance of each content item and render it in a list. For example, you might want to show the add to cart button at the back of the body, but the price and SKU fields occupy their current location. There are at least two ways that we can achieve this:
A: We completely take over the rendering work of our list
B: We create a new shape, show our button in the Productpart driver, and use Placement.info to place it anywhere we want in the content item.
Both of these methods are good, but I would recommend using method B as much as possible, because it is more flexible. For example, if a site administrator decides to extend the content type of the book at some stage, add a field or part that will be rendered automatically, there is no need to modify the template for that part of the field or part.
If you are writing a custom theme that has specific requirements to affect the appearance and behavior of the entire content item, then you can choose Method A, and then you have complete freedom. However, each time you add a part or field, you have to update the template, which will deprive you of the freedom to win.
We'll use method B to display this button below BodyPart (you can read more about shapes and areas here).
To create a new shape, we first modify the driver for our Productpart:
To create a new shape, we first modify our Productpart driver:
protected override Driverresult Display (Productpart part, string displayType, dynamic Shapehelper) {
Return Combined (
Contentshape ("Parts_product", () = Shapehelper.parts_product (
Price:part. Price,
Sku:part. Sku
)),
Contentshape ("Parts_product_addbutton", () = Shapehelper.parts_product_addbutton ())
);
}
Here we create an extra shape named "Parts_product_addbutton" and return it through Combinedresult, created using the combined method. The Combinedresult class is a simple class derived from Driverresult and holds the Driverresults IEnumerable.
In fact, we're telling Orchard to render both the shape named Parts_product and the shape named Parts_product_addbutton.
Next, we create a new template file named "Parts.Product.AddButton.cshtml" that will contain the shape's markup:
<button>add to Shoppingcart</button>
Now we need to tell orchard where this shape needs to be presented, modify the Placement.info file as follows:
<Placement>
<place parts_product_edit= "Content:1"/>
<place parts_product= "content:0"/>
<place parts_product_addbutton= "Content:after"/>
</Placement>
We added a third <Place/> element configuration Parts_product_addbutton shape, which is displayed after the content area. The key is to understand that the content item presented here is a shape, and each shape can have sub-shapes. "Content" is the name of a shape inside a content shape itself. For visibility, use shapeTracer:
What we see in the left pane is the tree structure of the entire created shape. The
list shape is created by the Containerpart driver. The shape of each content is created by orchard, and the container for the shape is created by the driver for the content part.
As you can see, our Productpart driver created two shapes: parts_product and Parts_product_addbutton . Also note that Parts_product_addbutton is the last in the list. The order that is configured in the Placement.info file determines the sequence in which a shape is added to the parent shape
To enable shape tracking, make sure that you have the designer Tools module installed. Once installed, you can enable the Shape tracking feature. Don't forget to turn this feature off when your site is published to a production server, because it's not very likely to improve the performance of your site.
Add product to ShoppingCart
Now, we have a button that we need to make it clickable when doing something useful!
We are an excellent MVC developer and we will create a controller to handle the actions of the POST request. Each time the user clicks the "Add to Cart" button, we'll call the action.
We will continue to create a Controllers (Controller) folder into our module and add a controller named Shoppingcartcontroller .
We will also add an action (activity) named Add that contains an ID parameter that represents the product to be added to our shopping cart.
We also need to decide what the user will see when he presses the button. For this demo, we redirect the user to the Shopping cart page (we'll create it in a moment).
The original code should look like this:
Please note that we use an HTTP POST request. While this is not required, the HTTP specification suggests that when modifying the state on the server, you should issue a post instead of get. Since our "Add" action will change our user's shopping cart, we use a post.
In order for the button to call this method, we need to modify the tag of "Parts.Product.AddButton.cshtml" Add <FORM> element:
@using (Html.BeginForm ("Add", "ShoppingCart", new {id =-1})) {
<button type= "Submit" >add to Shoppingcart</button>
}
Please note that our current "id" hard-coded value is specified as-1. When we want to add a product, we need to replace it with the product ID.
In order to get this information, we need to include it in our shape, so we need to modify the Productpart driver:
protected override Driverresult Display (Productpart part, string displayType, dynamic Shapehelper) {
Return Combined (
Contentshape ("Parts_product", () = Shapehelper.parts_product (
Price:part. Price,
Sku:part. Sku
)),
Contentshape ("Parts_product_addbutton", () = Shapehelper.parts_product_addbutton (
Productid:part. Id
))
);
}
We have added a parameter to call Parts_product_addbutton named ProductID, which will become the property of our template model.
Return to our template and modify it:
@{
var productId = (int) Model.productid;
}
@using (Html.BeginForm ("Add", "ShoppingCart", new {id = productId})) {
<button type= "Submit" >add to Shoppingcart</button>
}
It's easy. Now that we've strung our buttons with our shopping cart controller, it's time to create an actual ShoppingCart class to manage our customer shopping cart!
Let's create a new folder called Services (service) and create a Ishoppingcart interface and a ShoppingCart class to implement the interface.
While it is not necessary to define an interface, it is considered a good practice to make our controllers and other classes dependent on abstractions rather than concrete implementations. This is usually desirable when we write unit tests for our modules, which allows us to use a "fake" (mocked) version as a proxy implementation ishoppingcart.
The initial version of our Ishoppingcart will look like this:
Using System.Collections.Generic;
Using Orchard.Webshop.Models;
Namespace Orchard.Webshop.Services {
Public interface Ishoppingcart:idependency {
Ienumerable<shoppingcartitem> Items {get;}
void Add (int productId, int quantity = 1);
void Remove (int productId);
Productrecord getproduct (int productId);
Decimal Subtotal ();
Decimal Vat ();
Decimal total ();
Decimal ItemCount ();
}
}
We will also create a class Shoppingcartitem into the models folder that looks like this:
Using System;
Namespace Orchard.Webshop.Models {
[Serializable]
public sealed class Shoppingcartitem
{
public int ProductId {get; private set;}
private int _quantity;
public int Quantity
{
get {return _quantity;}
Set
{
if (Value < 0)
throw new IndexOutOfRangeException ();
_quantity = value;
}
}
Public Shoppingcartitem ()
{
}
Public Shoppingcartitem (int productId, int quantity = 1)
{
ProductId = ProductId;
Quantity = Quantity;
}
}
}
The Shoppingcartitem class will contain the IDs of the products that have been added and their number.
The initial ShoppingCart implementation will look like this:
Using System.Collections.Generic;
Using System.Linq;
Using System.Web;
Using Orchard.data;
Using Orchard.Webshop.Models;
Namespace Orchard.Webshop.Services {
public class Shoppingcart:ishoppingcart
{
Private ReadOnly iworkcontextaccessor _workcontextaccessor;
Private ReadOnly irepository<productrecord> _productrepository;
Public ienumerable<shoppingcartitem> Items {get {return itemsinternal.asreadonly ();}}
Private HttpContextBase HttpContext
{
get {return _workcontextaccessor.getcontext (). HttpContext; }
}
Private List<shoppingcartitem> Itemsinternal
{
Get
{
var items = (list<shoppingcartitem>) httpcontext.session["ShoppingCart"];
if (items = = null)
{
Items = new list<shoppingcartitem> ();
httpcontext.session["ShoppingCart"] = items;
}
return items;
}
}
Public ShoppingCart (Iworkcontextaccessor workcontextaccessor, irepository<productrecord> productRepository)
{
_workcontextaccessor = Workcontextaccessor;
_productrepository = productrepository;
}
public void Add (int productId, int quantity = 1)
{
var item = items.singleordefault (x = X.productid = = ProductId);
if (item = = NULL)
{
item = new Shoppingcartitem (productId, quantity);
Itemsinternal.add (item);
}
Else
{
Item. Quantity + = Quantity;
}
}
public void Remove (int productId)
{
var item = items.singleordefault (x = X.productid = = ProductId);
if (item = = NULL)
Return
Itemsinternal.remove (item);
}
Public Productrecord getproduct (int productId)
{
Return _productrepository.get (PRODUCTID);
}
public void Updateitems ()
{
Itemsinternal.removeall (x = x.quantity = = 0);
}
Public decimal Subtotal ()
{
return Items.select (x = getproduct (X.productid). Price * x.quantity). Sum ();
}
Public decimal Vat ()
{
return Subtotal () *. 19m;
}
Public decimal Total ()
{
return Subtotal () + Vat ();
}
Public decimal ItemCount () {
return items.sum (x = x.quantity);
}
private void Clear ()
{
Itemsinternal.clear ();
Updateitems ();
}
}
}
It's basically just a httpcontext.session collection of wrappers that we used to store the Shoppingcartitems list.
Note that we use a hard-coded value to specify a VAT rate of 19%, but we will later make our module available to the user to configure.
Also note that we need to add a system.web (version 4.0) reference so that you can use the httpcontextbase type.
To get a HttpContext, we inject an instance of Iworkcontextaccessor , which allows us to access the current request and related data.
To calculate some totals for our shopping cart, it requires product entities that can be loaded from the database. So, we inject irepository <ProductRecord>.
If it does not exist in the list, the Add method creates a new Shoppingcartitem instance, and if there is an instance, it only accumulates the total (Amount) attribute.
To use our ShoppingCart and shoppingcartcontroller, you need to add a reference to one of its instances. The simplest way is to have orchard inject a constructor. But in order for orchard to register our class to the dependency container, we need to inherit the idependency.
Let's move on and we will inherit from Idependency to Ishoppingcart:
Namespace Orchard.Webshop.Services {
Public interface Ishoppingcart:idependency {
...
}
}
Now, we can modify the dependencies on ShoppingCartController.cs on Ishoppingcart and complete the "Add" method:
Using SYSTEM.WEB.MVC;
Namespace Orchard.Webshop.Controllers {
public class Shoppingcartcontroller:controller {
Public Shoppingcartcontroller (Ishoppingcart ShoppingCart) {
_shoppingcart = ShoppingCart;
[HttpPost]
Public ActionResult ADD (int id) {
_shoppingcart.add (ID, 1);
Return redirecttoaction ("Index");
}
}
}
In order to enable our users to see the shopping cart, we need to create a view for it. Let's name it "Index":
Public ActionResult Index () {
return View ();
}
By logic, the next step would be to create a view of "Index". However, we want the theme developers to be able to "reload" the HTML that we render by default, so they should be able to "reload" the Index view.
I tried to create a view in my module and put it in my custom theme, but found that orchard used the module instead of the view in the custom theme.
I can't say that this is the design in itself or where I got it wrong (it won't be the first time), but I think the best solution is to return a shaperesult, not viewresult, because the shape is basically a view, but more powerful (the shape can have a substitute).
So let's return a shape, instead of returning a view. In order to create a shape, we will use Shapefactory to help us. We can inject a shapefactory into the constructor by orchard.
The modified code now looks like this:
Using SYSTEM.WEB.MVC;
Using Orchard.displaymanagement;
Using Orchard.mvc;
Using Orchard.Webshop.Services;
Namespace Orchard.Webshop.Controllers {
public class Shoppingcartcontroller:controller {
Private ReadOnly Ishoppingcart _shoppingcart;
Public Shoppingcartcontroller (Ishoppingcart ShoppingCart, ishapefactory shapefactory) {
_shoppingcart = ShoppingCart;
_shapefactory = shapefactory;
}
[HttpPost]
Public ActionResult ADD (int id) {
_shoppingcart.add (ID, 1);
Return redirecttoaction ("Index");
}
Public ActionResult Index () {
var shape = _shapefactory.create ("ShoppingCart");
return new Shaperesult (this, shape);
}
}
}
We also need to create a template file for the "Shopping cart" shape. Create a file named "shoppingcart.cshtml" to the View folder:
Let's go ahead and when we add items to the shopping cart, see what happens:
Click on the "Add To Cart" button:
Well, it doesn't look right. So, why do we get a 404?
The answer is that our module is really just an area of MVC, orchard in the routes (routing) collection that includes the name of our module as the route value of the zone.
Therefore, the correct path should be:
/orchard.webshop/shoppingcart/add/21 rather than/CONTAINERS/SHOPPINGCART/ADD/21.
We resolve this by modifying the AddButton template, which contains the value of area:
@using (Html.BeginForm ("Add", "ShoppingCart", new {area = "orchard.webshop", id = productId})) {
<button type= "Submit" >add to Shoppingcart</button>
}
Now, let's try again:
That's not what we wanted, but at least we took a step forward.
The problem now is that Orchard validates the post problem by Antiforgeryauthorizationfilterattribute S.
Now, we can either turn this feature off or add security-related fields. We'll do it later, because it might be the right thing to do. Fortunately, this is easy because orchard provides an auxiliary method that can generate a form (form) that will automatically include a hidden security field:
@using (Html.beginformantiforgerypost (url.action ("Add", "ShoppingCart", new {area = "orchard.webshop", id = productId}) )) {
<button type= "Submit" >add to Shoppingcart</button>
}
It doesn't hurt, does it?
Now, let's try again:
Absolutely right! But what about all the other things, like the main menu, the CSS and the layout?
Do we need to provide a master page for our ShoppingCart?
Absolutely not: We just have to tell orchard that this shape should complement the content area layout shape. We can add the [Theme] attribute to our "Index" method to achieve:
[Themed]
Public ActionResult Index () {
var shape = _shapefactory.create ("ShoppingCart");
return new Shaperesult (this, shape);
}
When we try again:
We know that life is good. Even better, we can add the features we want to the page without limit! We know that the most important part: how to create and render shapes, how to create controllers to return shapes and layouts, and the layout of parts.
Of course there are quite a lot of things to learn, such as all the other integrations and extensibility points, how to extend the management interface, or the use of cache modules, the use of Contentmanager to manage and query content, and so on.
But the most important thing at this stage is that if you know how to build an ASP. NET MVC application, you can start creating the Orchard module with confidence.
Render Shopping Cart
It's easy to render a shopping cart, but we'll make it interesting by incorporating Knockoutjs.
Write a Orchard online store module from scratch (6)-Create a shopping cart service and controller