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
[su_note note_color=”#fafafa”]There’s a lot of moving parts and a good mental model helps.[/su_note]
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. ALarge 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.
On the wire:
{"error":true,"errorMessage":"An error ocurred.","message":null}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); } }[su_note note_color=”#fafafa”]What you can do is wrap your implementation of the
ProcessRequest()
withtry {} catch {}
to make sure you leave a stack trace behind before you re-throw.[/su_note]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:... &amp;amp;lt;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"&amp;amp;gt; &amp;amp;lt;/script&amp;amp;gt; ...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.
[su_note note_color=”#fafafa”]The
executePipeline()
dynamically loads the required scripts and their source path in the inspector isn’t matching your file system path[/su_note]You will find them at:
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: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.
[su_note note_color=”#fafafa”]If you do give it a JavaScript file-behind then you need to make sure your script produces a correct no-op processor.[/su_note]
An empty file, for example, will break it:
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
isundefined
.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 theT
you parameterize thePipelineProcessorRequest<T>
with (more in the next section). It’s a JSON-annotated object that thecontext
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 avalue
andTargetItemContext
acceptstargetItemId
.[su_note note_color=”#fafafa”]You can build your own
ItemContext
if you need to pass more than a single argument (for that you haveargument
on theItemContext
) or would like to load your arguments with semantic value[/su_note]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 andFieldEditorCommand
.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&amp;amp;lt;ItemContext&amp;amp;gt; { 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 notvirtual
so you can’t mock it and it’sprivate 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&amp;amp;lt;ItemContext&amp;amp;gt; { 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
andvirtual
. Helps testability and extensibility at the same time so everybody wins.[su_divider][/su_divider]
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!
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 🙂
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.
Pingback: Cloning presentation details and associated datasources with Sitecore SXA | Adrian Liew