Writing an Orchard Webshop Module from scratch - Part 11

11. Customizing the Product content list in the Admin: Display Types

This is part 11 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:

  • DisplayTypes: what they are and how to use them;
  • Customize the content list in the Admin by creating summary admin shapes and templates for ProductPart.

Download the source code

In part 4 we saw how to create the ProductPart and render it by generating two shapes using content part drivers: Parts_Product and Parts_Product_AddButton.
When we have a look at the list of contents from within the admin, we see that these shapes are rendered exactly the same (with the difference being that the HTML is styled using TheAdmin theme):

Now, most of us would rather not display the "Add to shoppingcart" buttons here.
We might even want to customize the rendering of the Parts_Product shape as well. For example, we may want to put both Price and Sku on a single line in order to minimize the height of the content record.

Let's see how we can do that.

First, we'll start with removing the "Add to shoppingcart" button from the admin view.

Removing the Shoppingcart button

If you remember from part 4, we are generating the Parts_Product_AddButton shape from the ProductPart driver:

Drivers/ProductDriver.cs:

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
                     ))
                );
        }

What we want to do is only render the Parts_Product_AddButton shape on the front end, not on the backend.

So how can we do this?
 

DisplayTypes

DisplayTypes enable us to further control where and if a shape should be rendered.
Whenever Orchard renders a content item, it will invoke the IContentManager.BuildDisplay method, which looks like this:

dynamic BuildDisplay(IContent content, string displayType = ""string groupId = "");

Notice the second parameter called displayType.

Whenever a content item is being rendered on the front end, Orchard passes the string "Detail" into the displayType argument.
Likewise, Orchard passes the display type "Summary" whenever it renders a List of content items.

And finally, whenever a content item is being rendered as part of a list in the back end, "SummaryAdmin" is specified as the displayType.

So how can we use these? We can take advantage of it in at least two places:

1. From within our driver's Display method.

2. From within Placement.info.

So we can do two things here: we could either update our Display method so that it will only create the Parts_Product_AddButton shape if displayType != "SummaryAdmin", like so:

protected override DriverResult Display(ProductPart part, string displayType, dynamic shapeHelper)
        {
            var driverResults = new List<DriverResult> {
                ContentShape("Parts_Product", () => shapeHelper.Parts_Product(
                    Price: part.Price,
                    Sku: part.Sku
                ))
            };
 
            if (displayType != "SummaryAdmin")
            {
                driverResults.Add
                (
                    ContentShape("Parts_Product_AddButton", () => 
                        shapeHelper.Parts_Product_AddButton(ProductId: part.Id))
                );
            }
 
            return Combined(driverResults.ToArray());
        }

 

Or, we could simply update Placement.info to achieve the exact same effect:

<Match DisplayType="SummaryAdmin">
    <Place Parts_Product_AddButton="-" />  
  </Match>

That was even more easy! The benefit of using Placement.info over implementing logic in our driver is simplicitly: using some simple configuration, we can determine where and when to render a shape.
Rendering, positioning and hiding shapes based on certain conditions is a common task in Orchard, and Placement.info is a great and easy way to orchestrate things.

The way Placement.info works is as follows: when we create shapes from our content part drivers, Orchard needs to decide where to render these shapes. To make the decision, Orchard uses the Placement.info file.

Now you may be wondering if first creating some shapes and then checking if they should be rendered or not is efficient. Especially in cases where shapes might be expensive to create (in terms of construction time and memory allocation).
However, note that we're not actually passing shapes into the ContentShape method. Instead, we're passing in a lambda that creates the shape. This lamda is only invoked if the shape needs to be actually rendered. How nifty!

Perhaps you have already made the mistake (I did) by accidently passing in a shape directly, instead of a lamda:

ContentShape("Parts_Product_AddButton", shapeHelper.Parts_Product_AddButton(ProductId: part.Id))


What will happen now is that an exception will be thrown and catched (logged and swallowed) up somewhere in the stack. The result is that the shape will not be rendered.

Should you ever wonder why some shape isn't being rendered even though you did everything else right, make sure that you passed in a lamda instead of a shape:

ContentShape("Parts_Product_AddButton", () => shapeHelper.Parts_Product_AddButton(ProductId: part.Id))

Right, so now our admin screen is fixed:

One final thing we are going to do is to put the Price and Sku fields on a single line to cleanup the screen a little.
We'll do this by creating another shape to be rendered for the "SummaryAdmin" display type.

Let's go ahead and modify the Display method of our ProductDriver as follows:

Drivers/ProductDriver.cs:

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


Notice that we added a content shape called "Parts_Product_SummaryAdmin".

The next thing to do is create a template for it:

Parts/Product.SummaryAdmin.cshtml:

@{
    var price = (decimal)Model.Price;
    var sku = (string)Model.Sku;
}
<article>
    <strong>Price:</strong> @price.ToString("c") | <strong>Sku:</strong> @sku
</article>


Finally, we need to add a <Place> element for this shape in Placement.info. We need to wrap it inside a <Match> element so that it will only be rendered for the SummaryAdmin display type. We also need to specifiy that the "Parts_Product" shape should not be rendered in SummaryAdmin mode.

Placement.info:

<Placement>
...
 
  <Match DisplayType="SummaryAdmin">
    <Place Parts_Product_AddButton="-" />
    <Place Parts_Product="-" />
    <Place Parts_Product_SummaryAdmin="Content:0" />
  </Match>
</Placement>

And now our admin screen looks like this:

Much better!

In conclusion, Placement.info is a powerful tool that enables fine-grained control over what shapes get rendered where under which circumstances, such as DisplayType.

Tags: orchard

blog comments powered by Disqus

Social Networks

Latest Tweets