Monday 16 May 2016

Submitting a page to workflow with associated datasources

I've recently inherited a project at a client where all content - both pages and datasource items - have been assigned a workflow, and the pages are comprised of numerous components with their own datasources.  The workflow assigned to the content is simple, but the issue the client was facing was that when they were using Experience Editor, they would go into a page and lock it for editing, then change some text - but the text was in the datasource of the component that had been included on the page! This meant when they clicked 'submit' to send their changes for approval, it would submit their page item, but not the datasource item that they had changed.  As authors who were new to Sitecore (some of whom were new to CMSs altogether) this was very confusing to them.

To be honest I was a little surprised that Sitecore doesn't automatically submit datasource and other content items changed through Experience Editor with the page item, and even more surprised that I couldn't find anything online on how others had tackled the issue.

Fortunately Sitecore makes this relatively painless to sort out through workflow actions.  Create a new /sitecore/templates/System/Workflow/Action item under your workflow's Submit command item.  In the "Type string" field enter the name of your custom class (I called mine SubmitRelated).  Our class will find all the datasource items included  in presentation details of the current item (page) being submitted, and if these items are locked by the same user it will assume they should be submitted as well, so will "submit" (change the workflow state) and unlock them.
Getting the datasources in the presentation details of an item can be found in a few blog posts, but the easiest I've found is Brent Svac's post.
public class SubmitRelated
{
  private const string AwaitingApproval = "{46DA5376-10DC-4B66-B464-AFDAA29DE84F}";

  /// <summary>
  /// Finds all related content locked by the current user and moves to approval state.
  /// </summary>
  /// <param name="args">Workflow arguments</param>
  public void Process(WorkflowPipelineArgs args)
  {
    Item workflowItem = args.DataItem;

    foreach (Item datasource in workflowItem.GetDataSourceItems().Where(i => i.Locking.IsLocked()))
    {
      string lockOwnerName = datasource.Locking.GetOwner();
      if (!string.IsNullOrEmpty(lockOwnerName) 
        && Security.Accounts.User.FromName(lockOwnerName, false) == Context.User)
      {
        using (new EditContext(datasource))
        {
          datasource[FieldIDs.WorkflowState] = AwaitingApproval;
          datasource.Locking.Unlock();
        }
      }
    }
  }
}

Edit 6 Sep 2016: It looks like Sitecore 8.2 brings some excellent functionality to the Experience Editor, including (amongst other things) a whole bunch of additions around managing related datasources! Excellent work Sitecore devs.

Wednesday 11 May 2016

Sitecore PXM and ODG project (part 5 – PXM InDesign custom task)

  1. Introduction
  2. Non-editable fields
  3. Architecture
  4. Customising ODG email
  5. PXM InDesign custom task

For the final post on this project I will run through how we can add some custom functionality to InDesign, as we have previously created a custom (non-editable) snippet which can be used in InDesign, however there is no way to create one except through Sitecore.

The plan is to create a custom PXM task that can be executed in InDesign, so that when a Page item is selected (in the project panel) and we execute our task, a Non-editable Snippet item will be created under that Page.

There are only a couple of posts that I've seen on how to create a custom task, however it's more than enough to go off for our scenario which is not terribly complex.  Those posts cover how to create a custom task, so follow along and set up your custom task.  As is mentioned in the posts, our task is passed a dictionary of various values from InDesign; in our case the only relevant one is the ID of the item selected in the project panel, which is ci_projectPanel.  Once we know that we can get the relevant item, check that it is a Page, and create a new item beneath it.  Simple :)

I've also created  a gist for the full class, in a more legible format.  This code is untested but should work as described.
public string ExecuteTask(Dictionary<string, object> dictionary)
{
  Item page = Sitecore.Context.Database.GetItem(new ID(dictionary["ci_projectPanel"].ToString()));
  if (page.TemplateID != new ID("{6BFA47BA-F73C-48DB-9170-C0CC94179EC7}"))
  {
    return "Please select a page under which to add the non-editable snippet";
  }

  page.Add("Non-Editable", new TemplateID(new ID("{C0FD5401-A2A5-4205-831D-DF06120B389E}")));
  return $"Created custom snippet under page {page.Name}.";
}

Wednesday 4 May 2016

Sitecore Custom Editor - Jump to Datasource

I recently worked with a client who was very new to Sitecore and CMSs in general, and was struggling to get their head around the concept of a component (with a datasource) being shown on the page, so that the content was not coming from the page item itself, but from the datasource content. To help them out I decided to look into whether I could add a custom tab which would show a preview of the page (yes, there is already a preview editor) but with the components with datasources highlighted (in a coloured border) and a clickable link to the datasource itself.

The new editor


First things first, we'll need a custom editor! There are a few blog entries already on how to do this, and it's very straightforward.  Now let's have a look at what we need to put in our new editor's aspx file.

As I mentioned, there's already a preview editor, but can we reuse or extend that?  Having a look at the item (in the core db) /sitecore/content/Applications/Content Editor/Editors/Layouts/Preview we can see the relevant page that is being shown is /sitecore/shell/~/xaml/Sitecore.Shell.Applications.ContentEditor.Editors.Preview.aspx which actually corresponds to the file sitecore\shell\Applications\Content Manager\Editors\Preview\Preview.xaml.xml which references the class Sitecore.Shell.Applications.ContentEditor.Editors.Preview.PreviewPage in the Sitecore.Client assembly.

Now I know how to read XML and XSLT, but I know I certainly prefer to write .NET, so the first thing I did was copy the necessary code over to C#.  I've created a gist for the CustomEditor.aspx and CustomEditor.aspx.cs files, which are based on the original PreviewPage, but not identical - I only included the code I deemed necessary, and on line 109 of the .cs file I have added a custom URL parameter so that we can identify that the page is being viewed in our custom editor: urlString["custom_editor"] = "true";

At this stage we can add our custom "editor" to the page and see a preview.

Highlighting the datasources


My plan to highlight the datasources and turn them into a link was to wrap the relevant renderings in a
<div class="rendering" data-datasource-id="{the-datasource-id}"> </div>

and then use javascript/CSS to highlight and make the region clickable (opening the datasource item in the content editor).

Fortunately wrapping a rendering is quite simple.  A quick Google search sent me to a page on ctor.io where I found exactly what was needed, except that the Title field in that example was changed to ID so that it can be used in the data- attribute of the HTML above.  We also only want to wrap the renderings in this way when we're in our custom editor, so we need to check the URL and ensure it contains our "custom_editor" parameter (above).
public class RendererWrapper : GetRendererProcessor
{
  public override void Process(GetRendererArgs args)
  {
    // Make sure the page is being viewed in the correct place
    if (Context.GetSiteName() == "shell") return;
    if (!Context.RawUrl.Contains("custom_editor")) return;
    if (!(args.Result is ViewRenderer)) return;

    if (args.Rendering == null || !ID.IsID(args.Rendering.DataSource)) return;

    Item dataSourceItem = args.PageContext.Database.GetItem(args.Rendering.DataSource);
    if (dataSourceItem == null) return;

    // Add wrapper rendering
    WrapperModel model = new WrapperModel
    {
      Renderer = (ViewRenderer)args.Result,
      Id = dataSourceItem.ID
    };

    args.Result = new ViewRenderer
    {
      Model = model,
      Rendering = args.Rendering,
      ViewPath = "/Views/Common/RenderingWrapper.cshtml"
    };
  }
}
public class WrapperModel
{
  public ViewRenderer Renderer { get; set; }
  public ID Id { get; set; }
}

and our RenderingWrapper.cshtml:
@model Sitecore.Common.Website.Enhancements.WrapperModel

<div class="rendering" data-datasource-id="@Model.Id">
    @Html.Partial(Model.Renderer.ViewPath, Model.Renderer.Model)
</div>

and of course we need to include the processor in the pipeline:
<pipelines>
  <mvc.getRenderer>
    <processor 
      patch:after="processor[@type='Sitecore.Mvc.Pipelines.Response.GetRenderer.GetViewRenderer, Sitecore.Mvc']"
      type="Sitecore.Common.Website.Enhancements.RendererWrapper, Sitecore.Common.Website"/>
  </mvc.getRenderer>
</pipelines>

at this point when we preview the page in our custom editor, our renderings which have datasources should be wrapped in our new div tag.  A little CSS for divs with the class "rendering" can quickly add a coloured border and turn the cursor in to a pointer.

Linking it together


The final step is to make the divs clickable - ie. opening the selected datasource in the Content Editor.  Digging around I found that the way, in Content Editor, to open an item using javascript is scForm.postEvent("", "", "item:load(id={item-id},language=en)"), where item-id is the ID of the item to open.  The main issue is that our page is being displayed in an IFrame, which is displayed in an IFrame (the custom editor tab) which is in the Content Editor frame; so the event needs to propagate from the page up 2 levels to the Content Editor where it should call the aforementioned postEvent() method.  Not the biggest issue in the world, we can use the javascript parent variable to access the parent frame.

So on our page we have the click handler:
$(function () {
  $('.rendering').click(function (e) {
    if (parent != null) {
      e.preventDefault();
      parent.renderingClicked($(this).data('datasourceId'));
    }
  });
});

which calls the handler in our custom editor .aspx (in the gist above)
function renderingClicked(datasourceId) {
  parent.scForm.postEvent("", "", "item:load(id=" + datasourceId + ",language=en)");
}

which tells the Content Editor to display our item!