Writing an Orchard Webshop Module from scratch - Part 5

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

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

In order for site visitors to be able to add products to their shoppingcart, we need some sort of product catalog so they can browse the online store.
In essence, a product catalog is a web page that lists the products available for purchase.

Before 1.4, we could utilise the Lists module using the ContainerPart and ContainablePart. However, this approach had quite some limitations. For example, we could only use one content type per list.
But as of Orchard 1.4, we now have a much more flexible, extensible and powerful module at our disposal: the Projections module.

The Projections Module

The Projector module basically enables us to create a Query and use a Projection to render the results of that query.
Which is perfect for our purpose: we want to display a list of all content items whose content type have a product part attached.
We could even go as far as to allow the user to create queries themselves and for example create a Featured Products query.
The module comes with a widget as well, which we could use to show a list of Featured products, or maybe even a Recommended list of products based on the current user's browsing history or purchase history.

For a great introduction to Projections, checkout this article

Creating the ProductCatalog Query

Let's start with defining a Query that will select all of our products.

1. Add a new Query:

 

2. Provide a title for the new query: "Product Catalog":

3. Now that your Query has been created, click either Edit or the name of the query:

4. Click "Add a new Filter":

5. We now get a list of available filters:

6. However, there is no filter we can use to select all content items whose content type have a ProductPart attached, so let's write some code to provide that filter. Our filter will return all content items that have a ProductPart attached. I would want to show you how to create a more reusable filter so that the user could select one or more product parts themselves, but unfortunately I haven't been able to find out how to do this at this time. When I do, I'll update this post. For now, we'll just write a simple filter that gets the job done.  

First, add a reference to the Orchard.Projector project, since we will be using some types from this module. We also need to update our Module.txt to let Orchard know that our module has a dependency on the module:

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.Projections

 

Next, create a new folder called "Filters" and add a new file called "ProductPartFilter.cs"

/Filters/ProductPartFilter.cs:

using Orchard.Localization;
using Orchard.Projections.Descriptors.Filter;
using Skywalker.Webshop.Models;
using IFilterProvider = Orchard.Projections.Services.IFilterProvider;
 
namespace Skywalker.Webshop.Filters
{
    public class ProductPartFilter : IFilterProvider {
        public Localizer T { getset; }
 
        public ProductPartFilter() {
            T = NullLocalizer.Instance;
        }
 
        public void Describe(DescribeFilterContext describe)
        {
            describe.For(
                "Content",          // The category of this filter
                T("Content"),       // The name of the filter (not used in 1.4)
                T("Content"))       // The description of the filter (not used in 1.4)
 
                // Defines the actual filter (we could define multiple filters using the fluent syntax)
                .Element(
                    "ProductParts",     // Type of the element
                    T("Product Parts"), // Name of the element
                    T("Product parts"), // Description of the element
                    ApplyFilter,        // Delegate to a method that performs the actual filtering for this element
                    DisplayFilter       // Delegate to a method that returns a descriptive string for this element
                );
        }
 
        private void ApplyFilter(FilterContext context) {
 
            // Set the Query property of the context parameter to any IHqlQuery. In our case, we use a default query
            // and narrow it down by joining with the ProductPartRecord.
            context.Query = context.Query.Join(x => x.ContentPartRecord(typeof (ProductPartRecord)));
        }
 
        private LocalizedString DisplayFilter(FilterContext context) {
            return T("Content with ProductPart");
        }
    }
}

 

The first time you write a filter, you may feel a bit wary (I did, anyway), because the code doesn't directly show you what data is used where, and the "Element" method is not too specifc, if you feel me. However, if you just start by copying existing filters and tweak some values here and there, you will quickly become acqainted with the API and suddenly all of the above code makes sense.

Basically, we have defined a filter that will select all content items who have a ProductPart attached. We implemented this query by taking advantage of the Join method, which takes a lambda that passes an AliasFactory (the x variable), which has a neat little ContentPartRecord method that creates a join with the ProductPartRecord table for us. This will effectively select all the content that with a ProductPart.

7. Compile the module and refresh the admin. You should now see something like the following screen:

And just like that, we created a new filter!

8. Select the "Product Parts" filter, which will add the filter to your query:

9. Test the query and the filter by pressing the "Preview" button. You should now see a list with all content items that have a product part attached:

Excellent! Now that we have our Query in place, it's time to create a Projection.

Projections

A Projection is just another content type. It has a ProjectionPart attached, which enables us to select a Query that we want to use to render.

Let's go ahead and create a new Projection:

 

Be sure that you selected the Product Catalog query. Also be sure to check the "Show on main menu" checkbox so that the projection will appear on the main menu. I used "Product Catalog" as the text of the menu item. (Warning: 1.5 has completely redesigned navigation features, so this checkbox may not be there when you're using 1.5. There will be other means to show a content item on the main menu though.)

Note that the query uses the Default Layout. The Projector module allows us to define multiple layouts for our queries. Out of the box we get List and Grid. List is basically rendered as a <ul> list, while Grid is basically rendered as a <table>. We can provide alternates for these layouts if we want to, since they are all shapes in the end. If creating an alternate doesn't suffice, it's possible to create your own layout. Although that is outside the scope of this post, you can easily see how it's done by looking at the implementation for Grid and List in the Projections module.

 

Now let's have a look at the front end:

We now have a basic product catalog implemented as a Query that our site administrator can manage. What's more, he can create new queries with ease, configure sorting, filtering, etc.
When you click the "more" link, you will see the details of the product:

This is one of the great features of Orchard: you don't have to stick with just pages that have a definite set of properties: a Page is just a content type with certain parts attached to it, and you're free to add more parts to those as well.
In our demo, we were able to construct a whole new type and reuse functionality such as the Tags and Comments parts.

But wait: didn't we attach a ProductPart to our Book content type? We did, dind't we? But they are not showing up in the catalog nor the product details. So what are we missing?

The reason that we're not seeing these fields is because we didn't implement the Display method on the ProductPartDriver. The driver of a content part is (primarily) responsible for creating shapes that represent the view of a content part.

Now that we want to display our ProductPart on the front end, let's go back to our ProductPartDriver and add the following code:

protected override DriverResult Display(ProductPart part, string displayType, dynamic shapeHelper)
        {
            return ContentShape("Parts_Product", () => shapeHelper.Parts_Product(
                    Price: part.UnitPrice,
                    Sku: part.Sku
                ));
        }

 

Orchard will call the Display method whenever the front-end needs to display a content item that has the ProductPart attached to it.
The Display method will return a ContentShapeResult by calling into the ContentShape method, which is somewhat analogous to ASP.NET MVC invoking an action method that returns an ActionResult by calling into, say the PartialView method.
The ContentShapeResult will contain the name of the shape as well as the shape itself, which is a dynamic Clay object. We create a shape by using the shapeHelper object and calling a method that is defined on the fly. We pass this method a set of arguments, which will be stored on our shape as properties. The resulting shape will become the Model of the Razor template file, whose name will have to match the name of our shape according to the shape naming rules. Check out the docs for an excellent introduction on shapes!

So we are creating a shape named "Parts_Product". Orchard convention dictates that this means it expects a Razor template file to exist with a filename of "Parts.Product.cshtml" or "Parts/Product.cshtml" (the underscore is replaced with a . or a /).
If we named our shape "Jacky_Chan_Goes_Mad", we would have to name our Razor file "Jacky.Chan.Goes.Mad.cshtml". Obviously we should use meaningful names that accurately describes the characteristics of our shape. Prefixing shapes with "Part" is a good convention, since our shape will represent the view of our ProductPart.

So don't forget to create a file named "Parts.Product.cshtml" under the Views folder or, as we will do here, a file called "Product.cshtml" under "/Views/Parts".

Views/Parts/Product.cshtml:

@{
    var price = (decimal) Model.Price;
    var sku = (string) Model.Sku;
}
<article>
    Price: @price<br/>
    Sku: @sku
</article>

Or else you will be sorry:

Before Orchard will render our Parts_Product shape, however, we also need to configure its placement using Placement.info:

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

 

If you forget to create a placement for a shape, it will not get rendered.

The line in bold instructs Orchard to add any shape named "Parts_Product" to a local zone named "Content" at position 0. Position is used as a sort index to determine the order in which to render the shapes within a zone.
For more information on Placement, I encourage you to read more about it in the docs.

Now refresh the front end (don't forget to save your changes first):

And the details page now looks like this:

And that's all it takes to create a functional product catalog. For this example, we created a new Query and a Projection that leverages this query, but it is easy for the site administrator to define new queries and projections as he see fits!

In the next parts, we will be creating shopping cart functionality that allows our site visitors to actually add products to their shopping cart and proceed to checkout.

Part 6 -> Creating the ShoppingCart service and controller

 

Download the source: Part05.zip

5 comments

  • ... Posted by Jalal Posted 05/01/2012 06:00 PM

    Hi, Thank you for great tutorial.

    I'm using orchard 1.4 and i cant find: "part.WithType(...)" method reference and "RoutePart" data type reference. Are they change? where are they?

    I already referenced to "Orchard.Core" in my module project.

  • ... Posted by Andrew Posted 02/02/2015 09:27 PM [http://www.nourl.com]

    Hi,

    Great article, however I'm having a issue on getting the filter to display on the content category on the query.

    using Orchard.Localization;
    using Orchard.Projections.Descriptors.Filter;
    using Orchard.Webshop.Models;
    using IFilterProvider = Orchard.Projections.Services.IFilterProvider;
    
    namespace Orchard.Webshop.Filters
    {
        public class ProductPartFilter : IFilterProvider {
            public Localizer T { get; set; }
    
            public ProductPartFilter() {
                T = NullLocalizer.Instance;
            }
    
            public void Describe(DescribeFilterContext describe)
            {
                describe.For(
                    "Content",          // The category of this filter
                    T("Content"),       // The name of the filter (not used in 1.4)
                    T("Content"))       // The description of the filter (not used in 1.4)
    
                    // Defines the actual filter (we could define multiple filters using the fluent syntax)
                    .Element(
                        "ProductParts",     // Type of the element
                        T("Product Parts"), // Name of the element
                        T("Product parts"), // Description of the element
                        ApplyFilter,        // Delegate to a method that performs the actual filtering for this element
                        DisplayFilter       // Delegate to a method that returns a descriptive string for this element
                    );
            }
    
            public void ApplyFilter(FilterContext context) {
    
                // Set the Query property of the context parameter to any IHqlQuery. In our case, we use a default query
                // and narrow it down by joining with the ProductRecord.
                context.Query = context.Query.Join(x => x.ContentPartRecord(typeof(ProductRecord)));
            }
    
            public LocalizedString DisplayFilter(FilterContext context) {
                return T("Content with ProductPart");
            }
    
        }
    }
    
    Any Idea where im going wrong?
    
  • ... Posted by Brian Herbert Posted 03/17/2015 03:49 PM [http://www.certaintysoftware.com]

    It should be noted that the Display method of the driver will be called for every part of each item when the Content list page is displayed. This means that you do not want to build the shape in your driver when displaying the list of every content item. You can do this by checking the displayType parameter, like this:

        protected override DriverResult Display(ProductPart part, string displayType, dynamic shapeHelper)
        {
            if (displayType == "SummaryAdmin")
                return null;
    
            return ContentShape("Parts_Product", () => shapeHelper.Parts_Product(
                    Price: part.UnitPrice,
                    Sku: part.Sku
                ));
        }
    
  • ... Posted by Ned Posted 04/05/2017 06:45 PM

    Hi,

    Great article, but I am unable to get the filter to display the contents on the query. I get the message "The query returned no results". Kindly assist, I find this material very useful for my understanding Orchard CMS.

  • ... Posted by Ned Posted 08/16/2017 11:42 AM

    discovered that I should have published the content items that were added inorder for them to display on the Manage Queries page at the admin end, when Preview is clicked.

Leave a comment