Conditional Pages – Part 2

Posted Friday, June 15, 2012 12:42 AM by Kevin

Over the weekend, I posted part 1 of a discussion of this thing I created that I’m calling “conditional pages”.  I promised to elaborate on the technical details, so here I am with some code samples and instructions that hopefully will allow others to reproduce what I have done.

Before I begin, let me once again thank Sitecore CTO John West - without whose blog posts and assistance on the SDN forums, I might have never figured this stuff out.  I haven’t found a better source of information on rules engine customizations.  If you’re looking for more information, check out his blog here http://goo.gl/5V4Gg

This whole process begins when a request comes in to the Sitecore pipelines.  By implementing a custom StartTrackingProcessor, I am able to intercept the request, inspect the item that was requested and if it is a conditional page, execute the rules defined on the page to determine what item should replace the originally requested item in the Sitecore context.

To create my custom processor, I just created a class that derives from Sitecore’s StartTrackingProcessor and implemented an override for the Process() method.  The declaration looks like this:

    public class ConditionalPageTrackingProcessor : 
StartTrackingProcessor { public override void Process(StartTrackingArgs args) { } }

Within my Process() method, I first check if Sitecore.Context.Item is using the Conditional Page template.  I described this template in my previous post, but here’s a quick screenshot of what it looks like in Sitecore:

Screen Shot 2012-06-11 at 9.45.58 PM

I’ll explain the path you see in the source for the Rules field in a little bit.

If the requested item is not a conditional page, then there is nothing for my processor to do so the code just returns.  However, if it IS a conditional page – I pull the value from the Rules field like this:

    string rulesXml = Context.Item["Rules"];

Note, there is no strongly-typed Field-derived class for fields of type Rules, so I’m pulling out the raw XML string.  This string now needs to be parsed, and that is done by a method on the Sitecore.Rules.RuleFactory class - ParseRules<T>():

    var parsed = RuleFactory.ParseRules<ConditionalPageRuleContext>
(Sitecore.Context.Database, rulesXml);

Now you may be asking – what is this ConditionalPageRuleContext class?  This is my custom rules engine context.  It derives from Sitecore.Rules.RulesContext and adds a property that indicates what item (if any) the rules want the user redirected to.  The concept of a rules context seems a bit confusing at first, but suffice it to say that this class is what allows the rules engine to communicate information back to the code that executes it.  Here’s what the ConditionalPageRuleContext class looks like in code:

    public class ConditionalPageRuleContext : Sitecore.Rules.RuleContext
    {
        public Item NewItem { get; set; }
    }

In order to proceed, I need to construct an object of this class and tell it what item it is acting on:

    ConditionalPageRuleContext ruleContext = 
new ConditionalPageRuleContext(); ruleContext.Item = Context.Item;

And then, I run the rules that were parsed – passing in my rule context:

    RuleList<ConditionalPageRuleContext> rules = 
        new RuleList<ConditionalPageRuleContext>();
    rules.Name = Context.Item.Paths.Path;
    rules.AddRange(parsed.Rules);
    rules.Run(ruleContext);

When this returns, I check if ruleContext.NewItem was set and if it was, I set Sitecore.Context.Item = ruleContext.NewItem.

So wait – how does the NewItem property on my context get set by the rules engine?  Let’s take a closer look at an example rule that redirects some users to a page “Test 1” and others to “Test 2” instead:

image

The actions here are “set current item to …”.  This is a custom rules engine action that only runs within a ConditionalPageRuleContext and sets the NewItem property of that context to the specified value.  Here’s the code:

    public class SwitchToItemAction<T> : 
        RuleAction<T> where T : 
            Rules.ConditionalPageRuleContext
    {
        public string ItemId { get; set; }

        public override void Apply(T ruleContext)
        {
            ruleContext.NewItem = 
                ruleContext.Item.Database.GetItem(new ID(ItemId));
        }
    }

If you haven’t seen a custom action before, the public property gets set by Sitecore to the value selected in the rule editor dialog (the ID of “Test 1” or “Test 2” in the above example), and the Apply() method gets called only for the actions whose conditions pass.  Making this action available in the rules editor requires something in Sitecore.  Remember that source path on the Rules field for the Conditional Page template?

Inside the /sitecore/System/Settings/Rules/ folder, I created a folder named “Conditional Page”.  Within that folder, I created a folder named “Actions”.  The rules editor looks at the field’s source and restricts the actions that can be chosen to those within the “Actions” sub-folder.  So, to configure my action, I created an item in this Actions folder named “Switch to Item” using the /sitecore/templates/System/Rules/Action template.

There are two fields to fill in on this item – Text and Type.  Type is simply a pointer to the above SwitchToItemAction class (in the usual Namespace.Class, Assembly format).  Text is special.  Mine looks like this:

image

The text outside of the square brackets is what the user will see in the rules editor.  Inside the square brackets are four comma-separated values:

ItemId – The name of the property on the class to be set to the selected value.

Tree – The type of UI element to present to the user for choosing a value.

root=… – An argument passed to the UI element.  In this case, the root item for the tree.

an item – The text to show in the rules editor before an item has been chosen.

Note – I haven’t found a documented list of available UI elements and what their arguments are.  I just saw another example using “Tree”.

There’s one more thing I found it necessary to do to make everything work…  My StartTrackingProcessor was never called if the conditional page item didn’t have a layout set on it.  Instead, Sitecore would abort in an earlier pipeline and display the “layout not found” error.  I just fixed this by setting the standard values for the Conditional Page template to use a blank layout.

If you read Part 1 of this series, I talked quite a bit about a Page Design condition…  Reading and writing data from a site visitor’s profile is pretty simple, but I’ll cover that and how to create a custom rules engine condition in my next blog post. :)

I hope this helps somebody.  Please, let me know if you use this idea or think of any ways I could improve it.  And if you have any questions, just send them to me via direct message on Twitter - @williamsk000.

Comments

No Comments