Experience Editor: Commands, Pipelines, Requests

The SCORE team is busy working on v1.5 and the main theme for this release is Sitecore 8. Last week I was enabling (SPEAK-ifying) our Page Editor ribbon extensions and this post is a collection of thoughts and tips that I gathered along the way.

Moving Parts

There’s a lot of moving parts and a good mental model helps.

Your typical Experience Editor button (let’s look at “Move”, for example) has the following:

  • An item in the core database that represents a button. A Large Button, for example.
  • A command JavaScript file that can tell the ribbon whether it should be enabled and what to do when the button is clicked
  • A rendering to attach the command. No need to write a custom rendering (though it would be cool if we had to) but you do need to drop it onto your button’s presentation details. It’s self-service in Sitecore 8. The buttons render themselves.
  • A pipeline item* in the core database that represents a sequence of actions to be performed when the button is clicked (if you decide to implement is as a pipeline. I will get back to this in one of my next posts).
  • Another rendering to associate the pipeline with the button
  • An item for each pipeline processor
  • A JavaScript file for each pipeline processor. If your pipeline steps can’t do their duty without talking to the server (which is likely) you will create a request class to back each one up. To re-iterate: your pipeline processor item has a backing JavaScript file that has a backing C# class.
  • A config patch to connect the dots between a request name and request implementation for each request class (probably as many as many processors there are in your pipeline)

* – an alternative way to encapsulate your logic and make it reusable is a custom require.js module. I will get back to it in my next posts.

Reference: A new look to buttons in Experience Editor

Server Side Errors

The PipelineProcessorRequest<T> will unfortunately hide server-side errors.

In the browser:
error

On the wire:

{"error":true,"errorMessage":"An error ocurred.","message":null}

In the logs:
log

The reason is quite simple:

public override Response Process(RequestArgs requestArgs)
{
    try
    {
        // ...
        processorResponse.ResponseValue = this.ProcessRequest(); // your request implements this one
       // ...
    }
    catch (Exception ex)
    {
        // Can I have my stack trace please? 
        return this.GenerateExceptionResponse(Translate.Text("An error ocurred."), ex.Message);
    }
}
What you can do is wrap your implementation of the ProcessRequest() with try {} catch {} to make sure you leave a stack trace behind before you re-throw.

Client Side Errors

When the SPEAK ribbon renders, it loads the main scripts plus all the commands. The individual renderings responsible for parts of the ribbon do that. I will get back to it. If you inspect the ribbon’s iframe you will see something like this:

...
<script type="text/javascript" charset="utf-8" async 
        data-requirecontext="_" 
        data-requiremodule="/sitecore/shell/client/Sitecore/ExperienceEditor/Commands/ScoreEditMetaData.js"
        src="/sitecore/shell/client/Sitecore/ExperienceEditor/Commands/ScoreEditMetaData.js">
</script>
...

If any of your commands needs debugging you will easily find it in the inspector. The client-side pipelines, however, are not pre-loaded like commands are. Your command is probably doing something along the lines of (taken from Sitecore’s Delete.js command ):

execute: function (context) {
  context.app.disableButtonClickEvents();
  
  Sitecore.ExperienceEditor.PipelinesUtil.executePipeline(context.app.DeleteItemPipeline, function () {
    Sitecore.ExperienceEditor.PipelinesUtil.executeProcessors(Sitecore.Pipelines.DeleteItem, context);
  });

  context.app.enableButtonClickEvents();
}

I will sure write about the client-side SPEAK pipelines and the way Experience Editor works with them in the upcoming posts but let’s see how you can troubleshoot your processors.

The executePipeline() dynamically loads the required scripts and their source path in the inspector isn’t matching your file system path

You will find them at:

blog-pipelines-debug

Happy debugging!

Empty Pipeline Processor

When building your SPEAK pipelines for your Experience Editor commands you may decide to create the initial structure and then build the processors one by one. Like this one I did for a Duplicate button:

blog-pipeline-core

If you don’t give your processor a JavaScript file-behind it will be skipped. Won’t be loaded. Your four-processors pipelines will be a three-processors pipeline.

If you do give it a JavaScript file-behind then you need to make sure your script produces a correct no-op processor.

An empty file, for example, will break it:

blog-pipeline-empty-processor

To understand why, we need to look at how Experience Editor loads SPEAK pipelines. It’s good to know anyway. When the pipeline is loaded here’s what the server sends to the browser for the four-processors pipeline that has an empty JavaScript in one of the processors (simplified):

define("processor1.js", ["sitecore"], function (Sitecore) {
  // ...
});

define("processor2.js", ["sitecore"], function (Sitecore) {
  // ...
});

define("processor3.js", ["sitecore"], function (Sitecore) {
  // ...
});

define(["sitecore","processor1.js","processor2.js","processor3.js","processor4.js"], 
  function(Sitecore, p0, p1, p2, p3) {
    var pipelines = Sitecore.Pipelines;
    var pipeline = pipelines["MyPipeline"];
   
    if (pipeline == null) { 
      pipeline = new pipelines.Pipeline("MyPipeline"); 
    }

    p0.priority = 1;
    p0.name = "processor1";
    pipeline.add(p0);

    p1.priority = 2;
    p1.name = "processor2";
    pipeline.add(p1);

    p2.priority = 3;
    p2.name = "processor3";
    pipeline.add(p2);

    p3.priority = 4;
    p3.name = "processor4";
    pipeline.add(p3);

    pipelines.add(pipeline);
  }
);

You see, right? There’s a special require.js module that will wire up the processors into a pipeline. This script is a little bit optimistic and it will break if p3 is undefined.

Here’s how your empty processor should look like to be a no-op that can be wired into the pipeline and even executed when it’s called:

define(["sitecore"], function (Sitecore) {
    return {
        execute: function (context) {
            // no-op
        }
    };
});

Custom Context

Your server-side requests receive data from the client side via ItemContext. That’s the T you parameterize the PipelineProcessorRequest<T> with (more in the next section). It’s a JSON-annotated object that the context from the client side is mapped to:

public class ItemContext : Context
{
    [JsonIgnore]
    private Item item;

    [JsonProperty("itemId")]
    public string ItemId { get; set; }

    [JsonProperty("language")]
    public string Language { get; set; }

    [JsonProperty("deviceId")]
    public string DeviceId { get; set; }
   
    // ...
}

There’s a few variations that accept extra parameters. ValueItemContext, for example, accepts a value and TargetItemContext accepts targetItemId.

You can build your own ItemContext if you need to pass more than a single argument (for that you have argument on the ItemContext) or would like to load your arguments with semantic value

Your context:

public class FieldEditorItemContext : ItemContext
{
    [JsonProperty("fields")]
    public string Fields { get; set; }

    [JsonProperty("fieldsSections")]
    public string FieldsSections { get; set; }

    [JsonProperty("saveItem")]
    public string SaveItem { get; set; }

    // ...
}

And then in the client side:

context.currentContext.saveItem = args.saveItem;
context.currentContext.fieldsSections = args.sections;
context.currentContext.fields = args.fields;

Sitecore.ExperienceEditor.PipelinesUtil.generateRequestProcessor(
    "ExperienceEditor.Score.GenerateFieldEditorUrl", 
    function(response) {  }).execute(context);

Don’t worry. I will show you the Field Editor in one of my next posts. If you need it now here’s a good tutorial from Thomas Stern. Thomas is using SaveItem = true which makes Field Editor write back to the item. I will show you how to keep it all in Experience Editor until the user explicitly saves the page. That’s how we used to have it with Sheer and FieldEditorCommand.

Testability

Last but not least. Those server-side requests will extend PipelineProcessorRequest<T>. Here’s how you might write a request to check whether a current item can be duplicated by the current user:

public class CanDuplicateRequest : PipelineProcessorRequest<ItemContext>
{
    public override PipelineProcessorResponseValue ProcessRequest()
    {
        this.RequestContext.ValidateContextItem();
            
        var target = this.RequestContext.Item.Parent;

        return new PipelineProcessorResponseValue()
        {
            Value = target.Access.CanCreate()
        };
    }
}

Let’s unit test it with Sitecore.FakeDb. The only thing is … the RequestContext property is not virtual so you can’t mock it and it’s private set so you can’t stub it out either. You have two options:

Integration Style Test

[TestCase(false, false)]
[TestCase(true, true)]
public void ShouldCheckRightsToDuplicate(bool canCreate, bool allowed)
{
    //arrange
    var home = new DbItem("home") {Access = {CanCreate = canCreate}};
    home.Add(new DbItem("child"));

    using (var db = new Db { home    })
    {
        Item source = db.GetItem("/sitecore/content/home/child");

        var request = new CanDuplicateRequest();

        const string json = @"
            {{
                ""database"": ""master"",
                ""itemId"": ""{0}"",
                ""language"": ""en"",
                ""version"": 1,
            }}";

        var args = new RequestArgs("Doesnotmatter", new NameValueCollection(), string.Format(json, source.ID));

        // act
        var response = request.Process(args) as PipelineProcessorResponse;

        // assert
        response.ResponseValue.Value.Should().Be(allowed);
    }
}

Code Around It

public class CanDuplicateRequest : PipelineProcessorRequest<ItemContext>
{
    public override PipelineProcessorResponseValue ProcessRequest()
    {
        this.RequestContext.ValidateContextItem();
            
        return DoProcessRequest(RequestContext.Item);
    }

    public virtual PipelineProcessorResponseValue DoProcessRequest(Item item)
    {
        var target = item.Parent;

        return new PipelineProcessorResponseValue()
        {
            Value = target.Access.CanCreate()
        };
    }
}

You can now test your logic without having to stage everything for the outer method.

Recent versions of Sitecore are doing a much better job on the testability front but I still see a lot of room for improvement. We need more smaller methods that are public and virtual. Helps testability and extensibility at the same time so everybody wins.

There’s a lot I wanted to write about but then it would no longer be a blog post. I made a few promises and I plan on keeping them. The next post will probably be on SPEAK pipelines and require.js modules. Or maybe the Field Editor in Experience Editor. Or maybe the last part in my Dynamic Product Details Pages series. Or maybe something else. My blogging To-Do stack has overflowed long time ago.

Stay tuned!

Pavel Veller

2 comments on Experience Editor: Commands, Pipelines, Requests

Anders LaubMarch 15, 2015 - Reply

Really nice post! Sitecore should have included the exception stacktrace in the json response for us to see.

You can read more about the pipeline concept origins here http://laubplusco.net/making-sense-speak-pipelines/

I’ve been wanting to write a post about how it is used in the experience editor for some time now, but haven’t had the time. There is too much to write about 🙂

Pavel VellerMarch 16, 2015 - Reply

Yea, you’re right, there’s a lot indeed! I read your post on SPEAK pipelines and was going to (and sure will) refer to it when I get to write some more about them.

Add a Comment

Your email address will not be published. Required fields are marked *

Or request call back