Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
Experience Sitecore! | All posts by martin

Experience Sitecore!

Martin Miles on Sitecore

SUGCon 2017 insights and takeaways

I have attended SUGCon in Amsterdam this year, as usual - it was a blast! What I like Sitecore events for - they always have such an enthusiastic atmosphere of hundreds of best technology professionals united together "on the same boat".

This time I was proud to announce the winners of the community award - The Most Productive Sitecore Authors of 2016. Three luxury looking trophies found their owners, well-deserved guys!


Anyway, I have got few thoughts/takeaways from that wonderful event.

Sitecore Helix has become a proven and reliable development approach for Sitecore already. The more complex your solution is - the more likely you'd use Helix. I am now working on a multinational and multi-project implementation for an insurance service provider and we are using Helix. Given that we have also a distributed development teams, it became crucial that we are doping the development under the same guidelines and each new team member, already familiar with Helix and SOLID principles, becomes productive quite quickly. Also deployment, quality assurance as well as the rest of day-to-day activities - are going to the same standards. My own opinion on Helix is that "if it is cooked well - it serves you well"

My greatest impression after attending SUGCon was after I saw how Mark Stiles combined Microsoft Cognitive Services with Sitecore. The future is already here!

SXA and Sitecore Commerce are also two going trends. As you know, Sitecore has purchased Commerce Server and has now had intensively committed to the product. Sitecore Experience Accelerator is a different beast, that would best suit for large brands with multisite implementations, that simplifies governance and maintainability of them.

Sitecore PaaS becomes closer and closer to the real world. I'd personally not use it now, due to many (not yet sorted) incompatibilities, but the pace is impressing. Christof Claessens has presented about the way Sitecore works on Azure PaaS and relevant modules compatibilities.

Stephen Pope presented Sitecore Publishing Service 2.0 - an app written with .NET core that now increases publishing performance dramatically. The idea behind service is that it separates publishing process from the Sitecore UI by creating standalone service with a publishing queue, that allows aligning publishing load.

Nick Hills from True Clarity gave a great portion of insights on how Sitecore DevOps on AWS and personalisation are done for such a large implementation as for EasyJet.

JavaScript Service (or simply JSS) - another great impression was the last event at SUGCon, presented by Alex Shyba and Adam Weber. Smoothless javascript and front-end stuff working with node.js and Sitecore layouts will bring an excellent user experience. However, that was more a demo of the proof of concept and team needs to complete the development.

There was much more, but this is what comes to my mind (and from the notes) for the moment...

And finally, those 250+ of lucky to become Sitecore MVPs of 2017 were awarded a symbol of commitment to the Sitecore and its community:

Helix + Glass Mapper + T4 Templates = Code Generation

The objective of given exercise is to achieve automatic code generation of Glass Mapper model (an interface) from a corresponding serialized interface template.

First of all, you need to install Visual Studio Extension for T4 templates: for Visual Studio 2017 or 2015. Also very recommend installing Devart T4 Editor - a markup highlighter and intellisense for T4 templates, that is also a plugin for Visual Studio 2017 or 2015.

Another prerequisite is Glass Mapper itself, obviously. I would advise going with BoC.Glass.Mapper.Sc by Chris Van Steeg from Sitecore NuGet repository. The fork contains several fixes for code-first and content search usage (project fork GitHub page).

The main thing you need is Sitecore CodeGenerator, located in https://github.com/hermanussen/sitecore.codegenerator

You'll also need to have "base" templates - GlassGenerator.tt and GlassMappedClassTemplate.tt as below:


SitecoreTemplates.tt - is the template you are going to copy to each module where you may need to generate models, you may want to rename each instance of it to reflect module name.

Once copied, just paste a list of your interfaces templates into SitecoreTemplates.tt (or WhateverYouCallIt.tt, in the example below I called that same as a module name) and save the file, or optionally right-clicking "SitecoreTemplates.tt" and choosing "Run Custom Tool". You'll see you interfaces magically generated as the child items of *.tt file, with all the references set up (in my case ILink.gen.cs and ILinkMenuItem.gen.cs)

NavigationTemplates.tt (from Feature/Navigation module):

<#@ template language="C#" debug="True" #>
<#@ output extension="gen.txt" #>
<#@ include file="T4Toolbox.tt" #>
<#@ include file="$(SolutionDir)tools\CodeGeneration\T4.Templates\base\GlassGenerator.tt" #>
<#
	GlassGenerator generator = new GlassGenerator(
			"master",
			new [] { 
					"/sitecore/templates/Feature/Navigation/_Link",
					},
			(fieldId, fieldOptions) =>
				{
				});
    generator.Run();
    
	WriteLine("These files were generated:");
#>



I am attaching these TT files for your convenience below:

GlassGenerator.tt (3.6KB), GlassMappedClassTemplate.tt (3.8KB) and SitecoreTemplates.tt (1020B).

If you need to change the way how the code is being generated please consider modifying "GlassGenerator.tt" and "GlassMappedClassTemplate.tt" file located in "Configuration\CodeGeneration\Templates\Base\" solution folder.


Notes: you may have Sitecore.CodeGenerator as a part of your solution, or unload from it - code generation still would work, just please make sure template files are referencing the right paths. Samples provided above are configured as at the image above.


Helix: An approach for restoring WebRoot to an initial state of vanilla Sitecore installation

I have an interesting approach to organizing work with Helix. Imagine a quite standard situation, when you have an already running solution with a code base outside web folder, into which gulp script deploys into. Actually, default Helix scenario.

So, you've been working on some module, then halfway down you've renamed that module or simply decided to remove it from solution. After changes finishing with renaming /deleting, and following redeploying the solution, you may potentially get an error (or not get it), but in any case, sooner or later you find out that your web folder still has an old module in a previous stage, dll and configs. To fix that, a DLL of that module needs to be deleted from bin folder manually, as well as all the corresponding old config files from App_Config/Include folder. Not a nice approach...

Things get even worse if you continuously repeat that delete/rename exercise with other modules, increasing chances of malfunction or false positive, and the far you go with that - the more complicated is to clean up the ends. Can you imagine how many unwanted trails are left in your web folder?

For sanity purpose, it is the best practice to ensure that your code is deployed into a clean copy of Sitecore at webroot so that you can be sure that it functions as expected (or not). So how would you achieve obtaining an exactly the same clean Sitecore as you have installed initially, and make this procedure super quick and repeatable? In case you are working on Sitecore previous to version 9 and have used SIM tool in order to install your Sitecore instance, then the answer is obvious - just use SIM backup / restore for that Sitecore instance, quick and reliable. But what if not? What if you are using the latest Sitecore 9?

The method I am suggesting may be sort of arguably, however, it does the job perfectly, quickly and keeps things consistent and at the same place. What I do - I create an isolated git branch called SitecoreFiles_CM at the same git repository where the main codebase sits, and commit an initial state of Sitecore to that branch. Saying initial I mean exactly the same files as they were installed, before Sitecore was even ever accessed - a clean installation. Of course, not all the files are pushed to remote: things like configurations with my individual (for my local dev instance) passwords do not need to be shared with other developers as well as Sitecore license file. That's why on top of that branch that is pushed to remote, I create one more local commit having these individual settings/changes that I do not push. When I have a need to restore vanilla Sitecore, I navigate to web folder and restore folder to that commit. The next step is git clean command required to get rid of the rest of unversioned files on top of vanilla Sitecore that were deployed from the codebase. As simple, as functional.

  git reset --hard
  git clean -fdx


Both codebase and the corresponding vanilla Sitecore are kept together at the same git repo, for consistency.

Additional benefit: with git diff, you may always see the difference between current web folder state and initial clean install. Not to say, that you can restore each individual file from that set!

When doing platform version upgrades, I upgrade my major codebase along with SitecoreFiles_CM branch, so they again remain consistent Looking ahead, no one stops you from having also a SitecoreFiles_CD branch with a CD set of files, however that is outside of current discussion.

Hope this helps!

Creating "Change item ID" module. Part 1 - Identifying the requirements

I am working on a bit of unusual solution at he moment and one of the "features" I was surprised to own was and external feed of data that serves me the content in various formats including GUIDs of items existing in my Sitecore database.Since feed is not well flexible, I decided to become flexible myself and one of challenges I rose for myself was changing IDs of existing items in Sitecore.

As you know, ID is read-only property of Item in Sitecore, it is being generated upon item's creation and remains the same for entire lifetime. Driven by curiosity, I started investigating how to achieve that and decided to look up any existing implantation. What can be similar to changing ID? Of course, that would be Rename feature.

Let's take a look at Rename icon in Content Editor context menu, when right clicking on an item. It is located at /sitecore/content/Applications/Content Editor/Context Menues/Default/Rename and sends the following message

item:rename(id=$Target)

that calls rename command. So why not to create one more command right after Rename that will send message:

item:changeid(id=$Target)

Of course, we need to associate that to a corresponding business class that inherits from Command:

<commands>
  <command name="item:changeid" type="ChangeId.ChangeIdCommand,ChangeId" patch:after="command[@name='item:rename']" />
</commands>

Let's follow Sitecore architectural principles and implement our change ID logic in a form of a pipeline, let's call it uiChangeIdForItem. In that case new ChangeIdCommand wil do nothing else but trigger the pipeline:

[Serializable]
public class ChangeIdCommand : Command
{
    public override void Execute(CommandContext context)
    {
        Assert.ArgumentNotNull(context, "context");
        if (context.Items.Length != 1 || context.Items[0] == null)
        {
            return;
        }

        StartPipeline("uiChangeIdForItem", context.Items[0]);
    }

    public override CommandState QueryState(CommandContext context)
    {
        Assert.ArgumentNotNull(context, "context");
        if (context.Items.Length != 1)
        {
            return CommandState.Disabled;
        }

        Item obj = context.Items[0];
        if (obj.Appearance.ReadOnly || !obj.Access.CanRename() || IsLockedByOther(obj))
        {
            return CommandState.Disabled;
        }

        return base.QueryState(context);
    }

    private static void StartPipeline(string pipelineName, Item item)
    {
        Assert.ArgumentNotNullOrEmpty(pipelineName, "pipelineName");
        Assert.ArgumentNotNull(item, "item");

        NameValueCollection parameters = new NameValueCollection();
        parameters.Add("database", item.Database.Name);
        parameters.Add("id", item.ID.ToString());
        parameters.Add("language", item.Language.ToString());
        parameters.Add("version", item.Version.ToString());

        Context.ClientPage.Start(pipelineName, parameters);
    }
}

Okay, but what would processors our pipeline consist from? To answer that we need to understand how it will be renaming an item and what additional -pre and -post processing steps may occur, passing parameters between steps and fallback scenarios. So let's take into consideration the following concerns:

1. Changing ID cannot be done directly in the code by using Sitecore API, that isn't something that is or will be supported in future. But by using API we can serialize and deserialize an item, so changing ID would be a matter of simply changing several bytes in plain text file. So based on that we already have a consequence of three processors: Serialize ==> ChangeId ==> Deserialize.

2. Deserialization of an item with modified ID will create a new item with new ID that would be an absolute clone of original item rather then updating ID of existing item. That means I can end up having two exact items, so I need to delete the one having original ID. That adds RemoveOriginalItem processor somewhere at the end of pipeline, when the rest is done.

3. To obtain new item I would need it to get from somewhere, so let's ask a user to provide that by raising a popup input box from UI. That adds an additional processor, let's call it GetNewName.

4. Changing ID is a dangerous task so a user should clearly realise what he/she is doing and understand consequences, that should be ideally an admin or at least a developer. So we should not allow to perform this operation to everyone but should check whether a user has appropriate permissions. Of course, permissions can be set externally from Security Editor but that's only UI and can be bypassed by sending the same command message from JavaScript or developers tools in browser. That's why it is essential to verify permissions at the backend prior to doing anything else, so that results in CheckPermissions processor in the beginning of pipeline.

5. Items in SItecore are organised hierarchically so if we change ID of an item by deserializing it into a new clone item, children still remain under original item that we are right about to delete that will remove children as well. So to correct that we need to explicitly move all the descendants under a new item. That can become an addtional MoveChildren processor after deserialization process.

6. Also if original item was referenced by any other items, those references will become broken once delete original item will be deleted so need to implement UpdateReferences processor. If there are many references to an item it may take a considerable time to update them all, so in user-friendly manner we need to prompt that and ask whether a user still want to process. Why not a candidate for yet another CheckNumberOfReferences processor?

Combining 10 above processors together in the right order makes will look as below:

<processors>
  <uiChangeIdForItem>
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="CheckPermissions" />
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="VerifyUnsupportedTypes" />
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="GetNewName" />
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="CheckLinks" />
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="Serialize" />
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="ChangeId" />
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="Deserialize" />
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="CopyTemplateChildren" />
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="RepairLinks" />
    <processor mode="on" type="ChangeId.ForItem,ChangeId" method="RemoveSourceItem" />
  </uiChangeIdForItem>
</processors>

So far, so good! Once we mentioned all the requirements and are certain about the steps, let's do the actual implementation in second part of this blog post.

Creating "Change item ID" module. Part 2 - The implementation

In the first part of his blog post we have identified the requirements, designed the consequence of uiChangeIdForItem pipeline processors, as well as created necessary Sitecore plumbing for that. So now let's focus on actual business logic and processors implementation.

The implementation of processors:

  1. CheckPermissions
  2. VerifyUnsupportedTypes
  3. GetNewName
  4. CheckNumberOfReferences
  5. Serialize
  6. ChangeId
  7. Deserialize
  8. MoveChildren
  9. UpdateReferences
  10. RemoveOriginalItem


1. CheckPermissions
Let's check permissions first. I borrowed this processor from uiRename pipeline and slightly modified that.
public virtual void CheckPermissions(ClientPipelineArgs args)
{
    Assert.ArgumentNotNull(args, "args");
    if (!SheerResponse.CheckModified())
    {
        return;
    }

    Item obj = GetItem(args);
    if (obj == null)
    {
        HandleItemNotFound(args);
    }
    else
    {
        if (!obj.Access.CanRename() || obj.Appearance.ReadOnly)
        {
            Context.ClientPage.ClientResponse.Alert(Translate.Text("You do not have permission to change ID \"{0}\".", obj.DisplayName));
            args.AbortPipeline();
        }

        if (Context.IsAdministrator || !obj.Locking.IsLocked() || obj.Locking.HasLock())
        {
            return;
        }

        Context.ClientPage.ClientResponse.Alert(Translate.Text("You cannot edit this item because '{0}' has locked it.", obj.Locking.GetOwnerWithoutDomain()));
        args.AbortPipeline();
    }
}

2. VerifyUnsupportedTypes
At this stage I want to disallow changing IDs for template fields. By that moment I haven't resolved how to change fields accurately, so will avoid that for now,
public virtual void VerifyUnsupportedTypes(ClientPipelineArgs args)
{
    Assert.ArgumentNotNull(args, "args");

    Item item = GetItem(args);
    if (item == null)
    {
        HandleItemNotFound(args);
    }
    else
    {
        if (item.TemplateID == new ID("{455A3E98-A627-4B40-8035-E683A0331AC7}") || item.Template.Name == "Template field")
        {
            Context.ClientPage.ClientResponse.Alert("You cannot modify ID of template field, but you may do that for template itself");
            args.AbortPipeline();
        }
    }
}

3. GetNewName
Asking user to provide new ID in the popup input box and validate that input value.
public virtual void GetNewName(ClientPipelineArgs args)
{
    Assert.ArgumentNotNull(args, "args");

    string header = Translate.Text("Change ID");
    if (args.IsPostBack)
    {
        if (string.IsNullOrEmpty(args.Result) || args.Result == "null" || args.Result == "undefined")
            args.AbortPipeline();

        else if (args.Result.Trim().Length == 0)
        {
            Context.ClientPage.ClientResponse.Alert("The name cannot be blank.");
            Context.ClientPage.ClientResponse.Input("Enter new ID for the item:", string.Empty,
                ID_FORMAT, "'$Input' is not a valid ID.", ID_LENGTH, header);

            args.WaitForPostBack();
        }
        else
        {
            Item obj = GetItem(args);

            if (obj == null)
            {
                HandleItemNotFound(args);
            }
            else
            {
                Context.ClientPage.ServerProperties["NewID"] = args.Result;
                args.IsPostBack = false;
            }
        }
    }
    else
    {
        Item obj = GetItem(args);
        if (obj == null)
        {
            HandleItemNotFound(args);
        }
        else
        {
            Context.ClientPage.ClientResponse.Input("Enter new ID for the item:", obj.ID.ToString(), ID_FORMAT, 
                "'$Input' is not a valid ID.", ID_LENGTH, header);

            args.WaitForPostBack();
        }
    }
}

4. CheckNumberOfReferences
This processor is not mandatory and presents for the sake of user experience. 
public virtual void CheckNumberOfReferences(ClientPipelineArgs args)
{
    Assert.ArgumentNotNull((object)args, "args");
    if (args.IsPostBack)
    {
        if (args.Result == "yes")
        {
            return;
        }

        args.AbortPipeline();
    }
    else
    {
        Item obj = GetItem(args);
        if (obj == null)
        {
            HandleItemNotFound(args);
        }
        else
        {
            string checkLinksMessage = GetCheckLinksMessage(obj);
            if (string.IsNullOrEmpty(checkLinksMessage))
            {
                return;
            }

            SheerResponse.Confirm(checkLinksMessage);
            args.WaitForPostBack();
        }
    }
}
GetCheckLinksMessage is calculating number of references for the item we are changing ID for and prompts that operation may take a longer time once more than 100 references are found,
private static string GetCheckLinksMessage(Item item)
{
    Assert.ArgumentNotNull(item, "item");
    string str = string.Empty;
    if (GetLinks(item) > 100)
    {
        str = Translate.Text("This operation may take a long time to complete.\n\nAre you sure you want to continue?");
    }

    return str;
}

5. Serialize
That is where our main logic begins. Let's serialize original item first using Sitecore API so by default it will write under /Data/serialization folder outside web root.
public virtual void Serialize(ClientPipelineArgs args)
{
    Assert.ArgumentNotNull(args, "args");

    Item item = GetItem(args);

    if (item == null)
    {
        HandleItemNotFound(args);
    }
    else
    {
        try
        {
            Serializer.SerializeItem(item);
        }
        catch (Exception e)
        {
            Context.ClientPage.ClientResponse.Alert($"Failed to serialize item: {item.DisplayName}");
            args.AbortPipeline();
        }
    }
}

And Serializer class is quite simple static class:

public static class Serializer
{
    public static void SerializeItem(Item item)
    {
        if (item != null)
        {
            Manager.DumpItem(item);
        }
    }

    public static void RestoreItem(string serializedItemPath, Database database)
    {
        var options = new LoadOptions(database) {ForceUpdate = true};

        using (new Sitecore.SecurityModel.SecurityDisabler())
        {
            Manager.LoadItem(serializedItemPath, options);
        }
    }
}

6. ChangeId
Then need to locate serialized file and modify it. In order to open I am again relying on Sitecore API by calling PathUtils.GetFilePath(new ItemReference(item)) method so it will return me the same path item was previously serialized. Then I read the file and simply replace old ID to the new ID and save it back. 
public virtual void ChangeId(ClientPipelineArgs args)
{
    Assert.ArgumentNotNull(args, "args");

    string newId = StringUtil.GetString(Context.ClientPage.ServerProperties["NewID"]);
    if (string.IsNullOrEmpty(newId) || newId == "null" || newId == "undefined")
    {
        return;
    }

    var item = GetItem(args);

    if (item == null)
    {
        HandleItemNotFound(args);
    }
    else
    {
        try
        {
            string serializedItemPath = GetSerializationPath(item);

            if (File.Exists(serializedItemPath))
            {
                string text = File.ReadAllText(serializedItemPath);

                text = text.Replace(item.ID.ToString(), newId);

                File.WriteAllText(serializedItemPath, text);
            }
        }
        catch (Exception e)
        {
            Context.ClientPage.ClientResponse.Alert("Cannot find serialized file");
            args.AbortPipeline();
        }
    }
}

7. Deserialize
Deserialize file back with modified ID. Sitecore allows multiple items with the same name at the same level so it will end up having two exactly the same items next to it other with new and old IDs.
public virtual void Deserialize(ClientPipelineArgs args)
{
    Assert.ArgumentNotNull(args, "args");

    Item item = GetItem(args);

    if (item == null)
    {
        HandleItemNotFound(args);
    }
    else
    {
        try
        {
            string serializedItemPath = GetSerializationPath(item);
            Serializer.RestoreItem(serializedItemPath, item.Database);
        }
        catch (Exception e)
        {
            Context.ClientPage.ClientResponse.Alert($"Failed to deserialize item: {item.DisplayName}");
            args.AbortPipeline();
        }
    }
}

8. MoveChildren
Since items can have children, the next step obviuosly would be to relocate all children recursively from underneath old item to become the children of a new item. Than especially makes sense when you're modifying a template item.
public virtual void MoveChildren(ClientPipelineArgs args)
{
    Assert.ArgumentNotNull(args, "args");
    var item = GetItem(args);
    if (item == null)
    {
        HandleItemNotFound(args);
    }
    else
    {
        try
        {
            //if (item.Paths.FullPath.StartsWith("/sitecore/templates"))
            {
                string newId = StringUtil.GetString(Context.ClientPage.ServerProperties["NewID"]);
                var newIdItem = item.Database.GetItem(new ID(newId));

                var children = item.Children;
                foreach (Item child in children)
                {
                    child.MoveTo(newIdItem);
                }                            
            }
        }
        catch (Exception e) 
        {
        }
    }
}

9. UpdateReferences
Apart from having child items, an item can be referenced by other items, the entire data relationship in Sitecore is built on that principle. The good news is that in recent versions of Sitecore references are GUIDs and no more paths. So what is now important - to go through all the referrers and update them with a GUID of new item. Remember step 4 where we made CheckNumberOfReferences processor that counted refs and prompted if there are too many? Those are the links to be modified at this stage:
public virtual void UpdateReferences(ClientPipelineArgs args)
{
    Assert.ArgumentNotNull(args, "args");

    var item = GetItem(args);
    if (item == null)
    {
        HandleItemNotFound(args);
    }
    else
    {
        try
        {
            string newId = StringUtil.GetString(Context.ClientPage.ServerProperties["NewID"]);
            Relink(item, newId);
        }
        catch (Exception e)
        {
            Context.ClientPage.ClientResponse.Alert("Error while updating links: " + e.Message);
            args.AbortPipeline();
        }
    }
}

10. RemoveOriginalItem
And finally, when everything else is done and we've got new item with all children and references modified. So it is good now o delete an original item
public virtual void RemoveOriginalItem(ClientPipelineArgs args)
{
    Assert.ArgumentNotNull(args, "args");

    Item item = GetItem(args);

    if (item == null)
    {
        HandleItemNotFound(args);
    }
    else
    {
        try
        {
            using (new SecurityDisabler())
            {
                item.Delete();
            }
        }
        catch (Exception e)
        {
            Context.ClientPage.ClientResponse.Alert("Error deliting initial item");
            args.AbortPipeline();
        }
    }
}  

There will be also final part of this blog post later that will add unit testing, sum up the results and highlight what can be done to make it better.

I will ask readers to forgive me at this stage for not giving a package with ChangeId module - haven't it properly tested yet. However anyone curious can clone it from GitHub repo and give a try yourself.

Know your tools: The easiest way to install Habitat - Habitat Solution Installer

Working with Helix often encourages you to perform quick look-ups into the live-standard-implementation - Habitat project. That's why you have to have it installed. I remember the first time I installed Sitecore Habitat in October 2015 and how complicated that seemed at glance.

Luckily we now got a nice tool, that does exactly what it is named for - Habitat Solution Installer written by Neil Shack. So, let's go ahead and install Habitat into a new non-traditional destination using that tool.

Firs of all, let's grab Habitat Solution Installer itself from Sitecore Marketplace. Once downloaded, run HabitatInstaller.exe.


First screen takes three most important Habitat setting that we usually need to change as well as asks for the solution root folder where it will install the code. Once Install is clicked - it will download an archive of master branch from Habitat GitHub repository.


Then it will extract downloaded archive into temporal folder. By the way, you may alternate both path to master archive and your temporal files folder by clicking Settings button on the first (main) screen.


After extracting files, it will run npm install so you need to have node installed as a prerequisite.


Once finished, Habitat Solution Installer will display confirmation box.


So, what it has done - it installed and configured project code folder. But what hasn't it done?

1. It does not install Sitecore. You need to have it installed as another prerequisite, so that you provide Sitecore web folder and hostname to installer as shown on first screenshot. The best way would be to install using SIM (marketplace link). While installing Sitecore, make sure you're installing the right version corresponding to to codebase at Habitat master branch, you may look it up at Habitat Wiki page.

2. Not just to say you need to install Sitecore itself, you also need to install Web Forms for Marketers of the version corresponding to you Sitecore instance. And to ensure WFFM installation not failing, you need to install MongoDB prior to. Luckily that can be done in one click using SIM:


Finally, when all above is done, you may run gulp tasks from Task (View => Other Windows => Task Runner Explorer in Visual Studio 2015). Since npm install was already done for you - tasks are loaded as normal:


That's it! After Sitecore items are deserialised into your Sitecore instance, you'll be able to run Habitat website (however do not forget to publish from master to web unless you run it in live mode). The final result comes in you browser:


Know your tools: The easiest way to add a new project into your Helix solution

UPDATE: just came across a better solution from kamsar, that generates code, serialization and tests for the module. Please use that one instead of described below, however, the principle described below is exactly the same so you may refer for the sake of understanding.

---------------------------- original text below ----------------------------

I have found a great solution that makes adding a new project into existing Helix / Habitat solution so transparent and painless - Prodigious Helix Generator.

The guidance is well done and describes all thee steps. Is is soooo easy, and removes pain of fixing some forgotten references / namespaces / etc after you accidentally found out. It also optionally creates TDS project skeleton for your module, once you need to serialise items (why not to add Unicorn in addition to?).

So, to make it work you need to install yeoman first and afterwards the generator tool itself:

npm install -g yo
npm install -g generator-prodigious-helix

One important thing to mention is that not to mess with the paths and to get new module generated under the right path - run it at the root of your solution / repo.

You are at a choice of 3 options. In order to add a foundation, type in:

yo prodigious-helix:foundation

to make a feature:

yo prodigious-helix:feature

and for project even simpler:

yo prodigious-helix

As for example, below I am creating a new foundation module called AdminTools for the solution named HelixSolution. That will result in creating me a project called HelixSolution.Foundation.AdminTools within a same name folder (created) at the right path under Foundation folder. All the namespaces, references, settings and naming conventions would be auto-generated and correct!


After answering those 3 questions, you'll get your stuff generated:


That's it!

The only thing I noticed it that it appends solution name under App_config folder: App_Config\Include\HelixSolution\Feature\Feature.AdminTools.config so depending on your setup you may need to move Feature\Feature.AdminTools.config one level up into Include folder.

Hope it will start saving your time as much as it saves mine.

Migrating existing code to Helix. Fixing invalid dynamic placeholders

Recently I have inherited a project that utilised dynamic placeholder in a weird way:

public static HtmlString DynamicPlaceholder(this SitecoreHelper helper, string placeholderKey)
{
    if (string.IsNullOrEmpty(RenderingContext.Current.Rendering?.DataSource))
        return helper.Placeholder(placeholderKey);

    var currentRenderingId = Guid.Parse(RenderingContext.Current.Rendering.DataSource);
    return helper.Placeholder($"{placeholderKey}_{currentRenderingId}");
}

instead of more commonly used way suggested by Jason Bert:

namespace DynamicPlaceholders.Mvc.Extensions
{
	public static class SitecoreHelperExtensions
	{
		public static HtmlString DynamicPlaceholder(this SitecoreHelper helper, string placeholderName)
		{
			string text = PlaceholdersContext.Add(placeholderName, RenderingContext.Current.Rendering.UniqueId);
			return helper.Placeholder(text);
		}
	}
}

In two words, the first approach generates placeholder name using an ID of datasource item. The second approach is more traditional and is default for Helix. It was suggested by Jason Bert and also utilised by sitecore - that relies on rendering's UniqueID. In both cases, dynamic placeholders look like: /content/name-of-placeholder_017c3643-0fef-475c-95d2-bb1107beb664.

So I need to update it across entire solution. Let's iterate our items.

First of all, I do not need to go through all of item, but only those that are pages and have presentation configured, as I am going to adjust it. Secondly, those items are located under /sitecore/content folder. As many of you should know, presentation details (or deltas) for a page are kept within two fields of Standard Template - Renderings and Final Renderings that correspond to configuration you see at Shared Layout and Final Layout tabs. These thoughts resulted in using following Sitecore query to identify those items:

/sitecore/content//*[@__Renderings != '' or @__Final Renderings != '']

So far, so good. Also need to mention that I will perform the operation for master database only and for Default device:

private const string DatabaseName = "master";
private const string DefaultDeviceId = "{FE5D7FDF-89C0-4D99-9AA3-B5FBD009C9F3}";

I am using Sitecore presentation API in order to achieve my goal. Below is my Iterate() method:

public Dictionary<Item, List<KeyValuePair<string, string>>> Iterate()
{
    var result = new Dictionary<Item, List<KeyValuePair<string, string>>>();

    var master = Factory.GetDatabase(DatabaseName);
    var items = master.SelectItems(ItemsWithPresentationDetailsQuery);

    var layoutFields = new[] {FieldIDs.LayoutField, FieldIDs.FinalLayoutField};

    foreach (var item in items)
    {
        foreach (var layoutField in layoutFields)
        {
            var changeResult = ChangeLayoutFieldForItem(item, item.Fields[layoutField]);

            if (changeResult.Any())
            {
                if (!result.ContainsKey(item))
                {
                    result.Add(item, changeResult);
                }
                else
                {
                    result[item].AddRange(changeResult);
                }
            }
        }
    }

    return result;
}

That method iterates through each item from returned from the query and calls ChangeLayoutFieldForItem for that item twice - for both presentation fields Renderings and Final Renderings.

private List<KeyValuePair<string, string>> ChangeLayoutFieldForItem(Item currentItem, Field field)
{
    var result = new List<KeyValuePair<string, string>>();

    string xml = LayoutField.GetFieldValue(field);

    if (!string.IsNullOrWhiteSpace(xml))
    {
        LayoutDefinition details = LayoutDefinition.Parse(xml);

        var device = details.GetDevice(DefaultDeviceId);
        DeviceItem deviceItem = currentItem.Database.Resources.Devices["Default"];

        RenderingReference[] renderings = currentItem.Visualization.GetRenderings(deviceItem, false);

        var datasourceGuidsToRenderingUniqueIdMap = renderings
            .Where(r => !string.IsNullOrWhiteSpace(r.Settings.DataSource))
            .Select(r => new KeyValuePair<string, string>(Guid.Parse(r.Settings.DataSource).ToString(), r.UniqueId));

        if (device?.Renderings != null)
        {
            foreach (RenderingDefinition rendering in device.Renderings)
            {
                if (!string.IsNullOrWhiteSpace(rendering.Placeholder))
                {
                    var verifiedPlaceholderKey = FixPlaceholderKey(rendering.Placeholder, datasourceGuidsToRenderingUniqueIdMap);
                    result.Add(new KeyValuePair<string, string>(rendering.Placeholder, verifiedPlaceholderKey));
                    rendering.Placeholder = verifiedPlaceholderKey;
                }
            }

            string newXml = details.ToXml();

            using (new EditContext(currentItem))
            {
                LayoutField.SetFieldValue(field, newXml);
            }
        }
    }

    return result;
}

Further down it creates a list of matches for all the renderings within current presentation field of that particular item - datasource GUIDs matching to unique rendering IDs to go through it and do a replacement - that is done in FixPlaceholderKey method. Once everything is replaced, save the field by calling LayoutField.SetFieldValue() of course wrapping that call with EditContext as we are modifying item's field value.

FixPlaceholderKey is a simple method that just does case insensitive replacement by using Regex:

private string FixPlaceholderKey(string renderingInstancePlaceholder, IEnumerable<KeyValuePair<string, string>> map)
{
    var value = renderingInstancePlaceholder;

    foreach (var oldValue in map)
    {
        value = Regex.Replace(value, oldValue.Key, Guid.Parse(oldValue.Value).ToString(), RegexOptions.IgnoreCase);
    }

    return value;
}

That is pretty everything about the dynamic placeholder fixing logic.

But as for my case - I was aware that I'll need to run this dynamic placeholder replacements few more times in future and will likely need to implement some other similar small tools. So I decided that it would be great to place it under /sitecore/admin folder along with other admin tools - exact location by purpose! And since we're now going Helix, I decided to follow good principles and created a foundation module called AdminTools, where I will be adding similar admin folder tools. So here's how it looks for me in the Solution Explorer:


FixDynamicPlaceholders.aspx is a classical ASP.NET WebForm with one line markup (ah-h, I was so lucky not to deal with Webforms for couple past years till the moment)

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="FixDynamicPlaceholders.aspx.cs" Inherits="HomeServeUsa.Foundation.AdminTools.sitecore.admin.Maintenance.FixDynamicPlaceholders" %>

and the codebehind. Since FixDynamicPlaceholders.aspx is admin tool page - codebehind is inherited from Sitecore.sitecore.admin.AdminPage:

public partial class FixDynamicPlaceholders : AdminPage
{
    protected void Page_Load(object sender, EventArgs e)
    {
        var fixRenderings = new DynamicPlaceholdersModifyService();
        var result = fixRenderings.Iterate();

        OutputResult(result);
    }

    private void OutputResult(Dictionary<Item, List<KeyValuePair<string, string>>> result)
    {
        Response.ContentType = "text/html";

        Response.Write($"<h1>{result.Count} items processed</h1>");
        foreach (var pair in result)
        {
            Response.Write($"<h3>{pair.Key.Paths.FullPath}</h3>");

            foreach (var kvp in pair.Value)
            {
                if (kvp.Key != kvp.Value)
                {
                    Response.Write($"<div>{kvp.Key} ==> {kvp.Value}</div>");
                }
            }
        }
    }
}

Finally, it is complete. Hope this helps someone!

P.S. of course, that would be easier (and more elegant) to perform with Sitecore PowerShell extension. But unfortunately I am the only person in the organisation who uses it regardless of my promotional activity.

How to make Content Editor search by plain GUIDs without braces and dashes? (part 2)

In the first part of this story I demonstrated how to create an extra search pipeline processor to search by plain GUIDs (those without dashes and curly braces). Now it a good time to achieve the same functionality by another approach.

So, how actually values are coming into search pipeline? While debugging I came across the method TreeSearch_Click - the one was top method from backend call stack (Sitecore.Client.dll) so I tried to find where on frontend it is being called from.

It was called from <WEB_ROOT>/sitecore/shell/Applications/Content Manager/default.aspx file, right by inline script:

javascript:if(event.keyCode==13){var result = scForm.postEvent(this,event,'TreeSearch_Click',true);scContent.fixSearchPanelLayout();return result;}

That code runs by hitting enter on search textbox, there's also similar handler for clicking magnifier icon:

javascript:var result = scForm.postEvent(this,event,'TreeSearch_Click',true);scContent.fixSearchPanelLayout();return result;

In both cases we are POST-ing javascript this that refer to a calling element. So, obviously, if somehow intercept that value - it will be possible to do manipulation on it right on the DOM, and changes would be reflected by UI immediately. That is something missing at part one of this story. Doing that before call to postEvent will post already updated value.

So I ended up with modifying default.aspx by adding a pre-post function to check whether the value is a plain GUID and format it appropriately in that case (I admit I am not the best frontend developer in the world so that implementation could be way better):

onkeydown="javascript:if(event.keyCode==13){ verifyPlainGuid(this); var result = scForm.postEvent(this,event,'TreeSearch_Click',true);scContent.fixSearchPanelLayout();return result;}"

and for icon handler

onclick="javascript:verifyPlainGuid(this); var result = scForm.postEvent(this,event,'TreeSearch_Click',true);scContent.fixSearchPanelLayout();return result;

calling the following javascript function as defined:

function verifyPlainGuid(parameter)
{

    if (parameter & amp; & parameter.className == "scSearchButton") {

        var input = parameter.parentElement.parentElement.parentElement.parentElement.children[0].children[0];
        substituteRegexIfMatches(input);
    }

    if (parameter.id == "TreeSearch")
    {
        substituteRegexIfMatches(parameter);
    }

    function substituteRegexIfMatches(inputElement) {

        var shortGuidRegexPatterns = '[0-9a-fA-F]{8}[0-9a-fA-F]{4}[0-9a-fA-F]{4}[0-9a-fA-F]{4}[0-9a-fA-F]{12}';

        if (inputElement.value.match(shortGuidRegexPatterns))
        {

            var plainGuid = inputElement.value;

            inputElement.value = "{" + plainGuid.substring(0, 8) + "-" + plainGuid.substring(8, 12) + "-" + plainGuid.substring(12, 16)
            + "-" + plainGuid.substring(16, 20) + "-" + plainGuid.substring(20, 32) + "}";
        }
    }
}

That's it - all the changes and it works even better. So, let's compare part 2 against part 1:

Pros:

  • does not modify either \bin folder or configuration, there's no app pool recycle happening
  • no inclines into pipelines, GUID transformation logic runs only when indeed required, not on each run
  • runs at frontend and do not interfere backend
  • modified GUID is reflected at searchbox immediately

Cons:

  • required modification of original sitecore-shipped file (default.aspx)
  • this file tends to changes rather frequently

For all updates (versions) of Sitecore 8.2 - download a package

If you're on Sitecore 8.1, you may need to manually substitute default.aspx located at <WEB_ROOT>/sitecore/shell/Applications/Content Manager folder:

Sitecore 8.1 update 3 - link
Sitecore 8.1 update 2 - link
Sitecore 8.1 update 1 - link
Sitecore 8.1 initial release - link

P.S. What can be done even better? I think, Sitecore should add this functionality to their future versions, as it covers one of the quite frequently used case for us. Ideally, while searching by short GUID it should return Direct Hit result if the item with such ID exists, along with other results from content search.

Sussex Sitecore UserGroup - 22 March 2017


What's on? Finally I have overcome all hardships on arranging with the venue, sponsorship etc. and am proud to announce the first Sussex Sitecore User Group!

When does it happen? It will take place on Wednesday 22nd of March in Brighton - the heart and largest city of Sussex. One of complexities of finding a meeting place was to find an adequate venue in less than 5 minutes walk from Brighton train station as I realised that most of guests will come from either London, or other parts of Sussex but in any case they will arrive at Brighton station.

How to get there? Brighton is only 1 hour of travel from London Bridge or Victoria station. Direct trains also go there from Kings Cross / St. Pancrass, Blackfriars and Farrington. There will be a train at 17.42 from Victoria, 17.20 from Blackfriars and 17.57 from London Bridge. Please see the train times planner to find appropriate option. And the venue itself is less than 5 minutes of walk from station:


Address: 30 Cheapside, Brighton BN1 4GD (main entrance on Blackman Street)

What is agenda? We are planning to have a traditional Sitecore event with complementary drinks and snacks but with what is more important - very interesting topics plus open discussion.

Any contact? Would you like to present - please PM me on Slack @martin_miles or Twitter @SitecoreMartin.

Please RSVP at the event's MeetUp page so that we have a god idea of numbers.

Looking forward to meet you there!

canlı tv