Wednesday, August 3, 2016

Sitecore Speak - Hands-on and how-to expand the Experience Editor

Mild introduction


A while back, we hatched the idea to create a new Marketplace module based on our experience and the feedback of editors with regards to content management in sites that are both heavy on amount of hosted sites and the available languages.
All of that and much more detailed information with regards to the EasyLingo module can be found on Gert Gullentops blog here: http://ggullentops.blogspot.be/2016/08/easylingo.html

However, the first version only featured an integration into the Content Editor as a new bar in your editing pane of the selected item.
We decided to up our game a little and introduce our new functionality into the Experience Editor.

My first idea was that this would go rather smoothly, because, let's face it, all I had to do was look at how Sitecore does it now and find a way to display the same HTML we were already rendering into the Content Editor. So, how hard could it be?

Note that I have no (zero) experience on creating or using Speak components offered by or through Sitecore.

First steps and approach

My first, somewhat naive idea was to go into doc.sitecore and find the tutorial, how-to or whatever that would guide me into realizing my goal. Turns out that the doc.sitecore site is either really good at hiding that kind of information or (as it might turn out), my searching skills are well below par.

My next idea was to go into stackexchange or the community and look for someone who had had the same challenge before him/her and learn from that. Unfortunately, the same conclusion as mentioned above applies here.

I then decided to dive head first into the Sitecore Core DB, all the relevant DLL's and the config files.
This last approach was the one that got me there in the end, but not without some pitfalls and concerns on how I had to approach this issue. As well as some support from, well..., Sitecore support. (Thanks Andrey Krupskiy)

Inspecting Sitecore Speak as-is

I decided to have the added functionality displayed in a control bar. Much in the same way as the Navigation Bar is shown as a control in the Experience Editor. So, in theory, if I would look into the core and find out how this one is configured I should be able to derive what to do next.
And that is where I got stuck the first time. Turns out that Sitecore still supports Sheer UI next to Speak, but Speak is enabled by default. This means that everything you find in the core definitions on the ribbons are configured to handle both scenarios...
And that is where my confusion set in... I noticed that on the Core configuration item for the navigation bar, there was a command defined onclick : "webedit:toggletreecrumb".
However, everything this command referred to based itself on Sheer UI. And all the HTML that I was inspecting in the frontend of my Sitecore dummy project was rendering Speak controls.

So, just to be clear: Everything worked out in the end. And with the information below I hope to provide ample insight in how you could go ahead and (ab)use the Speak functionalities to get your own speak interface up and running.

Step by Step guide

I've tried to walk through this step by step without makeing too many awkward jumps. I'll try to summarive at the bottom to give insight on which items/files/configs are required to go forward.

Showing the toggle button on the View chunk of your ribbon

First things first; we needed this bar to be toggled on and off. Exactly like any of the other (optional) bars. To do this:

  1. Navigate to the Core database
  2. Navigate into: /sitecore/content/Applications/WebEdit/Ribbons/WebEdit/View/Show
  3. Create (or copy) a new "Small Check Button" 
  4. Ignore the Click property, but do fill in the Header, Tooltip & ID value (smells like best practice doesn't it)
  5. Modify the Rendering (raw values) to adhere to the code block below:

<r>
<d id="{FE5D7FDF-89C0-4D99-9AA3-B5FBD009C9F3}">
 <r 
  id="{BDE651C9-7988-4950-8E01-EA80106563A2}" 
  par="
   Click=trigger%3abutton%3acheck&amp;
   Command=YOUR-JS-COMMAND&amp;
   RegistryKey=%2fCurrent_User%2fPage+Editor%2fShow%2fYOUR-BARNAME&amp;
   PageCodeScriptFileName=%2fsitecore%2fshell%2fclient%2fSitecore%2fExperienceEditor%2fCommands%2fYOUR-JS-FILENAME.js&amp;
   PostponedCall=0" 
  uid="{7996FBF1-10B4-457F-B2CD-E120559F08DC}" 
 />
</d>
</r>

Not on the above, obviously you can choose whichever path you see fit for the JS file as well as the registry key you choose to work with.

Making the toggle button actually work

The above made sure you got a checkbox that will be able to toggle your bar in and out of the experience editor. But it is time to hook up that JS file and show some first bar to render.

Let's start with creating the JS file we just referred to from our toggle button:

define(["sitecore", "/-/speak/v1/ExperienceEditor/ExperienceEditor.js"], function (Sitecore, ExperienceEditor) {
  Sitecore.Commands.YOUR-JS-COMMAND =
  {
    canExecute: function (context) {
      context.app.YOURRIBBONBAR.set("isVisible", context.button.get("isChecked") == "1");
      context.app.setHeight();
      return true;
    },
    execute: function (context) {
      ExperienceEditor.PipelinesUtil.generateRequestProcessor("ExperienceEditor.ToggleRegistryKey.Toggle", function (response) {
        response.context.button.set("isChecked", response.responseValue.value ? "1" : "0");
        response.context.app.YOURRIBBONBAR.set("isVisible", response.responseValue.value);
        response.context.app.setHeight();
      }, { value: context.button.get("registryKey") }).execute(context);
    }
  };
});


The above JS can now be executed successfully but doesn't have any clue on what to toggle visible as we have not yet defined the YOURRIBBONBAR anywhere.. Time to head back down into the Core db of Sitecore.

  1. Navigate to the Core database
  2. Navigate into: /sitecore/client/Applications/ExperienceEditor/Common/Layouts/Renderings/Ribbon/
  3. Create a new Folder: YOURBAR
  4. Create a new "View Rendering" in the newly created folder.
  5. Fill in the Path field with a reference into your cshtml file, which forexample could be here:
    /sitecore/shell/client/Sitecore/Speak/Ribbon/Controls/YOURBAR/YOURBAR.cshtml
The cshtml file is simply the place where you register your control:

@using Sitecore.Mvc
@using YOURNAMESPACE
@model Sitecore.Mvc.Presentation.RenderingModel
@Html.Sitecore().Controls().YOURBARCONTROL(Model.Rendering)

Now, in order for this control to be rendered, you need to create the control extension class:

using System.Web;
using Sitecore.Diagnostics;
using Sitecore.Mvc;
using Sitecore.Mvc.Presentation;

namespace YOURNAMESPACE
{
    public static class ControlsExtension
    {
        public static HtmlString YOURBARCONTROLEXTENSION(this Controls controls, Rendering rendering)
        {
            Assert.ArgumentNotNull(controls, "controls");
            Assert.ArgumentNotNull(rendering, "rendering");
            return new HtmlString(new YOURBARCONTROL(controls.GetParametersResolver(rendering)).Render());
        }
    }
}

And in turn, this control extension class requires a (duh) control to render:

using System.Collections.Generic;
using System.Web.UI;
using Sitecore.Diagnostics;
using Sitecore.ExperienceEditor.Speak.Caches;
using Sitecore.ExperienceEditor.Speak.Ribbon;
using Sitecore.Globalization;
using Sitecore.Mvc.Presentation;
using Sitecore.Web;
using Sitecore.Web.UI.Controls;

namespace YOURBARNAMESPACE
{
    public class YOURBARCONTROL : RibbonComponentControlBase
    {
        public YOURBARCONTROL()
        {
            InitializeControl();
        }

        public YOURBARCONTROL(RenderingParametersResolver parametersResolver)
            : base(parametersResolver)
        {
            Assert.ArgumentNotNull(parametersResolver, "parametersResolver");
            InitializeControl();
        }

        protected virtual IList<ComponentBase> Controls { get; set; }

        protected void InitializeControl()
        {
            Class = "sc-YOURBARCSS";
            DataBind = "visible: isVisible";
            ResourcesCache.RequireJs(this, "ribbon", "YOURBAR.js");
            ResourcesCache.RequireCss(this, "ribbon", "YOURBAR.css");
            HasNestedComponents = true;
            Controls = new List<ComponentBase>();
        }

        protected override void PreRender()
        {
            base.PreRender();
            Attributes["data-sc-itemid"] = RibbonDatabase.GetItem(WebUtil.GetQueryString("itemid")).ID.ToString();
            Attributes["data-sc-dic-go"] = Translate.Text("Go");
            Attributes["data-sc-dic-edit"] = Translate.Text("Edit");
            Attributes["data-sc-dic-edit-tooltip"] = Translate.Text("SOME TOOLTIP.");
            Attributes["data-sc-dic-treeview-tooltip"] = Translate.Text("SOME VIEW TOOLTIP");
        }

        protected override void Render(HtmlTextWriter output)
        {
            base.Render(output);
            AddAttributes(output);
            output.AddAttribute(HtmlTextWriterAttribute.Class, Class);
            output.AddAttribute(HtmlTextWriterAttribute.Id, "YOURBARCONTENT" + Attributes["data-sc-itemid"]);
            output.RenderBeginTag("nav");
            output.RenderBeginTag(HtmlTextWriterTag.Div);
            output.AddAttribute(HtmlTextWriterAttribute.Style, "display=none");
            output.RenderEndTag();
            output.RenderEndTag();
        }
    }
}

As you can see. this control in itself hardly does anything at all. It just renders some Nav tag and applies a display=none to a div the is included. But the magic start to happen here when it, in the InitializeControl method registers your final (phew, are we almost there?) JS and CSS files that will handle the actual bar content generation.

So.... That was easy wasn't it? All we had to do is register some things, make some classes and bam, we have...
well nothing really at this point...

We still need to define what is to come in our JS file and make sure we are able to call our backend code. And remember that View Rendering we created? We still need to make sure that gets used somewhere from Sitecore.

Bringing all the final block together

Let us start of with doing the last modification we need to do in the Core database to make sure that our ViewRendering is displayed correctly.

  1. Navigate into the Core database
  2. Navigate into this item: /sitecore/client/Applications/ExperienceEditor/Ribbon
  3. Go into the Renderings field of this item
  4. Add a reference to your ViewRendering ID

<r id="VIEWRENDERINGGUID HERE"
 par="Id=YOURRIBBONBAR&amp;IsVisible=0" 
 ph="PageEditBar.Content" 
 uid="{45348FD9-3458-4284-B0E1-18153E3516B5}" />

The 'ph' (placeholder) can remain the same, just make sure to refer to the right ViewRendering and create a unique uid (untested what happens if it is not unique, but lets play safe)...

And finally only a few steps remain. The sample of YOURBAR.JS and how to call your own business logic through Speak calls.

YOURBAR.JS as stored under ...sitecore\shell\client\Sitecore\Speak\Ribbon\Controls\YOURBAR:

define(
  [
    "sitecore",
    "/-/speak/v1/ExperienceEditor/RibbonPageCode.js",
    "/-/speak/v1/ExperienceEditor/ExperienceEditor.js"
  ],
function (Sitecore, RibbonPageCode, ExperienceEditor) {
    Sitecore.Factories.createBaseComponent({
        name: "YOURRIBBONBAR",
        base: "ControlBase",
        selector: ".sc-YOURBAR",
        attributes: [
        ],

        initialize: function () {
            document.RIBBONBARCONTEXT = this;
            window.parent.document.RIBBONBARCONTEXT = this;
            var mode = ExperienceEditor.Web.getUrlQueryStringValue("mode");
            this.model.on("change:isVisible", this.renderRIBBONBAR, this);
            ExperienceEditor.Common.registerDocumentStyles(["/-/speak/v1/ribbon/YOURBAR.css"], window.parent.document);
        },

        renderRIBBONBAR: function (itemId) {
            if (!itemId
              || typeof (itemId) == "object") {
                itemId = this.$el[0].attributes["data-sc-itemid"].value;
            }

            //Do some business logic
            this.requestBusinesslogic(itemId, this);

            //Build HTML
            var htmlSource = "<div class=\"sc-YOURBAR\">";            
            ... Do whatever here ...            
            htmlSource += "</div>";

            //Assign HTML to bar div
            var barContent = ExperienceEditor.ribbonDocument().getElementById("YOURBARCONTENT" + this.$el[0].attributes["data-sc-itemid"].value);
            barContent.innerHTML = htmlSource;
        },

        requestBusinesslogic: function (itemId, appContext) {
            var context = ExperienceEditor.generateDefaultContext();
            context.currentContext.itemId = itemId;
            ExperienceEditor.PipelinesUtil.generateRequestProcessor("YOURSPEAKCOMMAND", function (response) {
                appContext.SOMERESPONSEVALUEVARIABLE = response.responseValue.value;
            }).execute(context);
        },
    });
});

I have purposely not included the CSS file to which I refer... This depends on a case by case scenario so it has no added value in placing it here. You surely spotted all the css class references throughout the code by now, so that should be clear in itself.
Css files are stored under: .\sitecore\shell\client\Sitecore\Speak\Ribbon\Assets\Generated

So, that kind of concludes the various steps needed to get this additional bar rendered. One last (somewhat optional) step remains and that is the part where we actually call the backend through a Speak command

Speak Commands from JS

The block below is the JS part that actually uses the context to call the Speak command through a given context that can be set up with a variety of parameters.

requestBusinesslogic: function (itemId, appContext) {
    var context = ExperienceEditor.generateDefaultContext();
    context.currentContext.itemId = itemId;
    ExperienceEditor.PipelinesUtil.generateRequestProcessor("YOURSPEAKCOMMAND", function (response) {
 appContext.SOMERESPONSEVALUEVARIABLE = response.responseValue.value;
    }).execute(context);

This "YOURSPEAKCOMMAND" needs to be registered into the Sitecore speak config and this is done here:

<sitecore.experienceeditor.speak.requests>
      <request name="YOURSPEAKCOMMAND" type="YOURNAMESPACE.SPEAKCOMMANDCLASS, YOURDLL" />
</sitecore.experienceeditor.speak.requests>

And the commandclass definition:

using Sitecore.ExperienceEditor.Speak.Server.Contexts;
using Sitecore.ExperienceEditor.Speak.Server.Requests;
using Sitecore.ExperienceEditor.Speak.Server.Responses;
using Sitecore.Globalization;

namespace YOURNAMESPACE
{
    public class SPEAKCOMMANDCLASS : PipelineProcessorRequest<ItemContext>
    {
        public override PipelineProcessorResponseValue ProcessRequest()
        {
            if (RequestContext.Item == null)
            {
                return new PipelineProcessorResponseValue { AbortMessage = Translate.Text("The target item could not be found.")};
            }
            var stuff = SomeClass.DoSomething(RequestContext.Item);
            return new PipelineProcessorResponseValue { Value = stuff };
        }
    }
}

Do note that Sitecore ships with a whole lot of available SpeakCommands that you are free to call. This offered set is already very rich and diverse so check it out first before you start to make your own specific command. Although, in a lot of cases, this will prove to be the only way.

Summary

I honestly hope the above helped you either create your own bar of extra piece of Experience Editor Speak based component or has at least tought you on the different steps that may be needed and the skills required to develop this.
It was a much rougher patch then I initially expected but it worked out in the end and I have to say that I like the Speak calls especially. There is something clean about doing those very distinct, single-purpose calls into your backend that I liked.

But the list of files that were required to be changed or created is quite lengthy:
  • ShowXBar.js
  • XBar.js + XBar.css (or at least in most cases)
  • XBar.cshtml
  • ControlsExtension to register your XBarControl
  • XBar.cs
  • ShowXBar Sitecore item checkbox control
  • XBar Viewrendering control
  • Ribbon item to visualize the XBar

And why did I start calling everything XBar here? I'm not really sure, just call me chaotic.
Kind regards and thanks for the read.

No comments:

Post a Comment