This blog has moved to http://jermdavis.wordpress.com/

Friday 21 February 2014

Improving your Sitecore IA with relative DataSource Locations

As someone famous** once said, with great power comes great responsibility – and the power of Sitecore’s component-based page model puts a lot of responsibility on us developers to create a structure for component data sources that makes sense to content editors. The two most common patterns I find myself using are that of having a “shared content” folder somewhere in the content tree which reusable DataSource items live in, and having items as children of the component’s page. When using the “shared content” folder you can easily set the DataSource Location field for your UI component to point to location where all the relevant data gets filed, but you can’t easily do that if you want to have your DataSource items as children of the page. So you tend to end up leaving the DataSource Location field blank to allow the user to pick the current page as the place to create the new item.

Experience shows that doesn’t work too well in practice. When you don’t control where the DataSource items get stored, they tend to end up getting spread around the content tree and making a bit of a mess of the IA. That lead to some thinking about how we might be able to improve on this situation – is it possible to force the DataSource Location to be relative to the current item?

Well, a bit of digging through the code and some experimentation says it’s not too hard to provide relative DataSource Locations. You can set up a UI component with a relative path for the DataSource Location, and then manually create a folder under your page:

One

This just works! When you click the “Set Associated Content” button, the tree view shows the right folder:

Two

But sadly this doesn’t work in the situation where the folder called “Items” doesn’t exist. In that case, Sitecore tries to deal with the error condition of “that folder doesn’t exist” by setting the root of the tree above to the root of the content tree…. Not so good…

We could deal with this by using a Branch Template – when you create your page that could automatically create the Items folder too. But the maintenance of that approach is a bit tedious because you probably end up with one Branch Template for every one of your Page Templates, and you have to remember to change all your Insert Options to match. What would be much better is if we could magically create the “Items” folder whenever it was needed.

After a bit of research, I discover that when Sitecore puts up the “Select the Associated Content” dialog box, in the background it runs the “getRenderingDatasource” pipeline in order to work out what to show in the tree view. So extending this pipeline should enable us to ensure that the location exists. We can create an extension class by providing a method called “Process” which accepts the correct arguments object:

namespace Testing
{
  public class CreateRelativeDataSourceFolder
  {
    public void Process(GetRenderingDatasourceArgs args)
    {
    }
  }
}

And then we can add it to the pipeline with a quick configuration patch:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <getRenderingDatasource>
        <processor patch:before="processor[@type='Sitecore.Pipelines.GetRenderingDatasource.GetDatasourceLocation, Sitecore.Kernel']"
            type="Testing.CreateRelativeDataSourceFolder, Testing"/>
        </getRenderingDatasource>
      </pipelines>
    </sitecore>
  </configuration>

The “patch:before” attribute here tells Sitecore to insert this new item at the start of the pipeline, before Sitecore attempts to load the appropriate item – thus giving us the chance to create it first if necessary.


The first thing we need to do is get the value of the DataSource Location field for the component we’re setting the data source for. The GetRenderingDatasourceArgs parameter that i passed in to our pipeline processor includes a reference to this data – the args.RenderingItem property gives us access to the Sitecore Item for the UI component. So with that item we can grab the value of the field that stores the DataSource Location. We can get the ID of this field from the Sitecore UI, and write a quick bit of code to get the value and check it’s valid.

public class CreateRelativeDataSourceFolder
{
  private static ID DataSourceLocationField = 
             new ID("{B5B27AF1-25EF-405C-87CE-369B3A004016}");
  private static string RelativePath = "./";

  public void Process(GetRenderingDatasourceArgs args)
  {
    string dataSourceLocation = args.RenderingItem
             .Fields[DataSourceLocationField].Value;

    if (string.IsNullOrWhiteSpace(dataSourceLocation))
    {
      return;
    }

    if (!dataSourceLocation.StartsWith(RelativePath))
    {
      return;
    }
  }
}

Once we’ve got the value of the field we check that it’s not empty and that it starts with a “./” relative path. If either of these isn’t true then this pipeline component has nothing to do and we can bail out and let the rest of the pipeline sort things out for us.


With that done, the next step is to work out what the full Sitecore path of our relative item would be, and then check if this item exists in the database. If it does exist then we have nothing to do – we can just return control to the rest of the pipeline.  But if the item doesn’t exist, we can create it. And that means adding a few more lines of code. To create an item you need to have a name for it and to have the Template ID for the sort of item to create. In this case, the name is just the path specified for the DataSourceLocation without the preceding “./” on the front. And the ID for the “Folder” template is easy to find from the Sitecore UI. So that extends our basic code to this:

public class CreateRelativeDataSourceFolder
{
  private static ID DataSourceLocationField = 
      new ID("{B5B27AF1-25EF-405C-87CE-369B3A004016}");
  private static ID FolderTemplateID = 
      new ID("{A87A00B1-E6DB-45AB-8B54-636FEC3B5523}");
  private static TemplateID FolderTemplate =  
      new TemplateID(FolderTemplateID);
  private static string RelativePath = "./";

  public void Process(GetRenderingDatasourceArgs args)
  {
    string dataSourceLocation = args.RenderingItem
      .Fields[DataSourceLocationField].Value;

    if (string.IsNullOrWhiteSpace(dataSourceLocation))
    {
      return;
    }

    if (!dataSourceLocation.StartsWith(RelativePath))
    {
      return;
    }

    if (string.IsNullOrWhiteSpace(args.ContextItemPath))
    {
      return;
    }  

    string subFolderPath = args.ContextItemPath +
       dataSourceLocation.Substring(1);

    if (args.ContentDatabase.GetItem(subFolderPath) != null)
    {
      return;
    }

    Item currentItem = args.ContentDatabase
      .GetItem(args.ContextItemPath);

    if (currentItem == null)
    {
      return;
    }

    string newItemName = dataSourceLocation.Substring(2);

    using (new SecurityDisabler())
    {
      currentItem.Add(newItemName, FolderTemplate);
    }
  }
}

Recompile that, give it a test and now the child folder will be automatically created if it does not exist – success!


** The internet isn’t entirely sure if that was Stan Lee writing Uncle Ben from Spiderman, or Voltaire. Pick whichever you prefer…

Monday 17 February 2014

Confirming Sitecore Commands

Be honest people: how many of you have wasted a few hours of your life by accidentally clicking “Revert Database” when you meant to click “Update Database” after a long evening at the development coal face? I certainly have. And if you’re like me and tend to serialise only the things you’ve changed in your project, then that means you generally end up with a dead instance of Sitecore.

Once is enough for this sort of mistake, and it lead me to thinking about how you can customise the Sitecore UI to prevent these issues in a generic way. What I decided to try was some code to allow you to configure a confirmation dialog before the action of any command on the ribbon. And it turns out it’s not too hard to do.
The starting point for anything triggered by clicking a button in the ribbon is a class which inherits from Sitecore.Shell.Framework.Commands.Command, and they require an Execute() method:

namespace Blog.Examples
{
    public class ConfirmCommand : Command
    {
        public override void Execute(CommandContext context)
        {
        }
    }
}

Whenever you want to make use of a class derived from Command in your ribbon configuration, you need to register it in the Sitecore configuration. And that means a quick configuration patch. Each entry in the commands collection in configuration needs two bits of data. The “type” attribute is a normal .Net type descriptor for the class you’ve implemented, and the “name” attribute is the textual name you will use later when you specify what gets called when a ribbon button is clicked. So we can use example:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="extensions:confirm" 
 type="Blog.Examples.ConfirmCommand,Blog.Examples" />
    </commands>
  </sitecore>
</configuration>

With that done, we need to make the command present some UI. Having done a bit of spelunking through the commands that ship with Sitecore, they share a common pattern where the command launches a pipeline in order to allow the UI to display UI and then process its results:

public class ConfirmCommand : Command
{
    public override void Execute(CommandContext context)
    {
        Sitecore.Context.ClientPage.Start(this, "Confirm", 
context.Parameters);
    }

    protected void Confirm(ClientPipelineArgs args)
    {
    }
}

The next thing to consider is how the code will find out what it is supposed to be confirming. Looking at some of the commands that ship with Sitecore, they get parameters passed from their item in the ribbon in the definition of their click event:

image

Examining the code for these commands shows that the parameters inside the brackets are parsed into the Parameters collection of the commands context object as a name/value collection. A quick test shows that this means you can just wrap the command you want to confirm as a parameter of the confirm command itself. So, thinking this through the confirm command will also need to know what text to put in the confirmation dialog, so we need two parameters:

extensions:confirm(title=revert the database,
         cmd=itemsync:loaddatabase(revert=1))

The “title” parameter optionally provides the text to append to the dialog message, and “cmd” provides the command to be executed. And the code to process this looks like this: (Error handling and validation omitted for clarity)

protected void Run(ClientPipelineArgs args)
{
    string msg;
    if (!string.IsNullOrWhiteSpace(args.Parameters["title"]))
    {
        msg = "Are you sure you want to " + 
args.Parameters["title"] + "?";
    }
    else
    {
        msg = "Are you sure you want to do that?";
    }

    SheerResponse.Confirm(msg);
    args.WaitForPostBack();
}

The call to args.WaitForPostBack() signals to the internals of Sitecore that this code wants to know the response from the dialog box that we’ve displayed. What happens as a result of that is when the user makes their click our code gets run again, passing in a new parameter to say what button got clicked. So we need to refactor this method slightly to detect that we’ve had a “postback” from the dialog box, and process it appropriately. That makes our Run() method look like this:

protected void Run(ClientPipelineArgs args)
{
    if (args.IsPostBack)
    {
        if (args.Result == "yes")
        {
            string cmd = args.Parameters["cmd"];
            Sitecore.Context.ClientPage.SendMessage(this, cmd);
            return;
        }

        if (args.Result == "no")
        {
            return;
        }
    }
    else
    {
        string msg;
        if (!string.IsNullOrWhiteSpace(args.Parameters["title"]))
        {
            msg = "Are you sure you want to " + 
args.Parameters["title"] + "?";
        }
        else
        {
            msg = "Are you sure you want to do that?";
        }

        SheerResponse.Confirm(msg);
        args.WaitForPostBack();
    }
}

Simple enough change. When the code is run for the first time, we display the dialog. But on the second pass through we check the args.Result value. If the user does not confirm the action then we just return and do nothing else. But if they confirm that they do want the command to run, we use the SendMessage() API call to tell Sitecore to start the original command.

So all that’s left to do is to configure a ribbon button with the confirm command and try it out. The buttons in the Content Editor ribbon are defined in the Core database, at:

/sitecore/content/Applications/Content Editor/Ribbons/Chunks

So you can find your required ribbon button in there and adjust its configuration by wrapping the existing command with the new confirm command. To avoid messing things up while I’m testing, I’ve created a new ribbon button that wraps up the Revert Item command for testing purposes:

image

And with that saved, the ribbon updates and you can test out the command:

image

Success – and we can now wrap any command in the ribbon with a confirmation.

Bootstrapping…

This blog is a challenge to myself to try and write something technical once a week for a year.
Writing Sitecore and website code for a living means I keep looking at things and thinking “that would be of interest to others” but up until now I’ve never been organised enough to get stuff written down and published. So as an experiment I’m going to find at least one thing a week to write about.
Part experiment to see if I can do it, and part exercise in personal development. And part reason to stop sitting in front of the telly in the evenings playing games.
So, place your bets Ladies and Gents – how many weeks will I manage?