Sitecore 8: Media URL in Index

I have recently encountered a situation where I needed to index some media items. I am working on a Sitecore 8.2 solution and I have a simple custom web index on SOLR in which the media URL is a computed field. My code for the computed field was as simple as this:

public object ComputeFieldValue(IIndexable indexable)
{
  var item = (Item)(indexable as SitecoreIndexableItem);
  if (item == null)
  {
    return null;
  }

  return MediaManager.GetMediaUrl(item, mediaUrlOptions);
}

However, the results were a bit strange. Some of the index records had in media URL values that began with “/sitecore/shell”. After a bit of research, I found some explanation in this issue on StackOverflow: http://stackoverflow.com/questions/27272832/urllink-in-sitecore-indexs-returns-media-url-with-sitecore-shell-media. In my case the indexing was done from the Indexing Manager, but I still got these mixed results in terms of URLs, where some were relative and some were starting with “/sitecore/shell”. I also checked the Url field which should have been computed automatically in the Sitecore index, but in my case it was always null, so it was not an option. Maybe, as mentioned in the given Sitecore answer to the issue on StackOverflow, they did give up on computing this field for media items.

I followed the advice from the issue and computed all the URLs with Absolute Path set to false:

public object ComputeFieldValue(IIndexable indexable)
{
  var item = (Item)(indexable as SitecoreIndexableItem);
  if (item == null)
  {
    return null;
  }

  var mediaUrlOptions = new MediaUrlOptions
  {
    AbsolutePath = false
  };

  return MediaManager.GetMediaUrl(item, mediaUrlOptions);
}

With the above code, all the media URLs were created correctly, starting with “-/media/”. And when rendering the value of the media URL in an <a href=”@item.MediaUrl“> tag, which was the intended purpose, the media item could be properly downloaded.

Continuous Integration: Move Unicorn Files Outside of Website

In the previous blog post about Unicorn and Continuous Integration, Unicorn Sync: YML File Used by Another Process, I have mentioned some issues about the Unicorn Sync step and a temporary solution that helped us a lot. However, we had to improve the process because, even though we are talking about a DEV server, it is still better not to kill the website in IIS during the deploy.

The solution is to change the Deployment step in Octopus by replacing the Pre-deployment PowerShell script with a Deployment PowerShell script which moves the Unicorn files to a different folder, outside of the website. By doing this during the deploy, the YML files do not get to be used by the w3wp process, so it is possible to move them to a different location. Before we move the files, we ensure that the folder where we move them is empty, thus avoiding old files still being used. Below is the script we use:

$folder = "<Path>\Website\Unicorn"
$newfolder = "<Path>\Data\Unicorn"

if (Test-Path $newfolder) {
    Get-ChildItem -Path $newfolder -Force -Recurse |
    Sort-Object -Property FullName -Descending |
    Remove-Item -Recurse -Force
}

if (Test-Path $folder) {
    Get-ChildItem -Path $folder -Recurse | 
    Move-Item -Destination $newfolder -Force
}

Then, the only thing we need to change is the Unicorn.config file so that the targetDataStore points to the new location.

Sitecore 8: Changing Renderings Programmatically

I am sure that most of you working with Sitecore were at some point in the situation of upgrading a solution. This is not an easy task, especially if the upgrade does not work by simply going through the upgrade steps from a version to the next one. In my case, I am in the process of upgrading a Sitecore 7.0 solution to Sitecore 8.2, and because of the legacy code that we cannot use anymore,  my team and I decided that we will start from scratch with a clean Sitecore 8.2 solution, we will migrate the data using scripts, and we will completely rebuild the presentation layer. The data migration, though not a subject of this post, is done by adding the old master db in the new solution, thus allowing us to programmatically transfer items from the old master to the new master. Our approach did not pose many problems, but I did have to investigate a bit how to reuse components.

In the old solution, there are many components that contain a lot of content, which we don’t want to lose. Migrating the datasource items is not difficult, but we need to redo the relations between them and the components that have to appear on the pages. The old renderings are gone, but we will have new ones in the new solution. To be more specific, if in the old solution, on a page oldPageItem, there is a component with the rendering oldRendering, assigned to a placeholder oldPh and it has the datasource item oldDsItem, then in the new solution, on the page newPageItem with the same ID as oldPageItem, we will have the new rendering newRendering, assigned to the new placeholder newPh, but with the datasource item newDsItem with the same ID as oldDsItem. In other words, we are keeping the content, but completely changing the presentation layer. To do this automatically, I wrote the following piece of code:

/* get the page in the new master db */
var newPageItem = newMasterDb.GetItem(oldPageItem.ID);
if (newPageItem == null)
{
    return;
}

/* get the rendering list of the old page item */
var renderingsList = 
    oldPageItem.Visualization.GetRenderings(Context.Device, true);
if (renderingsList == null)
{
    return;
}

/* get the layout definition and the current device definition 
   from the new page item; we are working on the final layout */
LayoutField layoutField = 
    new LayoutField(newPageItem.Fields[FieldIDs.FinalLayoutField]);
LayoutDefinition layoutDefinition = 
    LayoutDefinition.Parse(layoutField.Value);
DeviceDefinition deviceDefinition = 
    layoutDefinition.GetDevice(Context.Device.ID.ToString());

/* go through all the old renderings and identify the one used on 
   the component that needs to be added on the new page item */
foreach (var rendering in renderingsList)
{
    /* we can identify the rendering by name 
       (we could also do it by ID) */
    if (rendering.RenderingItem.Name.Equals("oldRendering"))
    {
        /* get the datasource ID from the rendering */
        var datasourceId = rendering.Settings.DataSource;
        if (datasourceId != null)
        {
            var oldPlaceholder = rendering.Placeholder;
            var newPlaceholder = string.Empty;
            var newRenderingId = string.Empty;

            /* the same component could be rendered in more 
               than one placeholder, so check the name of 
               the old placeholder;
               if it is a nested placeholder in the main 
               placeholder, we need to include it in the check */
            if (oldPlaceholder.Equals("/oldMainPh/oldPh"))
            {
                /* assign the name of the new placeholder;                    
                   if it is a nested placeholder in the 
                   main placeholder, we need to include it */
                newPlaceholder = "/newMainPh/newPh";
                
                /* assign the GUID of the new rendering; 
                   this has to exist in the new solution */
                newRenderingId = "<newRenderingID>";
            }

            if (string.IsNullOrEmpty(newPlaceholder) || 
                string.IsNullOrEmpty(newRenderingId))
            {
                continue;
            }

            /* create the new rendering definition and add 
               it to the current device definition */
            RenderingDefinition newRenderingDefinition = 
                new RenderingDefinition();
            newRenderingDefinition.ItemID = newRenderingId;
            newRenderingDefinition.Placeholder = newPlaceholder;
            newRenderingDefinition.Datasource = datasourceId;
            
            deviceDefinition.AddRendering(newRenderingDefinition);
        }                             
    }
}

/* update the presentation layer of the new page item 
   with the layout definition obtained above */
using (new SecurityDisabler())
{
    newPageItem.Editing.BeginEdit();
    layoutField.Value = layoutDefinition.ToXml();
    newPageItem.Editing.EndEdit();
}

The above code assumes we are working on a given page item, but we can iterate through all the page items where we need to make these changes. We also assumed that the migrated content items to the new solution keep the same IDs as in the old solution.

Unicorn Sync: YML File Used by Another Process

I am currently working on a Sitecore 8.2 solution, for which we have set up Continuous Integration on a DEV server. This is achieved by using Team City and Octopus, and the deploy is performed upon push to GIT ’develop’ branch. In order to avoid transferring Sitecore items manually via packages, we have set up Unicorn and we have a step in the deploy process that performs the Unicorn Sync. The YML files are stored in a folder outside of the VS solution, but we have a .nuspec file that specifies that this folder should be part of the package for deploy. Then, on the DEV server, the deploy package is extracted under our Website, and this includes also the Unicorn folder.

We chose not to clean the whole Website directory before performing the deploy, but we do need the Unicorn folder to be clean, otherwise obsolete files will not be deleted. In order to do this, we have a Pre-deployment PowerShell script that deletes all the YML files:

$folder = "<Website-Path>\Unicorn"
if (Test-Path $folder) {
    get-childitem -path $folder -recurse | 
       remove-item -force -recurse
}

But sometimes, this script fails to run, causing the entire deploy to fail, and the error is this:

Remove-Item : Cannot remove item <Website-Path>\Unicorn\<Path-To-File>\<Filename>.yml: The process cannot access the file ‘<Filename>.yml’ because it is being used by another process.

From the Sitecore log, we can see that the process using the file is w3wp. This does not happen all the time, and never with the same file. The manual fix for this is to stop the corresponding Application Pool and Site from IIS , delete the file, then start again the Application Pool and the Site. The next run of the deploy process will work, but this is not a fix to the issue. The probable cause for the blocked file is the fact that it is stored under the Website, but configuring the Unicorn to store it outside of the Website folder needs a bit more investigating. The quicker solution that I found was to modify the PowerShell script, so that it stops and starts the Application Pool and the Site that we specify:

$folder = "<Website-Path>\Unicorn"
$appPoolName = "<AppPoolName>"
$websiteName = "<WebsiteName>"

Import-Module WebAdministration
Stop-WebSite $websiteName

if((Get-WebAppPoolState $appPoolName).Value -ne 'Stopped')
{
  Stop-WebAppPool -Name $appPoolName
}

if (Test-Path $folder) {
    get-childitem -path $folder -recurse | 
        remove-item -force -recurse
}

if((Get-WebAppPoolState $appPoolName).Value -ne 'Started')
{
  Start-WebAppPool -Name $appPoolName
}

Start-WebSite $websiteName

No deploys have failed since!

Sitecore 8.1 Geolocation: How to Detect Country by IP

The detection of a visitor’s IP is a very common requirement and there are many tools out there for it. But working with Sitecore prompts us to always look for the recommended IP detection methods, thus, in the past, we were using the MaxMind database for this. In the latest versions, Sitecore makes it easier for us and comes with an integrated module for IP Geolocation: https://doc.sitecore.net/sitecore_experience_platform/81/setting_up__maintaining/ip_geolocation.

Starting with the Sitecore 8.1 initial release, the IP Geolocation service is automatically installed, and it just needs to be enabled. Because it is strongly related to the xDB, the Analytics and Tracking must be enabled too in order for the geolocation to work. After all the setup is done, all we need is the proper code for benefiting from the gathered data. The purpose of this article is to show how to get the visitor’s country based on the detected IP address.

I have created a simple method that takes the IP address from the Analytics Tracker, see below:

private static IPAddress GetIpAddressFromTracker()
{   
    return Tracker.Current != null 
        && Tracker.Current.Interaction != null 
        && Tracker.Current.Interaction.Ip != null 
        ? new IPAddress(Tracker.Current.Interaction.Ip) : null;
}

Then, I created a method that uses the IP address to access the relevant Analytics data and to get the visitor’s country. The result of this method is the ISO code of the country, which should be enough to work with further on:

public static string GetCountryForCurrentIp()
{
    IPAddress ipaddress = GetIpAddressFromTracker();
    string country = string.Empty;

    if (ipaddress != null)
    {
        var geoIpOptions = new GeoIpOptions
        {
            Ip = ipaddress,
            MillisecondsTimeout = 1000,
            Id = GeoIpManager.IpHashProvider.ComputeGuid(ipaddress)
        };

        var geoIpResult = GeoIpManager.GetGeoIpData(geoIpOptions);

        if (geoIpResult?.GeoIpData != null)
        {
            country = geoIpResult.GeoIpData.Country != null 
                && !geoIpResult.GeoIpData.Country.Equals("N/A") 
                ? geoIpResult.GeoIpData.Country : string.Empty;
        }
    }

    return country;
}

Setting the Id like on the highlighted row was suggested to me by Sitecore Support and, by reading other articles on this topic, I came to the conclusion that this is what ensures a proper usage of the Sitecore Analytics data. Before having this line, the results were not accurate, and it had to do with the caching.

The geoIpResult.GeoIpData object is of type WhoIsInformation and contains a lot of other interesting data like city, region, latitude, longitude, that you can now access in the same manner as the country. Easily!

Sitecore 8.1 WFFM with MVC: Insert Form in Content Editor

In the latest versions of Sitecore, the trend is more and more to use MVC. So having an MVC project, built on Sitecore XP 8.1 rev.151003 (Initial Release) version, and wanting to use the Web Forms For Marketers module, version 8.1 rev. 151008, entails that we want to use the MVC Form rendering, instead of the classic Form sublayout. For a developer, this works perfectly well, but as a Content Editor, I encountered the issue that I could not select any placeholders in the Insert Form wizard. At first, I thought that the explanation for my issue is the same as the one in this article: http://blogs.perficient.com/microsoft/2014/04/web-forms-for-marketers-mvc-and-the-sitecore-page-editor/. It mainly has to do with the fact that in the Insert Form wizard, you will need to select the placeholder on which to add the form, but the list of placeholders cannot be created by Sitecore, because it scans the current page looking for sc:Placeholder controls, which are no longer used in MVC. But later on, I found out that this is not the case for my solution. In my case, there was a custom pipeline HttpRequestProcessor that interfered with the Insert Form wizard, so I found a workaround without changing the code from the custom pipeline.

The scenario is this:

1.) First, you open the Insert Form wizard:InsertForm

2.) Enter a name for the form, and click on either Create a blank form or Select a form to copy:

InsertFormWizard

3.) In the Select a Placeholder screen, you have to select something, otherwise, you cannot proceed to the next step. But in this case, instead of the placeholder list you would expect, the current item url is loaded inside the small wizard, which is a showstopper.

The solution to this problem begins with identifying where the request for placeholders is performed. This is done in a javascript file installed with the WFFM module: <website>\sitecore\shell\Applications\Modules\Web Forms for Marketers\script\Sitecore.PlaceholderManager.js. In the getPlaceholders function, we need to replace the Ajax request to the current url with an Ajax request to our own implementation of displaying the placeholder list. For this, we need a new very simple controller and a view in our project:

Controllers/MvcFormController.cs

public class MvcFormController : Controller
{       
    [HttpGet]
    public ActionResult Index()
    {
        return this.View();
    }
}

Views/MvcForm/Index.cshtml

<a id="ph_wffm" href="#" class="scPalettePlaceholder" title="wffm" onclick="Sitecore.PlaceholderManager.onPlaceholderClick(this, event, 'wffm');">
    <div class="scPalettePlaceholderTitle">wffm</div>
    <div class="scPalettePlaceholderTooltip" style="display: none">wffm</div>
</a>

In my case, I only need a single placeholder called ”wffm”. This was previously added in Sitecore, under Placeholder Settings:

RestrictingPlaceholdersScreen

Of course the logic can be taken even further, so that the placeholder list is retrieved dynamically. But in my case, this solution was good enough.

In the Sitecore.PlaceholderManager.js file, in the getPlaceholders function, we can adjust the Ajax request to our action:

this.getPlaceholders = function (url, success, failure) {
    if (url != null && url != '') {
        new Ajax.Request('/api/sitecore/MvcForm/Index', {
            method: 'get',
            asynchronous: false,
            onSuccess: function (transport) {
                success(transport.responseText);
            },
            onFailure: function (transport) {
                failure(null);
            }
        });
    }
    else {
        failure(null);
    }
}

Now, when I perform again the steps from my scenario, I can see the following screen for selecting the placeholder:

SelectPlaceholder

Remember to properly clear the cache of your browser before trying the scenario, so that you make sure that the changes in javascript are taken into consideration.

Sitecore 8.1 Language Dropdown Sorting in Content Editor

If you have any little experience with Sitecore, you know that the languages available for the website can be maintained under System->Languages, and that they can be seen in the Content Editor, in a dropdown on the right side. But in what order are they displayed in the dropdown and how to change this if we want? My project is built on Sitecore XP 8.1 rev. 160302, and this is actually the first version where Sitecore provides an easy way to change the language ordering.

Below I have the language dropdown with the initial order:

LanguageDropdownUnsorted

There is a setting in Sitecore.config, suggestively called ContentEditor.SortLanguages, which by default is set to false. This means that by default, the dropdown displays the languages in the order they were created. If I change this setting to true, the display order in the dropdown will be the same as the order from System->Languages, which is controlled by the Sort Order field of each Language item.

SortOrder

If you somehow don’t see the setting in Sitecore.config, this is how you define it:

<!--  CONTENT EDITOR SORT LANGUAGES
      Indicates whether the Content Editor show languages in the language selection dropdown
      in the same order as /sitecore/system/Languages child items.
      Default value: false
-->
<setting name="ContentEditor.SortLanguages" value="true"/>

Using a decompiler, you can see that this setting is checked in the OnLoad() method from Sitecore.Shell.Applications.ContentManager.Galleries.Languages.GalleryLanguagesForm (Sitecore.Client.dll):

if (Settings.ContentEditor.get_SortLanguages())
{
    languages = languages.OrderBy<Language, Language>(delegate(Language x)
    {
        return x;
    }, new LanguageComparer(currentItem.Database));
}

After changing the setting, I can see the languages in the dropdown ordered as in System->Languages:

LanguageDropdownSorted

If you want even more language dropdown customization, you could do this by opening the file ”<website>\sitecore\shell\Applications\Content Manager\Galleries\Languages\Gallery Languages.xml” and replacing the call to GalleryLanguagesForm (Sitecore.Client.dll) in the CodeBeside tag with your own class containing the custom code.