Monday, 28 November 2011

NHibernate query patterns redux– what does 3.2 bring to the table?

Some time ago, I wrote a brief cookbook of NHibernate query patterns. In this post I’m going to look at the same query patterns, but using some of the new features of NHibernate 3 – specifically the baked in Linq provider that makes querying a breeze. I’ve published the code for this to codeplex here.

If you want to follow along, you’ll need the adventure works sample database set up on your SQL server. I’m also going to ignore the mapping configuration for now, but in the sample code, I’ve used the HBM mapping format – if I were doing this “for real” though, I’d be using the new “Loquacious” configuration features to map up my domain classes – why it’s called Loquacious, I have no idea, but it’s basically a baked in replacement for fluent NHibernate.

Ok, so the tables we’re interested in for this exercise are shown below;

image

Find all addresses in the city of London.

Starting out nice and easy;

   1: var query = from a in _session.Query<Address>() 
   2:             where a.City == "London" 
   3:             select a;
Find all addresses in London, where postcode starts with SW

Still pretty simple, but a good test of how the provider translates the string operation “StartsWith” into SQL;

   1: var query = from a in _session.Query<Address>() 
   2:             where a.City == "London" && a.PostCode.StartsWith("SW") 
   3:             select a;

Find all addresses with a parent state/province that has a country/region code of GB

Querying down a join is simplistic;

   1: var query = from a in _session.Query<Address>() 
   2:             where a.StateProvince.CountryCode == "GB" 
   3:             select a;

Find all addresses with a parent state/province that has a country/region code of GB or FR

Still straight forward;

   1: var query = from a in _session.Query<Address>() 
   2:             where a.StateProvince.CountryCode == "GB" || a.StateProvince.CountryCode == "FR" 
   3:             select a;

Find all customer accounts with a "Home” address in the region of GB

Getting a bit more complex, we’re joining in the chain from customer to address to do some filtering. The query this generates is still nice and efficient;

   1: var query = from c in _session.Query<Customer>()
   2:             join ca in _session.Query<CustomerAddress>() on c equals ca.Customer
   3:             join a in _session.Query<Address>() on ca.Address equals a
   4:             where ca.Type.Name == "Home" && a.StateProvince.CountryCode == "GB"
   5:             select c;

Find all customer accounts with a “Home” address in the region of GB with more than one order

Now, this sounds complex, but actually, it’s the same query as above, but with some projections. No need to join the orders table in;

   1: var query = from c in _session.Query<Customer>()
   2:             join ca in _session.Query<CustomerAddress>() on c equals ca.Customer
   3:             join a in _session.Query<Address>() on ca.Address equals a
   4:             where ca.Type.Name == "Home" && a.StateProvince.CountryCode == "GB" && 
   5:                   c.Orders.Count() > 1
   6:             select c;

Find all customer accounts with a “Home” address in the region of GB with more than 2 orders and a total spend over $6000

Building on the last query, let’s do some more projections for our query.

   1: var query = from c in _session.Query<Customer>()
   2:             join ca in _session.Query<CustomerAddress>() on c equals ca.Customer
   3:             join a in _session.Query<Address>() on ca.Address equals a
   4:             where ca.Type.Name == "Home" && a.StateProvince.CountryCode == "GB" && 
   5:                   c.Orders.Count() > 2 && c.Orders.Sum( ov => ov.Total ) > 6000
   6:             select c;

In closing….

In my opinion, this makes life much easier than trying to remember how to use aliases, detached criteria and so on – although if you prefer that mechanism, it’s all still there, please yourself Smile

Tuesday, 27 September 2011

DRL DevJam 2011

As you may or may not know, I changed jobs in June (around the time of my last post) and my new employer has been keeping me pretty much swamped. If you’d like to complain about the lack of Orchard posts and other general guff, you can now do it in person! We’re hosting a developer evening at our head office here in Bolton – come along and not only can you beg my boss to give me some time to build more samples and posts, but also we’ve got a pretty decent evening planned including;

  • A coding challenge competition with prizes!
    • £100 amazon vouchers
    • Super top range mouse and keyboard
    • Command launcher USB pens
  • Fun
  • Lots of discussion about technology, kanban, lean and other agile-y topics
  • Fun
  • Food and drinks (there will be a healthy option if you want it!)
  • Fun
  • A caricature to remind you of your evening with us.
  • And more fun…

This is a new thing for us, it might become a regular gathering, or it might be a one off, we’ll see how it goes, but part of the reason for the event is, we’re looking for bright individuals like you to join our teams and we figured holding a nice geek out party (or DevJam as we like to call it) would be a great way to get to know you and you us.

So, if you’re interested, the date is 24th October 2011, from 6:30pm until 9pm. You can get more information about the event, our company and to register for your free place here: http://www.eventbrite.com/event/2173571210/efbnen

Hope to see you there!

Wednesday, 8 June 2011

Real world Orchard CMS–part 7–finding content

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/ (the code for this post is in part-07 subdirectory)

Preamble

In this post I want to take a look at how we can use the orchard API to discover content. Continuing in the series with our fictitious product company, we want our product pages to be able to easily display a list of related content. We’ll find this content using the built in tags module that orchard provides and we’ll build a widget that allows us to find content with specific tags and display it as a list. Our spec is pretty simple;

  • Allow administrator to drop a widget on a layer to find content that has specific tags.
  • Allow the widget to be configured to require that for content to match it only needs to have one of the tags
  • Allow the widget to be configured to require that for content to match it needs to have all of the tags
  • Allow the widget to control whether or not the current page/content should be excluded if it were to match the tags
  • Allow the widget to be configured to only retrieve N content items.
  • Allow the widget to be configured to present a simple list of content item titles as links or to display the content item in summary form.

Doing this we can use the widget for a variety of purposes;

  • Display a list of featured content on the homepage (content tagged “featured” and “home”)
  • Display a list of related content within a product page (content tagged “product-X”)
  • Display a list of FAQ articles and posts within a product page (content tagged “product-X” and “faq”)
  • etc…

Setting up

Well, we’ve got to start with our old friends that make up the overall widget the record, part, handler, driver, migration, placement.info, module.txt and editor and display templates (phew! a lot of plumbing huh?). I’ve covered these topics in a fair amount of detail already and the docs are already quite extensive so for this post I’ll discuss mainly the driver in more detail as this is where the guts of the content discovery goes on;

First of all, let’s start with the record: in the SampleSiteModule project, create /models/RelatedContentWidgetRecord.cs;

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

namespace SampleSiteModule.Models
{
    [OrchardFeature("RelatedContent")]
    public class RelatedContentWidgetRecord : ContentPartRecord
    {    
        public virtual string TagList { get; set; }
        public virtual int MaxItems { get; set; }
        public virtual bool ExcludeCurrentItemIfMatching { get; set; }
        public virtual bool MustHaveAllTags { get; set; }
        public virtual bool ShowListOnly { get; set; }
    }
}

Remember our module is exposing all of it’s different components as features that can be independently turned on/off, hence why all of our code is marked with the OrchardFeature attribute. The record describes all the settings we’ll use for our widget – the TagList property will be a comma separated list of tags to search for, MaxItems will control how many items we get at most, and the three bools control the widget behaviour – ExcludeCurrentItemIfMatching will strip out the current content item if it matches the rules (so a link to the current page doesn’t appear in the widget’s list), MustHaveAllTags controls whether all the specified tags must be present on the content items to be deemed a match or whether if can just contain at least one of them and ShowListOnly will allow the widget to render a simple list of links or a full blown summary view of the content items.

Next, we need to wrap the record up in a part - /models/RelatedContentWidgetPart.cs;

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Orchard.ContentManagement;
using Orchard.Environment.Extensions;

namespace SampleSiteModule.Models
{
    [OrchardFeature("RelatedContent")]
    public class RelatedContentWidgetPart : ContentPart<RelatedContentWidgetRecord>
    {
        [Required]
        public string TagList
        {
            get { return Record.TagList; }
            set { Record.TagList = value; }
        }

        [Required]
        [DefaultValue(5)]
        public int MaxItems
        {
            get { return Record.MaxItems; }
            set { Record.MaxItems = value; }
        }

        [DefaultValue(true)]
        public bool ExcludeCurrentItemIfMatching
        {
            get { return Record.ExcludeCurrentItemIfMatching; }
            set { Record.ExcludeCurrentItemIfMatching = value; }
        }
        
        [DefaultValue(false)]
        public bool MustHaveAllTags 
        { 
            get { return Record.MustHaveAllTags; }
            set { Record.MustHaveAllTags = value; }
        }

        [DefaultValue(true)]
        public bool ShowListOnly
        {
            get { return Record.ShowListOnly; }
            set { Record.ShowListOnly = value; }
        }
    }
}

Nothing groundbreaking in there, so moving swiftly on, the handler – /handlers/RelatedContentWidgetRecordHandler.cs

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

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

And the driver – for now, just create the following shell and we’ll build it up in a second – /drivers/RelatedContentWidgetDriver.cs

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

namespace SampleSiteModule.Drivers
{
    /// <summary>
    /// Content part driver for the related content widget
    /// </summary>
    [OrchardFeature("RelatedContent")]
    public class RelatedContentWidgetDriver : ContentPartDriver<RelatedContentWidgetPart>
    {
        protected override DriverResult Display(RelatedContentWidgetPart part, string displayType, dynamic shapeHelper)
        {
            var list = shapeHelper.List();

            return ContentShape("Parts_RelatedContentWidget",
                () => shapeHelper.Parts_RelatedContentWidget(
                        ShowListOnly : part.ShowListOnly,
                        ContentItems : list
                        ));
        }

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

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

Let’s setup a migration (note: in the code on codeplex, because I built this up in stages, the migration is a little different as it has a create and two updates to progressively add features) – create /Migration-RelatedContentWidget.cs;

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("RelatedContent")]
    public class MigrationsRelatedContentWidget : DataMigrationImpl
    {
        public int Create()
        {
            // Define the persistence table as a content part record with
            // my specific fields.
            SchemaBuilder.CreateTable("RelatedContentWidgetRecord", 
                table => table
                    .ContentPartRecord()
                    .Column("TagList", DbType.String, a => a.Unlimited())
                    .Column("MaxItems", DbType.Int32)
                    .Column("ExcludeCurrentItemIfMatching", DbType.Boolean)
                    .Column("MustHaveAllTags", DbType.Boolean)
                    .Column("ShowListOnly", DbType.Boolean)
                    );

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

            // Tell the content def manager that we have a content type 
            // the parts it contains and that it should be treated as a widget
            ContentDefinitionManager.AlterTypeDefinition("RelatedContentWidget",
                cfg => cfg
                    .WithPart("RelatedContentWidgetPart")
                    .WithPart("WidgetPart")
                    .WithPart("CommonPart")
                    .WithSetting("Stereotype", "Widget"));

            return 1;
        }
    }
}

All of that should be pretty familiar, creates the record table, defines the widget part and the widget type with appropriate stereotype etc. See earlier posts in this series if you’re not sure what this is doing.

Next, we need to update placement.info so our parts will appear;

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

    <Place Parts_RelatedContentWidget="Content:1"/>
    <Place Parts_RelatedContentWidget_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>

We need to define the editor template next, in /Views/EditorTemplates/Parts/RelatedContentWidget.cshtml

@model SampleSiteModule.Models.RelatedContentWidgetPart

<fieldset>
    <legend>Related Content</legend>
    <div class="editor-label">@T("Tags to find (comma separated)"):</div>
    <div class="editor-field">
        @Html.TextBoxFor( m => m.TagList )
        @Html.ValidationMessageFor( m => m.TagList )
    </div>

    <div class="editor-label">@T("Items must have all tags"):</div>
    <div class="editor-field">
        @Html.CheckBoxFor(m => m.MustHaveAllTags)
    </div>

    <div class="editor-label">@T("Exclude current content if matches?"):</div>
    <div class="editor-field">
        @Html.CheckBoxFor(m => m.ExcludeCurrentItemIfMatching)
    </div>
    
    <div class="editor-label">@T("Number of items to find"):</div>
    <div class="editor-field">
        @Html.TextBoxFor(m => m.MaxItems)
        @Html.ValidationMessageFor(m => m.MaxItems)
    </div>

    <div class="editor-label">@T("Show as list only? (Setting this will show just a basic list, otherwise the full summary view will be rendered for each item)"):</div>
    <div class="editor-field">
        @Html.CheckBoxFor(m => m.ShowListOnly)
    </div>
</fieldset>

and the display template in /Views/Parts/RelatedContentWidget.cshtml;

@using Orchard.ContentManagement;
@{
    IEnumerable<object> latest = Model.ContentItems;
}
@if (latest == null || latest.Count() < 1) {
<p>@T("No related content.")</p>
}
else {
    <ul class="content-items">
    @foreach (dynamic item in latest) 
    {
        string title = item.Title;
        ContentItem contentItem = item.ContentItem;
        <li class="content-item-summary">
        @if( Model.ShowListOnly )
        {
            @Html.ItemDisplayLink(title, contentItem)
        }
        else
        {
            @Display(item)
        }
        </li>
    }
    </ul>
}

We could have just left the default widget and list templates to render everything for us, but we wanted control over the list formatting and whether or not to just show links or full summary content, so in this case, we created the above template. Note the condition on ShowListOnly – if it’s not going to just show a list, we simply call @Display(item) and this will take care of  rendering out the content item in summary format (regardless of it’s type).

The last thing to do now is update module.txt to reflect our new feature;

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
    RelatedContent:
        Name: Related content widget
        Category: Sample Site
        Description: Widget to find content based on tags

Where are we

That’s a lot of stuff to put in place to get the widget ready and so far, it won’t do anything, which we’ll fix in a second, but you should now be able to enable this feature through the orchard admin interface and add a related content widget to your pages. You should see the following configuration;

image

and the display on the page will presently just show the no related content message (note here I’m using the default theme);

image

Updating the driver to get some content

This was one of the more difficult parts of this post, it took me an age to work out how the query interface worked, which was quite a surprise, but once you understand it, it’s pretty clear. Again, as ever, thanks to randompete and bertrandleroy on the forums for helping me out.

Getting content with one of the tags – the basic query

Lets start by just getting a list of content that has one of the tags, is ordered correctly and returns just the number of entries we’re looking for. To begin, we’re going to need an IContentManager injected by autofac that we can use to query content, so give the driver a constructor that takes one of these and store it into a private readonly field;

private readonly IContentManager _cms;

public RelatedContentWidgetDriver(IContentManager cms)
{
     _cms = cms;
}

We can now use this to query for content in our driver’s display method – change it as follows;

   1:          protected override DriverResult Display(RelatedContentWidgetPart part, string displayType, dynamic shapeHelper)
   2:          {
   3:              // Convert CSV tags to list
   4:              List<string> tags = new List<string>();
   5:              if (!String.IsNullOrWhiteSpace(part.TagList))
   6:              {
   7:                  Array.ForEach(part.TagList.Split(','), t =>
   8:                  {
   9:                      if (!String.IsNullOrWhiteSpace(t))
  10:                      {
  11:                          t = t.Trim();
  12:                          if (!tags.Contains(t))
  13:                              tags.Add(t);
  14:                      }
  15:                  });
  16:              }
  17:   
  18:              // If we have no tags.....
  19:              if (tags.Count < 1)
  20:              {
  21:                  return ContentShape("Parts_RelatedContentWidget",
  22:                      () => shapeHelper.Parts_RelatedContentWidget(
  23:                              ContentItems: shapeHelper.List()
  24:                              ));
  25:              }
  26:   
  27:              IEnumerable<TagsPart> parts = 
  28:                  _cms.Query<TagsPart, TagsPartRecord>()
  29:                  .Where(tpr => tpr.Tags.Any(t => tags.Contains(t.TagRecord.TagName)))
  30:                  .Join<CommonPartRecord>()
  31:                  .OrderByDescending(cpr => cpr.PublishedUtc)
  32:                  .Slice(part.MaxItems);
  33:   
  34:              // Create a list and push our display content items in
  35:              var list = shapeHelper.List();
  36:              list.AddRange(parts.Select(p => _cms.BuildDisplay(p, "Summary")));
  37:   
  38:              return ContentShape("Parts_RelatedContentWidget",
  39:                  () => shapeHelper.Parts_RelatedContentWidget(
  40:                          ShowListOnly : part.ShowListOnly,
  41:                          ContentItems : list
  42:                          ));
  43:          }

Ok, so the first section (lines 4-16) are just concerned with taking a CSV list from the widget’s part and splitting it down into a unique list of tags that we can search for. This takes into account spaces, empty items and so on, so just gives us a more sane list of tags to search for than what a user might have keyed. Lines 18-25 just deal with the condition where we have no tags at all, in which case we’re just returning the same as we did in our earlier example – an empty list that will just render the “no related content” message.

The code gets a little more interesting at line 27-32. Line 28 first of all starts a query against the TagsPart, so this will query against the properties exposed from any content item that has the tags part attached to it, on line 29 we then tell the query engine to build a query that looks at the tags associated with the content to determine if the tag name is in our provided list, if it is, it will be included in the results, if not, it won’t.

Line 30 then joins the common part onto the query for the next part of the query (this will do an inner join to bring the common part properties in) and line 31 orders the results, in descending order, by the published date exposed from the common part record. Finally, line 32 actually executes the query, getting just the first N rows only (MaxItems part property).

On line 35/36 we build a list shape and add the items we’ve found to it, asking the IContentManager to build the display shape for the item, in summary display mode (not necessarily what will be rendered, this just sets the shapes up into the shape tree).

Finally, line 38, we return the part shape as normal, containing the list we’ve build and the flag to indicate how we want the results formatted.

Excluding the current page from the results

Next we’ll get the widget to exclude the current page if that is appearing in the links. Interestingly there doesn’t appear to be any reliable way to ask orchard what the ID is of the current content item being rendered (if there is one, you won’t have one in a themed custom controller action perhaps), so, with a little help from randompete, I decided to take the current request URL and manually match it to the routepart urls to get the ID of the content item being rendered (most navigable content has the routepart).

With the content id in hand, it’s then trivial to add an exclusion to the above query so that item doesn’t get included. To start with though, we’ll need a way to get access to the current work context, so add a dependency and field on the driver for an IWorkContextAccessor;

private readonly IContentManager _cms;
private readonly IWorkContextAccessor _work;

public RelatedContentWidgetDriver(IContentManager cms, IWorkContextAccessor work)
{
    _cms = cms;
    _work = work;
}

And then, add a method to your driver that will get the current id of the page/content being rendered for the current url;

/// <summary>
/// Helper that will attempt to work out the current content id from the url
/// of the request.
/// </summary>
/// <param name="defaultIfNotFound"></param>
/// <returns></returns>
private int TryGetCurrentContentId(int defaultIfNotFound)
{
    string urlPath = _work.GetContext().HttpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2);

    var routableHit = _cms
        .Query<RoutePart, RoutePartRecord>(VersionOptions.Published)
        .Where(r => r.Path == urlPath)
        .Slice(1).FirstOrDefault();

    if (routableHit != null) return routableHit.Id;

    return defaultIfNotFound;
}

This isn’t doing anything particularly clever, it’s getting the URL of the current request (relative to the web app itself and stripping off the resultant ~/ from the beginning) and then performing another query against the RoutePart’s Path property to find the one that matches. If it is found, we return the id of the item that was found.

Back in our display method we can now use this to exclude the current item;

// Get current item
int currentItemId = -1;
if( part.ExcludeCurrentItemIfMatching )
    currentItemId = TryGetCurrentContentId(-1);

IEnumerable<TagsPart> parts =
    _cms.Query<TagsPart, TagsPartRecord>()
    .Where(tpr => tpr.Tags.Any(t => tags.Contains(t.TagRecord.TagName)))
    .Join<CommonPartRecord>()
    .Where(cpr => cpr.Id != currentItemId)
    .OrderByDescending(cpr => cpr.PublishedUtc)
    .Slice(part.MaxItems);

Extend the query to honour our MustHaveAllTags part property.

And finally, we’ll change our query according to the MustHaveAllTags property on our widget. The display method has changed a bit, so here it is in it’s entirety with the changed area highlighted;

protected override DriverResult Display(RelatedContentWidgetPart part, string displayType, dynamic shapeHelper)
{
    // Convert CSV tags to list
    List<string> tags = new List<string>();
    if (!String.IsNullOrWhiteSpace(part.TagList))
    {
        Array.ForEach(part.TagList.Split(','), t =>
        {
            if (!String.IsNullOrWhiteSpace(t))
            {
                t = t.Trim();
                if (!tags.Contains(t))
                    tags.Add(t);
            }
        });
    }

    // If we have no tags.....
    if (tags.Count < 1)
    {
        return ContentShape("Parts_RelatedContentWidget",
            () => shapeHelper.Parts_RelatedContentWidget(
                    ContentItems: shapeHelper.List()
                    ));
    }

    // See if we can find the current page/content id to filter it out
    // from the related content if necessary.
    int currentItemId = -1;
    if( part.ExcludeCurrentItemIfMatching )
        currentItemId = TryGetCurrentContentId(-1);

    // Setup a query on the tags part
    IContentQuery<TagsPart, TagsPartRecord> query = _cms.Query<TagsPart, TagsPartRecord>();

    if (part.MustHaveAllTags)
    {
        // Add where conditions for every tag specified
        foreach (string tag in tags)
        {
            string tag1 = tag; // Prevent access to modified closure
            query.Where(tpr => tpr.Tags.Any(t => t.TagRecord.TagName == tag1));
        }
    }
    else
    {
        // Add where condition for any tag specified
        query.Where(tpr => tpr.Tags.Any(t => tags.Contains(t.TagRecord.TagName)));
    }
            
    // Finish the query (exclude current, do ordering and slice max items) and execute
    IEnumerable<TagsPart> parts = 
        query.Join<CommonPartRecord>()
        .Where(cpr => cpr.Id != currentItemId)
        .OrderByDescending(cpr => cpr.PublishedUtc)
        .Slice(part.MaxItems);

    // Create a list and push our display content items in
    var list = shapeHelper.List();
    list.AddRange(parts.Select(p => _cms.BuildDisplay(p, "Summary")));

    return ContentShape("Parts_RelatedContentWidget",
        () => shapeHelper.Parts_RelatedContentWidget(
                ShowListOnly : part.ShowListOnly,
                ContentItems : list
                ));
}

As you can see, the only difference between “must have all tags” and “must have one of the tags” is the all tags adds a where clause for each tag directly – the rest of the query is unchanged.

Until next time

That’s it for this post – as ever, grab the code from codeplex: http://orchardsamplesite.codeplex.com under the part-07 folder (I’ve retired the final folder by the way as the posts are keeping up with the code now!). In the next post I’ll be looking at creating sub-navigation.

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!).