Writing an Orchard Webshop Module from scratch - Part 8

ated to be compatible for Orchard >= 1.4.

This is part 8 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'll be handling the following:

  • We'll implement the customer registration / login page, which is part of the checkout procedure;
  • We'll implement a readonly overview that shows the products in the shopping cart, the grand total to be charged and the shipping address of the customer;
  • We'll see how to secure certain pages or shapes that require a signed in user;
  • We'll talk a little bit about Orchard's Localization mechanism;
  • We'll see how to read and write attached fields to the CustomerPart and AddressPart.

Let's get started!


In order to store orders in our system, we need to relate them to the customer that placed the orders.
Let's consider some properties and features of a customer entity:

Defining the Customer

For our webshop's purposes, a customer should be able to register with the site and be able to log in if he's already registered.
Since Orchard has this functionality out of the box, we're going to make use of that.

What we're going to do is define a new CustomerPart. We could then either attach it to the User content type (which is defined by Orchard), or create a new content type called Customer which would consist of both the CustomerPart as well as the UserPart (which is also defined by Orchard).

Some of our module users might not like that we modify the User content type, while others might want to turn all of their site users into Customers.
Others might want to enable their customers to just buy something without registering as a user.
That means that we need a Customer content type, and have it optionally become a user by attaching the UserPart to it.
The downside is that if the site supports user registration, these users will not automatically become a Customer content item. Unless of course we handle a certain event that is raised when a new user is created.

We could  let the user of the module configure some settings that controls wether their customers are required to create a user account or not, but we'll leave that to VNext of our module.
For V1, let's go with the most common scenario and allow a customer to register as a user during the checkout procedure.

Creating the CustomerPart

Let's go ahead and create a new class in the Models folder called CustomerPartRecord:

Models/CustomerPartRecord.cs:

using System;
using Orchard.ContentManagement.Records;
 
namespace Skywalker.Webshop.Models
{
    public class CustomerPartRecord : ContentPartRecord {
        public virtual string FirstName { getset; }
        public virtual string LastName { getset; }
        public virtual string Title { getset; }
        public virtual DateTime CreatedUtc { getset; }
    }
}

We'll also create a new class called CustomerPart:

Models/CustomerPart.cs:

using System;
using Orchard.ContentManagement;
 
namespace Skywalker.Webshop.Models
{
    public class CustomerPart : ContentPart<CustomerPartRecord> {
 
        public string FirstName {
            get { return Record.FirstName; }
            set { Record.FirstName = value; }
        }
 
        public string LastName {
            get { return Record.LastName; }
            set { Record.LastName = value; }
        }
 
        public string Title {
            get { return Record.Title; }
            set { Record.Title = value; }
        }
 
        public DateTime CreatedUtc {
            get { return Record.CreatedUtc; }
            set { Record.CreatedUtc = value; }
        }
    }
}

 

You may be wondering if these properties suffice to represent a customer.
What about other information such as phonenumbers, newsletter subscriptions, address information, special discount codes, etc.?

Well, since we're using Orchard, the Customer content type is easily extended with content parts and content fields, either via the admin, 3rd party modules, or both.
We'll just add some fields by default using Migrations.

But if we are adding fields using Migrations anyway, why did we define FirstName, LastName and Title as properties on the class? Well, in certain cases, we need to be able to address the customer by name, for example to send a notification indicating that we received his or her order. and in most cases, a customer has at least a firstname, lastname and title. Implementing this information using content fields would work, but a customer without these fields wouldn't make much sense; especially when we're generating orders and invoices targeted at the customer.

With regards to address information, we could let the user add fields to the CustomerPart that represent an address, but most webshops will enable their customers to specify multiple addresses, for example, one invoice address and one shipping address, and perhaps multiple shipping addresses and invoice addresses.
To support such scenarios, we'll create an AddressPart and an Address content type.

Creating the AddressPart

Let's go ahead and create a class called AddressPartRecord:

Models/AddressPartRecord.cs:

using Orchard.ContentManagement.Records;
 
namespace Skywalker.Webshop.Models
{
    public class AddressPartRecord : ContentPartRecord {
        public virtual int CustomerId { getset; }
        public virtual string Type { getset; }
    }
}

 

Next, we'll create the AddressPart in the Models folder:

Models/AddressPart.cs:

using Orchard.ContentManagement;
 
namespace Skywalker.Webshop.Models
{
    public class AddressPart : ContentPart<AddressPartRecord> {
        
        public int CustomerId {
            get { return Record.CustomerId; }
            set { Record.CustomerId = value; }
        }
 
        public string Type {
            get { return Record.Type; }
            set { Record.Type = value; }
        }
    }
}

 

We'll keep the AddressPart slim and include just two essential properties:

  • CustomerId: to link the address to the customer
  • Type: represents the type or name of the address (e.g. "InvoiceAddress", "ShippingAddress"). We're using strings so that we could potentially add application specific address records and identify them using the Type (e.g. HolidayAddress if your boss needs to find you when some website need to be fixed).

We'll use Migrations to add some default fields to the AddressPart, such as AddressLine1, AddressLine2, so that our users can customize the address entity at will. Some people like to use fields like AddressLine1, AddressLine2, while others prefer to use names like StreetName, HouseNumber, etc. Since we're using content fields, the user can easily change them.

Let's go ahead and update our Migrations:

Migrations.cs (snippet):

public int UpdateFrom4()
        {
            SchemaBuilder.CreateTable("CustomerPartRecord", table => table
                .ContentPartRecord()
                .Column<string>("FirstName", c => c.WithLength(50))
                .Column<string>("LastName", c => c.WithLength(50))
                .Column<string>("Title", c => c.WithLength(10))
                .Column<DateTime>("CreatedUtc")
                );
 
            SchemaBuilder.CreateTable("AddressPartRecord", table => table
                .ContentPartRecord()
                .Column<int>("CustomerId")
                .Column<string>("Type", c => c.WithLength(50))
                );
 
            ContentDefinitionManager.AlterPartDefinition("CustomerPart", part => part
                .Attachable(false)
                );
 
            ContentDefinitionManager.AlterTypeDefinition("Customer", type => type
                .WithPart("CustomerPart")
                .WithPart("UserPart")
                );
 
            ContentDefinitionManager.AlterPartDefinition("AddressPart", part => part
                .Attachable(false)
                .WithField("Name", f => f.OfType("TextField"))
                .WithField("AddressLine1", f => f.OfType("TextField"))
                .WithField("AddressLine2", f => f.OfType("TextField"))
                .WithField("Zipcode", f => f.OfType("TextField"))
                .WithField("City", f => f.OfType("TextField"))
                .WithField("Country", f => f.OfType("TextField"))
                );
 
            ContentDefinitionManager.AlterTypeDefinition("Address", type => type
                .WithPart("CommonPart")
                .WithPart("AddressPart")
                );
 
            return 5;
        }

Nothing fancy going on here: we are creating the CustomerPartRecord table, CustomerPart, Customer content type, AddressRecord table, AddressPart and Address content type.

Next, we'll need two handlers: one for the CustomerPart and another one for AddressPart. These handlers will add a StorageFilter for the parts so that Orchard can load and save data for these parts.

We'll also add an ActivatingFilter<T>. An ActivatingFilter is invoked whenever a content type is instantiated. It enables us to weld-on a content part onto the activated content type. Although we added the UserPart to the Customer type, since there is no driver for the UserPart the Customer type will not contain a part of type UserPart; instead it will be a plain ContentPart base type instance. Unless we either create a driver for UserPart or add an ActivatingFilter<UserPart> for the Customer content type. Since there's no reason for us to create a UserPart driver, we'll use an ActivatingFilter:

Handlers/CustomerPartHandler.cs:

using Orchard.ContentManagement.Handlers;
using Orchard.Data;
using Orchard.Users.Models;
using Skywalker.Webshop.Models;
 
namespace Skywalker.Webshop.Handlers
{
    public class CustomerPartHandler : ContentHandler {
        public CustomerPartHandler(IRepository<CustomerPartRecord> repository) {
            Filters.Add(StorageFilter.For(repository));
            Filters.Add(new ActivatingFilter<UserPart>("Customer"));
        }
    }
}

 

UserPart is defined in the Orchard.Users module, so make sure to add a project reference to that library. Update the module manifest with this dependency as well to be complete (even though most, if not, all Orchard installations will have this module installed and enabled by default):

Dependencies: Orchard.Projector, Orchard.Forms, Orchard.jQuery, AIM.LinqJs, Orchard.Knockout, Orchard.Users

Aside: Whenever you create a new content type that has a UserPart, don't attach the CommonPart as well. Doing so will cause a StackOverflowException when you sign in with that new user type. This happens because whenever Orchard news up a content item, it invokes all content handlers, including the CommonPartHandler. The CommonPartHandler will try to assign the currently loggedin user, but in doing so it will have to load that user. Loading that user will again invoke the CommonPartHandler, which in turn will invoke the AuthenticationService to get the current user, and so on.


The AddressPartHandler is nice and simple: just add a StorageFilter for the AddressPartRecord repository:

Handlers/AddressPartHandler.cs:

using Orchard.ContentManagement.Handlers;
using Orchard.Data;
using Skywalker.Webshop.Models;
 
namespace Skywalker.Webshop.Handlers
{
    public class AddressPartHandler : ContentHandler {
        public AddressPartHandler(IRepository<AddressPartRecord> repository) {
            Filters.Add(StorageFilter.For(repository));
        }
    }
}

 

We'll also need to create two drivers, two editor templates and update Placement.info:

Drivers/CustomerPartDriver.cs:

using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Skywalker.Webshop.Models;
 
namespace Skywalker.Webshop.Drivers
{
    public class CustomerPartDriver : ContentPartDriver<CustomerPart> {
 
        protected override string Prefix {
            get { return "Customer"; }
        }
 
        protected override DriverResult Editor(CustomerPart part, dynamic shapeHelper) {
            return ContentShape("Parts_Customer_Edit", () => shapeHelper.EditorTemplate(TemplateName: "Parts/Customer", Model: part, Prefix: Prefix));
        }
 
        protected override DriverResult Editor(CustomerPart part, IUpdateModel updater, dynamic shapeHelper) {
            updater.TryUpdateModel(part, Prefix, nullnull);
            return Editor(part, shapeHelper);
        }
    }
}

 

Drivers/AddressPartDriver.cs:

using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Skywalker.Webshop.Models;
 
namespace Skywalker.Webshop.Drivers
{
    public class AddressPartDriver : ContentPartDriver<AddressPart> {
 
        protected override string Prefix {
            get { return "Address"; }
        }
 
        protected override DriverResult Editor(AddressPart part, dynamic shapeHelper) {
            return ContentShape("Parts_Address_Edit", () => shapeHelper.EditorTemplate(TemplateName: "Parts/Address", Model: part, Prefix: Prefix));
        }
 
        protected override DriverResult Editor(AddressPart part, IUpdateModel updater, dynamic shapeHelper) {
            updater.TryUpdateModel(part, Prefix, nullnull);
            return Editor(part, shapeHelper);
        }
    }
}

Views/EditorTemplates/Parts/Customer.cshtml:

@model Skywalker.Webshop.Models.CustomerPart
<fieldset>
    <div class="editor-label">@Html.LabelFor(x => x.Title)</div>
    <div class="editor-field">@Html.EditorFor(x => x.Title)</div>
 
    <div class="editor-label">@Html.LabelFor(x => x.FirstName)</div>
    <div class="editor-field">
        @Html.EditorFor(x => x.FirstName)
        @Html.ValidationMessageFor(x => x.FirstName)
    </div>
    
    <div class="editor-label">@Html.LabelFor(x => x.LastName)</div>
    <div class="editor-field">
        @Html.EditorFor(x => x.LastName)
        @Html.ValidationMessageFor(x => x.LastName)
    </div>
</fieldset>

Views/EditorTemplates/Parts/Address.cshtml:

@model Skywalker.Webshop.Models.AddressPart
<fieldset>
    <div class="editor-label">@Html.LabelFor(x => x.Type)</div>
    <div class="editor-field">@Html.EditorFor(x => x.Type)</div>
 
    <div class="editor-label">@Html.LabelFor(x => x.CustomerId)</div>
    <div class="editor-field">
        @Html.EditorFor(x => x.CustomerId)
        @Html.ValidationMessageFor(x => x.CustomerId)
    </div>
</fieldset>

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" />
  <Place Parts_Customer_Edit="Content:0" />
  <Place Parts_Address_Edit="Content:0" />
</Placement>

 

No go ahead and let Orchard execute the new migration step by refreshing (F5) the admin.
Once the migration has upgraded the database schema, we will see two new tables:

 

Now that we have the models in place, let's get started with the next screen: the registration / login page.

User Registration / Login

The first thing we'll ask our customers is whether they already have an account or if they want to create one, so let's create an action called SignupOrLogin on a new controller named CheckoutController:

CheckoutController.cs:

using System.Web.Mvc;
using Orchard;
using Orchard.Localization;
using Orchard.Mvc;
using Orchard.Themes;
 
namespace Skywalker.Webshop.Controllers
{
    public class CheckoutController : Controller
    {
        private readonly IOrchardServices _services;
        private Localizer T { getset; }
 
        public CheckoutController(IOrchardServices services)
        {
            _services = services;
        }
 
        [Themed]
        public ActionResult SignupOrLogin() {
 
            return new ShapeResult(this, _services.New.Checkout_SignupOrLogin());
        }
    }
}

 

We're returning a new ShapeResult that holds a shape called Checkout_SignupOrLogin (notice that we're using the New property of IOrchardServices; it's just an IShapeFactory) so let's create a template for it in the Views folder:

Checkout.SignupOrLogin.cshtml:

@{
    Style.Require("Skywalker.Webshop.Common");
}
<article>
    <p>@T("Are you a returning customer or a new customer?")</p>
    
    <ul class="action bullets">
        <li><a href="@Url.Action("Login""Checkout"new { area = "Skywalker.Webshop" })">I am a <strong>returning customer</strong> and already have an account</a></li>
        <li><a href="@Url.Action("Signup""Checkout"new { area = "Skywalker.Webshop" })">I am a <strong>new customer</strong></a></li>
    </ul>
</article>

 

Notice that we are creating two links pointing to non-existing actions: Login and Signup; we will create these actions shortly. Also notice that even though action names are just strings, ReSharper marks the non-existing action names to be unresolvable. Nice.

The css class "action" and "bullets" are new, so let's add the following css rules to common.css:

common.css (snippet):

...
ul.bullets li {
    background: url("../images/bullets.png") no-repeat;
    line-height: 30px;
    list-style: none;
    padding-left: 15px;
}
 
ul.bullets.action li {
    background-position: 0 0;
}
 
ul.bullets.action li:hover {
    background-position: 1px 0;
}
 
ul.bullets li a {
    text-decoration: none;
}
 
ul.bullets li a:hover {
    color: #434343;
}

 

Save the following image as "bullets.png" to the Images folder:

Navigating to http://localhost:30320/OrchardLocal/Skywalker.Webshop/Checkout/SignupOrLogin should show the following screen:

Obviously, we wouldn't want the user to type that url in by hand. Instead, we want to redirect the user to this page when he hits the "Proceed to checkout" button. Remember that this button submits the form to the ShoppingCartController's Update action method? Of course you do, so let's implement the "Checkout" switch case by redirecting to the SignupOrLogin action method of the CheckoutController:

Controllers/ShoppingCartController.cs (snippet):

[HttpPost]
        public ActionResult Update(string command, UpdateShoppingCartItemViewModel[] items)
        {
            UpdateShoppingCart(items);
 
            if (Request.IsAjaxRequest())
                return Json(true);
 
            switch (command)
            {
                case "Checkout":
                    return RedirectToAction("SignupOrLogin""Checkout");
                case "ContinueShopping":
                    break;
                case "Update":
                    break;
            }
            return RedirectToAction("Index");
        }

Nice. But what if the user is already logged in? Would he still need to specify whether he's a new or returning customer? I would hope not, so let's update the SignupOrLogin action method in case that the site visitor is already loggedin.
We'll go ahead and implement that, plus stub out some other actions:

Controllers/CheckoutController.cs:

using System.Web.Mvc;
using Orchard;
using Orchard.Mvc;
using Orchard.Security;
using Orchard.Themes;
 
namespace Skywalker.Webshop.Controllers {
 public class CheckoutController : Controller {
  private readonly IAuthenticationService _authenticationService;
  private readonly IOrchardServices _services;
 
  public CheckoutController(IOrchardServices services, IAuthenticationService authenticationService) {
   _authenticationService = authenticationService;
   _services = services;
  }
 
  [Themed]
  public ActionResult SignupOrLogin() {
 
   if (_authenticationService.GetAuthenticatedUser() != null)
    return RedirectToAction("SelectAddress");
 
   return new ShapeResult(this, _services.New.Checkout_SignupOrLogin());
  }
 
  [Themed]
  public ActionResult Signup() {
   var shape = _services.New.Checkout_Signup();
   return new ShapeResult(this, shape);
  }
 
  [Themed]
  public ActionResult Login() {
   var shape = _services.New.Checkout_Login();
   return new ShapeResult(this, shape);
  }
 
  [Themed]
  public ActionResult SelectAddress() {
   var shape = _services.New.Checkout_SelectAddress();
   return new ShapeResult(this, shape);
  }
 
  [Themed]
  public ActionResult Summary() {
   var shape = _services.New.Checkout_Summary();
   return new ShapeResult(this, shape);
  }
 }
}

Now we're ready to implementing the Signup screen.


The Signup screen


The Signup screen will contain a form that consists of some of the fields that we want to collect.
Now, we could dynamically build this form using IContentManager.BuildEditor (which will invoke the driver's Editor method of each part and field of the specified content item), but in this case we'll want more fine grained control over which fields will and won't be displayed.

Aside: if IContentManager.BuildEditor supported display types, as IContentManager.Display does, we would be able to render a dynamically created form and use Placement.info to specify what shapes to render at what position within the local zone. However, as of Orchard 1.5, there's a new CustomForms module available which we could probably use as well. I haven't played with this module yet, but it's definitely worth a try. For this demo, we'll use the "classic" MVC approach and create form templates manually.

Let's create a view model named SignupViewModel for which we'll design the Signup screen.

ViewModels/SignupViewModel.cs:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Web.Mvc;
 
namespace Skywalker.Webshop.ViewModels
{
    public class SignupViewModel : IValidatableObject {
        [StringLength(10), Display(Name = "Title")]
        public string Title { getset; }
 
        [StringLength(50), RequiredDisplay(Name = "Firstname")]
        public string FirstName { getset; }
 
        [StringLength(50), RequiredDisplay(Name = "Lastname")]
        public string LastName { getset; }
 
        [StringLength(255), RequiredDataType(DataType.EmailAddress), Display(Name = "Email")]
        public string Email { getset; }
 
        [StringLength(255), RequiredDataType(DataType.Password), Display(Name = "Password")]
        public string Password { getset; }
 
        [StringLength(255), RequiredDataType(DataType.Password), Compare("Password"), Display(Name = "Repeat password")]
        public string RepeatPassword { getset; }
 
        public bool ReceiveNewsletter { getset; }
        public bool AcceptTerms { getset; }
 
        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
            if (!AcceptTerms)
                yield return new ValidationResult("You need to accept our terms and conditions in order to make use of our services");
        }
    }
}

 

Next, we'll create the markup for the Checkout_Signup shape.

Views/Checkout.Signup.cshtml:

@using Skywalker.Webshop.ViewModels
@{
    var signup = (SignupViewModel)Model.Signup;
 
    Style.Require("Skywalker.Webshop.Common");
}
 
<h2>@T("New customer")</h2>
<p>@T("Please fill out the form below")</p>
 
@Html.ValidationSummary()
 
@using (Html.BeginFormAntiForgeryPost(Url.Action("Signup""Checkout"new { area = "Skywalker.Webshop" }))) {    
    <article class="form">
        <fieldset>
            <ul>
                <li>
                    <div class="field-label">@Html.LabelFor(m => signup.Title, T("Title"))</div>
                    <div class="field-editor">@Html.EditorFor(m => signup.Title)</div>
                </li>
                <li>
                    <div class="field-label">@Html.LabelFor(m => signup.FirstName, T("First name"))</div>
                    <div class="field-editor">@Html.EditorFor(m => signup.FirstName)</div>
                </li>
                <li>
                    <div class="field-label">@Html.LabelFor(m => signup.LastName, T("Last name"))</div>
                    <div class="field-editor">@Html.EditorFor(m => signup.LastName)</div>
                </li>
            </ul>
        </fieldset>
    
        <fieldset>
            <ul>
                <li>
                    <div class="field-label">@Html.LabelFor(m => signup.Email, T("Email"))</div>
                    <div class="field-editor">@Html.EditorFor(m => signup.Email)</div>
                </li>
                <li>
                    <div class="field-label">@Html.LabelFor(m => signup.Password, T("Password"))</div>
                    <div class="field-editor">@Html.EditorFor(m => signup.Password)</div>
                </li>
                <li>
                    <div class="field-label">@Html.LabelFor(m => signup.RepeatPassword, T("Repeat password"))</div>
                    <div class="field-editor">@Html.EditorFor(m => signup.RepeatPassword)</div>
                </li>
            </ul>
        </fieldset>
    
        <fieldset>
            <ul>
                <li>
                    <div class="checkbox-and-label">
                        <div class="checkbox">@Html.CheckBoxFor(m => signup.ReceiveNewsletter)</div>
                        <div class="label">@Html.LabelFor(m => signup.ReceiveNewsletter, T("Subscribe to our mailing list"))</div>
                    </div>
                </li>
                <li>
                    <div class="checkbox-and-label">
                        <div class="checkbox">@Html.CheckBoxFor(m => signup.AcceptTerms)</div>
                        <div class="label">
                            <label for="@Html.FieldIdFor(m => signup.AcceptTerms)">
                                @Html.Raw(T("I have read and accept the <a href=\"{0}\" target=\"_blank\">Terms and Conditions</a>""#").ToString())
                            </label>
                        </div>
                    </div>
                </li>
            </ul>
        </fieldset>
    
        <footer class="commands">
            <ul>
                <li class="align left"><a href="#">@T("Cancel")</a></li>
                <li class="align right"><button type="submit">@T("Next")</button></li>
            </ul>
        </footer>
    </article>
}

 

 

Aside: Localization in Orchard

If you're wondering what the @T("Some text") expression means: T is a delegate to a method that will try to find a translation for the specified string by looking it up in a translation file in the PO format, which can be stored in ~/Modules/Skywalker.Webshop/App_Data/Localization/<culture-code>/orchard.module.po.

A PO file may look something like  this:

App_Data/Localization/nl-NL/Sample.po:

msgid "More"
msgstr "Meer"
 
msgid "Search"
msgstr "Zoeken"

You can then use T to translate "More" and "Search":

Sample.cshtml:

<a href="#">@T("More")</a>

Which will be rendered as:

<a href="#">Meer</a>

If no translation could be found, then Orchard will simply render the passed string (e.g. "More").

Checkout the documentation for detailed information and instructions: http://docs.orchardproject.net/Documentation/Creating-global-ready-applications

We also need to add some css rules to "common.css" for aesthetics.

Styles/common.css (snippet):

article.form 
{
    padding: 10px;
    background: #f6f6f6;
}
 
article.form input[type="text"]input[type="password"]input[type="email"]input[type="tel"] 
{
    width: 250px;
}
 
article.form fieldset{
    margin-bottom: 20px;
}
 
article.form fieldset ul {
    list-style: none;
    margin: 0;
}
 
article.form fieldset ul li {
    margin-bottom: 3px;
}
 
article.form fieldset ul li:after {
    clear:both;
    height:0;
    content:".";
    display:block;
    visibility:hidden;
    zoom:1;
}
 
article.form fieldset ul li .field-label {
    float: left;
    width: 250px;
}
 
article.form fieldset ul li .field-editor {
    float: left;
}
 
article.form fieldset ul li .field-label label:after {
    content":";
}
 
article.form .checkbox-and-label:after {
    clear:both;
    height:0;
    content:".";
    display:block;
    visibility:hidden;
    zoom:1;
}
 
article.form .checkbox-and-label .checkbox {
    float: left;width: 25px;
}
 
article.form .checkbox-and-label .label {
    float: left;
}
 
article.form footer.commands {
    padding-top: 20px;
}
 
article.form footer.commands ul {
    list-style: none;
    margin: 0;
}
 
article.form footer.commands ul:after {
    clear:both;
    height:0;
    content:".";
    display:block;
    visibility:hidden;
    zoom:1;
}
 
article.form footer.commands ul button {
    margin: 0;
}

 

Next we'll create an overloaded version of the Signup method that will be invoked when the form is submitted:

Controllers/CheckoutController.cs (snippet):

[HttpPost]
  public ActionResult Signup(SignupViewModel signup) {
   if (!ModelState.IsValid)
    return new ShapeResult(this, _services.New.Checkout_Signup(Signup: signup));
 
   // TODO: Create a new account for the customer 
   return RedirectToAction("SelectAddress");
  }

What we're doing here is see if the modelstate is valid or not. If it's invalid, we return the Checkout_Signup shape and pass in the signup model to redisplay the provided information to the user.

If there are no model binding errors, we should create a new customer account and proceed to the "SelectAddress" screen, where the customer can provide invoice and shipping address information.
First, let's talk about creating a new customer account.

Creating the Customer account

Creating a customer account involves creating a new content item instance of the Customer content type.
As you'll recall, the Customer content type consists of the UserPart and the CustomerPart.

Since we are taking a dependency on Orchard.Users (where UserPart is defined), let's update our Module manifest:

Module.txt:

...
Dependencies: Orchard.jQuery, Orchard.Widgets, Orchard.Users 

We also need to add a reference to the Orchard.Users project so that we can access the properties of a UserPart instance from code.

Let's go ahead and create a new CustomerService class that will take care of creating a new Customer for us.

Services/ICustomerService.cs:

using Orchard;
using Skywalker.Webshop.Models;
 
namespace Skywalker.Webshop.Services
{
    public interface ICustomerService : IDependency {
        CustomerPart CreateCustomer(string email, string password);
    }
}

 

Services/CustomerService.cs:

using Orchard;
using Orchard.ContentManagement;
using Orchard.Security;
using Orchard.Services;
using Orchard.Users.Models;
using Skywalker.Webshop.Models;
 
namespace Skywalker.Webshop.Services
{
    public class CustomerService : ICustomerService
    {
        private readonly IOrchardServices _orchardServices;
        private readonly IMembershipService _membershipService;
        private readonly IClock _clock;
 
        public CustomerService(IOrchardServices orchardServices, IMembershipService membershipService, IClock clock)
        {
            _orchardServices = orchardServices;
            _membershipService = membershipService;
            _clock = clock;
        }
 
        public CustomerPart CreateCustomer(string email, string password)
        {
            // New up a new content item of type "Customer"
            var customer = _orchardServices.ContentManager.New("Customer");
 
            // Cast the customer to a UserPart
            var userPart = customer.As<UserPart>();
 
            // Cast the customer to a CustomerPart
            var customerPart = customer.As<CustomerPart>();
 
            // Set some properties of the customer content item (via UserPart and CustomerPart)
            userPart.UserName                  = email;
            userPart.Email                     = email;
            userPart.NormalizedUserName        = email.ToLowerInvariant();
            userPart.Record.HashAlgorithm      = "SHA1";
            userPart.Record.RegistrationStatus = UserStatus.Approved;
            userPart.Record.EmailStatus        = UserStatus.Approved;
 
            // Use IClock to get the current date instead of using DateTime.Now (see http://skywalkersoftwaredevelopment.net/orchard-development/api/iclock)
            customerPart.CreatedUtc = _clock.UtcNow;
 
            // Use Ochard's MembershipService to set the password of our new user
            _membershipService.SetPassword(userPart, password);
 
            // Store the new user into the database
            _orchardServices.ContentManager.Create(customer);
 
            return customerPart;
        }
    }
}

To summarize the above coude, creating a customer involves the following steps:

  1. Create a new content item of the Customer content type using the ContentManager by calling the New method.
  2. Get the UserPart that is attached to the Customer content item so that we can set its username, email, and password related stuff.
  3. Save the new instance by calling the Create method on the ContentManager.

Note that we are using a IClock to get the current date. The reason that we are doing it like this is to improve testability by decoupling a dependency on the DateTime system resource.

Next, we'll update our Signup action method that will use the CustomerService.

Controllers/CheckoutController.cs (snippet):

[HttpPost]
  public ActionResult Signup(SignupViewModel signup) {
   if (!ModelState.IsValid)
    return new ShapeResult(this, _services.New.Checkout_Signup(Signup: signup));
 
   var customer       = _customerService.CreateCustomer(signup.Email, signup.Password);
   customer.FirstName = signup.FirstName;
   customer.LastName  = signup.LastName;
   customer.Title     = signup.Title;
 
   _authenticationService.SignIn(customer.User, true);
 
   return RedirectToAction("SelectAddress");
  }

The _customerService expression is a private field on the class, which is injected via the constructor:

public CheckoutController(IOrchardServices services, IAuthenticationService authenticationService, ICustomerService customerService) {
   _authenticationService = authenticationService;
   _services = services;
   _customerService = customerService;
  }

Note that we are automatically signing in the customer right after its creation. The reason for this is that the next pages will be secured, and we don't want to lose our customer by having him first activate his account and then sign in again.
Also note that we created a property called User on the CustomerPart. That property is implemented as follows:

Models/CustomerPart.cs:

using System;
using Orchard.ContentManagement;
using Orchard.Security;
using Orchard.Users.Models;
 
namespace Skywalker.Webshop.Models
{
    public class CustomerPart : ContentPart<CustomerPartRecord> {
 
        public string FirstName {
            get { return Record.FirstName; }
            set { Record.FirstName = value; }
        }
 
        public string LastName {
            get { return Record.LastName; }
            set { Record.LastName = value; }
        }
 
        public string Title {
            get { return Record.Title; }
            set { Record.Title = value; }
        }
 
        public DateTime CreatedUtc {
            get { return Record.CreatedUtc; }
            set { Record.CreatedUtc = value; }
        }
 
        public IUser User {
            get { return this.As<UserPart>(); }
        }
    }
}

 

The resulting screen should look like this:

Before we continue on to the next screen, let's implement the Login screen.

The Login screen

If the customer indicates that he's a returning customer, he will click the "I am a returning customer and already have an account" link, which will take him to the Login action method of CheckoutController.

First, let's define the view model:

ViewModels/LoginViewModel.cs:

using System.ComponentModel.DataAnnotations;
 
namespace Skywalker.Webshop.ViewModels
{
    public class LoginViewModel
    {
        [Required]
        [DataType(DataType.EmailAddress)]
        public string Email { getset; }
 
        [Required]
        [DataType(DataType.Password)]
        public string Password { getset; }
 
        public bool CreatePersistentCookie { getset; }
    }
}

We already defined the Login action, so let's complete the Checkout.Login template:

Views/Checkout.Login.cshtml:

 

@using Skywalker.Webshop.ViewModels
@{
    var login = (LoginViewModel)Model.Login;
 
    Style.Require("Skywalker.Webshop.Common");
}
 
<h2>@T("Returning customer")</h2>
<p>@T("Please login using the form below")</p>
 
@Html.ValidationSummary()
 
@using (Html.BeginFormAntiForgeryPost(Url.Action("Login""Checkout"new { area = "Skywalker.Webshop" }))) {
    
    <article class="form">
            
        <fieldset>
            <ul>
                <li>
                    <div class="field-label">@Html.LabelFor(m => login.Email, T("Email"))</div>
                    <div class="field-editor">@Html.EditorFor(m => login.Email)</div>
                </li>
                <li>
                    <div class="field-label">@Html.LabelFor(m => login.Password, T("Password"))</div>
                    <div class="field-editor">@Html.EditorFor(m => login.Password)</div>
                </li>
            </ul>
        </fieldset>
    
        <fieldset>
            <ul>
                <li>
                    <div class="checkbox-and-label">
                        <div class="checkbox">@Html.CheckBoxFor(m => login.CreatePersistentCookie)</div>
                        <div class="label">@Html.LabelFor(m => login.CreatePersistentCookie, T("Remember me next time"))</div>
                    </div>
                </li>
            </ul>
        </fieldset>
    
        <footer class="commands">
            <ul>
                <li class="align left"><a href="#">@T("Cancel")</a></li>
                <li class="align right"><button type="submit">@T("Next")</button></li>
            </ul>
        </footer>
    
    </article>
}

 

The screen should look like this:

To handle the form submission, we'll create an overloaded version of the Login action method. Beause that method requires some additional Orchard goodies, here's the complete code until now:

Controllers/CheckoutController.cs:

using System.Web.Mvc;
using Orchard;
using Orchard.Localization;
using Orchard.Mvc;
using Orchard.Security;
using Orchard.Themes;
using Skywalker.Webshop.Services;
using Skywalker.Webshop.ViewModels;
 
namespace Skywalker.Webshop.Controllers {
 public class CheckoutController : Controller {
  private readonly IAuthenticationService _authenticationService;
  private readonly IOrchardServices _services;
  private readonly ICustomerService _customerService;
  private readonly IMembershipService _membershipService;
  private Localizer T { getset; }
 
  public CheckoutController(IOrchardServices services, IAuthenticationService authenticationService, ICustomerService customerService, IMembershipService membershipService) {
   _authenticationService = authenticationService;
   _services = services;
   _customerService = customerService;
   _membershipService = membershipService;
   T = NullLocalizer.Instance;
  }
 
  [Themed]
  public ActionResult SignupOrLogin() {
 
   if (_authenticationService.GetAuthenticatedUser() != null)
    return RedirectToAction("SelectAddress");
 
   return new ShapeResult(this, _services.New.Checkout_SignupOrLogin());
  }
 
  [Themed]
  public ActionResult Signup() {
   var shape = _services.New.Checkout_Signup();
   return new ShapeResult(this, shape);
  }
 
  [HttpPost]
  public ActionResult Signup(SignupViewModel signup) {
   if (!ModelState.IsValid)
    return new ShapeResult(this, _services.New.Checkout_Signup(Signup: signup));
 
   var customer       = _customerService.CreateCustomer(signup.Email, signup.Password);
   customer.FirstName = signup.FirstName;
   customer.LastName  = signup.LastName;
   customer.Title     = signup.Title;
 
   _authenticationService.SignIn(customer.User, true);
 
   return RedirectToAction("SelectAddress");
  }
 
  [Themed]
  public ActionResult Login() {
   var shape = _services.New.Checkout_Login();
   return new ShapeResult(this, shape);
  }
 
  [ThemedHttpPost]
  public ActionResult Login(LoginViewModel login) {
 
   // Validate the specified credentials
   var user = _membershipService.ValidateUser(login.Email, login.Password);
 
   // If no user was found, add a model error
   if (user == null) {
    ModelState.AddModelError("Email", T("Incorrect username/password combination").ToString());
   }
 
   // If there are any model errors, redisplay the login form
   if (!ModelState.IsValid) {
    var shape = _services.New.Checkout_Login(Login: login);
    return new ShapeResult(this, shape);
   }
 
   // Create a forms ticket for the user
   _authenticationService.SignIn(user, login.CreatePersistentCookie);
 
   // Redirect to the next step
   return RedirectToAction("SelectAddress");
  }
 
  [Themed]
  public ActionResult SelectAddress() {
   var shape = _services.New.Checkout_SelectAddress();
   return new ShapeResult(this, shape);
  }
 
  [Themed]
  public ActionResult Summary() {
   var shape = _services.New.Checkout_Summary();
   return new ShapeResult(this, shape);
  }
 }
}

Thanks to Orchard's out of the box IMembershipService, that was really quite easy, don't you think?

Great. Let's continue with the SelectAddress screen.

The Select Address screen

In the Select Address screen, we will show both the invoice address as well as the shipping address to the customer.
If the customer leaves the shipping address fields empty, we'll just ship the products to the invoice address. Using Javascript, we could enhance the UI by hiding the shipping address fields, and show them when the customer clicks a button indicating that he would like to have his order shipped to another address, but I wouldn't want to rob you from some good excersises so I'll leave that up to you.

We'll start by defining two view models: AddressViewModel and AdressesViewModel.

ViewModels/AddressViewModel.cs:

using System.ComponentModel.DataAnnotations;
 
namespace Skywalker.Webshop.ViewModels
{
    public class AddressViewModel {
        [StringLength(50)]
        public string Name { getset; }
 
        [StringLength(256)]
        public string AddressLine1 { getset; }
 
        [StringLength(256)]
        public string AddressLine2 { getset; }
 
        [StringLength(10)]
        public string Zipcode { getset; }
 
        [StringLength(50)]
        public string City { getset; }
 
        [StringLength(50)]
        public string Country { getset; }
    }
}

ViewModels/AddressesViewModel.cs:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
 
namespace Skywalker.Webshop.ViewModels
{
    public class AddressesViewModel : IValidatableObject {
 
        [UIHint("Address")]
        public AddressViewModel InvoiceAddress { getset; }
 
        [UIHint("Address")]
        public AddressViewModel ShippingAddress { getset; }
 
        public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            var address = InvoiceAddress;
 
            if (string.IsNullOrWhiteSpace(address.AddressLine1))
                yield return new ValidationResult("Addressline 1 is a required field"new[] { "InvoiceAddress.AddressLine1" });
 
            if (string.IsNullOrWhiteSpace(address.Zipcode))
                yield return new ValidationResult("Zipcode is a required field"new[] { "InvoiceAddress.Zipcode" });
 
            if (string.IsNullOrWhiteSpace(address.City))
                yield return new ValidationResult("City is a required field"new[] { "InvoiceAddress.City" });
 
            if (string.IsNullOrWhiteSpace(address.Country))
                yield return new ValidationResult("Country is a required field"new[] { "InvoiceAddress.Country" });
        }
    }
}

 

The reason we're manually validating AddressesVM instead of annotating the required fields on AddressVM with the RequiredAttribute is that the ShippingAddress is not required; only the InvoiceAddress is required. Of course there are other ways to implement conditional validation, but this seemed the easiest way given the annotations we currently have. If you're interested in conditional validation using DataAnnotations, you should check the following blog post: http://blogs.msdn.com/b/stuartleeks/archive/2011/10/06/flexible-conditional-validation-with-asp-net-mvc-3.aspx.

Let's continue and add the following markup to the SelectAddress template:

Views/Checkout.SelectAddress.cshtml:

@using Skywalker.Webshop.ViewModels
@{
    var addresses = (AddressesViewModel) Model.Addresses;
 
    Style.Require("Skywalker.Webshop.Common");
}
<h2>Address Details</h2>
<p>@T("Please provide us with your billing address and shipping address. If both addresses are the same, then you only need to provide us with your invoice address.")</p>
 
@using (Html.BeginFormAntiForgeryPost(Url.Action("SelectAddress""Checkout"new { area = "Skywalker.Webshop" }))) {
    <article class="form">
    
        @Html.EditorFor(m => addresses.InvoiceAddress)
        @Html.EditorFor(m => addresses.ShippingAddress)
    
        <footer class="commands">
            <ul>
                <li class="align left"><a href="#">@T("Cancel")</a></li>
                <li class="align right"><button type="submit">@T("Next")</button></li>
            </ul>
        </footer>
    </article>
}

We also need to create an editor tempate for the AddressViewModel:

Views/EditorTemplates/Address.cshtml:

@model Skywalker.Webshop.ViewModels.AddressViewModel
 
<fieldset>
    <legend>@ViewData.ModelMetadata.GetDisplayName()</legend>
    <ul>
        <li>
            <div class="field-label">@Html.LabelFor(m => m.Name, T("Name"))</div>
            <div class="field-editor">@Html.EditorFor(m => m.Name)</div>
        </li>
        <li>
            <div class="field-label">@Html.LabelFor(m => m.AddressLine1, T("Address line 1"))</div>
            <div class="field-editor">@Html.EditorFor(m => m.AddressLine1)</div>
        </li>
        <li>
            <div class="field-label">@Html.LabelFor(m => m.AddressLine2, T("Address line 2"))</div>
            <div class="field-editor">@Html.EditorFor(m => m.AddressLine2)</div>
        </li>
        <li>
            <div class="field-label">@Html.LabelFor(m => m.Zipcode, T("Zipcode"))</div>
            <div class="field-editor">@Html.EditorFor(m => m.Zipcode)</div>
        </li>
        <li>
            <div class="field-label">@Html.LabelFor(m => m.City, T("City"))</div>
            <div class="field-editor">@Html.EditorFor(m => m.City)</div>
        </li>
        <li>
            <div class="field-label">@Html.LabelFor(m => m.Country, T("Country"))</div>
            <div class="field-editor">@Html.EditorFor(m => m.Country)</div>
        </li>
    </ul>
</fieldset>

 

The resulting screen should look like this:

The "Name" field should contain of the owner of the address. If there are no addresses stored yet for the customer, we will provide the customer's name for this field.
If the user is a returning customer, we need to populate the fields with the existing information. 

Let's modify our SelectAddress action method:

Controllers/CheckoutController.cs (snippets):

[Themed]
  public ActionResult SelectAddress() {
   var currentUser = _authenticationService.GetAuthenticatedUser();
 
   if (currentUser == null)
    throw new OrchardSecurityException(T("Login required"));
 
   var customer = currentUser.ContentItem.As<CustomerPart>();
   var invoiceAddress = _customerService.GetAddress(customer.Id, "InvoiceAddress");
   var shippingAddress = _customerService.GetAddress(customer.Id, "ShippingAddress");
 
   var addressesViewModel = new AddressesViewModel {
    InvoiceAddress = MapAddress(invoiceAddress),
    ShippingAddress = MapAddress(shippingAddress)
   };
 
   var shape = _services.New.Checkout_SelectAddress(Addresses: addressesViewModel);
   if (string.IsNullOrWhiteSpace(addressesViewModel.InvoiceAddress.Name))
    addressesViewModel.InvoiceAddress.Name = string.Format("{0} {1} {2}", customer.Title, customer.FirstName, customer.LastName);
   return new ShapeResult(this, shape);
  }

...

private AddressViewModel MapAddress(AddressPart addressPart) {
   dynamic address = addressPart;
   var addressViewModel = new AddressViewModel();
 
   if (addressPart != null) {
    addressViewModel.Name = address.Name.Value;
    addressViewModel.AddressLine1 = address.AddressLine1.Value;
    addressViewModel.AddressLine2 = address.AddressLine2.Value;
    addressViewModel.Zipcode = address.Zipcode.Value;
    addressViewModel.City = address.City.Value;
    addressViewModel.Country = address.Country.Value;
   }
 
   return addressViewModel;
  }

The first thing we do is get the currently signedin user via the IAuthenticationService. If it returns null, it means there is currently no user signed in. If that's the case, we'll throw an OrchardSecurityException, which will be handled by Orchard by redirecting the user to the login page.

Next we will get the CustomerPart via the user so that we can get to its address information. We do that by making a call to ICustomerService.GetAddress, which is a new method:

Services/ICustomerService.cs:

using Orchard;
using Skywalker.Webshop.Models;
 
namespace Skywalker.Webshop.Services
{
    public interface ICustomerService : IDependency {
        CustomerPart CreateCustomer(string email, string password);
        AddressPart GetAddress(int customerId, string addressType);
    }
}

Services/CustomerService.cs (snippet):

public AddressPart GetAddress(int customerId, string addressType) {
            return _orchardServices.ContentManager.Query<AddressPartAddressPartRecord>().Where(x => x.CustomerId == customerId && x.Type == addressType).List().FirstOrDefault();
        }

 

As you can see, it's a simple query that selects all addresses where the CustomerId is equal to the specified customerId and the Type is equal to the specified addressType.

Next, we are creating two AddressViewModels using a private helper method called MapAddress.
The interesting part of this method is that we are casting the AddressPart instance to dynamic

This enables us to query the values of the attached fields (which we did from Migrations) as if these fields were mere properties of the AddressPart.

Finally we check if the Name field of the AddressViewModel instance is empty. If it is, we will supply a default value by setting it to the full name of the customer. 

Let's continue and implement the overloaded version of the SelectAddress action method that will store the address:

Controllers/CheckoutController.cs (snippet):

[ThemedHttpPost]
  public ActionResult SelectAddress(AddressesViewModel addresses) {
   var currentUser = _authenticationService.GetAuthenticatedUser();
 
   if (currentUser == null)
    throw new OrchardSecurityException(T("Login required"));
 
   if (!ModelState.IsValid) {
    return new ShapeResult(this, _services.New.Checkout_SelectAddress(Addresses: addresses));
   }
 
   var customer = currentUser.ContentItem.As<CustomerPart>();
   MapAddress(addresses.InvoiceAddress, "InvoiceAddress", customer);
   MapAddress(addresses.ShippingAddress, "ShippingAddress", customer);
 
   return RedirectToAction("Summary");
  }

 

Again, we're getting the current user, throw an exception if its null, and return the same view if there is a model binding error.

If there are no model binding errors, we'll map the retrieved address fields to the address parts of the customer by calling an overloaded version of MapAddress, which looks like this:

private AddressPart MapAddress(AddressViewModel source, string addressType, CustomerPart customerPart) {
   var addressPart = _customerService.GetAddress(customerPart.Id, addressType) ?? _customerService.CreateAddress(customerPart.Id, addressType);
   dynamic address = addressPart;
 
   address.Name.Value         = source.Name.TrimSafe();
   address.AddressLine1.Value = source.AddressLine1.TrimSafe();
   address.AddressLine2.Value = source.AddressLine2.TrimSafe();
   address.Zipcode.Value      = source.Zipcode.TrimSafe();
   address.City.Value         = source.City.TrimSafe();
   address.Country.Value      = source.Country.TrimSafe();
 
   return addressPart;
  }

Notice that on the first line, we are loading an AddressPart for the specified customer. If no AddressPart could be found, we create one by calling the ICustomerService.CreateAddress method, which looks like this:

Services/ICustomerService.cs (snippet):

AddressPart CreateAddress(int customerId, string addressType);

Services/CustomerService.cs (snippet):

public AddressPart CreateAddress(int customerId, string addressType) {
            return _orchardServices.ContentManager.Create<AddressPart>("Address", x => {
                x.Type = addressType;
                x.CustomerId = customerId;
            });
        }
        

Al we do is tell Orchard to create a new ContentItem of type Address, and return it in the form of an AddressPart.

The call to TrimSafe is a call to a custom extension method:

Helpers/StringExtensions.cs:

namespace Skywalker.Webshop.Helpers
{
 public static class StringExtensions {
  public static string TrimSafe(this string s) {
   return s == null ? string.Empty : s.Trim();
  }
 }
}

It simply allows us to trim a string, without getting a nasty NullReferenceException if the string was null.
Don't forget to include the Skywalker.Webshop.Helpers namespace in the CheckoutController.cs file. 

The Summary screen

Before we redirect the customer to the payment service provider, we'll show a summary of the order he is about to place.

We already created the Summary action, so let's enter the following markup into a new template called "Checkout.Summary.cshtml":

Views/Checkout.Summary.cshtml:

@using Orchard.ContentManagement
@using Skywalker.Webshop.Models
@{
    Style.Require("Skywalker.Webshop.Checkout.Summary");
    var shoppingCart = Model.ShoppingCart;
    var invoiceAddress = Model.InvoiceAddress;
    var shippingAddress = Model.ShippingAddress;
    var items = (IList<dynamic>)shoppingCart.ShopItems;
    var subtotal = (decimal)shoppingCart.Subtotal;
    var vat = (decimal)shoppingCart.Vat;
    var total = (decimal)shoppingCart.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" >
        <h2>Review your order</h2>
        <p>Please review the information below. Hit the Place Order button to proceed.</p>
        <table>
            <thead>
                <tr>
                    <td>Article</td>
                    <td class="numeric">Unit Price</td>
                    <td class="numeric">Quantity</td>
                    <td class="numeric">Total Price</td>
                </tr>
            </thead>
            <tbody>
                @for (var i = 0; i < items.Count; i++) {
                    var item = items[i];
                    var product = (ProductPart) item.Product;
                    var contentItem = (ContentItem) item.ContentItem;
                    var title = item.Title;
                    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">@quantity</td>
                        <td class="numeric">@totalPrice.ToString("c")</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="3">Total:</td>
                    <td class="numeric">@total.ToString("c")</td>
                    <td></td>
                </tr>
            </tfoot>
        </table>
    </article>
    
    <article class="addresses form">
        <div class="invoice-address">
            <h2>Invoice Address</h2>
            <ul class="address-fields">
                <li>@invoiceAddress.Name.Value</li>
                <li>@invoiceAddress.AddressLine1.Value</li>
                <li>@invoiceAddress.AddressLine2.Value</li>
                <li>@invoiceAddress.Zipcode.Value</li>
                <li>@invoiceAddress.City.Value</li>
                <li>@invoiceAddress.Country.Value</li>
            </ul>
        </div>
        <div class="shipping-address">
            <h2>Shipping Address</h2>
            <ul class="address-fields">
                <li>@shippingAddress.Name.Value</li>
                <li>@shippingAddress.AddressLine1.Value</li>
                <li>@shippingAddress.AddressLine2.Value</li>
                <li>@shippingAddress.Zipcode.Value</li>
                <li>@shippingAddress.City.Value</li>
                <li>@shippingAddress.Country.Value</li>
            </ul>
        </div>
    </article>
    
    <article>
        <div class="group">
            <div class="align left"><a href="#">Cancel</a></div>
            <div class="align right"><button type="submit" name="command" value="CreateOrder">Place Order</button></div>
        </div>
    </article>
}

 

We'll also define a new css resource called "Webshop.Checkout.Summary":

ResourceManifest.cs:

// Define the Checkout Summary style sheet
manifest.DefineStyle("Skywalker.Webshop.Checkout.Summary").SetUrl("checkout-summary.css").SetDependencies("Skywalker.Webshop.Common");

Styles/checkout-summary.css:

article.shoppingcart {
    width: 100%;
}
 
article.shoppingcart table {
    width: 100%;   
}
 
article.shoppingcart td {
    padding: 7px 3px 4px 4px;
    vertical-align: middle;
}
 
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.addresses {
    margin: 10px 0 10px 0;
    padding: 0 40px 10px 20px;
}
 
article.addresses:after {
    clear:both;
    height:0;
    content:".";
    display:block;
    visibility:hidden;
    zoom:1;
}
 
article.addresses .invoice-address{
    float: left;
}
 
article.addresses .shipping-address{
    float: right;
}
 
ul.address-fields {
    margin: 0;
    list-style: none;   
}

And let's not forget to update our Summary action method:

Controllers/CheckoutController.cs (snippet):

[Themed]
  public ActionResult Summary() {
   var user = _authenticationService.GetAuthenticatedUser();
 
   if (user == null)
    throw new OrchardSecurityException(T("Login required"));
 
   dynamic invoiceAddress = _customerService.GetAddress(user.Id, "InvoiceAddress");
   dynamic shippingAddress = _customerService.GetAddress(user.Id, "ShippingAddress");
   dynamic shoppingCartShape = _services.New.ShoppingCart();
 
   var query = _shoppingCart.GetProducts().Select(x => _services.New.ShoppingCartItem(
    Product: x.ProductPart,
    Quantity: x.Quantity,
    Title: _services.ContentManager.GetItemMetadata(x.ProductPart).DisplayText
   ));
 
   shoppingCartShape.ShopItems = query.ToArray();
   shoppingCartShape.Total     = _shoppingCart.Total();
   shoppingCartShape.Subtotal  = _shoppingCart.Subtotal();
   shoppingCartShape.Vat       = _shoppingCart.Vat();
 
   return new ShapeResult(this, _services.New.Checkout_Summary(
    ShoppingCart: shoppingCartShape,
    InvoiceAddress: invoiceAddress,
    ShippingAddress: shippingAddress
   ));
  }

 

Note that you need to inject IShoppingCart and store it in a private field called _shoppingCart.

Also note that we're doing something interesting here to get the title for the product.
Although we could have simply retrieved the title from the TitlePart (which is attached to the Product content type) , we're using the ContentManager's GetItemMetadata method instead.
The advantage of this is that it's more flexible: should we ever decide that the title of our product should not be implemented by TitlePart, but by some other means, we can implement that in the Product content handler.
To read more about this, see: /blog/overriding-contentitem-s-title-in-the-dashboard

And now we have our summary screen:


 

This part demonstrated how module development really feals like: simple create some controllers, views and business logic, and of course taking advantage of Orchard's rich API.

In the next part, we'll implement the "Place Order" button and look at how we can decouple generic commerce functionality from specific functionality.
For example, "paying for an order" is a generic requirement, but there are many specific ways to implement such a requirement. For example, some clients may want to use PayPal while others want to use Google Checkout or Credit Cards.

As we'll see, Orchard's Event Bus system will help us with the decoupling and leave implementing the specifics to other modules.

Part 9 -> Creating Orders and Communicating with the Payment Service Provider

Download Part08.zip

9 comments

  • ... Posted by Mounhim Posted 08/10/2012 01:03 PM

    In this part you state that one can add fields via the Admin UI to the customer part. How would one render those fields in the view you created. As for validation I assume the added fields implement their own validation.

  • ... Posted by Short URL Posted 12/19/2012 01:27 AM [http://linkpop.pro]

    I'm truly enjoying the design and layout of your blog. It's a very easy on the eyes which makes it much more enjoyable for me to come here and visit more often. Did you hire out a developer to create your theme? Fantastic work!

  • ... Posted by business debt consolidation Posted 12/19/2012 11:44 AM [http://loansbadcreditapp.org/]

    What's Happening i am new to this, I stumbled upon this I've discovered It absolutely helpful and it has helped me out loads. I am hoping to contribute & help different customers like its aided me. Good job.

  • ... Posted by Resveratrol Posted 12/21/2012 05:24 AM [http://transresveratrolweightloss.blogspot.in/]

    Thanks for sharing your info. I truly appreciate your efforts and I am waiting for your next write ups thanks once again.

  • ... Posted by Lashonda Posted 01/13/2013 09:07 AM [http://www.videofellow.com/ErnieKim]

    Tremendous issues here. I'm very happy to peer your article. Thanks so much and I'm having a look forward to contact you. Will you kindly drop me a mail?

  • ... Posted by David Posted 01/25/2013 07:36 PM [http://www.interiordesignblogs.net]

    I just couldn't go away your web site before suggesting that I really enjoyed the standard info an individual supply to your guests? Is going to be again incessantly in order to investigate cross-check new posts

  • ... Posted by As Posted 03/02/2015 08:38 AM

    Hi for customer you create two classes, CustomerPartRecord and CustomerPart. in my website i have book classes, BookPartRecord and BookPart, how we can make n-n relationship between customer and book? Thank You

  • ... Posted by Sipke Schoorstra Posted 03/02/2015 10:45 AM (Author)

    To create a many to many relationship between a customer and their books, you need to create a third table that stores this association. This table would be called something like CustomerBooks and consist of an Id, CustomerId and BookId.

    Some helpful topics on CodePlex:

    • https://orchard.codeplex.com/discussions/266763
    • http://orchard.codeplex.com/discussions/261687
  • ... Posted by Lawyerson Posted 10/31/2016 12:44 PM

    How can we get Customers to show up in the contents list in the dashboard without attaching a Common Part?

Leave a comment