Tuesday, 31 May 2011

Real world Orchard CMS–part 6–custom content types and custom content parts

As I said in my earlier post, this series is about creating a real world site with Orchard CMS, I’m not covering using Orchard to manage the site, just the technical development parts. All of the code is available on codeplex here : http://orchardsamplesite.codeplex.com/

Preamble

I’ve had to slow down on the posts this last week as I’m coming to the end of a re-architecture project for a UK retailer, so it’s been a bit hectic working late nights with no time to write, but to re-cap, so far in the last 5 parts, we’ve got a theme up and running, tidied it up a bit, created a dummy twitter widget and then in the last part, we made the twitter widget functional. I’ve been encouraged by the sudden leap in visitors to this blog since I started the Orchard series, so just wanted to say thanks for visiting and for the encouraging feedback I’ve received, makes me want to get more out there and make my own small contribution to building the community.

So, getting to the point for today - In this post we’re going to look at composing our own content types from existing modules. Orchard, rather than just defining content types as a set of properties, allows you to compose your content from various content parts. This is a pretty neat concept as you can easily define say an event content part and then attach it to any content item on your site, effectively allowing anything on the site to become an event.

For our purposes, we’re going to compose some content types from the common, orchard-in-built parts along with some parts and functionality exposed from Contrib.Voting, Contrib.Reviews and the Mello image gallery. We’ll also build a custom part which will allow us to provide simple synopsis information for when the product is rendered in a list and we’ll compose two content types – ProductList and Product, which will make up the concept of a product catalogue on our site. Our finished result should be as follows;

The listing page;

image

One of the product pages

image

Take note of some of the complexity here – we not only have the details of the product, but we’ve got some really rich functionality around ratings, feedback reviews, and screenshots presented in a nice way, and the best part is we don’t have to write any code to get this stuff! (well, apart from a migration and updating some theme templates). You’ll notice in the product page screenshot above, that we have some widgets to the right with TODO in them - we’ll be building on what we create today in future posts to provide navigation to product specific content (basically sub-level navigation being fed by the in-built menu/navigation tool), and to allow you to add the product to a shopping basket or register your interest if it’s pre-release, and finally, to automatically find and aggregate any content related to the product.

Getting the modules

If you aren’t working from the source code on codeplex, then you’ll need to use the gallery to get the latest modules for Contrib.Voting, Contrib.Reviews and the Mello image gallery. You can do this in a number of ways, but the easiest is probably through the Modules option in admin and go to the gallery tab to find the modules. From there, you can simply click install to get the module in question.

Defining a migration

In order to get our new content types defined, we need to tell orchard about the content types and which content parts are composed together to make those types up. So, in the SampleSiteModule project, create a new “Migration-ProductTypes.cs” file as follows;

using Orchard.ContentManagement.MetaData;
using Orchard.Core.Contents.Extensions;
using Orchard.Data.Migration;

namespace SampleSiteModule
{
    public class MigrationsProductTypes : DataMigrationImpl
    {
        public int Create()
        {
            // Define the product list type which will
            // contain body details, common, route, menu and be a container
            ContentDefinitionManager.AlterTypeDefinition("ProductList",
                cfg => cfg
                    .WithPart("BodyPart")
                    .WithPart("CommonPart")
                    .WithPart("RoutePart")
                    .WithPart("MenuPart")
                    .WithPart("ContainerPart")
                    .Creatable());

            // Define the product type which will
            // contain body details, common, route, be containable and have
            // the reviews and image gallery parts.
            ContentDefinitionManager.AlterTypeDefinition("Product",
                cfg => cfg
                    .WithPart("BodyPart")
                    .WithPart("CommonPart")
                    .WithPart("RoutePart")
                    .WithPart("ContainablePart")
                    .WithPart("ReviewsPart")
                    .WithPart("ImageGalleryPart")
                    .Creatable());

            return 1;
        }
    }
}

As I’m sure you already know, a migration is any class that inherits DataMigrationImpl and offers at least a Create method. That create method will be invoked whenever your feature is enabled for the first time – subsequent activations will only run any methods named UpdateFromX() if the current version already installed is represented by an UpdateFromX() method in your migration.

In our code, we’re quite simply telling the content definition manager of the existence of two new types – ProductList and Product.

ProductList is composed from the BodyPart (which gives us the usual html body content), the CommonPart, the RoutePart (to allow items of this type to have a url), the MenuPart (to allow items to appear on the menu) and the ContainerPart (instructing orchard that this type will contain other items).

The Product part follows the same structure, but we have ContainablePart instead of ContainerPart and we’ve also added the ReviewsPart and ImageGalleryPart which brings in the reviews functionality and the image gallery.

Modify the module.txt file

Next, we need to update our module.txt file as follows;

Name: Product Types
Category: Sample Site
Description: Content types for product list and main product page
Dependencies: Contrib.Reviews, Mello.ImageGallery
AntiForgery: enabled
Author: Tony Johnson
Website: http://www.deepcode.co.uk
Version: 1.0
OrchardVersion: 1.1
Features:
    TwitterWidget:
        Name: Twitter Widget
        Category: Sample Site
        Description: Widget for latest tweets

I decided that the main feature of this module should be the definition of these types. The other features we’re building are complimentary to the rest of the site, and something has to be the main feature. Notice the dependencies introduced which means this module can’t be enabled unless it can find the Reviews and ImageGallery parts, however it will automatically enable them if they are present and so, after you’ve built your module, in the modules admin tool you should now be able to enable the “Product Types” feature;

image

After enabling, you should then find options to create a new product list and product on the new menu;

image

Go ahead and create a new product list with a route url of /products (this will become important later when we set up some widget layers) and create a couple of products to go with it. You’ll need to create a  new image gallery with some images in it in order to get the screenshot images into a product page. Note, when creating your pages, it’s a good idea to make the url /products/product-name as this will make the widget layers easier later.

Once you’ve created some content, if you navigate to your new product list page you should have something like this;

image

image

Default part rendering

How quick was that! As you can see, we haven’t defined any templates yet so the default templates for the various parts that make up our types are kicking in. This is great out of the box, but it’s not quite how we want it – on the product list page we don’t want screenshots or the date/time meta data, we want our heading a little more consistent and we want to show more of the body text, then on the product page itself we want to move some of the parts around a little so they are more like what we have in the original screenshots. We’ll sort this out in our theme in a second, but first – the synopsis idea.

At the moment, the list view is rendering an extracted synopsis of the product from the main body content. Whilst that might be just what you’re looking for on your site, in our case we want to be able to provide a more condensed and meaningful synopsis text to appear here instead of the body content, so before we think about skinning this we’ll look at building a custom content part that will allow us to attach synopsis text and an optional thumbnail image to content types – when we sort the theme out later, we’ll change the rendering of the list view to only show the synopsis part, the title/link and the current review rating so it will look like this;

image

Synopsis part

Creating the synopsis part is quite straight forward, but involves quite a few steps. Just like when we created the twitter widget (the widget configuration is a part) we need to create a content part record which will describe the persistence, a content part which will wrap the record, a handler, a driver to build the shapes, a couple of templates – one for how to edit the content and one for the default rendering and create a migration. We’ll also need to update placement.info and module.txt for our module to describe where to put the various bits and tell orchard about the feature.

Lets crack on….

Create the record and part

In the model directory, create the following two files (should be self explanatory if you’ve read previous posts).

SynopsisRecord.cs

using Orchard.ContentManagement.Records;
using Orchard.Environment.Extensions;

namespace SampleSiteModule.Models
{
    [OrchardFeature("Synopsis")]
    public class SynopsisRecord : ContentPartRecord
    {
        public virtual string SynopsisText { get; set; }
        public virtual string ThumbnailUrl { get; set; }
    }
}

SynopsisPart.cs

using Orchard.ContentManagement;
using Orchard.Environment.Extensions;

namespace SampleSiteModule.Models
{
    [OrchardFeature("Synopsis")]
    public class SynopsisPart : ContentPart<SynopsisRecord>
    {
        public string Synopsis
        {
            get { return Record.SynopsisText; }
            set { Record.SynopsisText = value; }
        }

        public string ThumbnailUrl
        {
            get { return Record.ThumbnailUrl; }
            set { Record.ThumbnailUrl = value; }
        }
    }
}

Create the handler

In the handlers folder create SynopsisHandler.cs;

using Orchard.ContentManagement.Handlers;
using Orchard.Data;
using Orchard.Environment.Extensions;
using SampleSiteModule.Models;

namespace SampleSiteModule.Handlers
{
    [OrchardFeature("Synopsis")]
    public class SynopsisHandler : ContentHandler
    {
        public SynopsisHandler(IRepository<SynopsisRecord> repository)
        {
            Filters.Add(StorageFilter.For(repository));
        }
    }
}

Create the driver

And in the drivers folder create SynopsisDriver.cs;

using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.Environment.Extensions;
using SampleSiteModule.Models;

namespace SampleSiteModule.Drivers
{
    [OrchardFeature("Synopsis")]
    public class SynopsisDriver : ContentPartDriver<SynopsisPart>
    {
        protected override DriverResult Display(SynopsisPart part, string displayType, dynamic shapeHelper)
        {
            return ContentShape("Parts_Synopsis",
                () => shapeHelper.Parts_Synopsis(
                        Synopsis : part.Synopsis,
                        Thumbnail : part.ThumbnailUrl ));
        }

        protected override DriverResult Editor(SynopsisPart part, dynamic shapeHelper)
        {
            return ContentShape("Parts_Synopsis_Edit",
                () => shapeHelper.EditorTemplate(
                    TemplateName: "Parts/Synopsis",
                    Model: part,
                    Prefix: Prefix));
        }

        protected override DriverResult Editor(SynopsisPart part, IUpdateModel updater, dynamic shapeHelper)
        {
            updater.TryUpdateModel(part, Prefix, null, null);
            return Editor(part, shapeHelper);
        }
    }
}

Just a very quick refresher – the Display and Editor methods are called to build shapes that will be used when rendering this shape on the site and in the admin tool. In the case of the display method we’re creating a custom shape that gets added to the shape tree called Parts_Synopsis with properties for Synopsis and Thumbnail taken from the content part that orchard is presenting (as such a shape doesn’t necessarily have to reflect it’s underlying content part, we can add whatever we want into the properties of the shape).

Define a migration.

In the root of the module, create a file named "Migration-Synopsis.cs”, this should be second nature now:

using System.Data;
using Orchard.ContentManagement.MetaData;
using Orchard.Core.Contents.Extensions;
using Orchard.Data.Migration;
using Orchard.Environment.Extensions;
using SampleSiteModule.Models;

namespace SampleSiteModule
{
    [OrchardFeature("Synopsis")]
    public class MigrationsSynopsis : DataMigrationImpl
    {
        public int Create()
        {
            SchemaBuilder.CreateTable("SynopsisRecord",
                table => table
                    .ContentPartRecord()
                    .Column("SynopsisText", DbType.String, c => c.Unlimited())
                    .Column("ThumbnailUrl", DbType.String));

            // Tell the content def manager that our part is attachable
            ContentDefinitionManager.AlterPartDefinition(typeof(SynopsisPart).Name,
                builder => builder.Attachable());

            return 1;
        }
    }
}

Define the editor template

Create the following view in Views/EditorTemplates/Parts/Synopsis.cshtml;

@model SampleSiteModule.Models.SynopsisPart

<fieldset>
    <legend>Synopsis</legend>
    
    <div class="editor-label">@T("Synopsis"):</div>
    <div class="editor-field">
        @Html.TextAreaFor(m => m.Synopsis)
        @Html.ValidationMessageFor(m => m.Synopsis)
    </div>

    <div class="editor-label">@T("Thumbnail"):</div>
    <div class="editor-field">
        @Html.TextBoxFor( m => m.ThumbnailUrl )
        @Html.ValidationMessageFor( m => m.ThumbnailUrl )
    </div>
</fieldset>

Notice at the moment I’ve just thrown a textbox on there for the thumbnail – ideally I’d like this to select something in the media manager, but haven’t quite worked out how to do that yet. I’ll post something on this when I’ve worked out how to go about it.

Define the view template

And this view will be the default mechanism for rendering this part on a page – create Views/Parts/Synopsis.cshtml;

@using Orchard.Core.Routable.Models
@using Orchard.ContentManagement.ViewModels
@using Orchard.ContentManagement
@using Orchard.Core.Common.Models
@using SampleSiteModule.Models

@{
    Style.Require("Synopsis");
    var synopsisText = (Model.Synopsis.ToString()).Replace("\n", "</p><p>");
    var thumbnailUrl = Model.Thumbnail == null ? "" : Model.Thumbnail;    
}

@if (String.IsNullOrEmpty(thumbnailUrl))
{
    <div>
        <p>@Html.Raw(synopsisText)</p>
    </div>
}
else
{
    <div class="synopsis-container">
        <div class="synopsis-text">
            <p>@Html.Raw(synopsisText)</p>
        </div>
    </div>
    <div class="synopsis-thumbnail">
        <img src="@thumbnailUrl" width="150" height="150"/>
    </div>
    <div class="synopsis-clear"></div>
}

Create the resource manifest provider

You will notice that, in the view template, we have a Style.Require(“synopsis”) call in this template but we don’t have a stylesheet and how does the Style.Require work out where the actual CSS file is anyway? This is where a resource manifest provider comes in, which tells orchard how to resolve a Style.Require (or a Script.Require for that matter) and ensure it only gets included once. If the resource exists in the theme, the resource will be resolved from there first I believe (I did check, but it was a couple of weeks ago now and I’ve slept since then), but otherwise it will use the resource provided by the manifest provider.

Create a ResourceManifest.cs file in the root of the module project;

using Orchard.UI.Resources;

namespace SampleSiteModule
{
    /// <summary>
    /// Defines common resources for this module.
    /// </summary>
    public class ResourceManifest : IResourceManifestProvider
    {
        public void BuildManifests(ResourceManifestBuilder builder)
        {
            builder.Add().DefineStyle("Synopsis").SetUrl("synopsis.css");
        }
    }
}

Create the stylesheet

So now we have a resource manifest telling us where the resource is and we are using it in the view template, we’d better make it available. Create synopsis.css in the Styles folder;

DIV.synopsis-container
{
    float: left;
    width: 100%;
}
DIV.synopsis-text
{
    margin-left: 160px;
}
DIV.synopsis-thumbnail
{
    float: left;
    width: 150px;
    margin-left: -100%;
}
DIV.synopsis-clear
{
    content: ".";
    display: block;
    height: 0px;
    clear: both;
    visibility: hidden;
}

Update the placement.info file

We’re almost there – if we don’t tell orchard where to put our new parts by default, they won’t appear. Modify the placement.info file to add the highlighted lines;

<Placement>
    <Place Parts_TwitterWidget="Content:1"/>
    <Place Parts_TwitterWidget_Edit="Content:7.5"/>

  <!-- Synopsis -->
  <!-- Note the summary view is the only display mode... -->
  <Match DisplayType="Summary">
    <Place Parts_Synopsis="Content:10"/>
  </Match>
  <Place Parts_Synopsis_Edit="Content:8"/>
</Placement>

A quick word of explanation… We don’t actually want our synopsis part to appear anywhere other than on the listing screen. When orchard renders parts into the listing it sets the display type to summary and we can specify which display type we’re interested in within the placement.info file. In the above I’m saying that the Parts_Synopsis shape should be put into the content local zone at position 10, but only if the display type is summary. Clear as mud?

Update the module.txt file to expose our new synopsis feature

Finally to get this content part available, we need to tell orchard about it. Update module.txt as follows;

Name: Product Types
Category: Sample Site
Description: Content types for product list and main product page
Dependencies: Contrib.Reviews, Mello.ImageGallery
AntiForgery: enabled
Author: Tony Johnson
Website: http://www.deepcode.co.uk
Version: 1.0
OrchardVersion: 1.1
Features:
    TwitterWidget:
        Name: Twitter Widget
        Category: Sample Site
        Description: Widget for latest tweets
    Synopsis:
        Name: Synopsis
        Category: Sample Site
        Description: Allows synopsis to be added to types

Attach the synopsis to our product type in it’s migration

Ok, so that was all the plumbing for our new content part done, let’s go ahead and update the product types migration to include the synopsis on our product. Add the following method to Migration-ProductTypes.cs;

       public int UpdateFrom1()
        {
            ContentDefinitionManager.AlterTypeDefinition("Product",
                cfg => cfg.WithPart("SynopsisPart"));

            return 2;
        }

Go forth and view the beauty of your creation

Build, activate the synopsis feature and update the product types feature from the admin tool and when done, go ahead and fill in the synopsis fields of your already created products.

image

image

Check out my rendering

image

Er, yeah, this isn’t pretty is it! The synopsis data doesn’t appear in the main product pages, by design (see placement.info above), but the summary view is now horrendous, let’s try and sort all this out by updating our theme to render everything exactly where we want it and how we want it;

Tidying up the product listing using our theme’s placement.info

Interestingly enough, the largest part of the tidy up for the product listing is as easy as manipulating the placement.info file in our theme project. Update placement.info to add the highlighted lines below;

<Placement>
    <!-- Remove the page title from the homepage -->
    <Match Path="~/">
        <Place Parts_RoutableTitle="-"/>
    </Match>

    <!-- Remove metadata part from all pages and from blogs -->
    <Match ContentType="Page">
        <Place Parts_Common_Metadata="-"/>
    </Match>
    <Match ContentType="Blog">
        <Place Parts_Common_Metadata="-"/>
    </Match>

    <!-- Remove comment counts from blog posts and set the summary blog post body alternate -->
    <Match ContentType="BlogPost">
        <Place Parts_Comments_Count="-"/>
        <Match DisplayType="Summary">
            <Place Parts_Common_Body_Summary="Content:5;Alternate=Parts_BlogPostSummaryBody"/>
        </Match>
    </Match>

  <Match ContentType="Product">
    <!-- In summary view, remove the body summary (we will replace with synopsis),
            the meta data and the image gallery thumbnails -->
    <Match DisplayType="Summary">
      <Place Parts_Common_Body_Summary="-"/>
      <Place Parts_Common_Metadata_Summary="-"/>
      <Place Parts_ImageGallery="-"/>
    </Match>

    <!-- In detail view, remove the meda data, move the stars for ratings to the top
             put the image gallery below the content and define alternate wrappers for
             the image gallery and the body text -->
    <Match DisplayType="Detail">
      <Place Parts_Common_Metadata="-"/>
      <Place Parts_Stars_Details="Content:0"/>
      <Place Parts_ImageGallery="Content:2;Wrapper=Product_ImageGalleryWrapper"/>
      <Place Parts_Common_Body="Content:1;Wrapper=Product_BodyTextWrapper"/>
    </Match>
  </Match>
</Placement>

First off, we’re telling placement.info to work on the Product content type, then we have separate rules for when we’re showing a product in summary view and detail view. Concentrating on the summary view first, all we need to do is remove the Parts_Common_Body_Summary, Parts_Common_Metadata_Summary and Parts_ImageGallery shapes from the rendering to get the layout we’re interested in - you can find the names of shapes easily using the orchard shape tracing feature (yet more bowing to Sébastien Ros for this feature! , followed by mass apologies to everyone for the lame emoticon – please don’t leave!)

bow

Ok, so, that gives us just our title, rating and synopsis parts rendering as desired;

image

Hmmm, not quite – the title/link isn’t quite right …

Changing how the routable title is rendered in summary form.

We want our routable title to be a H4 as opposed to the H1 it is at the moment. The shape we’re interested in is Parts_RoutableTitle so we can create a new razor template in our theme’s Views folder called Parts.RoutableTitle.Summary.cshml (again you can find the template names that can be used to provide alternate shape rendering by using shape tracing). This template will be used whenever the RoutableTitle part is rendered in summary view;

@* This overrides how a routable title part is rendered in summary form *@

@using Orchard.ContentManagement
@{
    ContentItem contentItem = Model.ContentPart.ContentItem;
    string title = Model.Title.ToString();
}

<h4>@Html.ItemDisplayLink(title, contentItem)</h4>

And now our product list is just right…..

image

Reordering parts on the main product page and specifying alternates directly

So our product page now needs a bit of alternate/re-ordering love – this is what we have at the moment;

image

and this is what we’re aiming for;

image

Yes, the changes are subtle, but they are;

  • Removed meta data (published date) from the top
  • Added a “Details” title to the main body content
  • Moved screenshots to be above reviews
  • Added a “Screenshots” title to the screenshots section

Earlier, we changed the theme’s placement.info file to include the following;

    <Match DisplayType="Detail">
      <Place Parts_Common_Metadata="-"/>
      <Place Parts_Stars_Details="Content:0"/>
      <Place Parts_ImageGallery="Content:2;Wrapper=Product_ImageGalleryWrapper"/>
      <Place Parts_Common_Body="Content:1;Wrapper=Product_BodyTextWrapper"/>
    </Match>

You can probably guess what’s going on here, but first off, when we render this product in Detail mode, we get rid of the metadata shape. We then ensure that the Parts_Stars_Details part is right at the top of the local zone, move Parts_ImageGallery to position 2 and Parts_Common_Body to position 1, we also specify wrappers for the image gallery and body parts! In earlier posts we specified alternates directly in the placement.info file, and specifying wrappers is a similar concept. It allows you to define a shape that will be rendered around the target shape.

By specifying Product_ImageGalleryWrapper and Product_BodyTextWrapper, we can now create templates that will be used to wrap the target parts by naming them Product.ImageGalleryWrapper.cshtml and Product.BodyTextWrapper.cshtml respectively (in our template project’s views folder);

Product.ImageGalleryWrapper.cshtml

@using Orchard.ContentManagement;
@using Orchard.Widgets.Models;

<div>
    <h4>@T("Screenshots")</h4>
    @Display(Model.Metadata.ChildContent)
</div>

Product.BodyTextWrapper.cshtml

@using Orchard.ContentManagement;
@using Orchard.Widgets.Models;

<div>
    <h4>@T("Details")</h4>
    @Display(Model.Metadata.ChildContent)
</div>

That’s all folks

And we’re done! If all that sounded like a lot of stuff, it was, but we’ve achieved a great deal! I hope you find this useful, next up (hopefully next week):

  • Creating sub-navigation
    • Creating a widget that will allow navigation within sections of your site
  • Relating content
    • Creating a widget to find content on your site that may be related to the current content being viewed.
  • Basic shopping, advanced widgets and controller based content
    • This will be a multi-part post I think covering implementing basic shopping basket functionality
    • Creating a widget that renders initially as part of the normal rendering pipe and then updates with AJAX calls to a custom controller.
    • Implementing a custom controller to provide complete rendering control – to display our shopping basket – yet keeping the rendering themeable.

As ever, grab the code from codeplex: http://orchardsamplesite.codeplex.com under the part-06 folder (or just skip ahead to final, which is evolving quicker than the blog posts are!).

Tuesday, 17 May 2011

Real world Orchard CMS – part 5 – caching - lets get that twitter widget working.

As I said in my earlier post, this series is about creating a real world site with Orchard CMS, I’m not covering using Orchard to manage the site, just the technical development parts. All of the code is available on codeplex here : http://orchardsamplesite.codeplex.com/

Preamble

So far we’ve created a basic theme, created a twitter widget to display the latest twitter feeds and gone back and tidied up some parts of our theme that weren’t working out for us. In this post I’m going to quickly go back and tidy up a loose end that’s been bugging me - getting the twitter widget doing something useful. We originally created that part to just return some canned results, but I figure going back to address this is a good exercise as we can look at caching in Orchard, so without further ado, our twitter widget will shortly look like this;

image

Getting tweets

First off we need to get our ITwitterService concrete implementation to go to twitter and get the latest tweets for the person we’ve specified. Open the TwitterService.cs file in the services folder of the module project and change it as follows;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using Orchard.Environment.Extensions;
using SampleSiteModule.Models;

namespace SampleSiteModule.Services
{
    [OrchardFeature("TwitterWidget")]
    public class TwitterService : ITwitterService
    {
        const string FeedUrl = "http://api.twitter.com/1/statuses/user_timeline/{0}.rss";

        /// <summary>
        /// gets the latest tweets based on the configuration specified in the TwitterWidgetPart.
        /// Uses Linq2XML to load the feed url and select the tweets.
        /// </summary>
        /// <param name="twitterUserName">Name of the twitter user.</param>
        /// <param name="maxPosts">The max posts.</param>
        /// <returns></returns>
        public IList<Tweet> GetLatestTweetsFor(string twitterUserName, int maxPosts)
        {
            XDocument doc = XDocument.Load(String.Format(FeedUrl, twitterUserName));
            return (from item in doc.Descendants("item") 
                         select new Tweet
                         {
                             Text = TrimUserFrom((string) item.Element("title")),
                             DateStamp = (DateTime) item.Element("pubDate"),
                             Link = (string) item.Element("link")
                         })
                         .Take( maxPosts )
                         .ToList();
        }

        private string TrimUserFrom(string tweet)
        {
            int position = tweet.IndexOf(':');
            if (position == -1) return tweet;

            return tweet.Substring(position + 1);
        }
    }
}

I’ve highlighted the key parts – our method signature is slightly different (It felt wrong to pass a part across the boundary), and we’re using Linq2XML to get the latest tweets in RSS format from twitter for the username specified, we execute a simple linq query to get the items and construct a list of Tweet objects, all very straight forward. Notice that the service itself isn’t concerning itself here with caching, we’re going to take care of caching in the driver in this instance.

Modify the driver to use caching

using System;
using Orchard.Caching;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.Environment.Extensions;
using Orchard.Services;
using SampleSiteModule.Models;
using SampleSiteModule.Services;

namespace SampleSiteModule.Drivers
{
    /// <summary>
    /// Content part driver for the twitter widget part
    /// </summary>
    [OrchardFeature("TwitterWidget")]
    public class TwitterWidgetDriver : ContentPartDriver<TwitterWidgetPart>
    {
        private const string TwitterUserCacheKey = "TwitterWidgetPostsFor-{0}";
        private readonly ITwitterService _twitter;
        private readonly ICacheManager _cache;
        private readonly IClock _clock;

        public TwitterWidgetDriver(ITwitterService twitter, ICacheManager cache, IClock clock)
        {
            _twitter = twitter;
            _cache = cache;
            _clock = clock;
        }

        protected override DriverResult Display(TwitterWidgetPart part, string displayType, dynamic shapeHelper)
        {
            // Get tweets, from the cache or from twitter
            var tweets = _cache.Get(String.Format(TwitterUserCacheKey, part.TwitterUserName), ctx => 
                {
                    ctx.Monitor( _clock.When( TimeSpan.FromMinutes(part.CacheMinutes)));
                    return _twitter.GetLatestTweetsFor(part.TwitterUserName, part.MaxPosts);
                });

            return ContentShape("Parts_TwitterWidget",
                () => shapeHelper.Parts_TwitterWidget(
                        TwitterUserName: part.TwitterUserName ?? String.Empty,
                        Tweets: tweets));
        }

        protected override DriverResult Editor(TwitterWidgetPart part, dynamic shapeHelper)
        {
            return ContentShape("Parts_TwitterWidget_Edit",
                () => shapeHelper.EditorTemplate(
                    TemplateName: "Parts/TwitterWidget",
                    Model: part,
                    Prefix: Prefix));
        }

        protected override DriverResult Editor(TwitterWidgetPart part, IUpdateModel updater, dynamic shapeHelper)
        {
            updater.TryUpdateModel(part, Prefix, null, null);
            return Editor(part, shapeHelper);
        }
    }
}

This is almost the same as it was before, but we take two new dependencies -  an ICacheManager and an IClock. In the display method we then use _cache.Get which will inspect the cache to see if we have data that is already cache, within the duration specified, for the twitter user provided in the twitter widget configuration. If the cache determines that it doesn’t have cached data, it then executes the Func provided which in turn determines how to invalidate the cache (in this case when the clock has ticked over the specified number of minutes) and calls the twitter service as normal. The remainder of the code is as before.

Finally, make it look a bit prettier

The Views/Parts/TwitterWidget.cshtml file then changes as;

@using SampleSiteModule.Models

@if (Model.Tweets.Count < 1)
{
    <p>There are no tweets for @Model.TwitterUserName</p>
}
else
{
    foreach (Tweet tweet in Model.Tweets)
    {
        <p><a href="@tweet.Link" target="_blank">@tweet.Text</a>
            <br/>
            @tweet.FriendlyDate
        </p>            
    }
}

Final words

Easy huh? I’ve made a few other minor modifications to the source, mostly in structure and naming (eg: putting drivers and handlers into more sensible folders and renaming a couple of files), but nothing that really affects the build.

As ever, grab the code from codeplex: http://orchardsamplesite.codeplex.com under the part-05 folder (or just skip ahead to final, which is evolving quicker than the blog posts are!).

Real world Orchard CMS – part 4 – cleaning up the theme

As I said in my earlier post, this series is about creating a real world site with Orchard CMS, I’m not covering using Orchard to manage the site, just the technical development parts. All of the code is available on codeplex here : http://orchardsamplesite.codeplex.com/

Preamble

In the earlier parts of this series we created a basic theme, and a widget that will display latest twitter feeds (actually, that widget just displays canned results but if you look in the “final” directory in the source, you’ll find the finished working version of this widget). In this post I want to look at enhancing the theme somewhat as there are a few subtleties at the moment that aren’t right with how we want this theme to function.

If you’re following along, by now you should have added some content to your site, on the home page you should have;

  • A html widget that shows the featured product content (just mocked up at the moment)
  • A html widget with some “about us” content in the left hand column
  • The twitter widget in the left hand column
  • A blog created with a couple of dummy posts
  • A recent blog posts widget in the centre column
  • An additional html widget with some “support” content in the right hand column.

Like this;

image

But this isn’t quite how we want it. It should look more like this;

image

So there are 5 key things we need to sort out;

  • Page titles
    • Getting rid of it on the homepage
    • Restyling it everywhere else
    • Removing the publishing metadata (date) on pages
  • Using a filter provider to indicate we’re rendering the homepage
    • To make some discreet changes in the main layout template without having a separate template just for the homepage.
  • Making the widget titles stand out more (I believe this is what they call “making it pop” <<snigger>>)
  • Reformatting the recent blog posts widget on the homepage
  • Improving our main blog page to show full articles

Part 4.1 – sorting out page titles

Page titles in the theme at the moment serve their purpose, but they aren’t quite right for us in a number of ways. These are;

  • They show publish date/time with every page title, which on the blog list page can be a little cumbersome in particular.
    image
  • We don’t want the page title on the homepage at all, it’s a little ungainly.
  • We want them to “pop” more ;)

What we want to achieve is to change the way page titles are displayed from this, as the theme is presently configured;

image

to this… yes, I know, it’s only a subtle change, but <sarcasm>it “pops” now don’t you think</sarcasm>

image 

The first thing we want to look at is placement.info – placement.info is the theme writers friend, I suggest you go off an read about it in the documentation, but suffice it to say it allows you to send shapes you don’t want to oblivion or to provide alternate templates (or wrappers) in certain conditions. You can specify criteria, such as on a particular page, when rendering a particular content type, I want to hide the metadata shape, or I want to render it with an alternative template name and so on. We’re going to use placement.info to get rid of the title and metadata on the homepage and get rid of the metadata on other pages.

In your theme project, create a file named “Placement.info” in the root of the project, and fill it thus;

   1:  <Placement>
   2:      <!-- Remove the page title from the homepage -->
   3:      <Match Path="~/">
   4:          <Place Parts_RoutableTitle="-"/>
   5:      </Match>
   6:   
   7:      <!-- Remove metadata part from all pages and from blogs -->
   8:      <Match ContentType="Page">
   9:          <Place Parts_Common_Metadata="-"/>
  10:      </Match>
  11:      <Match ContentType="Blog">
  12:          <Place Parts_Common_Metadata="-"/>
  13:      </Match>
  14:  </Placement>

Lines 3,4,5 create a rule that whenever the homepage is being rendered, it should take the shape named Parts_RoutableTitle and send it to a local zone named “-“. This zone doesn’t exist, it’s used to send the shape rendering to purgatory. The routable title shape usually includes the metadata shape also (Parts_Common_Metadata – the shape with the published date/time on it), so by removing the entire title shape we also get rid of the metadata on the homepage.

Lines 7-10 remove the Parts_Common_Metadata shape from any page that is rendered. A page is of course a content type, and the match rule reflects this.

Similarly on lines 11-13 we tell orchard to remove the metadata shape from the blog list pages. Note this is not the blog post pages, where we want to keep that meta data to show when the post was published, blog post pages have a content type of BlogPost, so these lines will only affect the overall blog listing page.

So that part was simple enough, we’ve removed the bits we don’t want, so we now need to reformat the bits we wanted to change; Before we go on, ensure you understand the following bits of the orchard documentation, which explain things more clearly than I can;

In summary, a shape which is added to the pipeline for rendering will have a bunch of templates that you could override to change how it will be rendered. For instance, to change a page title with a shape name of Parts_RoutableTitle, you could just create a new razor file in your template’s view folder called Parts.RoutableTitle.cshtml and this will change every page title throughout your site. Alternates are also available, so you could create Parts.RoutableTitle-Page.cshtml and this will be the template used whenever the Parts_RoutableTitle shape is rendered within a content type of Page. For a full list of alternates, use the shape tracing feature of orchard 1.1 (and enable the url alternates feature for more alternates out of the box);

image

So, because we want to change the title on every page (except the homepage where we’ve removed it), we can just create Parts.RoutableTitle.cshtml in our views folder as follows;

<h1 class="page-title">@Model.Title</h1>

That’s all – if you’re wondering how to know the “Title” property is available on this dynamic, again, take a close look at the shape tracing feature, specifically the model tab (Anyone else want to send free beer to whomever wrote this little nugget of gold???)

All we then need is the H1.page-title class in our stylesheet;

h1.page-title{ font-size: 2em; border-bottom: 1px dotted black; margin-bottom: 8pt; padding-bottom: 2px; }

We should now be in a position where the title and metadata is removed from the homepage, the metadata is removed from the pages we don’t want it and our titles “pop”. Let’s go back to the homepage.

Part 4.2 closing the gaps on the homepage with a filter provider

Notice that between the promo content and the widget areas there is still some blank content. In fact if you inspect the rendered source you will find this;

<div id="layout-body" class="zone">    
    <div id="zone-body" class="without-sidebar">
        <div class="zone zone-content">
            <article class="content-item page">
                <header>
                </header>
            </article>
        </div>
    </div>
</div>

Which, in our layout.cshtml corresponds to;

@if (displayContent || displayBodySideBar)
{
    <div id="layout-body" class="zone">    
        <div id="zone-body" class="@bodyClasses">
            @Display(Model.Content)
        </div>

        @if (displayBodySideBar)
        {
            <div id="zone-body-sidebar">
            @Display(Model.BodySideBar)
            </div>
        }
    </div>     
}

Hmmm – so our Model.content is empty (so displayContent should have been false) but it’s clearly not null, so it’s rendering this whole section which we don’t really want if we’re on the homepage and I want that gap closed. I could create a separate layout for the homepage using url alternates, but to be honest this is so trivial I’d sooner just be able to check in the template code if this is the homepage and if so, don’t display the entire body content section, even if there IS content in there. We can use a FilterProvider for just this purpose;

Create a class file in your theme called LayoutFilter.cs with the following code;

using System.Web.Mvc;
using Orchard;
using Orchard.Mvc.Filters;

namespace SampleSite
{
    /// <summary>
    /// This filter just adds an IsHomepage flag to the Layout shape if the page
    /// being rendered is the homepage. We check this in the layout to determine
    /// whether to render the content area or not (this theme is used to just render
    /// widgets in zones on the homepage)
    /// </summary>
    public class HomepageLayoutFilter : FilterProvider, IResultFilter
    {
        private readonly IWorkContextAccessor _wca;

        public HomepageLayoutFilter(IWorkContextAccessor wca)
        {
            _wca = wca;
        }

        public void OnResultExecuting(ResultExecutingContext filterContext)
        {
            var workContext = _wca.GetContext();
            var routeValues = filterContext.RouteData.Values;

            if (((string)routeValues["area"]) == "HomePage")
                workContext.Layout.IsHomepage = true;
        }

        public void OnResultExecuted(ResultExecutedContext filterContext)
        {
        }
    }
}

This filter gets discovered automatically and called when a page render is executing. It gets an IWorkContextAccessor injected into it, which we can use to manipulate the shapes already in the shape tree. Our OnResultExecuting method gets called and we determine if the MVC area we are in is for the HomePage and if it is, we inject a property into the Layout shape that flags that this is the homepage. We then just need to take that into account in our layout.cshtml by changing this line;

var displayContent = (Model.Content != null);

to this;

var displayContent = !(WorkContext.Layout.IsHomepage == true);

Now when the homepage is rendered, that slightly annoying gap should close without having to create a whole separate template.

Part 4.3 – adding more distinction to the titles of widgets

Presently the widget titles on the homepage (and the rest of the site) look a little lost in the overall design – they look a little anaemic don’t you think;

image

We want them to look more like this;

image

Which is extremely simple, we just override the wrapper markup for widgets by creating “Widget.Wrapper.cshtml” in our views folder with the following code;

@* This overrides the default widget wrapper *@
@using Orchard.ContentManagement;
@using Orchard.Widgets.Models;
@{
    var title = ((IContent)Model.ContentItem).As<WidgetPart>().Title;
    var tag = Tag(Model, "article");
}
@tag.StartElement
    @if (HasText(title) || Model.Header != null)
    {
    <header>
        @if (HasText(title))
        {
        <h1 class="widget-title">@title</h1>
        }
        @Display(Model.Header)
    </header>
    }
    @Display(Model.Child)
    @if (Model.Footer != null)
    {
    <footer>
        @Display(Model.Footer)
    </footer>
    }
@tag.EndElement

This is very similar to the default widget wrapper, except we’re using H1 for the title with a CSS class of widget-title, we define this in our stylesheet as;

h1.widget-title{ font-size: 1.1em; font-weight: bold; border-bottom: 1px dotted black; margin-bottom: 8px; padding-bottom: 2px;}

And that gives us our new widget titles. We’ll be doing the same thing in another post later to add the twitter icon to our widget title for the twitter component (see the original mock up in the introductory post)

Part 4.4 - A less cluttered look to the recent blog post summaries

As you can see below, the amount of information we’re presenting on the homepage for a quick summary of posts is a little much;

image

We’re going to aim for something a little more succinct;

image 

Notice we’ve removed the tags, removed the comment count, make the title stand out a little more and also added who the author was. To achieve this we’re going to use the url alternates feature that can be found in the designer tools module – get it from the gallery and install it;

image

Ensure you have enabled the url alternates feature;

image

This feature provides some additional alternates for shapes based on url, we’re going to use an alternate for how to render blog post shapes, in summary form, on the home page. With this feature active, we can now create a shape template called Content-BlogPost-url-homepage.Summary.cshtml in our views folder. This rather long template name indicates that it will be called when rendering a summary display of a BlogPost content item when on the homepage url. The content of the file should be;

   1:  @*
   2:      This template changes the way blog post summaries 
   3:      are rendered on the homepage (Requires url alternates feature)
   4:  *@
   5:  @using Orchard.Core.Routable.Models
   6:  @using Orchard.ContentManagement.ViewModels
   7:  @using Orchard.ContentManagement
   8:  @using Orchard.Core.Common.Models
   9:   
  10:  @{
  11:      ContentItem item = Model.ContentItem;
  12:      RoutePart rpItem = item.As<RoutePart>();
  13:      BodyPart bpItem = item.As<BodyPart>();
  14:      
  15:      string linkUrl = Url.ItemDisplayUrl(item);
  16:  }
  17:   
  18:  <h4><a href="@linkUrl">@rpItem.Title</a></h4>
  19:  <div class="publishinfo">@Model.ContentItem.CommonPart.PublishedUtc by @Model.ContentItem.CommonPart.Owner.UserName</div>
  20:  <div>
  21:      <p>@Html.Raw(Html.Excerpt(bpItem.Text, 200).ToString())</p>
  22:  </div>

Nothing much to explain here,  we get the content item that is being rendered (the blog post) and get it’s Route content part and Body content part, then render it how we wanted it. EDIT: See the comment from Bertrand LeRoy on the part 3 post about working with dates.

Part 4.5 - A cleaner and fuller blog page

The final part of this (what has turned into a length) post is regarding the blog page. Most blogs display the full article of the most recent posts on the main blog page rather than just a summary but ours renders like this;

image

For our site, I’d sooner see the full article;

image

To achieve this, we look back to our old friend, placement.info, adding the following match rule and placements.

    <!-- Remove comment counts from blog posts and set the summary blog post body alternate -->
    <Match ContentType="BlogPost">
        <Place Parts_Comments_Count="-"/>
        <Match DisplayType="Summary">
            <Place Parts_Common_Body_Summary="Content:5;Alternate=Parts_BlogPostSummaryBody"/>
        </Match>
    </Match>

Notice we have nested match rules – the outer rule is matching against blog posts content type, the inner rule is only for when the display type for this content type is summary. We’re obviously getting rid of the comments count in general, but then in the summary rule, we’re also telling orchard to look for an alternate named Parts_BlogPostSummaryBody, which it will use when the match conditions are met and when it’s rendering a shape named Parts_Common_Body_Summary. Remember from the documentation that content is composed of multiple parts - the blog post is therefore made up of multiple parts and one of those is the body part (Parts_Common_Body), which when it’s rendered on the blog list page will render in summary form (Parts_Common_Body_Summary)….

So, this rule and placement is saying when I’m rendering a blog post and when I’m rendering it in summary form, and when the shape I’m rendering is the Parts_Common_Body_Summary shape, look for an alternate named Parts_BlogPostSummaryBody, which in turn translates to a template file of Parts.BlogPostSummaryBody.cshtml which should be in your views folder and have the following content;

@Model.Html

That’s it! Now, when your blog page renders it will show the entire blog post.

Final words

You should now have a site that looks more like our original design;

image

image

image

As a final word, forgive any inaccuracies in this series, I’m sure there will be some, I’ve only been looking at orchard now for a matter of days, I’m loving it’s flexibility and I’m sure I’ve made some mistakes along the way, but I do hope this gives you a steer in the right direction when theme-ing your sites. For the rest of the series, I’m planning on moving away from the skinning side of Orchard to look at some of the other cool things you can achieve. Don’t hold me to this, but the next parts I’m thinking will be something along the lines of;

  • Creating custom content types
    • Defining the product listing
    • Defining the product page and adding social features like voting
    • Adding screenshot library to the product page
  • Creating sub-navigation
    • Creating a widget that will allow navigation within sections of your site
  • Relating content
    • Creating a widget to find content on your site that may be related to the current content being viewed.
  • Basic shopping, advanced widgets and controller based content
    • This will be a multi-part post I think covering implementing basic shopping basket functionality
    • Creating a widget that renders initially as part of the normal rendering pipe and then updates with AJAX calls to a custom controller.
    • Implementing a custom controller to provide complete rendering control – to display our shopping basket – yet keeping the rendering themeable.

As always, the source is in the part-04 folder on code plex : http://orchardsamplesite.codeplex.com/

See you soon! Tony.