Insert Rules using Rules Magic

Who hasn’t run into something before that requires a new Folder type because it needs specific Insert rules, and have that folder type excluded in specific renderings because it shouldn’t be displayed?

Turns out, we don’t need different folder types anymore, Sitecore provides a very easy solution for this – the Rules engine.

Let’s say we have a News folder which can insert either a News item or a folder for Categories, the Categories folder can insert either a News item or a folder for Subcategories and the Subcategories can only insert News items. Sure, it’s not the most nicely or logically structured tree, but I needed an example ;-).

news structure

It goes without saying that we won’t be able to set the insert rules on the Folder template, as we need different insert options for different locations in the tree.

We can already set the News insert options manually if we’d like, because there won’t be a second news folder. The Category folder needs to be able to insert Folders (which is the default insert option for a Folder anyway) and a News item. We can’t go and set the insert options manually here, because there might be new categories later on. The same goes for the Subcategory.

There is already an Action defined for this, and there are some out-of-the-box Rules which suit our purpose in this case.,
If we do need to have our own, Sitecore is extensible enough for us to be able to customize Actions and Rules.

In /sitecore/System/Settings/Rules/Insert Options/Rules we can insert our own new rule.

As the Rule, we can use a Sitecore default ‘where the item is the specific item or one of its subitems’. The same goes for the action – we can use ‘add specific insert option’.

All we need to do is set the specific item to be News, and specific insert option to be our News article.

generic rule

So now we’ll have the insert option Folder and News article for News, Category and Subcategory. Of course, we don’t want the Folder insert option on our Subcategory. What can we do about this?

Well, if there’s an ‘add specific insert option’, I feel like there should also be a ‘remove specific insert option’. This rule doesn’t exist yet, so we’ll create one.

First of all, add the new Action to the /sitecore/System/Settings/Rules/Insert Options folder.

We can copy and paste the value for the Text field from the ‘add specific insert option’ rule – we’ll just change ‘add’ to ‘remove’.

removetext

In the Type field I’ll point to my class, which we’ll have to create first. Most of the logic already exists in Sitecore’s AddInsertOption class, so that’s a perfect starting point. In fact, it’s so perfect, we only have to change one line (well, 4 really) – it’s highlighted below.

namespace TestApplication.InsertOptions
{
  public class RemoveInsertOption<T> : RuleAction<T> where T:InsertOptionsRuleContext
  {
    // Fields
    private ID insertOptionId;

    // Methods
    public override void Apply(T ruleContext)
    {
      Assert.ArgumentNotNull(ruleContext, "ruleContext");
      var item = ruleContext.Item;
      if (item != null)
      {
        var item2 = item.Database.GetItem(this.InsertOptionId);
        if (item2 != null)
        {
          ruleContext.InsertOptions.RemoveAll(delegate(Item i)
                                     {
                                       return i.ID == item2.ID;
                                     });
        }
      }
    }

    // Properties
    public ID InsertOptionId
    {
      get
      {
        return (this.insertOptionId ?? ID.Null);
      }
      set
      {
        Assert.ArgumentNotNull(value, "value");
        this.insertOptionId = value;
      }
    }
  }
}

The InsertOptionId is set through the Text field of our Rule item.

The apply method works as follows:

ruleContext.Item checks the item the rule is run on. Item2 is then the specific template to remove. I use RemoveAll because of the off-chance it appears twice.

After building this we can add the Type field to point to our new class:

removetype

We can now create a new Rule as well. We need our rule to remove the insert rule when the item is under the News folder and the depth is 5 levels.

subcatrule

And our result after all this work.

News:

insertnews

Category:

insertcat

Subcategory:

insertsubcat

Rules in the Page Editor

This is a different way of disabling buttons on fields in Sitecore’s Page Editor, based on field values and the actual field, as was described in this post. This time I will be using the Sitecore Rules engine rather than removing items from a list.

The inspiration from this blog post came from @KevinObee, who put in this answer to the same question:

In short the answer he gave is: Use the Rules Engine as described here (a blog post by Adam Conn).

This sounded like a very interesting (not to mention powerful and elegant) solution, so here goes.

As Kevin gave a link to Sitecore’s blog, this was my starting point. I roughly followed Step 1 and Step 2 – but made my own Step 3. The only thing I didn’t follow is Step 2.4. I didn’t create the Command field.

I did this because I didn’t want to execute this rule for only my own command – the StackOverflow question specified it’s an existing (i.e. Sitecore out-of-the-box) command. This was my first issue – how do I get to the CommandStates of all buttons?

It turns out one can use the getQueryState pipeline. Found this out through this excellent post by @MikeReynolds. Thanks for that 😉

For my solution I created an include file:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getQueryState>
        <processor type="TestApplication.HidePageEditorButtonsUsingRules.RunRules, TestApplication" />
      </getQueryState>
    </pipelines>
  </sitecore>
</configuration>

My RunRules class:

public class RunRules
{
  public void Process(GetQueryStateArgs args)
  {
    Assert.ArgumentNotNull(args, "args");
    args.CommandState = QueryState(args.CommandContext);
  }

  public CommandState QueryState(CommandContext context)
  {
    Assert.ArgumentNotNull(context, "args");
    var ruleContext = new CommandRuleContext();
    ruleContext.Item = context.Items[0];
    var ruleItems = context.Items[0].Database.SelectItems("/sitecore/system/Settings/Rules/Command Rules/Rules/*");

    foreach (var ruleItem in ruleItems)
    {
      var rules = RuleFactory.ParseRules<CommandRuleContext>(ruleItem.Database, ruleItem["Rules"]);
      if (rules != null && rules.Count > 0)
      {
        rules.Run(ruleContext);
      }
    }

    return ruleContext.CommandState;
  }
}

So now I can put in a rule like:

initialrule

So far so good. The only problem is that the question also specified that there might be multiple fields on the page, some of which would have the buttons, some wouldn’t.

First of all, I created a new Condition to be able to facilitate this requirement:

newcondition

This is my SpecificField class:

public class SpecificField<T> : StringOperatorCondition<T> where T : CommandRuleContext
{
  public string Value { get; set; }
  protected override bool Execute(T ruleContext)
  {
    Assert.ArgumentNotNull(ruleContext, "ruleContext");
    var item = ruleContext.Item;
    if (item == null)
    {
      return false;
    }

    string fieldName = ruleContext.FieldName;
    if (string.IsNullOrEmpty(fieldName))
    {
      return false;
    }

    return base.Compare(fieldName.ToUpperInvariant(), Value.ToUpperInvariant());
  }
}

Now I can change my Rule field to add this condition:

updatedrule

I’ll need to change some other things as well in our previous code.

The Process method in the RunRules class now looks like this:

public void Process(GetQueryStateArgs args)
{
  Assert.ArgumentNotNull(args, "args");

  if (args.CommandContext.Parameters["field"] != null)
  {
    args.CommandState = QueryState(args.CommandContext);
  }
}

The updated lines of code are highlighted.

In the QueryState method of the RunRules class, I add the field to the commandContext like this:

public CommandState QueryState(CommandContext context)
{
  Assert.ArgumentNotNull(context, "args");
  var ruleContext = new CommandRuleContext();
  ruleContext.Item = context.Items[0];
  ruleContext.FieldName = context.Parameters["field"];
  var ruleItems = context.Items[0].Database.SelectItems("/sitecore/system/Settings/Rules/Command Rules/Rules/*");

  foreach (var ruleItem in ruleItems)
  {
    var rules = RuleFactory.ParseRules<CommandRuleContext>(ruleItem.Database, ruleItem["Rules"]);
    if (rules != null && rules.Count > 0)
    {
      rules.Run(ruleContext);
    }
  }

  return ruleContext.CommandState;
}

The added line is highlighted.

After building this and running it, the result for the title field is:

titlefield

And the subtitle field:

subtitlefield

Of course, all commands are now removed. This could be customized relatively easy by adding a new Condition to the Rule field, which would check the Command – very similar to the SpecificField class above.

Also, you might notice the Personalize button still displays. I’m not entirely sure why, but I’m thinking it’s a timing issue – it might be because the GetRenderingTestVariations gets added after the getQueryState pipeline runs. I haven’t really looked into this. If anyone can shed a light on this, please let me know in the comments.

Page Editor explorations

I read this StackOverflow question, and that made me wonder where the field buttons actually come from. I’d never looked into this before, so here goes.

The summary of the question is:

Can we disable the buttons on fields in Sitecore’s Page Editor, not only based on some field on some item, but also based on which field we have?

The buttons are added in the <getChromeData> pipeline. My initial answer to the question was that one could override the Process method in the GetFieldChromeData class, but that struck me as a little extreme. Especially because it’s a Sitecore processor, so if the Sitecore code gets upgraded, the override has to be changed as well.

So then I figured one could add a processor to the pipeline. I added:

<processor type=TestApplication.RemoveButtons, TestApplication/>

Directly after:

<processor type=Sitecore.Pipelines.GetChromeData.GetFieldChromeData, Sitecore.Kernel/>

My RemoveButtons class looks like this:

public class RemoveButtons : GetChromeDataProcessor
{
    public override void Process(GetChromeDataArgs args)
    {
Assert.ArgumentNotNull(args, “args”);
Assert.IsNotNull(args.ChromeData, “Chrome Data”);

        if (“field”.Equals(args.ChromeType, StringComparison.OrdinalIgnoreCase))
{
Field argument = args.CustomData[“field”] asField;
Assert.ArgumentNotNull(argument, “CustomData[\”{0}\”]”.FormatWith(new object[] { “field” }));

            if (argument.Name == “Title” && MainUtil.GetBool(argument.Item[“NeedsToBeCheckedToPersonalize”], false))
            {
args.ChromeData.Commands.RemoveAll(delegate(WebEditButton b)
{
return b.Header == “Personalize”;
});
}
}
}
}

First I’m checking whether it’s a field or placeholder or rendering. After that, I instantiate the field from the CustomData. Then, if the current field is the Title field and the checkbox NeedsToBeCheckedToPersonalize (which is, just to be clear, a random checkbox I added) is ticked, remove all buttons that are Personalize.

The result for the Title field:

Image

And my subtitle field:

Image

The two fields are on the same item, and of the same type (both are single-line text).

Placeholder Settings

I was playing around with Placeholder Settings, and thought it would be a nice topic for my next blog post – especially since I wanted to customize it a bit.

Adding placeholder settings

Placeholder settings can be added in different ways. The placeholder settings items are saved in the /sitecore/layout/Placeholder Settings folder.

The ‘global’ placeholder settings need a placeholder settings item to be created, then assigned a placeholder key (which needs to match the placeholder key in the sublayout) and can be assigned its allowed controls.

Another way is to add placeholder settings on the Layout details. The placeholder settings item will end up in the Placeholder Settings folder, but will not have a Placeholder Key set in the item itself. It will be bound to a placeholder on the layout.

Now, I had an article page, on which people could leave their comments through a Comment widget. This widget is enabled on some articles, but Content Editors can also remove it if they don’t want this functionality. This means they’d also have to be able to add it back again if they’ve removed it and changed their minds later – but I only want it on the page once, since it doesn’t make too much sense to have the same widget on the page twice.

When looking in the Core database (We can see all things Page Editor in /sitecore/content/Applications/WebEdit/Ribbons/WebEdit), we can see which command gets triggered by clicking the ‘new component’ button:

Image

Using the Commands.config file, this translates to the following command:

<command name=webedit:addrendering” type=Sitecore.Shell.Applications.WebEdit.Commands.AddRendering,Sitecore.Client/>.

In the AddRendering class there’s a private static void defined called RunGetPlaceholderRenderingsPipeline, which kicks off a pipeline like this:

CorePipeline.Run(“getPlaceholderRenderings”, args);


If we then look in the web.config and see what the getPlaceholderRenderings pipeline does:

<getPlaceholderRenderings>

<processor type=Sitecore.Pipelines.GetPlaceholderRenderings.GetAllowedRenderings, Sitecore.Kernel/>
<processor type=Sitecore.Pipelines.GetPlaceholderRenderings.GetPredefinedRenderings, Sitecore.Kernel/>
<processor type=Sitecore.Pipelines.GetPlaceholderRenderings.RemoveNonEditableRenderings, Sitecore.Kernel/>
<processor type=Sitecore.Pipelines.GetPlaceholderRenderings.GetPlaceholderRenderingsDialogUrl, Sitecore.Kernel/>

</getPlaceholderRenderings>

The easiest solution would then be to add a processor to the pipeline.

A processor needs a Process method, and in our case will expect GetPlaceholderRenderingsArgs as well.

I figured I would start off simple and just make it so I can’t add any control to the placeholder if that control is already there.

Here’s the code I wrote for this:

publicvoid Process(GetPlaceholderRenderingsArgs args)
{
    Assert.IsNotNull(args, “args”);
    List<Item> placeholderRenderings = args.PlaceholderRenderings;

    if (placeholderRenderings != null)
    {
        placeholderRenderings.RemoveAll(x => args.LayoutDefinition.Contains(x.ID.ToString()));
    }
}

Relatively easy – go get PlaceholderRenderings, remove all renderings that are already in the LayoutDefinition. Note that this means we can now only have the control once per page as we are looking at the complete LayoutDefinition and not just the controls added to a specific placeholder.

Then I added my processor before

<processor type=Sitecore.Pipelines.GetPlaceholderRenderings.GetPlaceholderRenderingsDialogUrl, Sitecore.Kernel/> in the <getPlaceholderRenderings>node

in the <getPlaceholderRenderings> node:

<processor type=TestApplication.PlaceholderSettingsTest, TestApplication/>

I then set up my page. In my page I can add either Random Sublayout or Random Rendering.

Image

I added Random Rendering to the page, so I would expect Random Sublayout to be the only addable control.

So – Add component -> Add to here…
Image

Perfect!

Of course, there should be some modification, such as having the ability it have controls only once per placeholder rather than once per page like this example.