Orchard Custom Workflow Activity

Topics: Customizing Orchard, Writing modules
Jan 14, 2015 at 2:13 PM
I have built a custom module in Orchard that creates a new part, type and a custom activity but I'm struggling with the last part of what I need to do which is to create a copy of all the content items associated with a specific parent item.

For instance, when someone creates a "Trade Show" (new type from my module), various subpages can be created off of it (directions, vendor maps, etc.) since the client runs a single show at a time. What I need to do is, when they create a new Trade Show, I want to get the most recent prior show (which I'm doing via _contentManager.HqlQuery().ForType("TradeShow").ForVersion(VersionOptions.Latest).ForVersion(VersionOptions.Published).List().Last() (positive that's not the most efficient way, but it works and the record count would be ~10 after five years), then find all of those child pages that correlate to that old show and copy them into new Content Items. They have to be a copy because on occasion they may have to refer back to parts with the old shows, or it could change, etc. All the usual reasons.

How do I go about finding all of the content items that reference that prior show in an Activity? Here is my full class for the Activity:

using System;
using System.Collections.Generic;
using System.Linq;
using Orchard.Autoroute.Services;
using Orchard.ContentManagement;
using Orchard.Localization;
using Orchard.Projections.Models;
using Orchard.Projections.Services;
using Orchard.Workflows.Models;
using Orchard.Workflows.Services;
using Orchard.Workflows.Activities;

namespace Orchard.Web.Modules.TradeShows.Activities
public class TradeShowPublishedActivity : Task
private readonly IContentManager _contentManager;
private readonly IAutorouteService _autorouteService;
private readonly IProjectionManager _projectionManager;

public TradeShowPublishedActivity(IContentManager contentManager, IAutorouteService autorouteService, IProjectionManager projectionManager)
    _contentManager = contentManager;
    _autorouteService = autorouteService;
    _projectionManager = projectionManager;

    T = NullLocalizer.Instance;

public Localizer T { get; set; }

public override LocalizedString Category
    get { return T("Flow"); }

public override LocalizedString Description
    get { return T("Handles the automatic creation of content pages for the new show."); }

public override string Name
    get { return "TradeShowPublished"; }

public override string Form
    get { return null; }

public override IEnumerable<LocalizedString> GetPossibleOutcomes(WorkflowContext workflowContext, ActivityContext activityContext)
    yield return T("Done");

public override IEnumerable<LocalizedString> Execute(WorkflowContext workflowContext, ActivityContext activityContext)
    var priorShow = _contentManager.HqlQuery().ForType("TradeShow").ForVersion(VersionOptions.Latest).ForVersion(VersionOptions.Published).List().Last();
    var tradeShowPart = priorShow.Parts.Where(p => p.PartDefinition.Name == "TradeShowContentPart").Single();

    //new show alias

    yield return T("Done");

My Migrations.cs file sets up the part that is used for child pages to reference the parent show like this:

ContentDefinitionManager.AlterPartDefinition("AssociatedTradeShowPart", builder => builder.WithField("Trade Show", cfg => cfg.OfType("ContentPickerField")
                                                                                                                              .WithDisplayName("Trade Show")
                                                                                                                              .WithSetting("ContentPickerFieldSettings.Attachable", "true")
                                                                                                                              .WithSetting("ContentPickerFieldSettings.Description", "Select the trade show this item is for.")
                                                                                                                              .WithSetting("ContentPickerFieldSettings.Required", "true")
                                                                                                                              .WithSetting("ContentPickerFieldSettings.DisplayedContentTypes", "TradeShow")
                                                                                                                              .WithSetting("ContentPickerFieldSettings.Multiple", "false")
                                                                                                                              .WithSetting("ContentPickerFieldSettings.ShowContentTab", "true")));
Then, my child pages (only one for now, but plenty more coming) are created like this:

ContentDefinitionManager.AlterTypeDefinition("ShowDirections", cfg => cfg.DisplayedAs("Show Directions")
                                                                             .WithPart("AutoroutePart", builder => builder.WithSetting("AutorouteSettings.AllowCustomPattern", "true")
                                                                                                                           .WithSetting("AutorouteSettings.AutomaticAdjustmentOnEdit", "false")
                                                                                                                           .WithSetting("AutorouteSettings.PatternDefinitions", "[{Name:'Title', Pattern: '{Content.Slug}', Description: 'international-trade-show'}]")
                                                                                                                           .WithSetting("AutorouteSettings.DefaultPatternIndex", "0"))
                                                                             .WithPart("CommonPart", builder => builder.WithSetting("DateEditorSettings.ShowDateEditor", "false"))
                                                                             .WithPart("AssociatedTradeShowPart") /* allows linking to parent show */
                                                                             .WithPart("ContainablePart", builder => builder.WithSetting("ContainablePartSettings.ShowContainerPicker", "true"))
Jan 16, 2015 at 2:23 AM
Edited Jan 16, 2015 at 4:25 AM
Tried with an 1.8.x version, and in a background task context (not an activity context)

First test on the content picker field. I didn't create an AssociatedTradeShowPart, I only added to a "child" content type a content picker field associated to a "parent" content type. Then, I created some children items where I've selected, with the picker field, the same parent instance. Here, the picker field value is a string "{n}" where n is the Id of the selected content item. So, I can access the parent and its children items like that
var parent = _contentManager.Query(VersionOptions.Published, "TradeShow").List().Last();
var childIds = _contentManager.Query<FieldIndexPart, FieldIndexPartRecord>(VersionOptions.Published)
    .Where(x => x.Record.StringFieldIndexRecords
        .FirstOrDefault().Value.Contains("{" + parent.Id.ToString() + "}"))
    .Select(x => x.Record.Id).ToList();
But, because your child type "ShowDirections" has a Containable part, I assume your parent type "TradeShow" has a ContainerPart. So, if you create each child item from their parent page (their container), or add them after, then, in the CommonPartRecord table, for each child item, you will see a value in the "Container_id" column. Then, to access the parent and its children, you have better to use this
var parent = _contentManager.Query(VersionOptions.Published, "TradeShow").List().Last();
var childIds = _contentManager.Query<CommonPart, CommonPartRecord>(VersionOptions.Published)
    .Where(x => x.Container != null && x.Container.Id == parent.Id)
    .Select(x => x.Id).ToList();
Jan 18, 2015 at 1:10 PM
This got me pretty close. Unfortunately I do have to have an activity context because I need the URL slug generated from the new show in order to make it part of the URL for all of the child pages - e.g. site.com/trade-show-1 is the parent, and all pages underneath that show need to be site.com/trade-show-1/show-directions (etc.). The LINQ queries I was running were having a really difficult time in getting the prior show (kept either returning the very first one or the new one despite ordering clauses), however I did finally get it to pull the correct one back. Unfortunately, children still had no content items associated with it even though I manually created one on the old show before I created the new show. Here is what I'm working with thus far, but since VS2013 doesn't support LINQ in the Immediate Window, debugging this is a bit tough.
            var priorShows = _contentManager.Query(VersionOptions.Published, TradeShowMigration.TradeShowTypeName).List();

            var priorShow = priorShows.Where(x => x.Id != workflowContext.Content.ContentItem.Id).Last();

            //children comes back with 0 items. note that I didn't qualify the type here, because there will probably be 30+ and I wanted it to be flexible
            var children = _contentManager.Query<CommonPart, CommonPartRecord>(VersionOptions.Published).Where(x => x.Container != null && x.Container.Id == priorShow.Id);

Jan 18, 2015 at 8:53 PM
Edited Jan 19, 2015 at 2:54 AM
Good, so you get your previous show by querying the last one, but not the one that was just created (in a previous activity I think). I first thought that you want to get the last show before you create a new one, this in the same activity

For the children, Query() return an IContentQuery, Where() is applied to this IContentQuery and, here, also return an IContentQuery. You need to use a List() somewhere to get an IEnumerable. You can use it before the Where() that will be applied to this IEnumerable and will return here an IEnumerable, but I think it's better to use the Where() filter directly on the Query() to limit the results returned from the database

At this point, you will get a CommonPart collection, so you have to use a Select() to project the sequence in the form you want. Finally, it depends of the kind of collection you want to use, e.g you can use ToList(). So, if you want to get the list of child content items, you can use
var children = _contentManager.Query<CommonPart, CommonPartRecord>(VersionOptions.Published)
    .Where(x => x.Container != null && x.Container.Id == container.Id)
    .List().Select(x => x.ContentItem).ToList();

For infos, about the way of using the content picker field as in a previous post where I applied the Where() clause to the IEnumerable returned by List(). In some case I had to add a null check
x.Record.StringFieldIndexRecords.FirstOrDefault().Value != null
When I tried to apply the Where() clause to the IContentQuery returned by Query(), it failed. To get it work I found this way
var items = _contentManager.Query<FieldIndexPart, FieldIndexPartRecord>(VersionOptions.Published)
    .Where(x => x.StringFieldIndexRecords.Any(
        f => f.Value.Contains("{" + parent.Id.ToString() + "}")))
    .List().Select(x => x.ContentItem).ToList();
Finally, with the same way and by re-apllying the Where() to the List(), this also works (note: here, I had to add a null check)
items = _contentManager.Query<FieldIndexPart, FieldIndexPartRecord>(VersionOptions.Published).List()
    .Where(x => x.Record.StringFieldIndexRecords.Any(
        f => f.Value != null && f.Value.Contains("{" + parent.Id.ToString() + "}")))
    .Select(x => x.ContentItem).ToList();
To be more complete, with the picker field, I think we also have to check the f.PropertyName. See in the database, for example it contains the name that you have done to this field in your content type... So, if it works with your Containable and Container parts, I think this is much simpler