Thursday 15 December 2016

Microsoft Azure certification (70-532 and 70-533)

Last month I managed to sneak in my 70-532 and 70-533 exams before the new content came in.  For those of you who are keen to have a crack, I would highly recommend the Pluralsight courses by Tim Warner which were absolutely great, and I could just watch  on the train on the way to/from work.  I'm not sure whether he's going to update them with the new content, but I would assume he will, as he seems to do quite well at keeping up to date despite the rapidly updating Azure environment.  I would also advise purchasing the practice tests to get a feel for the exam structure, and ensure you have the right level of knowledge (though the practice tests certainly don't cover 100% of the content, and some of it is a bit out of date).

Early next year I'm looking forward to having a go at the 70-534 exam and achieving my end goal of becoming a certified Microsoft Azure Architect!  With the release of Sitecore 8.2 update 1 and the ability to run Sitecore as an Azure web app, I'm really looking forward to using the existing ARM templates and writing some of my own to hook into the power (and cost saving) of Azure.  Between ARM templates, staging slots, auto-scaling, and the ease of the portal and powershell, there are just so many reasons to use Azure web apps! Many of our clients already host their Sitecore instances on Azure, and this will hopefully be a great reason for everyone to upgrade and migrate to the PaaS offering.  The latest release of Sitecore+Azure has been blogged to death, but stay tuned for some of my fresh tales from the cloud.

All the best until next year!

Tuesday 8 November 2016

Unit testing GlassController methods and Sitecore rendering parameters

I finally had some time to work on getting some unit tests set up for the project I'm working on, which is using Glass Mapper (handy in pre-Habitat projects).  I'd done some testing using Glass Mapper before (using FakeDb, which is awesome), but wanted to take things in a Habitat-style direction (xUnit, NSubstitute) and also hadn't done anything around testing rendering parameters.

Yes, you can stub ISitecoreContext and tell it what to return when your controllers use it (to grab datasources and items), but I wanted to really streamline as much as possible and not change any of my controller's uses of GetDatasourceItem<IMyInterface>() and GetRenderingParameters<IMyInterface>() - after all why not make use of GlassController and these methods if you're using Glass?

Glass gives us a nice way to stub datasource and rendering strings out through the RenderingContextWrapper property of GlassController (which is an IRenderingContext).  By creating a mock class we can pass through any datasource and rendering parameters (as a query string).

public class MockRenderingContext : IRenderingContext
{
    private readonly string _datasourceId;

    private readonly string _renderingParams;

    public MockRenderingContext(string datasourceId, string renderingParams)
    {
        _datasourceId = datasourceId;
        _renderingParams = renderingParams;
    }

    public string GetRenderingParameters()
    {
        return _renderingParams;
    }

    public string GetDataSource()
    {
        return _datasourceId;
    }

    public bool HasDataSource => !string.IsNullOrEmpty(_datasourceId);
}

We can assign this easily to our controller (which inherits from GlassController) in our test class:

ControllerToTest controller = new ControllerToTest(sitecoreContext)
{
    RenderingContextWrapper = new MockRenderingContext("/sitecore/content/mydatasource", "RenderingParam=1234")
};

All good so far.  This lets us grab the datasource, as it uses the SitecoreContext.GetItem<T>(datasource) to grab the relevant item.  Not so for the rendering parameters, unfortunately...  This calls GlassHtml.GetRenderingParameters<T>(renderingParametersString)!

So, what's the problem? I hear you ask.

Well, digging into what happens next (in GlassHtml): the rendering parameters string is split into KeyValuePairs, the template ID is retrieved from the Glass cache, and it ends up at:

public T GetRenderingParameters<T>(NameValueCollection parameters, ID renderParametersTemplateId) where T : class
{
    var item = Utilities.CreateFakeItem(null, renderParametersTemplateId, SitecoreContext.Database, "renderingParameters");

    using (new SecurityDisabler())
    {
        using (new EventDisabler())
        {
            using (new VersionCountDisabler())
            {
                item.Editing.BeginEdit();

                foreach (var key in parameters.AllKeys)
                {
                    item[key] = parameters[key];
                }

                T obj = SitecoreContext.Cast(item);

                item.Editing.EndEdit();
                item.Delete(); //added for clean up
                return obj;
            }
        }
    }
}


Now apparently because we're using FakeDb, the item that is dynamically built (to be cast into our parameters model) is not populated correctly!  So, let's override the appropriate method and do it the FakeDb way:

public class FakeGlassHtml : GlassHtml
{
    private readonly Db _db;

    public FakeGlassHtml(ISitecoreContext context, Db db) : base(context)
    {
        _db = db;
    }

    /// <summary>
    /// Method overridden to call our new method (below)
    /// </summary>
    /// <typeparam name="T">Type of rendering parameters to create</typeparam>
    /// <param name="parameters">Name/value collection of parameters</param>
    /// <returns>Populated parameters object</returns>
    public override T GetRenderingParameters<T>(NameValueCollection parameters)
    {
        if (parameters == null)
        {
            return default(T);
        }

        var config = SitecoreContext.GlassContext[typeof(T)] as SitecoreTypeConfiguration;

        if (config == null)
        {
            SitecoreContext.GlassContext.Load(new OnDemandLoader<SitecoreTypeConfiguration>(typeof(T)));
        }

        config = SitecoreContext.GlassContext[typeof(T)] as SitecoreTypeConfiguration;

        return GetRenderingParameters<T>(parameters, config?.TemplateId);
    }

    /// <summary>
    /// New method using FakeDB to create rendering parameters in the same way as the original Glass Mapper code
    /// </summary>
    /// <typeparam name="T">Type of rendering parameters to create</typeparam>
    /// <param name="parameters">Name/value collection of the parameters</param>
    /// <param name="renderParametersTemplateId">Rendering parameters template ID</param>
    /// <returns>Populated parameters object</returns>
    public new T GetRenderingParameters<T>(NameValueCollection parameters, ID renderParametersTemplateId) where T : class
    {
        if (renderParametersTemplateId.IsNull)
        {
            return default(T);
        }

        ID newId = ID.NewID;
        DbItem tempItem = new DbItem("temp", newId, renderParametersTemplateId);
        
        foreach (string key in parameters.AllKeys)
        {
            tempItem.Fields.Add(new DbField(key) { Value = parameters[key] });
        }

        _db.Add(tempItem);
        
        Item item = _db.GetItem(newId);

        T obj = SitecoreContext.Cast<T>(item);

        // Clean up
        item.Delete();
        (_db as CirrusDb)?.Remove(newId);

        return obj;
    }
}

Those of you with a keen eye might have noticed mention of DbWithRemove at the bottom there.  Seems that FakeDb has no remove method, even though its DataStorage class inside does.  I've just created a simple class to enable us to clean up our temporary item as per the original Glass code.

public class CirrusDb : Db
{
    public void Remove(ID itemId)
    {
        DataStorage.RemoveFakeItem(itemId);
    }
}

So now putting it all together in our test method:

using (Db db = SetupTestsite())
{
    using (new SiteContextSwitcher(FakeSite()))
    {
        ISitecoreContext sitecoreContext = new SitecoreContext(db.Database);
        
        ComponentsController controller = new ComponentsController(sitecoreContext)
        {
            RenderingContextWrapper = new MockRenderingContext(datasourceId, renderingParameterQueryString),
            GlassHtml = new FakeGlassHtml(sitecoreContext, db)
        };

        ViewResult result = controller.MyComponentMethod() as ViewResult;

        // Check your view and model

        Assert.NotNull(result);
        Assert.Equal("~/Views/Components/MyComponent.cshtml", result.ViewName);
        IMyModel model = result.ViewData.Model as IMyModel;
        Assert.NotNull(model);
        // ... etc.
    }
}

Hopefully some time soon I'll get to work on a Sitecore 8.2 project and get to try out testing with some of the new interfaces!

Monday 17 October 2016

Polyfill for CSS backdrop-filter (on whole page)

On my latest project, which was based on Bootstrap, the client (/ designer) wanted to substitute the default gray background of the modal window with a nice gaussian blur.
Default Bootstrap modal
Bootstrap modal with gaussian blur over background
Fortunately, literally a couple of weeks beforehand I'd skimmed a reddit post on a very similar thing (except the modal is blurred instead of the background).  CSS for browsers that support it, some sort of SVG fallback for those which don't.  Easy, I thought naively.  On closer inspection you'll notice that all the other methods actually blur one particular image which is used as the background.  This won't work in our case, we need to blur the entire rest of the page.

Ok first off, let's get to the code for browsers which support the backdrop-filter tag (as of this post, that's only iOS Safari, and Chrome if you have the "experimental web platform features" flag checked).  We can use the CSS @supports at-rule to make sure this is only handled by browsers which support it (and an inverse rule for the remainder).  This is pretty well supported, but you can always polyfill it as well.

@supports (backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px)) {
    .modal-backdrop.in {
        opacity: 1;
        background-color: transparent;
        -webkit-backdrop-filter: blur(4px);
        backdrop-filter: blur(4px);
    }
}

(the screenshot above is using this code in Chrome - with the flag enabled)

In all the digging through other posts on workarounds using SVG, and testing out various solutions I had a sudden moment of clarity (or madness).  What if I somehow got an image of the entire page and set that as the (blurred) background-image? Well, a SO search result shows that it's very much possible, and html2canvas is the framework to use.

Shut up and give me the code!

Ok ok...

The CSS:

@supports not ((backdrop-filter: blur(4px)) or (-webkit-backdrop-filter: blur(4px))) {
    .modal-backdrop {
        background-color: initial;
        -webkit-transition: all .25s ease-in-out;
        -moz-transition: all .25s ease-in-out;
        transition: all .25s ease-in-out;
        opacity: 0;
    }
}

And the Javascript:

if (!CSS.supports('backdrop-filter', 'blur(4px)') && !CSS.supports('-webkit-backdrop-filter', 'blur(4px)')) {
    $('[data-target="#myModal"]').click(function () {
        html2canvas(document.body).then(function (canvas) {
            $('.modal-backdrop').css({
                "background-image": 'url(' + canvas.toDataURL("image/png") + ')',
                "filter": "blur(4px)",
                "opacity": "1"
            });
        });
    });
}
Where [data-target="#myModal"] is the link/button/whatever that opens your modal.

Boom! We have a nice .25 second transition to a blurred screenshot of the page as our modal background.  By testing for support, theoretically we should have nice degradation back to the default Bootstrap modal (for things like IE10).

Note: I have not tested this in all browsers, or even many browsers, just the latest of Chrome, Firefox, Edge, and IE.  If you have a lot of components on your page then html2canvas can take a while to render the image.  Use (or don't) after testing per the requirements of your solution.

Monday 10 October 2016

"Invalid/missing hash was encountered" for documents

Reviewing our logs recently we discovered that we were getting an unusually large number of messages to the tune of
ERROR MediaRequestProtection: An invalid/missing hash value was encountered.

As we all know, Sitecore 7.5 added the concept of a hash which is required when resizing media items, so naturally we assumed that some images were being included incorrectly on a page somewhere, or the secret was incorrectly configured.  However, on closer inspection we realised that it wasn't actually images at all, but documents (which certainly aren't being resized, and surely don't need a hash).

Documents (mostly PDFs) were being uploaded to the media library, and then in a rich text field somewhere on the site, an author was linking to the document using insert link -> media item.

Upon reaching out to Sitecore, they were quick to reply that this was actually a bug in the current version of Sitecore (8.1 update 2 - 160302).  So if you're having a similar issue and want to clean out your logs a bit, you can contact support for the patch, and/or use the reference number 438674.

Wednesday 28 September 2016

It's always the simple things (a quick reminder about Sitecore access permissions)

Recently I inherited a project that has been ongoing for quite some time (so I'm not yet familiar with how it's been set up), and today I wasted a bunch of time debugging a general link field which wasn't generating the correct URL (in fact, it wasn't generating a URL at all!).  I figured it would be something simple, and sure enough it did turn out to be, but I got bogged down in the hunt long enough that I figured it was worth a post, on the off chance someone tries to search the same keywords I did (with no clues in the search results).

The general link field was on a datasource item, and was linking to another page on the site (ie using "Insert link" rather than "Insert external link" to create an internal link). This current project is using Glass Mapper (which I think distracted me a bit) and the Link.Url proprty was empty.  I thought maybe the field was being mapped incorrectly, but it all looked ok.  I tried grabbing the link field on the datasource Item directly (LinkField)RenderingContext.Current.Rendering.Item.Fields["MyLinkField"] and found that the InternalPath field was empty, and the TargetItem was null, even though the TargetID field was populated with the correct ID of the linked page.

Long story short, after digging through the code used to generate the URL I realised the Sitecore.Context.Database.GetItem("linkedItemId") was returning null, even though I could see the item in the Sitecore content editor.  Shortly thereafter it hit me: when would the context database return null for an item that obviously exists? When the current user doesn't have access to it! Yep that's right, the linked page had anonymous read access disabled, so that even though a link can be created by an author, the end user can see a link but cannot see the item and therefore the URL cannot be generated.

Moral of the story: if you can't get/see the link to an item, check the security and access permissions on that item!

Monday 26 September 2016

Adding a button to Experience Editor to open the image editor app

I ran across a question in Stack Overflow today about how to show "edit image" in Sitecore experience editor? and thought that would be an easy one to tackle.  Originally I figured I could copy the command from the existing link ("Edit Image") in the content editor and it would be a very simple answer, however it turns out that it's not quite so easy.

If you have access to a decompiler, take a look at the existing command Sitecore.Shell.Framework.Commands.Shell.EditImage as well as the existing command for selecting an image in Experience Editor Sitecore.Shell.Applications.WebEdit.Commands.ChooseImage. You'll see that they're similar, but unfortunately they use two different methods of opening a dialog (Windows.RunApplication and SheerResponse.ShowModalDialog), as well as reading the image from the context vs from the URL, so we can't reuse the existing class as-is. Fortunately the two classes are similar enough that you can easily re-use code from both, and make a class which opens the image editor in a modal dialog (as required by the experience editor).  I've posted a gist to the full class below.

Step 1:
Duplicate one of the buttons under /sitecore/system/Field types/Simple Types/Image/WebEdit Buttons
Change the icon and text to something meaningful to your authors.
Make the contents of the Click field: chrome:field:editcontrol({command:"webedit:changeimage"})
(annoyingly there's already a webedit:editimage which opens the image properties, so we can't name it that).

Step 2:
Create your custom command class (eg. EditImage.cs), make it serializable and inherit from WebEditImageCommand like the other EE buttons. I have posted the full class as a gist to make it easier to read and copy.

Step 3:
Open App_Config\Include\Sitecore.ExperienceEditor.config and duplicate the entry for webedit:chooseimage. Change the command name to webedit:changeimage as per step 1, and change the type to the custom command class you created in step 2.

When you select an image in the Experience Editor, you should now have an extra button in the list which opens the image editor in a modal window when clicked!

Extra 'edit image' button second from the left
After clicking the button, the image is opened in the image editor modal

Note: You'll have to refresh the window to see the updated image.  I leave it as an exercise for the reader to determine a way to get the image to refresh after closing the modal :) or maybe I'll write that up in a later post...

Tuesday 23 August 2016

Experiments in .NET Core

Ever since the release of .NET Core 1.0 I've been itching to get my hands dirty, and over the last few weeks I've finally managed to get around to having a play with it, so I thought I'd share some of my experiences and some of the bits that I've found aren't quite ready yet.

The solution I built was a REST Web API layer using Entity Framework, consumed by a MVC web application using NPM and Bower frontend packages (the usual JQuery, Bootstrap, etc.) and secured through a login using .NET Core Identity.  It's still a work in progress, and not as robust as it could (and possibly will) be, but gave me a chance to test out a lot of functionality and see where .NET Core still wasn't fully fleshed out.  Since I already use Visual Studio on Windows for work I didn't bother trying to use Visual Studio Code on OSX or Linux, though it seems very straightforward to do so.

I'm not going to go into detail about the differences that .NET core brings, as there are a ton of blog posts out there that cover it (not to mention the actual documentation).  Rather, I'll go through the steps in (roughly) the order I followed when creating the solution.

Initial Setup


When you're first starting out, the new .NET Core documentation is actually great; it's well laid-out and written in a way that's very easy for developers to understand, usually with each page building upon the last and grouped into logical sections.

When using Visual Studio on Windows, you have to make sure you're using 2015 update 3, and the .NET Core tooling, both of which can be found on the homepage of the .NET Core site.  After that, you can simply create a new project by selecting one of the .NET Core templates which appear in the list under Templates > Visual C# > .NET Core.

You have the usual options of Class Library (also used for test projects), Console Application, and ASP.NET Core Web Application. If you select the Web Application template you have the options of Empty, Web API, and Web Application, along with authentication options of No Authentication, Individual User Accounts (.NET Identity), Work and School Accounts (Active Directory), and Windows Authentication (Intranet).

In my case I had a class library (for domain models), and two Web Application templates: one for the Web API, and one for the Web Application (using Individual User Accounts for authentication).

The first new things you notice are: the project.json file (to handle the dependencies, framework versions, publish options, pre/post build commands, etc.), and in the case of web applications: Program.cs (which actually runs your app) Startup.cs (handles the bootstrapping / DI / etc.) and that app settings have moved to appsettings.json.  You'll also find that there are new NuGet packages for your usual frameworks (like EF or xUnit), and that the packages are a lot more modular (and therefore smaller with more dependencies).

Class Library - Models


There's not much to say about this project, other than make sure that the framework version(s) used in your project.json file is compatible with those in the other projects, otherwise it will let you reference the project, but it will not recognise the namespaces/classes you are trying to use!

I marked up my models with the System.ComponentModel.DataAnnotations like [Required] and [DataType], and one thing to keep in mind is to use [EmailAddress] and not just [DataType(DataType.EmailAddress)] if you want your inputs to come out as type="email" and validate correctly.

Web App - Web API


Creating this project from the template gives you pretty much everything you need, however for Entity Framework I also added the NuGet packages Microsoft.EntityFrameworkCore and Microsoft.EntityFrameworkCore.SqlServer.  For some reason the scaffolds for controllers didn't work for me in my Web API project (they did in  my MVC project), but if you create your project from the web app template you will find some controllers you can use / modify.

The new Web API controllers are quite similar to their predecessors, with the annotations having a slightly different syntax.  Controllers are decorated with [Route("Items")] and methods with the HTTP method they are associated with, eg. [HttpGet("{id}")] in a very similar way to attribute routing formerly worked; this method would be accessed at /Items/1234.  The only other real difference I found is returning an IActionResult instead of a IHttpActionResult.

I also wanted to use the VersionedRoute attribute to use headers to version my API, however haven't yet had a chance to fully implement the new approach I found through a quick search.

The new EF documentation tells you pretty much everything you need to know (specifically the ASP.NET section) including how to use the provided dependency injection that comes with .NET Core (in the Startup.cs file), and the slightly different syntax for defining your DB context.  Unfortunately Entity Framework had one of the first limitations I came across, which was that you can't (yet) return ad-hoc objects from raw queries, you have to return an entity type.  This meant that I couldn't optimise a couple of my queries as easily as I would have liked.

Web App - MVC Web App

The main MVC web application is where things really start to get fun.  Frontend things like css, images, and scripts are all grouped under wwwroot (which the server treats as /, so you don't have to reference /wwwroot in your src or href attribute) and if you use npm (package.json) or bower (bower.json) which VS natively supports, your dependencies are listed under the Dependencies node, similar to the existing References node.  The actual framework files can be found and/or included (eg. for scss) under /wwwroot/lib/.
You also get a bundleconfig.json file included in your project for bundling and minification, and VS supports both this and compilerconfig.json (depending on your extension of choice), both of which appear as tasks in the Task Runner Explorer.  Of course you could always stick with grunt or gulp, which VS has supported in the Task Runner Explorer for some time now, if that's your preference (right clicking on bundleconfig.json -> Bundler & Minifier with the extension installed actually lets you convert it to a gulp task(s) if you prefer).

If you select the relevant authentication when you create your project from the template, it will include all the bootstrapping necessary (for using Identity in my case) in the Startup.cs file, as well as providing you with some controller code you can use for logging in as well as securing your controllers and their methods.

To use session state and authentication using cookies you'll have to include:
    services.AddDistributedMemoryCache();
    services.AddSession();
in the ConfigureServices method and
    app.UseCookieAuthentication(new CookieAuthenticationOptions
    {
        LoginPath = new PathString("/Home/Login"),
        AccessDeniedPath = new PathString("/Home/Forbidden"),
        AutomaticAuthenticate = true,
        AutomaticChallenge = true
    });
    app.UseSession();
in the Configure method (of your Startup.cs).
Session is now located at HttpContext.Session and has getter and setter methods that you have to use.

Where before in your views you could just use Request["yourVariable"], however this has changed to Context.Request.Query["yourVariable"].

I also wanted to use areas for my site, since it was split into logical sections, so this means including an additional line of routing:
    routes.MapRoute(name: "areaRoute", template: "{area:exists}/{controller=Home}/{action=Index}");
to Startup.cs above the existing route, as well as an [Area("MyArea")] attribute to each Controller.
There's also an issue with publishing when you use areas, which (at the time of writing) requires a small amendment to the project.json file.

The whole concept of Html helper methods appears to be deprecated, or at least usurped by the concept of tag helpers, which I love; it maintains the syntax of the markup (so you're not mixing <tags>@{and code}</tags> as much, and adds to the HTML in a simple way that's easy to remember.  Plus it automatically adds the anti-forgery token to your form.  Validation hasn't changed much (I touched on annotating models earlier) and the tags support the newer HTML elements like input types of email and date which is absolutely necessary at this point, and can be extended easily enough if you want to support things like range sliders.  The one bit that I didn't quite get over was the fact that if your form field was bound to a property of a property of your model, it does not appear to be recognised by VS and appears red (even though the code works).  This may not be the correct way to to use the tag, but it certainly worked for me.

Running / Debugging

The biggest difference I found that .NET Core brings is in actually running the code.  To quote the documentation:
ASP.NET Core is completely decoupled from the web server environment that hosts the application. ASP.NET Core supports hosting in IIS and IIS Express, and self-hosting scenarios using the Kestrel and WebListener HTTP servers. Additionally, developers and third party software vendors can create custom servers to host their ASP.NET Core apps.
What this means is that your site no longer runs in IIS, but rather it runs in Kestrel (by default, otherwise your choice of server) and IIS acts as a reverse-proxy.  You still create a website node in IIS, and point it to the location at which your code is published, however for your app pool settings you should set the .NET CLR version to "No Managed Code".  See the publishing to IIS documentation for the full configuration, as there are some additional things you have to install.

This also means that when debugging you should not attach to w3wp.exe, but to dotnet.exe.  Of course, you can always hit the debugging button in VS to open your application in debug mode in IIS Express (if you haven't changed your project settings).

I didn't have much luck publishing with Web Deploy, even though it is supposed to be possible.  For some reason I get the error:
Configuring project completed successfully
publish: Published to C:\Users\myuser\AppData\Local\Temp\PublishTemp\Backoffice64
Published 1/1 projects successfully
Publishing with publish method [MSDeploy]
C:\Program Files (x86)\MSBuild\Microsoft\VisualStudio\v14.0\DotNet\Microsoft.DotNet.Publishing.targets(408,5): Error : An error occurred during publish.
Exception calling ".ctor" with "1" argument(s): "Invalid URI: The format of the URI could not be determined."
I had no luck searching for any of this message, so I switched to using the file system method of publishing which worked.

I also found that some of the time my breakpoints would not be hit in my controllers or views, despite my latest code being deployed.  I have no idea what was going on there, so I assume this is an issue that will hopefully be resolved in the coming updates.

Conclusion

I have to say all-in-all I was impressed with .NET Core.  Though there were a few bumps (and I'm still pretty damn confused by the names, versions and frameworks) I did manage to get everything building and running, and I love some of the newer tidbits like tag helpers and updated dependency management.

There are a few things I haven't got around to yet: 
  • I haven't done any performance testing, though I have heard that it performs very well; 
  • I haven't tried out xUnit, though it sounds very easy to get up and running; 
  • I haven't moved my authentication to my Web API layer yet, though that seems like it would be very similar to how it works currently;
  • I haven't tested out Visual Studio Code or Kestrel on Linux, though I'm very keen to try it out
At this point I don't think I'd recommend it for a major project or team environment (especially since there are packages that haven't been ported yet) though hopefully in the near future it will be ready for the task. For a small project like mine it certainly did the job very well, and will enable cost saving through being able to run the code on a Linux server rather than having to pay for a Windows licence.

I look forward to things like Octopus Deploy (mentioned in a community email), Umbraco (who are slowly working towards it), and  Sitecore (I'm sure eventually) moving to .NET Core, and in the meantime you can check whether your current solution can be upgraded by putting your project file into I Can Has .NET Core.

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!

Monday 4 April 2016

Sitecore PXM and ODG project (part 4 – customising email)

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

In the next couple of posts I'd like to focus on some of the customisations that can be made to improve or build upon the functionality provided by PXM and ODG out of the box.

Let's start with something simple: renaming the PDF file that's output from ODG.  Fortunately for us, Sitecore has made life a little easier by checking whether the name has already been set before setting its own, which means all we have to do is insert our "naming" processor before the others. If we look at Sitecore.PrintStudio.config, we can see in the <printPreview>, <printToDashboard> and <printToInDesign> pipelines it's the Sitecore.PrintStudio.PublishingEngine.Pipelines.RenderProjectXmlargs.PrintOptions.ResultFileName, so let's insert our processor before this one. In this case I only care about the <printToDashboard> pipeline, but the others will be the same.
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <printToDashboard>
        <processor
          patch:before="processor[@type='Sitecore.PrintStudio.PublishingEngine.Pipelines.RenderProjectXml, Sitecore.PrintStudio.PublishingEngine']"
          type="Custom.Pipelines.NameOutputFiles, Custom"/>
      </printToDashboard>
    </pipelines>
  </sitecore>
</configuration>

And the processor to set the name
public class NameOutputFiles : Sitecore.PrintStudio.PublishingEngine.Pipelines.IPrintProcessor
{
  public void Process(PrintPipelineArgs args)
  {
    args.PrintOptions.ResultFileName = Utils.GenerateFilename(Context.User, args.ProcessorItem.InnerItem.ID.Guid) + args.PrintOptions.ResultExtension;
  }

  public static string GenerateFilename(User user, Guid itemId)
  {
    return string.Concat(user.LocalName, "_", itemId, "_", DateTime.UtcNow.ToString("yyyy-MM-dd-HH-mm-ss-fff"));
  }
}

In this case I've separated the code generating the name into a separate method in case we need to re-use the same naming method elsewhere in the code (for example to generate a link to the document).

Now let's look at how we can add additional recipients to the email that ODG sends out once the document has been exported.  By default it sends an email to notify the user (who generated the document) that their document is ready, with a link to the document.  What if we want this to be CCd or BCCd to for example their boss, or a support email?

For our example, let's add a support email as a BCC to all emails sent.

The code which handles sending the email is in the Dashboard Service, however the code which tells the Dashboard Service which email(s) to use is in the printToDashboard pipeline, in the SendToDashboard class, so let's extend that and switch it out in a patch:
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <pipelines>
      <printToDashboard>
        <processor 
          patch:instead="processor[@type='Sitecore.PrintStudio.PublishingEngine.Pipelines.Dashboard.SendToDashboard, Sitecore.PrintStudio.PublishingEngine']"
          type="Custom.Pipelines.SendToDashboard, Custom"/>
      </printToDashboard>
    </pipelines>
  </sitecore>
</configuration>

So taking a look at SendToDashboard in the Send method, we can see the message is being built by SoapHelper.SendSoapMessage() call, and taking a look in SoapHelper we find that the CreateGroupNode method takes a BCC field, to which SendSoapMessage is just passing a blank string.

Unfortunately here's where things get a bit messy. We need to get BCC into SendSoapMessage, so ideally we would extend SoapHelper and add another override of SendSoapMessage which takes BCC as a parameter (with pretty much exactly the same code), however a lot of the methods that are used in SoapHelper are marked private (not protected) so we can't re-use the existing code!  At the end of the day I found it easiest to just copy the SendSoapMessage code directly into the new SendToDashboard class and use reflection to call CreateGroupNode() and CreateJobNode().  It would be great if Sitecore made these protected in the future, so we can extend the SoapHelper class (or expose the CC and BCC fields).

So now in our SendToDashboard class we have our new Send() method which calls the new SendSoapMessage() method with our BCC field.  Let's take things a little further and say we want to pass new info into this email that's being sent.  What if we want to pass the name of the user's document into the email so that they know which one it was (sounds pretty standard, right)?

Unfortunately this is just as messy.  In this case it's the CreateSoapMessageDetails() method we need to override to take the document name and pass it to SoapMessageDetails(), which should pass it into the string.Format() which builds the "Success" or "Failed" nodes.  Again none of these are protected, so we need to pretty  duplicate the code all the way through with our new parameter.  Don't forget to update the email template to make use of this new parameter!
public class SendToDashboard : Sitecore.PrintStudio.PublishingEngine.Pipelines.IPrintProcessor
{
  public void Process(PrintPipelineArgs args)
  {
    if (!string.IsNullOrEmpty(args.XmlResultFile))
    {
      Item processingJobItem = args.ProcessorItem.Database.GetItem(args.PrintJobId);
      this.Send(args.XmlResultFile, Path.Combine(args.PrintOptions.ResultFolder, args.PrintOptions.ResultFileName), args.PrintOptions.ResultFolder, processingJobItem, args.ProcessorItem.InnerItem.Name);
    }
    else
    {
      Logger.Info("Empty args.XmlResultFile");
      args.AddMessage("Empty args.XmlResultFile", PipelineMessageType.Error);
      args.AbortPipeline();
    }
  }
  private void Send(string resultXmlFileName, string pdfFileName, string absoluteFilePath, Item processingJobItem, string docName)
  {
    if (string.IsNullOrEmpty(resultXmlFileName))
    {
      return;
    }
    string dbServerIpAddress = WebConfigHandler.PrintStudioEngineSettings.DbServerIpAddress;
    string dbServerPort = WebConfigHandler.PrintStudioEngineSettings.DbServerPort;
    Assert.IsNotNullOrEmpty(dbServerIpAddress, "Missing PrintStudio.DBServer.IPAddress configuration");
    Assert.IsNotNullOrEmpty(dbServerPort, "Missing PrintStudio.DBServer.Port configuration");
    Language userLanguage = SitecoreHelper.GetUserLanguage(Context.User, processingJobItem.Database);
    XmlDocument resultDocument = new XmlDocument();
    Logger.Info("Sending to Dashboard: " + Context.User.Name + " (" + Context.User.Profile.Email + ") file " + resultXmlFileName);
    SendSoapMessage(Context.User.Name,
      Context.User.Name,
      Context.User.Profile.Email,
      resultXmlFileName,
      WebConfigHandler.PrintStudioEngineSettings.ResponseType,
      WebConfigHandler.PrintStudioEngineSettings.DashboardQueueName,
      WebConfigHandler.PrintStudioEngineSettings.DashboardServiceMethod,
      SitecoreHelper.GetMessage(processingJobItem, userLanguage, "Mail Subject") + " " + TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, TimeZoneInfo.FindSystemTimeZoneById(Constants.General.Timezone)).ToString("g"),
 string.Empty,
     CreateSoapMessageDetails(processingJobItem, resultDocument, userLanguage, docName).ToString(),
     SitecoreHelper.GetPrintFilePath(processingJobItem, "relative filename path"),
     absoluteFilePath,
     pdfFileName,
     $"{dbServerIpAddress}:{dbServerPort}",
     processingJobItem.Database.GetItem(new ID(Constants.Items.SiteSettings))?.Fields[Constants.Fields.SupportEmail]?.Value);
  }
  private static string SendSoapMessage(string userName, string loginName, string email, string jobXml, string responseType, string serviceType, string serviceMethod, string subject, string body, string messageBrands, string relativePath, string absolutePath, string fileName, string serviceUrl, string bcc)
  {
     string startTime = DateTime.UtcNow.ToString("s", DateTimeFormatInfo.InvariantInfo);
     XmlDataDocument xmlDataDocument = new XmlDataDocument();
     XmlNode groupNode = (XmlNode)typeof(SoapHelper).GetMethod("CreateGroupNode", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { xmlDataDocument, userName, userName, loginName, startTime, responseType, email, body, subject, string.Empty, bcc, string.Empty, "HTML" });
     XmlNode jobNode = (XmlNode)typeof(SoapHelper).GetMethod("CreateJobNode", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { xmlDataDocument, userName, "1", serviceType, serviceMethod, string.Empty, fileName, relativePath, absolutePath, string.Empty, string.Empty, jobXml, string.Empty, serviceUrl });
     typeof(SoapHelper).GetMethod("AddJobNode", BindingFlags.Static | BindingFlags.NonPublic).Invoke(null, new object[] { xmlDataDocument, groupNode, jobNode });
     if (!string.IsNullOrEmpty(messageBrands))
     {
       XmlNode xmlNode = groupNode.SelectSingleNode("//Jobs/Job");
       if (xmlNode != null && groupNode.OwnerDocument != null)
       {
         XmlElement element = groupNode.OwnerDocument.CreateElement("ResultMail");
         element.InnerXml = messageBrands;
         xmlNode.AppendChild(element);
       }
    }
    return SoapHelper.DashBoardWebService.Send("<?xml version=\"1.0\" encoding=\"utf-16\" ?>" + groupNode.OuterXml); // returns "Success"
  }
  private static StringBuilder CreateSoapMessageDetails(Item processingJobItem, XmlDocument resultDocument, Language language, string jobName)
  {
    string message1 = SitecoreHelper.GetMessage(processingJobItem, language, "Mail Header");
    string message2 = SitecoreHelper.GetMessage(processingJobItem, language, "Mail Footer");
    string message3 = SitecoreHelper.GetMessage(processingJobItem, language, "Ready Message");
    string message4 = SitecoreHelper.GetMessage(processingJobItem, language, "Rejected Message");
    return SoapMessageDetails(resultDocument, message3, message4, message1, message2, jobName);
  }
  private static StringBuilder SoapMessageDetails(XmlDocument resultDocument, string jobSuccess, string jobFailure, string mailHeader, string mailFooter, string jobName)
  {
    StringBuilder stringBuilder = new StringBuilder();
    XmlNode xmlNode1 = resultDocument.CreateElement("Header");
    xmlNode1.AppendChild(resultDocument.CreateCDataSection(mailHeader));
    stringBuilder.Append(xmlNode1.OuterXml);
    XmlNode xmlNode2 = resultDocument.CreateElement("Footer");
    xmlNode2.AppendChild(resultDocument.CreateCDataSection(mailFooter));
    stringBuilder.Append(xmlNode2.OuterXml);
    XmlNode xmlNode3 = resultDocument.CreateElement("Success");
    xmlNode3.AppendChild(resultDocument.CreateCDataSection(string.Format(jobSuccess, Context.User.Profile.FullName, Context.User.LocalName, jobName)));
    stringBuilder.Append(xmlNode3.OuterXml);
    XmlNode xmlNode4 = resultDocument.CreateElement("Failed");
    xmlNode4.AppendChild(resultDocument.CreateCDataSection(string.Format(jobFailure, Context.User.Profile.FullName, Context.User.LocalName, jobName)));
    stringBuilder.Append(xmlNode4.OuterXml);
    return stringBuilder;
  }
}

This was a bit of a messy one. Hopefully with newer releases of PXM and ODG Sitecore makes things a little more extensible!

Wednesday 16 March 2016

Sitecore PXM and ODG project (part 3 - architecture)

There are a LOT of components to set up when you're using PXM, ODG, and exporting your documents to PDF.  Adding even more complexity is the fact that this solution should really be running on a content delivery server accessible by the clients (or potentially multiple servers, in Azure IaaS/PaaS) whereas ODG natively runs in the Sitecore admin interface which would be on a content authoring server. Here's a list of things you'll need for the full setup:

Sitecore PXM ODG architecture
PXM + ODG architecture in authoring / delivery setup

Note: Unfortunately this setup can run multiple delivery servers (IaaS/PaaS), but will only really support one authoring server (IaaS) since there isn't really any way to balance the connections from delivery to the Dashboard/Processing/InDesign server side of things.  InDesign Server comes in a single or multi-instance licence, and if you need to scale then the multi-instance option on one high-spec'd authoring server would probably be best.

Firstly it's important to set up and ensure that Sitecore on content authoring is working correctly with PXM, ODG, and InDesign Server.  This functionality is "out of the box", but requires a lot of configuration which you can follow on the PXM youtube video series.  There has been an updated release of some of the PXM connectors to support CC 2015 so ensure you're using the correct software version and connector version for your OS.  The video series refers to the folder holding the exports / config / logs as PXMPublishing so that's what I will refer to it as here.

Once those pieces of software are setup, you should be able to:
  1. Import an InDesign document (through InDesign using the connector)
  2. Upload any media into the media library and replace the references in the PXM project so that they point to the media library rather than your hard drive (open the project through someone else's InDesign connector to verify)
  3. Create a document in ODG, replace various content / media, and preview it
  4. Export your document to PDF and preview the output PDF in the PXMPublishing/PublishFolder folder
There are various logs that can help you if you can't successfully complete all 4 steps:
  1. Sitecore logs
  2. PXM log (in with the rest of the Sitecore logs)
  3. Dashboard Server log (PXMPublishing/Logs/DashboardServer)
  4. Processing Server log (PXMPublishing/Logs/InDesignProcessingService)
  5. InDesign Server log (<InDesign Server directory>/Logs)
Once the content authoring server is running correctly, there are a few things that need to be setup so that everything works with the delivery server:
  • Ensure your PXMPublishing folder is accessible from both the authoring and delivery servers (through a file share or Azure File Storage works)
  • If you have a large amount of media (or very large media files) that you don't want to upload, ensure they're also in a shared location accessible from both servers (covered later in the blog)
  • Ensure the dashboard service port (by default 8070) on the CA server is open to the CD server (via both Azure and Windows firewall)
  • Ensure the InDesign server port (by default 8081) on the CA server is open to the CD server (via both Azure and Windows firewall)
  • Ensure the dashboard server in the web.config on the CD server is pointing to the URL / port of the dashboard server running on the CA server
  • Unfortunately a wrapper design controller will need to be created for the CD server to call the existing functionality. This wraps Sitecore.Odg.Controllers.DesignController.
  • If you're going to be allowing users to grab the output PDFs from the server, ensure the virtual folder is mapped in IIS on the CD server
Once that's done you should be able to call the GetCustomPreviewImage and ExportToPdf controller methods to get the preview image of your document, and export it to PDF, and view it via URL. For example:
$.ajax({
  dataType: 'json',
  url: '/api/sitecore/CustomDesign/GetCustomPreviewImage?&itemId=' + currentPageId + '&lang=en&forceNew=true&useHighRes=false&saveInPage=true',
  cache: false,
  success: function (response) {
    if (response.mediaUrl != null && response.mediaUrl != "") {
      updatePageThumbnail(response.thumbnailUrl);
      loadImageToCanvas(response.mediaUrl);
    }
  }
});
and
$.ajax('/api/sitecore/CustomDesign/ExportToPdf', {
    type: 'POST',
    data: {itemId: odgDocumentId, processingJobId: processingJobId, lang: language},
    success: function (result) {
        alert('Successfully sent for conversion to PDF')
    },
    error: function (result) {
        alert('Error saving.  Please try again or contact us if the issue reoccurs.');
    }
});
In the next blog we'll look into other customisations we can make to the process and pipelines to improve the user experience.

Monday 7 March 2016

Sitecore PXM and ODG project (part 2 – non-editable fields)

  1. Introduction
  2. Non-editable fields
  3. Architecture
  4. Customising ODG email
  5. PXM InDesign custom task
Since we were using as much Sitecore-provided PXM and ODG functionality as possible (and ODG provides pretty much all of the functionality) the first task was to find a way to selectively make specific fields in the documents editable. ODG out of the box makes all text and image fields in the document editable, however images like the company logo, and text like terms and conditions should not be editable by the end user. There are (at least) two ways to go about this:
  1. Use security to restrict the user's read/write access to the field
    This is a little risky, down the line when we go to export the document the read/write access may be required to export the text/image field to PDF
  2. Create a new template which inherits from the default snippet
    Digging into the Sitecore SPEAK code*, we can see that in ODG the snippets are loaded by doing a query on the template type ('get all child items with the snippet template type'). By extending the existing snippet this query fill filter out new snippet type, so all the non-editable fields can be hidden in here, however when export to PDF is run it's still treated as a snippet and the text is shown. The downside to this is that the custom snippet is not supported by the InDesign connector so even though the snippet will appear in InDesign, making items non-editable (creating the non-editable snippet and dragging items into this snippet) will have to be done in the content editor.
We ended up going with option 2, though option 1 may still work, I did not get a chance to investigate.
So it's easy as finding the P_Snippet template (at /sitecore/templates/Print Studio Templates/Publishing Engine/P_Snippet), and making a custom template which contains that as a base template.

custom_snippet
Custom non-editable snippet made from P_Snippet

Sitecore PXM and ODG project (part 1 - introduction)

  1. Introduction
  2. Non-editable fields
  3. Architecture
  4. Customising ODG email
  5. PXM InDesign custom task
This series of posts will cover my discoveries while using Print Exprience Manager (PXM) and Online Document Generator (ODG) with Sitecore 8.1 initial release. It won't go into any detail on the setup of PXM or ODG as there are great resources and videos for getting these set up.  Unfortunately because it is an internal client project I can't post screenshots, but by the time the project went live it allowed the business to import InDesign documents (created by marketing) into Sitecore, which then displayed these to the end user for selection. Once a document was seelcted the user could then edit specified text and imagery in the document, save their document, export it as a PDF, and allow it to be sent to print. Although similar in functionality to out-of-the-box ODG, it was decided that the user should not be allowed access to the Sitecore admin interface to use ODG. We did, however, use as much existing functionality in PXM and ODG as possible; it was effectively a content-delivery based re-skin/wrapper of ODG.
While the majority of the documentation around PXM focuses on how to import Sitecore content into an InDesign document and format it, our project was the reverse: given a number of already-created InDesign documents, we were to import these into Sitecore (PXM) and allow them to be editable in ODG for export as high quality PDF to be used by the printing companies. Unfortunately our marketing team had little interest in learning Sitecore, and little time to explain the workings of InDesign. Coming from a Sitecore background (with minimal Photoshop knowledge and no InDesign knowledge) this meant that I certainly tought myself a lot in a very short time.

InDesign

Edit: just after I wrote this an excellent article on how to use the Sitecore connector with InDesign was posted
InDesign itself is based primarily on XML and Sitecore takes advantage of this using the connector (an InDesign plugin) to easily create or import the InDesign elements into Sitecore as items in various sections of the in the content tree. I'd highly recommend watching the series of PXM videos linked above (or at least the first few) to get an idea of how it all works together.   The elements in the InDesign document (shapes, images, text, etc.) are imported into PXM (sub-items in Print Studio projects); whereas the layers, character styles, paragraph styles, and object styles, are saved in the master document (in the media library) which is then referenced by the InDesign project(s). Images are not automatically imported into Sitecore; rather, a reference to the image location on your hard drive is saved in a field (meaning unless others have the image in the same location on the same drive letter, it will not appear for them). If you later choose to import the image into Sitecore, a second field (media reference) overrides this hard drive location reference.
Similar to those familiar with CSS, InDesign has what I'll call "inline" styles (like style="" in HTML), and then defined character and paragraph styles (saved groups of styles given a name, like a CSS class). If you simply select a bunch of text, and hit "bold" it will save it inline, or you can save your styles as a character/paragraph style and apply that to your text (some styles, like centering text, can only be applied to a paragraph). Unfortunately I found out the hard way that the Sitecore connector does not recognise these "inline" styles, but rather relies on character/paragraph styles having been created, which when imported are saved in the master document. I say unfortunate, because none of the InDesign documents we were provided had any character or paragraph styles applied. If you're importing an InDesign document you'll certainly want to ensure these exist first or your document is going to look very strange when you load it from Sitecore. We also discovered that you shouldn't use an ampersand (&) in the style name, as this is misinterpreted by InDesign (as least for TextFrames) and your TextFrame will not be positioned (or even appear) correctly.

Sitecore PXM and ODG


master_documents
Master documents (where styles are stored)

PXM project
PXM Projects (where shapes are stored)

ODG
ODG items
(where user documents are stored)
Installing ODG adds a couple of new icons to the dashboard (in this case the useful one is Document Publisher) and a new ODG section to the content tree.  The SPEAK applications are simply a pretty frontend for manipulating the items in the content tree, which can be done manually by more advanced users.  This new content tree section allows the creation of Collections, which were used to categorise our documents and are stored in the Design Collections bucket; Projects which are a clone of the original PXM project specific to the user who creates it and for some reason aren't stored in a bucket (the fact that they are a clone is useful as it means updates to the original PXM project are automatically applied to all ODG documents cloned from it, but also can be problematic if your user wants to keep a version of the original); Design Templates which specify which formats the PXM projects can be exported as; and Design Documents which link them all together and say "the user has created a document referencing this project (clone) belonging to this collection which can be exported as PDF/other". ODG The Document Publisher app allows the creation of Collections to group documents, as well as creating a user-specific version of a PXM project, edit the text and image fields (field types configurable in the config) in this document, and export it to specified format (through the processing job field which links to the PXM Publishing Settings) PDF high / low quality and Flash out of the box.  These correspond with InDesign Server settings which can be found in <install_directory>\Adobe InDesign CC Server 2014\Resources\Adobe PDF\settings\mul\*.joboptions

Part 2 will cover our first customisation for ODG: specifying which fields should be editable.

Wednesday 24 February 2016

Extending an Image field (pt 2) - adding a SPEAK dialog

This is the second part of a 2-part post about how to extend the Sitecore image field, and add an image overlaid on top of the main image. If you haven't already, check out part 1 to see how to extend the image field. This post will focus on the SPEAK component which will allow the user to set the top/left coordinates that specify where our overlay will be placed over the main image.

So if you remember back to when we set up the Overlay button on our new field, we updated the Message field to use "overlay". Let's handle this message in our ImageWithOverlay.cs class.
/// The overlay message.
private const string OverlayMessage = "overlay";

/// The overlay application location.
private const string OverlayAppLocation = "/sitecore/client/Your Apps/OverlaySelector";

public override void HandleMessage(Message message)
{
    if (message["id"] != ID)
    {
        return;
    }

    string[] command = message.Name.Split(':');
    Assert.IsTrue(command.Length > 1, "Expected message format is control:message");

    if (command[1] == OverlayMessage)
    {
        Sitecore.Context.ClientPage.Start(this, "Overlay");
        return;
    }

    base.HandleMessage(message);
}

public void Overlay(ClientPipelineArgs args)
{
    if (args.IsPostBack)
    {
        if (!args.HasResult)
        {
            return;
        }

        XmlValue.SetAttribute(Models.Constants.CoordinatesAttribute, args.Result);
        Update();
        SetModified();
        SheerResponse.Refresh(this);
    }
    else
    {
        UrlString urlString = new UrlString(OverlayAppLocation);

        Item selectedImage = GetMediaItem();
        if (selectedImage != null)
        {
            urlString["fo"] = selectedImage.Uri.ToString();
        }

        string coords = XmlValue.GetAttribute(Models.Constants.CoordinatesAttribute);
        if (!string.IsNullOrEmpty(coords))
        {
            urlString["coords"] = coords;
        }

        SheerResponse.ShowModalDialog(new ModalDialogOptions(urlString.ToString()) { Width = "800px", Height = "275px", Response = true, ForceDialogSize = true });
        args.WaitForPostBack();
    }
}


This code handles the message and will open our SPEAK application that we're about to make. We're passing the existing coordinates (if they're set) and the Uri of the main image (so that we can display it in our SPEAK dialog).

Ok let's build our SPEAK app. This will show our main image with our overlay image over top, and allow the user to drag the overlay around to set its coordinates. It'll also have a 'save' and 'cancel' button which sets (or not) the coordinates on the main image field.

In Sitecore Explorer, expand core/sitecore/client/Your Apps. Right click it and create a /sitecore/client/Business Component Library/Templates/Pages/Speak-DialogPage called "OverlaySelector". Set the following properties:

  • Theme: Oxford

  • Subthemes: Dialogs

Open the design layout (right click -> tasks -> Design Layout, or Ctrl+U) and set
  • Layout: /sitecore/client/Speak/Layouts/Layouts/Speak-Layout


Add the following renderings:
TypeIDLocationOther
PageCodePage.CodePageCodeScriptFileName: /Scripts/Speak/Overlay.js
DialogPage.Body
DialogHeaderDialogHeader
DialogFooterDialogFooter
DialogContentMDialogContent
TextHeaderTitleDialogHeader.TitleText: Select a teardrop position
SectionMainImageDialogContent.Main
ImageOverlayImageMainImage.ContentAlt: Overlay, Height: 300, Width: 245, ImageUrl: /Content/Images/overlay.png
TextCoordinatesDialogContent.MainText: 100,100
ButtonSaveButtonDialogFooter.ButtonButtonType: Primary, Text: Select
ButtonCancelButtonDialogFooter.ButtonButtonType: Primary, Text: Cancel
RuleSaveButtonRulePage.BodyField: Rule, RuleItemId: (see below), TargetControl: SaveButton, Trigger: click
RuleCancelButtonRulePage.BodyField: Rule, RuleItemId: (see below), TargetControl: CancelButton, Trigger: click
Under your OverlaySelector item, create a /sitecore/cilent/Speak/Templates/Pages/PageSettings item which will have the settings for our dialog (for now, just the 2 rules for our buttons).
Under the PageSettings item, create: /sitecore/client/Speak/Layouts/Renderings/Resources/Rule Definition called CancelButtonRuleDefinition with rule: where always close the dialog
/sitecore/client/Speak/Layouts/Renderings/Resources/Rule Definition called SaveButtonRuleDefinition with rule: where always the dialog return value to component Coordinates text

The final step is to add our javascript for the component. We'll use jQuery UI to make the overlay draggable.
define(["sitecore", "jquery", "jqueryui"], function (_sc, $, ui) {
    var overlaySelectorDialog = _sc.Definitions.App.extend({
        initialized: function () {
            var app = this;
            var scale = 1;

            var itemUriString = _sc.Helpers.url.getQueryParameters(window.location.href)['fo'];
            var itemPath = null;
            try {
                var itemUri = new URL(itemUriString);
                itemPath = itemUri.pathname;
                if (itemPath == "" || itemPath.indexOf("?") > -1) throw "Invalid URL";
            } catch (e) {
                // Doesn't support URL (IE and pretty much FF as well)
                var slashes = itemUriString.indexOf("//");
                var query = itemUriString.indexOf("?");
                if (slashes > -1) itemPath = itemUriString.substring(slashes, query > -1 ? query : itemUriString.length);
            }

            var mainImage = document.querySelector('[data-sc-id="MainImage"]');
            if (itemPath == null || itemPath == "") {
                alert("Couldn't parse item URL for your background image");
            } else {
                var itemUriSplit = itemPath.substring(2).split("/");
                var database = new _sc.Definitions.Data.Database(new _sc.Definitions.Data.DatabaseUri(itemUriSplit[0]));
                database.getItem(itemUriSplit[1], function (item) {
                    if (item == null) alert("Couldn't find background image item in database for unknown reason");
                    else {
                        var imgWidth = parseInt(item.Width);
                        var imgHeight = parseInt(item.Height);
                        if (imgWidth > imgHeight) {
                            scale = 500 / imgWidth;
                            mainImage.style.height = Math.round(scale * imgHeight) + "px";
                        } else {
                            scale = 500 / imgHeight;
                            mainImage.style.width = Math.round(scale * imgWidth) + "px";
                        }
                        
                        mainImage.style.backgroundImage = "url('" + item.$mediaurl.replace("thn=1", "") + "&w=500')";

                        var coords = _sc.Helpers.url.getQueryParameters(window.location.href)['coords'];
                        if (coords != null && coords != "") {
                            app.Coordinates.set('text', coords);
                            var coordsSplit = coords.split(",");
                            jQuery('[data-sc-id="OverlayImage"]').css({ "left": (parseInt(coordsSplit[0]) * scale) + "px", "top": (parseInt(coordsSplit[1]) * scale) + "px" });
                        }
                    }
                });
            }

            mainImage.style.height = "500px";
            mainImage.style.width = "500px";
            mainImage.style.backgroundSize = "cover";

            jQuery('[data-sc-id="OverlayImage"]').draggable({
                containment: '[data-sc-id="MainImage"]',
                scroll: false,
                start: function (e, ui) {
                    app.Coordinates.set('text', Math.round(ui.position.left / scale) + "," + Math.round(ui.position.top / scale));
                },
                drag: function (e, ui) {
                    app.Coordinates.set('text', Math.round(ui.position.left / scale) + "," + Math.round(ui.position.top / scale));
                },
                stop: function (e, ui) {
                    app.Coordinates.set('text', Math.round(ui.position.left / scale) + "," + Math.round(ui.position.top / scale));
                }
            });
        }
    });
    return overlaySelectorDialog;
});
I've uploaded the full code to Github if you'd like to try it out for yourself.

Extending an Image field in Sitecore (part 1)

On my last project one of the more fun things I got to play with was the SPEAK framework, and today's posts (which will be quite large) will outline how to extend the default Sitecore Image field. We're simply going to add an additional icon which is overlaid on top of our main image (like a watermark), and use a simple SPEAK interface to allow it to be positioned (by being dragged, and setting x,y coordintes in pixels) by the content author.
So, first things first: since we're extending the Image field, let's go into the core database and make a duplicate of that field, which you can find at /sitecore/system/Field types/Simple Types/Image. I'm going to call mine "Image With Overlay". Let's also add a new button to open our SPEAK interface, so expand the Menu folder under the new item, and duplicate the Browse button; call it "Overlay", and change the Display Name field to "Overlay" as well. Now there are 2 more things we need to change: in the "Image With Overlay" item we just created, we need to update the Control field, and put overlay:ImageWithOverlay (we'll get back to this later). In the "Overlay" button item, update the Message field so that "open" is now "overlay". It should look like contentimage:overlay(id=$Target).
Ok now that we've got our field defined in Sitecore, let's add the code side of things. Let's extend the default Sitecore.Data.Fields.ImageField and add a new property called OverlayCoordinates to store the coordinates at which to display our overlay. Hopefully you're familiar with what Sitecore Image fields look like as raw values (if not, turn on raw values and have a look with an image with a custom alt tag and title) - we're going to store our coordinates as a new xml attribute in the same way, and this OverlayCoordinates property does just that.
using Sitecore.Data.Fields;

public class ImageWithOverlayField : ImageField
{
    public ImageWithOverlayField(Field innerField)
        : base(innerField)
    {
    }
    
    public ImageWithOverlayField(Field innerField, string runtimeValue)
        : base(innerField, runtimeValue)
    {
    }
    
    public string OverlayCoordinates
    {
        get
        {
            return GetAttribute(Constants.CoordinatesAttribute) ?? Constants.OverlayDefaultCoordinates;
        }

        set
        {
            SetAttribute(Constants.CoordinatesAttribute, value ?? Constants.OverlayDefaultCoordinates);
        }
    }
    
    public static implicit operator ImageWithOverlayField(Field field)
    {
        return field == null ? null : new ImageWithOverlayField(field);
    }
}
Ok now that we've extended our field to store the coordinates attribute, let's extend the actual class which renders our Image field, and show our overlay over top in the preview. The easiest way is to decompile Sitecore.Shell.Applications.ContentEditor.Image and extend+reuse what we can, and copy+update what we cannot reuse (ie. if it's a private method).
using System;
using System.Linq;
using System.Text;
using System.Web.UI;
using Sitecore;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
using Sitecore.Globalization;
using Sitecore.Resources.Media;
using Sitecore.Shell.Applications.ContentEditor;
using Sitecore.Text;
using Sitecore.Web;
using Sitecore.Web.UI.Sheer;
using Convert = System.Convert;

public class ImageWithOverlay : Image
{
    protected override void DoRender(HtmlTextWriter output)
    {
        Assert.ArgumentNotNull(output, "output");
        Item mediaItem = GetMediaItem();

        string src = GetSrc();
        string str1 = " src=\"" + src + "\"";
        string str2 = " id=\"" + ID + "_image\"";
        string str3 = " alt=\"" + (mediaItem != null ? WebUtil.HtmlEncode(mediaItem["Alt"]) : string.Empty) + "\"";
        string coordinates = XmlValue.GetAttribute(Common.Constants.CoordinatesAttribute);

        if (string.IsNullOrEmpty(coordinates))
        {
            coordinates = Common.Constants.OverlayDefaultCoordinates;
        }

        int[] coords = coordinates.Split(',').Select(int.Parse).ToArray();

        // base.DoRender(output);
        output.Write("
"); output.Write("
"); string dimensions = ""; string overlayDimensions = ""; int[] padding = { 12, 8 }; if (mediaItem != null) { int width = Convert.ToInt32(mediaItem["Width"]); int height = Convert.ToInt32(mediaItem["Height"]); double scale = 128.0 / height; dimensions = "width=\"" + Math.Round(scale * width) + "px\" height=\"" + Math.Round(scale * height) + "px\""; overlayDimensions = "left:" + (Math.Round(coords[0] * scale) + padding[0]) + "px;top:" + (Math.Round(coords[1] * scale) + padding[1]) + "px;"; } else { overlayDimensions = "left:" + padding[0] + "px;top:" + padding[1] + "px;"; } output.Write(""); output.Write(""); output.Write("
"); output.Write("
"); string details = GetDetails(); output.Write(details); output.Write("
"); output.Write("</div>"); } private Item GetMediaItem() { string attribute = XmlValue.GetAttribute("mediaid"); if (attribute.Length <= 0) { return null; } Language language = Language.Parse(ItemLanguage); return Client.ContentDatabase.GetItem(attribute, language); } private string GetSrc() { string src = string.Empty; MediaItem mediaItem = GetMediaItem(); if (mediaItem != null) { MediaUrlOptions thumbnailOptions = MediaUrlOptions.GetThumbnailOptions(mediaItem); int result; if (!int.TryParse(mediaItem.InnerItem["Height"], out result)) { result = 128; } thumbnailOptions.Height = Math.Min(128, result); thumbnailOptions.MaxWidth = 640; thumbnailOptions.UseDefaultIcon = true; src = MediaManager.GetMediaUrl(mediaItem, thumbnailOptions); } return src; } private string GetDetails() { string str1 = string.Empty; MediaItem mediaItem = GetMediaItem(); if (mediaItem != null) { Item innerItem = mediaItem.InnerItem; StringBuilder stringBuilder = new StringBuilder(); XmlValue xmlValue = XmlValue; stringBuilder.Append("
"); string str2 = innerItem["Dimensions"]; string str3 = WebUtil.HtmlEncode(xmlValue.GetAttribute("width")); string str4 = WebUtil.HtmlEncode(xmlValue.GetAttribute("height")); if (!string.IsNullOrEmpty(str3) || !string.IsNullOrEmpty(str4)) { stringBuilder.Append(Translate.Text("Dimensions: {0} x {1} (Original: {2})", str3, str4, str2)); } else { stringBuilder.Append(Translate.Text("Dimensions: {0}", str2)); } stringBuilder.Append("
"); stringBuilder.Append("
"); string str5 = WebUtil.HtmlEncode(innerItem["Alt"]); string str6 = WebUtil.HtmlEncode(xmlValue.GetAttribute("alt")); if (!string.IsNullOrEmpty(str6) && !string.IsNullOrEmpty(str5)) { stringBuilder.Append(Translate.Text("Alternate Text: \"{0}\" (Default Alternate Text: \"{1}\")", str6, str5)); } else if (!string.IsNullOrEmpty(str6)) { stringBuilder.Append(Translate.Text("Alternate Text: \"{0}\"", str6)); } else if (!string.IsNullOrEmpty(str5)) { stringBuilder.Append(Translate.Text("Default Alternate Text: \"{0}\"", str5)); } else { stringBuilder.Append(Translate.Text("Warning: Alternate Text is missing.")); } stringBuilder.Append("
"); stringBuilder.Append("
"); string str7 = WebUtil.HtmlEncode(xmlValue.GetAttribute(Common.Constants.CoordinatesAttribute)); stringBuilder.Append(!string.IsNullOrEmpty(str7) ? Translate.Text("Overlay coordinates: {0}", str7) : Translate.Text("Overlay coordinates: No coordinates set, using 100,100.")); stringBuilder.Append("
"); str1 = stringBuilder.ToString(); } if (str1.Length == 0) { str1 = Translate.Text("This media item has no details."); } return str1; } }
You'll notice the DoRender() method is where we're now including our overlay.png over top of the main image using relative and absolute CSS positioning. I've also added an extra line in the GetDetails() method which shows the user the currently set coordinates, like the "dimensions" and "alt" are currently shown below the image preview.
Ok so now that we've got our classes, let's hook up the Sitecore side of things with the code. For this, we'll need a new include file (which it's always best to put in a "zzz" folder so that it's included last).
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <fieldTypes>
      <fieldType name="ImageWithOverlay" type="SpeakImageOverlay.Models.Fields.ImageWithOverlayField, SpeakImageOverlay" />
    </fieldTypes>
    <controlSources>
      <source mode="on" namespace="SpeakImageOverlay.Models.Controls" assembly="SpeakImageOverlay" prefix="overlay"/>
    </controlSources>
  </sitecore>
</configuration>
The fieldType you'll notice is the ImageWithOverlayField class we created. The source namespace/assembly is where the ImageWithOverlay class lives. Do you remember back to the first step in the core database where we set our Control field to overlay:ImageWithOverlay? Hopefully you'll see now that "overlay" is the source prefix, and "ImageWithOverlay" is the field name.
Ok at this point you should be able to work with your new field in Sitecore! The Overlay button won't do anything, but you should see your overlay image and be able to set the main image as usual. Let's bind everything up using Glass Mapper so we can use it on the frontend as well.
I'm just using a basic Glass Mapper setup, so in GlassMapperScCustom.cs, in CreateResolver() where the container is being created, let's add the line: container.Register(Component.For().ImplementedBy().LifeStyle.Transient);. If you don't have a container set up, have a look into using the Glass.Mapper.Sc.CastleWindsor (make sure it's a compatible version). This will use our ImageOverlayMapper where possible to map our images. The code for this is pretty straightforward. I found it easier to copy some code from SitecoreFieldImageMapper.cs rather than extending it.
public class ImageWithOverlay : Glass.Mapper.Sc.Fields.Image
    {
        public virtual string OverlayCoordinates { get; set; }
    }
using System;
using Fields;
using Glass.Mapper.Sc;
using Glass.Mapper.Sc.Configuration;
using Glass.Mapper.Sc.DataMappers;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;

public class ImageOverlayMapper : AbstractSitecoreFieldMapper
{
    public ImageOverlayMapper()
        : base(typeof(ImageWithOverlay))
    {
    }
    
    public override object GetField(Field field, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
    {
        ImageWithOverlay img = new ImageWithOverlay();
        ImageWithOverlayField sitecoreImage = new ImageWithOverlayField(field);

        SitecoreFieldImageMapper.MapToImage(img, sitecoreImage);
        img.OverlayCoordinates = sitecoreImage.OverlayCoordinates;

        return img;
    }
    
    public override void SetField(Field field, object value, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
    {
        ImageWithOverlay img = value as ImageWithOverlay;
        if (field == null || img == null)
        {
            return;
        }

        var item = field.Item;

        ImageWithOverlayField sitecoreImage = new ImageWithOverlayField(field);

        SitecoreFieldImageMapper.MapToField(sitecoreImage, img, item);
        sitecoreImage.OverlayCoordinates = img.OverlayCoordinates;
    }
    
    public override string SetFieldValue(object value, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
    {
        throw new NotImplementedException();
    }
    
    public override object GetFieldValue(string fieldValue, SitecoreFieldConfiguration config, SitecoreDataMappingContext context)
    {
        Item item = context.Service.Database.GetItem(new ID(fieldValue));

        if (item == null)
        {
            return null;
        }

        MediaItem imageItem = new MediaItem(item);
        ImageWithOverlay image = new ImageWithOverlay();
        SitecoreFieldImageMapper.MapToImage(image, imageItem);
        image.OverlayCoordinates = Constants.OverlayDefaultCoordinates;
        return image;
    }
}
Now we can just use our ImageWithOverlay (the one which extends Glass.Mapper.Sc.Fields.Image) in our model and on our page!
@using Glass.Mapper.Sc.Web.Mvc
@model SpeakImageOverlay.Models.IPage

@{
 string[] coordinates = SpeakImageOverlay.Models.Constants.OverlayDefaultCoordinates.Split(',');
 if (Model != null &amp;&amp; Model.PageImage != null &amp;&amp; !string.IsNullOrEmpty(Model.PageImage.OverlayCoordinates))
 {
 coordinates = Model.PageImage.OverlayCoordinates.Split(',');
 }
}

<div style="position:relative;">
 @Html.Glass().RenderImage(Model, m =&gt; m.PageImage, new { style = "position: absolute; max-width: 100%;" }, true)
 <img src="~/Content/images/overlay.png" style="position:absolute;width:50px;top:@coordinates[1]px;left:@coordinates[0]px;" />
</div>
In the next post I'll run through how to add the SPEAK component to set the coordinates of our overlay image.