Category Archives: Sitecore

Continuous integration and deployment of Sitecore Habitat/Helix as an Azure Web App


Hello happy people, don’t we all just love Habitat/Helix. If you don’t know what I’m talking about(shame on you), please read about it here:
Sitecore Helix Documentation
Sitecore Habitat – good demo with code and all
Do a Helix Technical Workshop at Sitecore

Sitecore is now available as an Azure web app. This is nothing new but people are finally seeing it as the default option, which is great ๐Ÿ˜„

Rob Habraken and Bas Lijten have some really good posts regarding Sitecore Deployments on Azure:
Blue Green Sitecore Deployments on Azure (Rob Habraken)
Sitecore 8.2-1 on Azure Web Apps (Rob Habraken)
Zero downtime deployments with Sitecore on Azure (Bas Lijten)
Use the Sitecore Azure toolkit to deploy your on premises environment (Bas Lijten)

Ok lets move on… In this post I would like to share with you guys how to setup a Habitat/Helix solution as an Azure web app, where unicorn will be our deliverer/savior ๐Ÿ˜‰
Let me explain:
We will setup a clean Sitecore website in Azure.
Next will be to change the ci(continuous integration) gulp file, for copying and transforming files to a deploy folder.
Instead of using (item) packages we will use Unicorn as our “item deliverer”.
It will all be deployed to the azure web app instance with good old ftp(this is not the best approach but I just want to show you how easy it could be)
Everything will run in TeamCity or Visual Studio Team Services.

As always let’s divide our work into following steps:
1. Setup a clean Sitecore instance in Azure – Azure web app.
2. Setup folders and config files to transform on.
3. Prepare and add transformations for the continuous integration script.
4. Update the continuous integration script file – gulpfile-ci.js.
5. TeamCity – Add build steps.
6. Visual Studio Team Services – Add build tasks.

1. Setup a clean Sitecore instance in Azure – Azure web app

There are a lot of good posts on how you to setup Sitecore as an Azure web app.
Sitecore Azure Toolkit
Deploy a new Sitecore environment to Azure App Service
And really good posts from the Sitecore community:
An introduction to Sitecore on Azure Web Apps
Sitecore 8.2 update 1: Azure deployments, ARM, Web Deploy and the Sitecore Azure Toolkit
Sitecore on Azure: Create custom web deploy packages using the Sitecore Azure Toolkit

Let the posts guide you ๐Ÿ™‚

2. Setup folders and config files to transform on.

Instead of publishing and transforming files to the website folder, we will do that in a Deploy folder(Which will be placed in the root of the solution).
We will also have a folder for configs to copy from – ConfigsToBuildFrom(It will be placed in the root of the solution), the folder will contain the following files:

  • Website
    • Web.config
    • App_Config
      • Include
        • DataFolderUnicornMaster.config
        • UnicornData.config
    • Security
      • Domains.config
  • WebsiteCD
    • Web.config

Here we place the web.config’s (from the Azure CM instance and the Azure CD instance), some unicorn configs and the Domain.config.
UnicornData.config sets the sourceFolder:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <sc.variable name="sourceFolder" value="$(dataFolder)\unicorn" />
  </sitecore>
</configuration>

DataFolderUnicornMaster.config holds the path to the source of the serlialization files

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <sitecore>
    <unicorn>
      <defaults>
        <targetDataStore physicalRootPath="$(dataFolder)\Unicorn\$(configurationName)" useDataCache="false" type="Rainbow.Storage.SerializationFileSystemDataStore, Rainbow" singleInstance="true" xdt:Transform="Replace" xdt:Locator="Match(type)"/>
      </defaults>
    </unicorn>
  </sitecore>
</configuration>

3. Prepare and add transformations for the continuous integration gulp script.

Next will be to change(and add) transforming files. Instead of one transforming “type” we will have one per role. Here we have updated/changed the domain.config.transform file(from the habitat project) and the web.config.transform file(from the common project):
domain.config.local.transform
domain.config.cm.transform
domain.config.cd.transform

web.config.local.transform
web.config.cm.transform
web.config.cd.transform

Then in the gulpfile-ci.js(continuous integration script) we will have one transforming task(method) per build “role”.

We also need to transform some of the config patch files. In Foundation.Indexing we need to change the Foundation.Indexing.config file(right now its using the lucene index configuration). We will create two new transforms:
– Foundation.Indexing.config.cm.transform
– Foundation.Indexing.config.cd.transform.
They will replace the lucene index configuration with the cloud index configuration:

<?xml version="1.0" encoding="utf-8"?>

<configuration  xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <sitecore>
    <contentSearch>
      <indexConfigurations>
        <defaultLuceneIndexConfiguration>
          <add xdt:Transform="Remove" />
        </defaultLuceneIndexConfiguration>
        <defaultCloudIndexConfiguration type="Sitecore.ContentSearch.Azure.CloudIndexConfiguration, Sitecore.ContentSearch.Azure" xdt:Transform="InsertIfMissing">
          <fieldMap type="Sitecore.ContentSearch.Azure.FieldMaps.CloudFieldMap, Sitecore.ContentSearch.Azure" >
            <fieldNames hint="raw:AddFieldByFieldName">
              <field fieldName="all_templates" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.Collections.Generic.List`1[[System.String, mscorlib]]" settingType="Sitecore.ContentSearch.Azure.CloudSearchFieldConfiguration, Sitecore.ContentSearch.Azure" >
                <Analyzer type="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" />
              </field>
              <field fieldName="has_presentation" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.Boolean" settingType="Sitecore.ContentSearch.Azure.CloudSearchFieldConfiguration, Sitecore.ContentSearch.Azure"  />
              <field fieldName="has_search_result_formatter" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.Boolean" settingType="Sitecore.ContentSearch.Azure.CloudSearchFieldConfiguration, Sitecore.ContentSearch.Azure"  />
              <field fieldName="search_result_formatter" storageType="YES" indexType="UNTOKENIZED" vectorType="NO" type="System.String" settingType="Sitecore.ContentSearch.Azure.CloudSearchFieldConfiguration, Sitecore.ContentSearch.Azure" />
            </fieldNames>
          </fieldMap>
          <virtualFields type="Sitecore.ContentSearch.VirtualFieldProcessorMap, Sitecore.ContentSearch">
            <processors hint="raw:AddFromConfiguration">
              <add fieldName="content_type" type="Alite.Foundation.Indexing.Infrastructure.Fields.SearchResultFormatterComputedField, Alite.Foundation.Indexing"/>
            </processors>
          </virtualFields>
          <documentOptions type="Sitecore.ContentSearch.Azure.CloudSearchDocumentBuilderOptions,Sitecore.ContentSearch.Azure" >
            <fields hint="raw:AddComputedIndexField">
              <field fieldName="has_presentation" storageType="no" indexType="untokenized">Sitecore.Foundation.Indexing.Infrastructure.Fields.HasPresentationComputedField, Sitecore.Foundation.Indexing</field>
              <field fieldName="all_templates" storageType="no" indexType="untokenized">Sitecore.Foundation.Indexing.Infrastructure.Fields.AllTemplatesComputedField, Sitecore.Foundation.Indexing</field>
              <field fieldName="has_search_result_formatter" storageType="no" indexType="untokenized">Sitecore.Foundation.Indexing.Infrastructure.Fields.HasSearchResultFormatterComputedField, Sitecore.Foundation.Indexing</field>
              <field fieldName="search_result_formatter" storageType="no" indexType="untokenized">Sitecore.Foundation.Indexing.Infrastructure.Fields.SearchResultFormatterComputedField, Sitecore.Foundation.Indexing</field>
            </fields>
          </documentOptions>
        </defaultCloudIndexConfiguration>
      </indexConfigurations>
    </contentSearch>
  </sitecore>
</configuration>

In Foundation.LocalDatasource we need to change the Foundation.LocalDatasource.config file(right now its using the lucene index configuration).
We will create two new transforms:
– Foundation.LocalDatasource.config.cm.transform
– Foundation.LocalDatasource.config.cd.transform.
They will replace the lucene index configuration with the cloud index configuration:

<?xml version="1.0" encoding="utf-8"?>

<configuration  xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <sitecore>
    <contentSearch>
      <indexConfigurations>
        <defaultLuceneIndexConfiguration xdt:Transform="Remove" />
        <defaultCloudIndexConfiguration type="Sitecore.ContentSearch.Azure.CloudIndexConfiguration, Sitecore.ContentSearch.Azure" xdt:Transform="InsertIfMissing">
          <documentOptions>
            <fields hint="raw:AddComputedIndexField">
              <field fieldName="local_datasource_content" storageType="NO" indexType="TOKENIZED">Sitecore.Foundation.LocalDatasource.Infrastructure.Indexing.LocalDatasourceContentField, Sitecore.Foundation.LocalDatasource</field>
            </fields>
          </documentOptions>
        </defaultCloudIndexConfiguration>
      </indexConfigurations>
    </contentSearch>
  </sitecore>
</configuration>

In Common.Website we need to change the Common.Website.config file.
We will create two new transforms:
– Common.Website.config.cm.transform
– Common.Website.config.cd.transform
They will replace the rootHostName, Analytics.CookieDomain and (if you want) the Analytics.ClusterName:

<?xml version="1.0" encoding="utf-8"?>

<configuration  xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
    <sitecore>
      <sc.variable name="rootHostName" value="yourdomain" xdt:Transform="SetAttributes(value)" xdt:Locator="Match(name)"/>
      <settings>
        <setting name="Analytics.CookieDomain" value="yourdomain" xdt:Transform="SetAttributes(value)" xdt:Locator="Match(name)"/>
        <setting name="Analytics.ClusterName" value="yourdomain" xdt:Transform="SetAttributes(value)" xdt:Locator="Match(name)"/>
      </settings>
    </sitecore>
</configuration>

Finally in Habitat.Website we need to change the Habitat.Website.config file.
We will create two new transforms:
– Habitat.Website.config.cm.transform
– Habitat.Website.config.cd.transform

In Habitat.Website.config.cm.transform we will just change the target host name:

<?xml version="1.0" encoding="utf-8"?>

<configuration  xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <sitecore>
    <sites>
      <site name="habitat" targetHostName="yourSiteName.$(rootHostName)" xdt:Transform="SetAttributes(targetHostName)" xdt:Locator="Match(name)"/>
    </sites>
  </sitecore>
</configuration>

And in Habitat.Website.config.cd.transform we will follow jammykam’s advice from his great post – Disable Edit and Preview Modes on your CD servers:

<?xml version="1.0" encoding="utf-8"?>

<configuration  xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <sitecore>
    <sites>
      <site name="habitat" targetHostName="yourSiteName.$(rootHostName)" enablePreview="false" enableWebEdit="false" enableDebugger="false" xdt:Transform="SetAttributes(targetHostName,enablePreview,enableWebEdit,enableDebugger)" xdt:Locator="Match(name)" />
    </sites>
  </sitecore>
</configuration>

Instead of transforming you can go for Patch Override, there is a great post from jammykam – Helix: Patching your Config Patches for Specific Environments

4. Update the continuous integration script file – gulpfile-ci.js

So now we have setup the config and transformation files. Next will be to update our gulpfile-ci.js file, which will be used in TeamCity or Visual Studio Team Services.

We will add some new variables to the script, by pointing out paths to the new folders. (By default the “data folder” is the App_Data folder in a Sitecore Azure web app)

var webTransformationsToBuildFrom = path.resolve("./ConfigsToBuildFrom/Website");
var webCDTransformationsToBuildFrom = path.resolve("./ConfigsToBuildFrom/WebsiteCD");
var dataFolder = path.resolve("./Deploy/Data/App_Data");
var tempWebsite = path.resolve("./Deploy/Website");
var tempWebsiteCD = path.resolve("./Deploy/WebsiteCD");
config.websiteRoot = tempWebsite;

Here is the main task – CI-And-Prepare-Files-CM-CD:

gulp.task("CI-And-Prepare-Files-CM-CD", function (callback) {
    runSequence(
        "CI-Clean",
        "CI-Copy-Configs-CM",
        "CI-Publish",
        "CI-Copy-Website-CD",
        "CI-Copy-Configs-CD",
        "CI-Prepare-Files-CM",
        "CI-Prepare-Files-CD",
        "CI-Apply-Xml-Transform-CM",
        "CI-Apply-Xml-Transform-CD",
        "CI-Copy-Items-For-Unicorn",
        callback);
});

Let’s go trough the different tasks…
In CI-Clean we just clean the deploy folder.

gulp.task("CI-Clean", function (callback) {
    rimrafDir.sync(path.resolve("./Deploy"));
    callback();
});

CI-Copy-Configs-CM, will copy files from the ConfigsToBuildFrom folder to the Deploy folder.

gulp.task("CI-Copy-Configs-CM", function () {
    return gulp.src(webTransformationsToBuildFrom + "/**")
        .pipe(gulp.dest(tempWebsite));
});

CI-Publish, will publish all your projects to the Deploy/Website folder.

gulp.task("CI-Publish", function (callback) {
    config.buildConfiguration = "Release";
    runSequence(
        "Nuget-Restore",
        "Build-Solution",
        "Publish-Foundation-Projects",
        "Publish-Feature-Projects",
        "Publish-Project-Projects", callback);
});

CI-Copy-Website-CD, will copy all the files from Deploy/Website to Deploy/WebsiteCD.

gulp.task("CI-Copy-Website-CD", function () {
    return gulp.src(tempWebsite + "/**")
        .pipe(gulp.dest(tempWebsiteCD));
});

CI-Prepare-Files-CM, will go through and remove unwanted files and dll’s for our CM instance. I got the idea from this great post – Sitecore Habitat Deployment.

gulp.task("CI-Prepare-Files-CM", function (callback) {
    var excludeList = [
        tempWebsite + "\\bin\\{Sitecore,Lucene,Newtonsoft,System,Microsoft.Web.Infrastructure}*dll",
        tempWebsite + "\\bin\\*.pdb",
        tempWebsite + "\\compilerconfig.json.defaults",
        tempWebsite + "\\packages.config",
        tempWebsite + "\\App_Config\\Include\\{Feature,Foundation,Project}\\z.*DevSettings.config",
        tempWebsite + "\\App_Data\\*",
        "!" + tempWebsite + "\\bin\\Sitecore.Support*dll",
        "!" + tempWebsite + "\\bin\\Sitecore.{Feature,Foundation,Project}*dll",
        tempWebsite + "\\bin\\{Sitecore.Foundation.Installer}*",
        tempWebsite + "\\App_Config\\Include\\Foundation\\Foundation.Installer.config",
        tempWebsite + "\\README.md",
        tempWebsite + "\\bin\\HtmlAgilityPack*dll",
        tempWebsite + "\\bin\\ICSharpCode.SharpZipLib*dll",
        tempWebsite + "\\bin\\Microsoft.Extensions.DependencyInjection*dll",
        tempWebsite + "\\bin\\MongoDB.Driver*dll",
        tempWebsite + "\\bin\\Microsoft.Web.XmlTransform*dll"
    ];
    console.log(excludeList);

    return gulp.src(excludeList, { read: false }).pipe(rimraf({ force: true }));
});

Notice that we are not removing the serialization.config’s from the CM instance, we will use them for some Unicorn magic later ๐Ÿ™‚

CI-Prepare-Files-CD, will go through and remove unwanted files and dll’s for our CD instance. I got the idea from this great post – Sitecore Habitat Deployment.

gulp.task("CI-Prepare-Files-CD", function (callback) {
   var excludeList = [
        tempWebsiteCD + "\\bin\\{Sitecore,Lucene,Newtonsoft,System,Microsoft.Web.Infrastructure}*dll",
        tempWebsiteCD + "\\bin\\*.pdb",
        tempWebsiteCD + "\\compilerconfig.json.defaults",
        tempWebsiteCD + "\\packages.config",
        tempWebsiteCD + "\\App_Config\\Include\\{Feature,Foundation,Project}\\z.*DevSettings.config",
        tempWebsiteCD + "\\App_Data\\*",
        "!" + tempWebsiteCD + "\\bin\\Sitecore.Support*dll",
        "!" + tempWebsiteCD + "\\bin\\Sitecore.{Feature,Foundation,Project}*dll",
        tempWebsiteCD + "\\bin\\{Sitecore.Foundation.Installer}*",
        tempWebsiteCD + "\\App_Config\\Include\\Rainbow",
        tempWebsiteCD + "\\App_Config\\Include\\Unicorn",
        tempWebsiteCD + "\\App_Config\\Include\\Rainbow*.config",
        tempWebsiteCD + "\\App_Config\\Include\\Unicorn*.config",
        tempWebsiteCD + "\\App_Config\\Include\\Foundation\\*.Serialization.config",
        tempWebsiteCD + "\\App_Config\\Include\\Feature\\*.Serialization.config",
        tempWebsiteCD + "\\App_Config\\Include\\Project\\*.Serialization.config",
        tempWebsiteCD + "\\App_Config\\Include\\DataFolderUnicornMaster.config",
        tempWebsiteCD + "\\App_Config\\Include\\Foundation\\Foundation.Installer.config",
        tempWebsiteCD + "\\README.md",
        tempWebsiteCD + "\\bin\\HtmlAgilityPack*dll",
        tempWebsiteCD + "\\bin\\ICSharpCode.SharpZipLib*dll",
        tempWebsiteCD + "\\bin\\Microsoft.Extensions.DependencyInjection*dll",
        tempWebsiteCD + "\\bin\\MongoDB.Driver*dll",
        tempWebsiteCD + "\\bin\\Microsoft.Web.XmlTransform*dll",
        tempWebsiteCD + "\\bin\\Rainbow*dll",
        tempWebsiteCD + "\\bin\\Unicorn*dll",
        tempWebsiteCD + "\\bin\\Kamsar.WebConsole*dll"
    ];
    console.log(excludeList);

    return gulp.src(excludeList, { read: false }).pipe(rimraf({ force: true }));
});

Here we will remove everything that has to do with Unicorn.

CI-Apply-Xml-Transform-CM, will run all the CM transformations.

gulp.task("CI-Apply-Xml-Transform-CM", function () {
     var layerPathFilters = ["./src/Foundation/**/*.CM.transform", "./src/Feature/**/*.CM.transform", "./src/Project/**/*.CM.transform", "!./src/**/obj/**/*.CM.transform", "!./src/**/bin/**/*.CM.transform"];
    return gulp.src(layerPathFilters)
        .pipe(foreach(function (stream, file) {

            var fileToTransform = file.path.replace(/.+code\\(.+)\.CM.transform/, "$1");
            util.log("Applying configuration transform: " + file.path);
            return gulp.src("./scripts/applytransform.targets")
                .pipe(msbuild({
                    targets: ["ApplyTransform"],
                    configuration: "Release",
                    logCommand: false,
                    verbosity: "minimal",
                    stdout: true,
                    errorOnFail: true,
                    maxcpucount: 0,
                    toolsVersion: config.buildToolsVersion,
                    properties: {
                        Platform: config.buildPlatform,
                        WebConfigToTransform: tempWebsite,
                        TransformFile: file.path,
                        FileToTransform: fileToTransform
                    }
                }));
        }));
});

CI-Apply-Xml-Transform-CD, will run all the CD transformations.

gulp.task("CI-Apply-Xml-Transform-CD", function () {
     var layerPathFilters = ["./src/Foundation/**/*.CD.transform", "./src/Feature/**/*.CD.transform", "./src/Project/**/*.CD.transform", "!./src/**/obj/**/*.CD.transform", "!./src/**/bin/**/*.CD.transform"];
    return gulp.src(layerPathFilters)
        .pipe(foreach(function (stream, file) {

            var fileToTransform = file.path.replace(/.+code\\(.+)\.CD.transform/, "$1");
            util.log("Applying configuration transform: " + file.path);
            return gulp.src("./scripts/applytransform.targets")
                .pipe(msbuild({
                    targets: ["ApplyTransform"],
                    configuration: "Release",
                    logCommand: false,
                    verbosity: "minimal",
                    stdout: true,
                    errorOnFail: true,
                    maxcpucount: 0,
                    toolsVersion: config.buildToolsVersion,
                    properties: {
                        Platform: config.buildPlatform,
                        WebConfigToTransform: tempWebsiteCD,
                        TransformFile: file.path,
                        FileToTransform: fileToTransform
                    }
                }));
        }));
});

CI-Copy-Items-For-Unicorn, will grab all the serialized items(yml files) and put them in following path – Deploy/Data/App_Data/Unicorn(for our CM instance).

gulp.task("CI-Copy-Items-For-Unicorn", function () {
    return gulp.src("./src/**/serialization/**/*.yml")
        .pipe(gulp.dest(dataFolder + "/Unicorn/"));
});

CI-Sync-Unicorn-CM. will be called from TeamCity or Visual Studio Team Services to sync the Sitecore items.

gulp.task("CI-Sync-Unicorn-CM", function (callback) {
    var options = {};
    options.siteHostName = habitat.getStageUrl();
    options.authenticationConfigFile = config.websiteRoot + "/App_config/Include/Unicorn.SharedSecret.config";

    unicorn(function () { return callback() }, options);
});

When task CI-And-Prepare-Files-CM-CD is done, we should have a Deploy folder ready (for deployment):

To make it even better, next step could be to make a custom web deploy package (using the Sitecore Azure Toolkit).
Check out Sitecore on Azure: Create custom web deploy packages using the Sitecore Azure Toolkit from Bas Lijten

5. TeamCity – Add build steps

The next move is to automate our deployment, I was thinking of using TeamCity and Visual Studio Team Services.

Let’s start with TeamCity. I will not explain on how to setup TeamCity, but it’s quite straight forward. You can read all about it here – Installing and Configuring the TeamCity Server

I added some plugins in order to run the xunit tests and execute the gulp tasks:
xUnit-TeamCity
TeamCity.Node (gulp)

Next we will create the build steps:
1. Nuget Restore
NuGet Installer
Solution: MySolution.sln

2. Build solution
Visual Studio (sln)
Build file path: MySolution.sln
Targets: Rebuild
Configuration: Release

3. Run all tests
xUnit
Version 2.1.0 (AnyCPU/MSIL/.NET 4.5)
Included assemblies: **\bin\debug*Test*.dll

4. npm install
Command Line
Custom script: npm install

5. Generate deploy files for Azure
Gulp
Run targets: CI-And-Prepare-Files-For-CM-CD

6. Upload files To Azure CM
FTP Upload
Target FTP server: ftp://xxxx.ftp.azurewebsites.windows.net/site/wwwroot/
Source folder: %teamcity.build.workingDir%/Deploy/Website/**

7. Upload files To Azure CD
FTP Upload
Target FTP server: ftp://xxxx.ftp.azurewebsites.windows.net/site/wwwroot/
Source folder: %teamcity.build.workingDir%/Deploy/WebsiteCD/**

8. Copy Unicorn files
FTP Upload
Target FTP server: ftp://xxxx.ftp.azurewebsites.windows.net/site/wwwroot/
Source folder: %teamcity.build.workingDir%/Deploy/Data/**

9. Sync items with Unicorn
Gulp
Run targets: CI-Sync-Unicorn-CM

In step 8 and 9 that’s where all the magic happens ๐Ÿ™‚
We will upload all the serialized Sitecore items and thanks to the serialization.config’s(which we didn’t remove) we can now call task CI-Sync-Unicorn-CM and sync all the serialized sitecore items to our Azure CM instance – love it !

6. Visual Studio Team Services – Add tasks

Visual Studio Team Services is a great service and it’s easy to get started with. The best of it all – it’s all in the cloud. Read all about it here – I want to to do it now!!

Here are our created build tasks:

Finally we need to setup a Trigger, we don’t want to start the build manually. In this case we will listen to the master branch from our Bitbucket repository:

That’s it guys, I must say that I fell in love with Visual Studio Team Services – Put it all in the cloud !!

Thatโ€™s all for now folks ๐Ÿ™‚

Advertisements

Make your Sitecore Habitat/Helix solution run with Solr

Hello happy people, I hope you are enjoying the summer ๐Ÿ™‚

Habitat is great and it’s always a joy to work with it. Again a big applaud to the team behind Habitat and Helix concept, great job guys!!
If you are new to Habitat, check these links and be amazed ๐Ÿ™‚
Sitecore Helix Documentation
Sitecore Habitat

How about we devote a day for Helix/Habitat – Happy Helix/Habitat day ๐Ÿ˜Ž

In this post I would like to share with you guys how you can make your Habitat/Helix solution run on Solr. There are a lot of great posts on how to set up Solr for Sitecore and even posts on how to setup Solr for Habitat:

The Sitecore 8.2 with SOLR 6.2 blogposts series
Configuring Solr search with Sitecore 8
Configuring Solr for use with Sitecore 8
Setting up SOLR in Habitat

I could not find any posts on how to make Solr run with a Habitat solution, so lets make one ๐Ÿ™‚

Lets break down the work into following steps:
1. Install/setup Solr – Here we will be using Docker
2. Make changes in the Habitat solution in order to make it work with Solr (Habitat is using Lucene)
3. Disable Lucene config files and enable Solr config files – Automate it by using PowerShell

1. Install/setup Solr

We need to setup Solr, why not set it up using Docker. I got inspired by Mr Laub – Anders Laub, he has created a great repository on the github – Docker containers for Sitecore development.
Lets follow his instructions and setup the Solr.

Download and install Docker.
Docker v17.06.0 or later – Download here
Don’t forget to register and share your C folder.

Download/clone Mr Laub’s great repository
at the github- Docker containers for Sitecore development

Notice all the lovely cores/indexes(with config files) that has been created by Anders:

If you have issues or some crazy ideas, please contribute to Ander’s great repository ๐Ÿ™‚

Open Powershell as admin in /containers/solr/bitnami-6.6-sitecore/
Run > docker-compose up (I’m using Windows Powershell)
Wait for SOLR to start
Now it’s time to see if it’s working, check out the url: http://localhost:8983/solr

Notice that each index has its own core, sitecore-master-index, sitecore-web-index etc.

Lets test the sitecore-web-index, by browsing to this url:

http://localhost:8983/solr/sitecore_web_index/select?indent=on&q=*:*&wt=json
{
  "responseHeader":{
    "status":0,
    "QTime":0,
    "params":{
      "q":"*:*",
      "indent":"on",
      "wt":"json"}},
  "response":{"numFound":0,"start":0,"docs":[]
  }}

Ok cool ๐Ÿ™‚ So now we have a running Solr, next will be to connect it to our habitat solution and fill the indexes(cores) with data.

2. Make changes in the Habitat solution

Habitat is using Lucene so we need to do some changes in order to make it work with Solr.

Here are the projects that needs to be changed/updated:
Sitecore.Foundation.Indexing
Sitecore.Foundation.LocalDataSource
Sitecore.Feature.News
Sitecore.Feature.PageContent
Sitecore.Feature.Person

Ok, we will start by changing the config files that contains indexConfigurations.
Foundation.Indexing.config – Right now the config is for Lucene, lets change it for Solr. Replace the content in the node indexConfigurations with the following:

<defaultSolrIndexConfiguration>
	<fieldMap type="Sitecore.ContentSearch.SolrProvider.SolrFieldMap, Sitecore.ContentSearch.SolrProvider">
	  <fieldNames hint="raw:AddFieldByFieldName">
		<field fieldName="all_templates" returnType="stringCollection" fieldNameFormat="{0}_sm"  multiValued="true" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" settingType="Sitecore.ContentSearch.SolrProvider.SolrSearchFieldConfiguration, Sitecore.ContentSearch.SolrProvider">
		  <Analyzer type="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" />
		</field>
		<field fieldName="has_presentation" returnType="bool" storageType="YES" indexType="UNTOKENIZED" vectorType="NO" boost="1f"  settingType="Sitecore.ContentSearch.SolrProvider.SolrSearchFieldConfiguration, Sitecore.ContentSearch.SolrProvider" />
		<field fieldName="has_search_result_formatter" returnType="bool" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f"  settingType="Sitecore.ContentSearch.SolrProvider.SolrSearchFieldConfiguration, Sitecore.ContentSearch.SolrProvider" />
		<field fieldName="search_result_formatter" returnType="string" storageType="YES" indexType="UNTOKENIZED" vectorType="NO"  settingType="Sitecore.ContentSearch.SolrProvider.SolrSearchFieldConfiguration, Sitecore.ContentSearch.SolrProvider" />
	  </fieldNames>
	</fieldMap>
	<virtualFields type="Sitecore.ContentSearch.VirtualFieldProcessorMap, Sitecore.ContentSearch">
	  <processors hint="raw:AddFromConfiguration">
		<add fieldName="content_type" type="Sitecore.Foundation.Indexing.Infrastructure.Fields.SearchResultFormatterComputedField, Sitecore.Foundation.Indexing"/>
	  </processors>
	</virtualFields>
	<documentOptions type="Sitecore.ContentSearch.SolrProvider.SolrDocumentBuilderOptions, Sitecore.ContentSearch.SolrProvider">
	  <fields hint="raw:AddComputedIndexField">
		<field fieldName="has_presentation" storageType="YES" indexType="untokenized" >Sitecore.Foundation.Indexing.Infrastructure.Fields.HasPresentationComputedField, Sitecore.Foundation.Indexing</field>
		<field fieldName="all_templates" storageType="YES" fieldNameFormat="{0}_sm"  multiValued="true" indexType="tokenized" >Sitecore.Foundation.Indexing.Infrastructure.Fields.AllTemplatesComputedField, Sitecore.Foundation.Indexing</field>
		<field fieldName="has_search_result_formatter" storageType="YES" indexType="untokenized" >Sitecore.Foundation.Indexing.Infrastructure.Fields.HasSearchResultFormatterComputedField, Sitecore.Foundation.Indexing</field>
		<field fieldName="search_result_formatter" storageType="YES" indexType="untokenized" >Sitecore.Foundation.Indexing.Infrastructure.Fields.SearchResultFormatterComputedField, Sitecore.Foundation.Indexing</field>
	  </fields>
	</documentOptions>
</defaultSolrIndexConfiguration>

Foundation.LocalDatasource.config – Right now the config is for Lucene, lets change it for Solr. Replace the content in the node indexConfigurations with the following:

<defaultSolrIndexConfiguration >
  <fieldMap type="Sitecore.ContentSearch.SolrProvider.SolrFieldMap, Sitecore.ContentSearch.SolrProvider">
    <fieldNames hint="raw:AddFieldByFieldName">
      <field fieldName="local_datasource_content" returnType="string" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" settingType="Sitecore.ContentSearch.SolrProvider.SolrSearchFieldConfiguration, Sitecore.ContentSearch.SolrProvider">
        <Analyzer type="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" />
      </field>
    </fieldNames>
  </fieldMap>
  <documentOptions type="Sitecore.ContentSearch.SolrProvider.SolrDocumentBuilderOptions, Sitecore.ContentSearch.SolrProvider">
    <fields hint="raw:AddComputedIndexField">
      <field fieldName="local_datasource_content" storageType="YES" indexType="TOKENIZED">Sitecore.Foundation.LocalDatasource.Infrastructure.Indexing.LocalDatasourceContentField, Sitecore.Foundation.LocalDatasource</field>
    </fields>
  </documentOptions>
</defaultSolrIndexConfiguration>
</indexConfigurations>

Ok that was easy, now we need to identify the indexed fields. The reason is that in Solr the indexed fields will look like something like this: title_t, summary_t etc (In Lucene it’s just title, summary).

If you have studied the Habitat code you will see that it’s using predicate builder for the search. In the Search method in Sitecore.Foundation.Indexing.Services.SearchService you will see it’s calling method AddContentPredicates(queryable, query)

public virtual ISearchResults Search(IQuery query)
{
  using (var context = this.SearchIndexResolver.GetIndex(this.ContextItem).CreateSearchContext())
  {
    var queryable = this.CreateAndInitializeQuery(context);

    queryable = this.AddContentPredicates(queryable, query);
    queryable = this.AddFacets(queryable, query);
    queryable = this.AddPaging(queryable, query);
    var results = queryable.GetResults();
    return this.SearchResultsFactory.Create(results, query);
  }
}

The method goes through all the IndexingProviders and grabs the search predicates (by calling GetQueryPredicate). If a Foundation or a Feature project has searchable content, then there is an IndexingProviders.

 
private IQueryable<SearchResultItem> AddContentPredicates(IQueryable<SearchResultItem> queryable, IQuery query)
{
  var contentPredicates = PredicateBuilder.False<SearchResultItem>();
  foreach (var provider in IndexingProviderRepository.QueryPredicateProviders)
  {
    contentPredicates = contentPredicates.Or(provider.GetQueryPredicate(query));
  }
  return queryable.Where(contentPredicates);
}

For instance in the Sitecore.Feature.PageContent project you will find a PageContentIndexingProvider, in Sitecore.Feature.News you will find NewsIndexingProvider etc.

Lets take a look at the PageContentIndexingProvider in the Sitecore.Feature.PageContent project, at method GetQueryPredicate. Notice the indexed fields which are placed in the fieldNames array.

  
public Expression<Func<SearchResultItem, bool>> GetQueryPredicate(IQuery query)
{
  var fieldNames = new[] {Templates.HasPageContent.Fields.Title_FieldName, Templates.HasPageContent.Fields.Summary_FieldName, Templates.HasPageContent.Fields.Body_FieldName};
  return GetFreeTextPredicateService.GetFreeTextPredicate(fieldNames, query);
}

Here are the fields in the Templates class:

public const string Title_FieldName = "Title";
public const string Summary_FieldName = "Summary";
public const string Body_FieldName = "Body";

Finally in GetFreeTextPredicateService.cs the search predicates will be built:

 
public Expression<Func<SearchResultItem, bool>> GetFreeTextPredicate(string[] fieldNames, IQuery query)
{
  var predicate = PredicateBuilder.False<SearchResultItem>();
  if (string.IsNullOrWhiteSpace(query.QueryText))
  {
    return predicate;
  }
  foreach (var name in fieldNames)
  {
    predicate = predicate.Or(i => i[name].Contains(query.QueryText));
  }
  return predicate;
}

So now we know how to figure out the names of the indexed fields for the PageContent.
To locate the rest of the indexed fields just identify the Indexingprovider in each projects(NewsIndexingProvider, PersonIndexingProvider and LocalDatasourceIndexingProvider).

We can do this in two ways, code change or update the schema.xml files.

Option 1 – Make code changes.
If we go for this option we will have to change the indexed field names for the IndexingProviders. Instead of the “Lucene” field names we will use “Solr” field names(in method GetQueryPredicate). Update the Templates.cs with the new field names:

public const string Title_FieldName = "title_t";
public const string Summary_FieldName = "summary_t";
public const string Body_FieldName = "body_t";

Do the same for rest of the โ€œLuceneโ€ fields in your habitat solution.

Now the search predicate builder will work just fine ๐Ÿ™‚

Option 2 – Update the schema.xml files in each core.
With this approach we don’t need to do any code changes but we need to update the schema.xml in each core. Lets take a look at the schema.xml in core sitecore_web_index, locate the fields:

<field name="_content" type="text_general" indexed="true" stored="false" />
<field name="_database" type="string" indexed="true" stored="true" />
<field name="_path" type="string" indexed="true" stored="true" multiValued="true" /> 
...

Right under the fields in the xml file, we need to create the new “Lucene” fields and then copy the Solr fields to them. Like this:

<field name="title" type="text_general" indexed="true" stored="true" />
<field name="summary" type="text_general" indexed="true" stored="true" />
<field name="body" type="text_general" indexed="true" stored="true" />

<copyField source="title_t" dest="title" />
<copyField source="summary_t" dest="summary" />
<copyField source="body_t" dest="body" />

Do the same for rest of the “Lucene” fields in your Habitat solution.

Now you will have a working search predicate ๐Ÿ™‚

Next are the facets, in the search method(above) you will see it’s calling the AddFacets(queryable, query) method. It uses the facets values from sitecore, you will find them here:
/sitecore/system/Settings/Buckets/Facets/Foundation/Indexing/Content Type

Please update the field name value to: content_type_t
(It’s a virtual field, that means it will be processed in Solr at runtime)

3. Disable Lucene config files and enable Solr config files

The final step is to to disable all the Lucene configs files and enable the Solr config files in your website folder. You can of course do it manually but…
Anders Laub mentions in his repository Docker containers for Sitecore development, about setting up SOLR in Habitat for an automated approach. The post he is referring to is great.
Instead of doing the job manually we will use a Powershell script and it will be triggered by a gulp task.

Here is the Powershell script:

function Set-SCSearchProvider($rootPath)
{
    $validInput = $true;
    #test that path is valid
    If (!(Test-Path -Path $rootPath))
    {
        Write-Host "The supplied path was invalid or inaccessible." -ForegroundColor Red;
        $validInput = $false;
    }
    If ($validInput)
    {
        Write-Host "Set to Solr." -ForegroundColor Yellow;
        $selectedProvider = "Solr";
        $deselectedProvider = "Lucene";
        #enumerate all config files to be enabled
        $filter = "*" + $selectedProvider + "*.config*";
        $filesToEnable = Get-ChildItem -Recurse -File -Path $rootPath -Filter $filter;
        foreach ($file in $filesToEnable)
        {
            Write-Host $file.Name;
            if (($file.Extension -ne ".config"))
            {
                $newFileName = [io.path]::GetFileNameWithoutExtension($file.FullName);
                $newFile = Rename-Item -Path $file.FullName -NewName $newFileName -PassThru;
                Write-Host "-> " $newFile.Name -ForegroundColor Green;
            }
        }
        #enumerate all config files to be disabled
        $filter = "*" + $deselectedProvider + "*.config*";
        $filesToDisable = Get-ChildItem -Recurse -File -Path $rootPath -Filter $filter;
        foreach ($file in $filesToDisable)
        {
            Write-Host $file.Name;
            if ($file.Extension -eq ".config")
            {
                $newFileName = $file.Name + ".disabled";
                $newFile = Rename-Item -Path $file.FullName -NewName $newFileName -PassThru;
                Write-Host "-> " $newFile.Name -ForegroundColor Green;
            }
        }
    }
}

Lets’s put it in the Configuration folder in your Habitat solution.

Here is the gulp script task that executes the Powershell script:

var exec = require("child_process").exec;
gulp.task("Setup-Solr-Config", function (callback) {
  
  exec("Powershell.exe -executionpolicy remotesigned . .\\setup-solr.ps1; Set-SCSearchProvider -rootPath '" + config.websiteRoot + "'",
    function (err, stdout, stderr) {
      console.log(stdout);
      callback(err);
    });
});

We will put it in our gulpfile.js.

Finally we are done.

Please try this and let me know if you have any issues/questions.

Don’t forget to rebuild your indexes in Sitecore.

Thatโ€™s all for now folks ๐Ÿ™‚

Sitecore + Xamarin Workbooks = Interactive Sitecore learning

Xamarin Workbooks is the perfect tool/media for interactive learning. Xamarin Workbooks provides you a blend of documentation and code that is perfect for experimentation, learning, and creating guides and teaching aids. Mainly the idea was to use it to run “Xamarin code” but it also works great for all kinds of .Net libraries, like Scott Hanselmann describes in his post, Xamarin .NET Workbooks – Interactive Computing is a stellar learning tool

The page at https://developer.xamarin.com/workbooks/ is FILLED with amazing example workbooks and lessons, and it’s growing. It has section not only on C# but Android, Games, Graphics as a concept, iOS, WPF, and so much more.

And as Scott says, we can do all kind of workbooks and it’s perfect for learning – interactive learning. So lets do some Sitecoring in the Xamarin Workbooks. ๐Ÿ˜„

First we need to install and setup Xamarin Workbooks, it’s very easy. Go to Installation and Requirements and follow instructions. (It works on Mac and Windows)

Cool, lets fire it up and select “Console”.

Now you can start playing around ๐Ÿ™‚ In order to execute the code, hit the small play button below the “code window”.

You can also reference external dll’s, locally or by Nuget package. To do it locally:
referenceDll

By Nuget package,ย search for packages by browsing toย File > Add Package. Adding a package will automatically bring inย #rย statements referencing package assemblies, allowing you to use them right away.

referenceNuget

This is very cool, I hope that the Sitecore people will take a look at the Xamarin Workbooks and add it to the Sitecore documentation. I mean interactive documentation – How cool is that ๐Ÿ™‚

I’ve created a Sitecore.Workbook at the GitHub – Sitecore-Workbooks. Feel free to download and test it out.
Sitecore.workbook

The cool thing it’s all in markdown, here is the content from the Sitecore.Workbook file ๐Ÿ˜‰

Welcome to Sitecore Workbooks

Workbooks are live documents that mix text, code and results in the same document.

Look at following code,ย  if you notice there is a โ€œplay iconโ€ at the bottom after each โ€œcode windowโ€. Hit play and the code will execute ๐Ÿ™‚

string test = "<h1>Let's do some Xamarin Workbooks</h1>";

Now we will add the AsHtml()

test.AsHtml();

Ok letโ€™s make a 64base image. First we need some base64 data.

string image = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0i...";

We will put the image variable into a โ€œhtml stringโ€ and render it as html.

string htmlString = $"<img src="{image}" height="100" />";
htmlString.AsHtml();

Time for some sitecoring

Next we will reference an external dll, we can do that by referencing them locally. We will need the Sitecore.Kernel.dll. In order to make it work, you will need to setup/create a folder with the sitecore dll.

#r "C:\Habitat\dlls\Sitecore.Kernel.dll"

Or we can reference nuget packages. Letโ€™s grab the NSubstitute package.

#r "NSubstitute"

Ok, time for some Sitecore coding. How about we create an Item ๐Ÿ™‚

using NSubstitute;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Globalization;

Database db = Substitute.For();
ID itemId = ID.NewID;
Language language = Language.Current;
ItemDefinition definition = new ItemDefinition(itemId, "Some item name", ID.NewID, ID.NewID);
ItemData data = new ItemData(definition, language,Sitecore.Data.Version.First, new FieldList());
Item item = Substitute.For(itemId, data, db);

Sitecore Habitat

Next we will try some Sitecore.Habitat stuff. Lets play with Sitecore.Feature.Search, we will need a reference to the Sitecore.Feature.Search dll

#r "C:\Habitat\dlls\Sitecore.Feature.Search.dll"

Letโ€™s try the PagedSearchResults and see/test that it works properly.

using Sitecore.Feature.Search.Models;

int page = 1;
int totalResults = 10;
int pagesToShow = 1;
int resultsOnPage =2;
PagedSearchResults searchResults = new PagedSearchResults(page, totalResults, pagesToShow, resultsOnPage);
double totalPages = Math.Ceiling(totalResults / (double)resultsOnPage);

searchResults.TotalPagesCount == totalPages;

Sitecore Mobile SDK for Xamarin

Lets start by adding the nuget package, Sitecore.MobileSDK.

#r "Sitecore.MobileSDK"
#r "Sitecore.MobileSDK.PasswordProvider.Interface"

Cool, ok guys. ๐Ÿ™‚ Now we have the package, time to do some coding.

How about grab some code from method TestParseCorrectData in AuthenticateResponseParserTest at

To make it easier we will remove the NUnit stuff. Check out the code below, press play and see the result.

using System;
using System.Threading;
using System.Threading.Tasks;
using Sitecore.MobileSDK.API.Exceptions;
using Sitecore.MobileSDK.Authenticate;

var response = "{\"statusCode\":200,\"result\":{}}";
WebApiJsonStatusMessage message = AuthenticateResponseParser.ParseResponse(response, CancellationToken.None);
message.StatusCode.Equals(200);

Try to remove row โ€œmessage.StatusCode.Equals(200);โ€œ and hit play again

var response = "{\"statusCode\":200,\"result\":{}}";
WebApiJsonStatusMessage message = AuthenticateResponseParser.ParseResponse(response, CancellationToken.None);

Thatโ€™s all for now folks ๐Ÿ™‚

Create your own TimePicker(macro) personalization rule in Sitecore

timepickermacro

Once more unto the breach, dear friends, once more ๐Ÿ™‚ This time I want to share with you how to make your own TimePicker macro, which will be used by a custom rule.

I needed a rule where I could present content at a specific time during the day. That means I’m NOT interested in the date, ONLY the time.
There are a bunch of nice rules but not a “Time rule”. So what to do?
Well let’s do a rule then, unfortunately I could not find a TimePicker macro.
Something like this would be nice:
[TimeField,Time,,pick a time]

So what is a macro? A macro is used by the rules. A typical macro could be the datetimepicker or a numeric input.
Here is how the datetimepicker macro is used in a rule:
[DateField,DateTime,,the date]
The macros are listed under Rules at: /sitecore/system/Settings/Rules/Definitions/Macros
macros

What we need to do is to create a new macro. Let’s start by adding/creating a new macro template in /sitecore/system/Settings/Rules/Definitions/Macros. We will call it – Time.
timemacroempty
We will come back to the macro item, when we have some code to point to ๐Ÿ™‚

We want the Timepicker macro to be similar to the Datetimepicker macro but without the date. Why not use dotPeek to find out how Sitecore did. After a lot of hair pulling I finally figured it out.

Let me give you a quick explanation on how it works:
The macro will do a SheerResponse call(ShowModalDialog), here it will give an url to an XML file (see it as a page/dialog for the datetimepicker).
The url points to the default.aspx page in Sitecore/shell, which will “generate” a dialog containing a datetimepicker. The xml file will also need a “code beside” in order to present the datetime picker.

So for the datetimepicker we have the following:
DateTimeMacro.cs (in Sitecore.Kernel)
DateTimeSelector.xml (in folder Sitecore\shell\Applications\Dialogs\DateTimeSelectors)
DateTimeSelector.cs – The “code beside” for the xml file (in Sitecore.Client)

Now lets do the same for our new Timepicker, first we create the macro:

public class TimeMacro : IRuleMacro
{
	public void Execute(XElement element, string name, UrlString parameters, string value)
	{


		Assert.ArgumentNotNull((object) element, "element");
		Assert.ArgumentNotNull((object) name, "name");
		Assert.ArgumentNotNull((object) parameters, "parameters");
		Assert.ArgumentNotNull((object) value, "value");

		SheerResponse.ShowModalDialog(new UrlString(UIUtil.GetUri("control:Sitecore.Shell.Applications.Dialogs.TimeSelector")).ToString(), "580px","475px", string.Empty, true);

	}

}

This little puppy:
UIUtil.GetUri("control:Sitecore.Shell.Applications.Dialogs.TimeSelector")
Generates the following url:
/sitecore/shell/default.aspx?xmlcontrol=Sitecore.Shell.Applications.Dialogs.TimeSelector

Time to get confused ๐Ÿ™‚ Sitecore.Shell.Applications.Dialogs.TimeSelector is not an xml file, it’s an element in an xml file. Here is our new xml file:

<?xml version="1.0" encoding="utf-8" ?>
<control xmlns:def="Definition" xmlns="http://schemas.sitecore.net/Visual-Studio-Intellisense">
  <Sitecore.Shell.Applications.Dialogs.TimeSelector>
    <FormDialog Header="Time" Text="Select a time." Icon="People/32x32/clock.png">
      <CodeBeside Type="Sandbox.Sitecore.Dialogs.CustomTimeSelector, Sandbox.Sitecore" />
      
      <div style="text-align: center;">
        <TimePicker ID="Time"/>
      </div>

    </FormDialog>
  </Sitecore.Shell.Applications.Dialogs.TimeSelector>
</control>

This is how the popup/dialog will look like.
I put the xml file together with the DatetimeSelector file in folder:
Sitecore\shell\Applications\Dialogs\DateTimeSelectors

Let’s do the code beside class(defined in the xml file), here you find the TimePicker control with the property name Time.

public class CustomTimeSelector : DialogForm
{
	/// <summary>Gets or sets the time.</summary>
	/// <value>The time.</value>
	protected TimePicker Time { get; set; }

	/// <summary>Raises the load event.</summary>
	/// <param name="e">The <see cref="T:System.EventArgs" /> instance containing the event data.</param>
	protected override void OnLoad(EventArgs e)
	{
		base.OnLoad(e);
		if (Context.ClientPage.IsEvent)
			return;

		Time.Value = DateTime.Now.TimeOfDay.ToString();
	}

	/// <summary>Handles a click on the OK button.</summary>
	/// <param name="sender">The sender.</param>
	/// <param name="args">The arguments.</param>
	protected override void OnOK(object sender, EventArgs args)
	{
		SheerResponse.SetDialogValue(Time.Value);
		base.OnOK(sender, args);
	}
}

The TimePicker, that was indeed confusing for me. I started out by creating a new TimePicker field, adding it to the core database etc. I thought it would be a normal field in the content editor. But that was so wrong… Here we use the Sitecore.Web.UI.HtmlControls. The good thing is that I did not have to create a TimePicker control, there is already one(used by the DatetimePicker control).

When ok button is hit, we will set the time value in the macro(in a rule).

Ok, so now we have done the macro. We will now update the new macro item, Time, in Sitecore. We need to point to the code for the macro class:
macroset

If you want to test it, you can easily browse to the following url:
your site/sitecore/shell/default.aspx?xmlcontrol=Sitecore.Shell.Applications.Dialogs.TimeSelector
controlinaction

To make it complete, we need a rule. We can call it – Time of day. Check out my previous post how to make a custom rule, Compare dates in your Sitecore personalization rule.
The text for the rule:
when current time is [operatorid,Operator,,compares to] [TimeField,Time,,pick a time]
Notice our new macro: [TimeField,Time,,pick a time] ๐Ÿ™‚

Here is the code for the rule. The TimeField property will hold the time that was selected from the Timepicker macro.

public class TimeOfDayCondition<T> : OperatorCondition<T> where T : RuleContext
{

	public string TimeField { get; set; }

	protected override bool Execute(T ruleContext)
	{
		Assert.ArgumentNotNull((object)ruleContext, "ruleContext");
		
		ConditionOperator conditionOperator = base.GetOperator();

		TimeSpan? timeToCompare = ConvertTime(TimeField);

		if (!timeToCompare.HasValue)
			return false;

		return TimeSpanComparer(DateTime.Now.TimeOfDay, timeToCompare.Value, conditionOperator);
		
	}


	private TimeSpan? ConvertTime(string timeToConvert)
	{
		DateTime time;
		if (!DateTime.TryParse(timeToConvert, out time))
			return null;

		return time.TimeOfDay;

	}


	private bool TimeSpanComparer(TimeSpan timeSpan1, TimeSpan timeSpan2, ConditionOperator conditionOperator)
	{
			
		switch (conditionOperator)
		{
			case ConditionOperator.Equal:
				return timeSpan1.Equals(timeSpan2);
			case ConditionOperator.LessThan:
				return timeSpan1 < timeSpan2;
			case ConditionOperator.LessThanOrEqual:
				return timeSpan1 <= timeSpan2;
			case ConditionOperator.GreaterThan:
				return timeSpan1 > timeSpan2;
			case ConditionOperator.GreaterThanOrEqual:
				return timeSpan1 >= timeSpan2;
			case ConditionOperator.NotEqual:
				return !timeSpan1.Equals(timeSpan2);
			default:
				return false;
		}

	}

}

In method TimeSpanComparer we will compare current time with the time in the TimeField property, for that we will use the Operator condition(LessThan, GreaterThan etc.)

Here is the rule in action:
timepickermacro

Keep on personalization out there.

Thatโ€™s all for now folks ๐Ÿ™‚

Compare dates in your Sitecore personalization rule

experienceeditorcompa

Hello good people,

First of all I would like to thank Sitecore for awarding me the Sitecore’s Technology MVP 2017. I’m truly honoured.

Today I’m going to share with you how easy it is to create/make a date comparer rule.

In this scenario we will do a pretty dumb rule ๐Ÿ™‚
There will be two values(date picker) which will be compared by using the operator condition.

Jeff Darchuk has a great post on how to create/add rules in Sitecore – Lets use that rules engine!
So why not follow his instructions ๐Ÿ™‚

1. Create a new tag to identify our new group of rules, we will call it TestRules.
/sitecore/system/Settings/Rules/Definitions/Tags
createtag

2. Go to path: /sitecore/system/Settings/Rules/Definitions/Elements. Here you will see a bunch of rules, lets create a new Element Group and call it TestRules:
createelementfolder

3. Associate the new tag TestRules with the rule element folder(TestRules) by setting the default tag on the rule element.
tagdefault

4. We want the rule to be accessible when the editors are working in the Experience Editor(Presentation Details), let’s add the new tag(TestRules) to the conditional rendering rules:
rulecontext

Now let us create the rule and select the condition alternative:
createrule

So now we have a an empty condition rule called – DateComparerRule. Next will be to add some logic to it, we will add the two date fields and a condition comparer.
createrule

Here is the “text” code:
when [DateFieldOne,DateTime,,first date] [operatorid,Operator,,compares to] [DateFieldTwo,DateTime,,second date]

Let me give you a quick explanation.
DateFieldOne and DateFieldTwo – These are the properties in our custom rule
DateTime – This is the datepicker
“first date” and “second date” – The texts you want to show the editor, when he/she is using the rule.

operatorid – Will hold what “operator condition” you have selected(greater than, less than etc)
Operator – The operator picker
“compares to” – The text you want to show to the editor, when he/she is using the rule.

Ok so lets see how the rule look and feels for the editors.
experienceeditor
experienceeditordateexperienceeditorcompa

Yes it seems to work ok, next will be to add some code for the rule.

Here is the actual code for our custom rule,DateComparerRule. We need to inherit from OperatorCondition in order to compare the two date field properties: DateFieldOne and DateFieldTwo.

public class DateComparerRule<T> : OperatorCondition<T> where T : RuleContext
{

	public string DateFieldOne { get; set; }

	public string DateFieldTwo { get; set; }

	protected override bool Execute(T ruleContext)
	{
		Assert.ArgumentNotNull((object)ruleContext, "ruleContext");

		if (!IsDate(DateFieldOne) || !IsDate(DateFieldTwo))
			return false;

		ConditionOperator conditionOperator = base.GetOperator();

		return DateComparer(DateUtil.ParseDateTime(DateFieldOne, DateTime.MinValue), DateUtil.ParseDateTime(DateFieldTwo, DateTime.MinValue), conditionOperator);
	}

	private bool IsDate(string date)
	{
		DateTime tempDate;
		return DateTime.TryParse(date, out tempDate);
	}

	private bool DateComparer(DateTime date1, DateTime date2, ConditionOperator conditionOperator)
	{
			
		switch (conditionOperator)
		{
			case ConditionOperator.Equal:
				return date1.Equals(date2);
			case ConditionOperator.LessThan:
				return date1 < date2;
			case ConditionOperator.LessThanOrEqual:
				return date1 <= date2;
			case ConditionOperator.GreaterThan:
				return date1 > date2;
			case ConditionOperator.GreaterThanOrEqual:
				return date1 >= date2;
			case ConditionOperator.NotEqual:
				return !date1.Equals(date2);
			default:
				return false;
		}

	}
	
}

In order to get what ConditionOperator the editor has selected(greater than, less than etc.) we use the base method base.GetOperator(). From method DateComparer we will return the outcome of the comparison between the two properties: DateFieldOne and DateFieldTwo.

What we have left is to update the new rule, DateComparer, in Sitecore. We need to tell it where to find the code we have created. We do that by updating field Type(in section Script) with the following:
Sandbox.Sitecore.Rules.Conditions.DateComparer;Sandbox.Sitecore

This is not a big thing but I want you guys to see how easy it is to make your own custom rules.

Keep on personalization out there.

Thatโ€™s all for now folks ๐Ÿ™‚

Handle URL redirects in a Sitecore multisite solution using FileMapProvider

redirect-me-baby_3

I would like to share with you guys how you could do URL redirects in a Sitecore multisite solution with Microsoft’s URL Rewrite Module using the FileMapProvider.

FilemapProvider is very cool, it reads the URL mappings from a text file.

It can be used instead of the built-in rewrite maps functionality when the amount of rewrite map entries is very large and it is not practical to keep them in a web.config file.

And the best thing of all:
No need to do any custom pipelines in Sitecore – it’s clean ๐Ÿ™‚
You don’t have to do an IIS reset after the redirect file has been changed/updated.

The idea is this:
We have a Sitecore solution with several websites, each website will have a number of redirects. In Sitecore we will have a Bucket with “URL Redirects” items, each item will contain an old and a new url. From the Content Editor we want to be able to click on a command button that will trigger a remote event(we need it to work in a multi-server environment), which will write the redirects to a text file(for the FileMapProvider).

How about divide the work into following:
1. Configure the FileMapProvider/s.
2. Setup the URL redirects in Sitecore.
3. Create the command button in Sitecore Content Editor.
4. Create the (remote) event and write redirects to file.

1. Configure the FileMapProvider/s.

The FileMapProvider is part of the URL Rewrite Extensibility Samples, which allows us to use custom providers for the rewrite\redirect rules – in our case: external .txt file with URL’s.

/old/catalog/product; /new/category/product
/old/contactus/index; /new/contactus

In our multisite scenario we will have two websites running, testsite1.com and testsite2.com. Which means we will have two FileMapProvider’s, one for each website.

Let’s have a look at how the FilemapProviders are configured in the UrlRewrite.config. Yes it’s a web.config transformation(because Sitecore config file patching only works within the sitecore section)

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <system.webServer>
	<rewrite>
	  <providers>
		<provider name="FileMapProviderForTestSite1" type="FileMapProvider, Microsoft.Web.Iis.Rewrite.Providers, Version=7.1.761.0, Culture=neutral, PublicKeyToken=0545b0627da60a5f"  xdt:Transform="Insert">
		  <settings>
			<add key="FilePath" value="D:\Files\Data\UrlRedirectsForTestSite1.txt"/>
			<add key="IgnoreCase" value="1"/>
			<add key="Separator" value=";"/>
		  </settings>
		</provider>
		<provider name="FileMapProviderForTestSite2" type="FileMapProvider, Microsoft.Web.Iis.Rewrite.Providers, Version=7.1.761.0, Culture=neutral, PublicKeyToken=0545b0627da60a5f"  xdt:Transform="Insert">
		  <settings>
			<add key="FilePath" value="D:\Files\Data\UrlRedirectsForTestSite2.txt"/>
			<add key="IgnoreCase" value="1"/>
			<add key="Separator" value=";"/>
		  </settings>
		</provider>
	  </providers>
	  <rules>
		<rule name="RuleForFileMapProviderForTestSite1" stopProcessing="false" xdt:Transform="Insert">
		  <match url="(.*)"/>
		  <conditions>
		    <add input="{HTTP_HOST}" pattern="^testsite1.com$"/>
			<add input="{FileMapProviderForTestSite1:{REQUEST_URI}}" pattern="(.+)"/>
		  </conditions>
		  <action type="Redirect" url="{C:1}" appendQueryString="false"/>
		 </rule>
		 <rule name="RuleForFileMapProviderForTestSite2" stopProcessing="false" xdt:Transform="Insert">
		  <match url="(.*)"/>
		  <conditions>
		    <add input="{HTTP_HOST}" pattern="^testsite2.com$"/>
			<add input="{FileMapProviderForTestSite2:{REQUEST_URI}}" pattern="(.+)"/>
		  </conditions>
		  <action type="Redirect" url="{C:1}" appendQueryString="false"/>
		</rule>
	  </rules>
	</rewrite>
  </system.webServer>
</configuration>		

If you notice there are two FileMapProvider configurations, one provider for each site – how cool is that ๐Ÿ™‚ The provider points to a text file(containing the redirects for that specific site).
Now in order to get the redirect magic to work we need to set up a rule. If you look at the rule RuleForFileMapProviderForTestSite1, you will see that this rule will only work for website – testsite1.com.
We do that by using pattern(below) to check if it matches with HTTP_HOST:

pattern="^testsite1.com$"

Then we use/get the “site” specific provider, FileMapProviderForTestSite1, to do the redirect.

Tip!
If you guys don’t want to install the URL Rewrite Extensibility Sample on a test or production server, fear not. Just grab it from your GAC on your local dev machine.
Fire up your PowerShell(for windows) and use the SUBST Command. Suppose you want to create a G Drive (G for GAC), use the following command:

SUBST G: C:\WINDOWS\ASSEMBLY

Look in folder G:\GAC_MSIL and you will find Microsoft.Web.Iis.Rewrite.Providers ๐Ÿ™‚

2. Setup the URL redirects in Sitecore

Next will be to setup the URL redirects in sitecore. The folder structure for the redirects.
treeredirects

UrlRedirectsItem: It will contain two fields – NewUrl and OldUrl
redirectitem

UrlRedirectsFolderItem: I’ve added a field in order to figure out what site it belongs to.
folderitem

3. Create the command button in Sitecore Content Editor

Next will be to create the command that will trigger the writing of redirects to a text file. I wanted the button only to be visible when the “UrlRedirects folder” is selected/marked:
contenteditorcommand
I also added a confirm dialog, in case the editor wants to cancel the request.

In the Sitecore Core database we will add the command button, we will use the Large Button template. Navigate to /sitecore/content/Applications/Content Editor/Ribbons/Contextual Ribbons and create the new ribbon – Sync Url Redirects:
corecommand
We will also create a new command – SyncUrlRedirects:ToFile

In order for the command button only to be visible for a specific template(in this case the Url Redirects folder) we need to go to the template and then in the Appearance Section locate the drop tree field, Ribbon. And finally select our newly created ribbon – Sync Url Redirects:
templatecommand

We will patch the new command to a config file:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <commands>
      <command name="SyncUrlRedirects:ToFile" type="Sandbox.Website.Commands.SyncUrlRedirectsFromSitecoreToFileCommand,Sandbox.Website" />
    </commands>
  </sitecore>
</configuration> 

Now we need to add some code for the command button.
In the Execute method we will verify that we are standing on the UrlRedirectsSiteFolder item, next will be to get all the children(deriving from template UrlRedirect). The GetChildrenByTemplateWithFallback method is an extension method from the Sitecore.ContentSearch.Utilities.
I need to send some parameters to the Confirm dialog(number of redirect items and the item id of the Redirects Folder) and finally call/execute the Confirm dialog by using Sitecore.Context.ClientPage.Start.

public class SyncUrlRedirectsFromSitecoreToFileCommand : Command
{

	private const string QueryParamRedirectsToBeSynced = "count";
	private const string QueryParamFolderId = "folderid";

	public override void Execute(CommandContext context)
	{
		Assert.ArgumentNotNull(context, "context");

		Assert.IsNotNull(context.Items, "context items are null");

		Assert.IsTrue(context.Items.Length > 0, "context items length is 0");

		Item item = context.Items[0];

		Assert.IsNotNull(item, "Item is null");

		Assert.IsTrue(item.IsDerived(SitecoreTemplates.UrlRedirectsSiteFolder.ID), "Item is not of template UrlRedirectsSiteFolder");

		int numberOfRedirects =
		item.GetChildrenByTemplateWithFallback(SitecoreTemplates.UrlRedirect.ID.ToString()).Count();

		context.Parameters.Add(QueryParamRedirectsToBeSynced, numberOfRedirects.ToString());
		context.Parameters.Add(QueryParamFolderId, item.ID.ToString());

		Sitecore.Context.ClientPage.Start(this, "Confirm", context.Parameters);
	}

	protected void Confirm(ClientPipelineArgs args)
	{

		if (args.IsPostBack)
		{
			switch (args.Result)
			{
				case "yes":
					SyncUrlRedirectsEvent syncUrlRedirectsEvent = new SyncUrlRedirectsEvent(args.Parameters[QueryParamFolderId]);
					Sitecore.Eventing.EventManager.QueueEvent(syncUrlRedirectsEvent, true, true);
					return;

				case "no":
					return;
			}
		}

		SheerResponse.Confirm($"It is important that you publish all redirects before you continue. {args.Parameters[QueryParamRedirectsToBeSynced]} redirects will be synced.");
		args.WaitForPostBack();

	}

	public override CommandState QueryState(CommandContext context)
	{
		Assert.ArgumentNotNull(context, "context");

		if (context.Items.Length != 1)
			return CommandState.Hidden;

		Item item = context.Items[0];

		Assert.IsNotNull(item, "First context item is null");

		Sitecore.Data.Version[] versionNumbers = item.Versions.GetVersionNumbers(false);
		if (versionNumbers == null || versionNumbers.Length == 0 || item.Appearance.ReadOnly)
			return CommandState.Disabled;

		if (!item.Access.CanWrite() || !item.Access.CanRemoveVersion() || IsLockedByOther(item))
			return CommandState.Disabled;

		return base.QueryState(context);

	}

}

In the Confirm method we will check if the if the editor hits the yes button, if so we will call the SyncUrlRedirectsEvent and pass along the item id of the Redirects Folder.

4. Create the (remote) event and write redirects to file

In order to write the redirects to a file on a CD server, we will need a remote event – SyncUrlRedirectsEvent. The event is quite simple, it will just hold the item id of the Redirects Folder.
Here is the event and its EventArgs class:

[DataContract]
public class SyncUrlRedirectsEvent
{
	public SyncUrlRedirectsEvent(string urlRedirectsSiteFolderItemId)
	{
		UrlRedirectsSiteFolderItemId = urlRedirectsSiteFolderItemId;
	}

	[DataMember]
	public string UrlRedirectsSiteFolderItemId { get; protected set; }

}

[Serializable]
public class SyncUrlRedirectsEventArgs : EventArgs, IPassNativeEventArgs
{
	public SyncUrlRedirectsEventArgs(string urlRedirectsSiteFolderItemId)
	{
		UrlRedirectsSiteFolderItemId = urlRedirectsSiteFolderItemId;
	}

	public string UrlRedirectsSiteFolderItemId { get; protected set; }
}

Next is the eventhandler, that is the one which will be called/executed on the CD server, so we can write the redirects for the FileMapProvider. Here we fetch all redirects from the “UrlRedirectsSiteFolderItem id”(which we got from the SyncUrlRedirectsEventArgs) and then write them to a text file.
I will not go into how the SyncUrlRedirectsService works, it’s quite straightforward. If you are interested in how it’s done just ping me and I will show you ๐Ÿ™‚

public class SyncUrlRedirectsEventHandler
{
	private static ILogger _logger;
	private readonly ISyncUrlRedirectsService _syncUrlRedirectsService;
	private readonly IDatabaseRepository _databaseRepository;


	public SyncUrlRedirectsEventHandler()
	{
		_logger = IocContext.NonRequestContainer.GetInstance<ILogger>();
		_syncUrlRedirectsService = IocContext.NonRequestContainer.GetInstance<ISyncUrlRedirectsService>();
		_databaseRepository = new DatabaseRepository();
			
	}

	public virtual void OnRemoteSyncUrlRedirects(object sender, EventArgs e)
	{

		if (e == null)
		{
			_logger.Error("SyncUrlRedirectsEventArgs is empty");
			return;
		}
			

		SyncUrlRedirectsEventArgs syncUrlRedirectsEventArgs = (SyncUrlRedirectsEventArgs) e;

		if (syncUrlRedirectsEventArgs.UrlRedirectsSiteFolderItemId.IsNullOrWhiteSpace())
		{
			_logger.Error("UrlRedirectsSiteFolder item id is missing in SyncUrlRedirectsEventArgs");
			return;
		}

		Item urlRedirectsSiteFolderItem =
			_databaseRepository.ContextDatabase.GetItem(new ID(syncUrlRedirectsEventArgs.UrlRedirectsSiteFolderItemId));


		if (urlRedirectsSiteFolderItem == null)
		{
			_logger.Error("UrlRedirectsSiteFolderItem does not exist on {0}. Id: {1}", _databaseRepository.ContextDatabase.Name, syncUrlRedirectsEventArgs.UrlRedirectsSiteFolderItemId);
			return;
		}

		string sitename = urlRedirectsSiteFolderItem.GetString(SitecoreTemplates.UrlRedirectsSiteFolder.Fields.UrlRedirectsSiteName);

		if (sitename.IsNullOrWhiteSpace())
		{
			_logger.Error("Site name is not set in UrlRedirectsSiteFolder item id, {0}", urlRedirectsSiteFolderItem.ID);
			return;
		}


		IList<string[]> urlRedirectsList =	urlRedirectsSiteFolderItem.GetChildrenByTemplateWithFallback(SitecoreTemplates.UrlRedirect.ID.ToString())
			.Select(
				item =>
					new[] {item.GetString(SitecoreTemplates.UrlRedirect.Fields.UrlRedirectOldUrl), item.GetString(SitecoreTemplates.UrlRedirect.Fields.UrlRedirectNewUrl) }).ToList();


		Tuple<IList<string>, string> redirectsAndFilePath =  _syncUrlRedirectsService.PrepareRedirectsAndFilePathForFile(urlRedirectsList, sitename);

		_syncUrlRedirectsService.TryWriteRedirectsToFile(redirectsAndFilePath, sitename);

	}


	public static void Run(SyncUrlRedirectsEvent syncUrlRedirectsEvent)
	{
		_logger.Information("SyncUrlRedirectsEventHandler - Run", typeof(SyncUrlRedirectsEventHandler));

		SyncUrlRedirectsEventArgs args = new SyncUrlRedirectsEventArgs(syncUrlRedirectsEvent.UrlRedirectsSiteFolderItemId);

		Event.RaiseEvent("syncurlredirects:remote", args);
	}
}

The Run method is called from a Hook.

To glue it all together(event and eventhandler) we need to register and trigger the eventhandler, for that we will use a hook. See the hook as a very light pipeline.

public class SyncUrlRedirectsHook : IHook
{
	public void Initialize()
	{
		Sitecore.Eventing.EventManager.Subscribe(new Action<SyncUrlRedirectsEvent>(SyncUrlRedirectsEventHandler.Run));
	}
}

Here is the config file for the event and the hook:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
	<hooks>
	  <hook type="Sandbox.Website.Redirect.Events.SyncUrlRedirectsHook, Sandbox.Website" />
	</hooks>
    <events>
      <event name="syncurlredirects:remote">
        <handler type="Sandbox.Website.Redirect.Events.SyncUrlRedirectsEventHandler, Sandbox.Website" method="OnRemoteSyncUrlRedirects" />
      </event>
    </events>
  </sitecore>
</configuration>

Instead of using a hook, you can use a pipeline(for the event). Here are some great documentation regarding events in Sitecore:
http://sitecore-community.github.io/docs/pipelines-and-events/events/

Thatโ€™s all for now folks ๐Ÿ™‚

Extend your Sitecore “Multilist Search Control” with a SPEAK treeview

advancedsearch2

In this post I would like to share with you guys how you can extend a control, in this case the Multilist with Search control, and add some very nice SPEAK functionality to it ๐Ÿ™‚

Let say you want to add some search functionality to the “Multilist with Search” control. How about giving the editors the possibility to use a treeview for selecting one or many items. ๐Ÿ™‚

We will add a third menu item(to the current two: Select all and Deselect all), called Advanced Search, which will open the treeview in a SPEAK dialog.
advanced4

We will divide the work into following:
1. Create a custom control(“Multilist with Search”) which will inherit BucketList and add some custom functionality to it.
2. Add the new control to the Sitecore Core database together with the third (new) menu item, called Advanced search.
3. Finally we will make a pop up(SPEAK dialog) which will present the tree view.

Create a custom control

Since the control “Multilist with Search” is of type Sitecore.Buckets.FieldTypes.BucketList, we will need the custom control to inherit the type. We want to open a SPEAK dialog window from the control, we will do this by adding a third menu item(next to Select all and Deselect all).

Let me give you guys a quick explanation how the custom control works. In the DoRender method we need to store some data in hidden fields, the current item and the start search item. The method HandleMessage will be called when you press a menu item(Select all, Deselect all and the new one Advanced search). If it finds the message “bucketsearchlist:openadvancedsearch” it will run the ClientPage.Start pipeline. The pipeline will call the OpenDialog method.

using System.Text.RegularExpressions;
using Sitecore.Buckets.FieldTypes;
using Sitecore.Diagnostics;
using Sitecore.Web.UI.Sheer;

namespace Sandbox.Website.Code.SitecoreCustom.Fields
{
	public class CustomMultilistSearch : BucketList
	{


		private const string PrefixCurrentItemId = "_currentItemId";
		private const string PrefixStartSearchLocation = "_startSearchLocation";

		public override void HandleMessage(Message message)
		{


			Assert.ArgumentNotNull((object)message, "message");
			base.HandleMessage(message);

			if (message["id"] != this.ID)
				return;

			if (message.Name != "bucketsearchlist:openadvancedsearch")
				return;

			Sitecore.Context.ClientPage.Start(this, "OpenDialog");

		}

		protected void OpenDialog(ClientPipelineArgs args)
		{
			if (args.IsPostBack)
			{
				Sitecore.Context.ClientPage.SendMessage(this, "item:refresh");
				return;
			}


			string url = GenerateSpeakUrl();

			SheerResponse.ShowModalDialog(new ModalDialogOptions(url)
			{
				Width = "600",
				Height = "600",
				Response = true,
				ForceDialogSize = true,
			});

			args.WaitForPostBack(true);
		}

		private string GenerateSpeakUrl()
		{
			string itemId = Sitecore.Context.ClientPage.ClientRequest.Form[$"{this.ID}{PrefixCurrentItemId}"];

			string loadPathId = Sitecore.Context.ClientPage.ClientRequest.Form[$"{this.ID}{PrefixStartSearchLocation}"];

			string rootItemInfo = GetRootItemInfo(loadPathId);

			string url =
				$"/sitecore/client/Sandbox/Applications/Dialogs/AdvancedGroupSearch?loadpathid={loadPathId}&rootiteminfo={rootItemInfo}&id={itemId}&database={Sitecore.Context.ContentDatabase.Name}&language={Sitecore.Context.Language.Name}&version=1";

			return url;
		}


		private string GetRootItemInfo(string loadPathtId)
		{
			Item item = Sitecore.Context.ContentDatabase.GetItem(new ID(loadPathtId));

			string imgSrc = Regex.Match(ThemeManager.GetIconImage(item, 16, 16, "", ""), "<img.+?src=[\"'](.+?)[\"'].*?>", RegexOptions.IgnoreCase).Groups[1].Value;

			return $"{item.DisplayName},{Sitecore.Context.ContentDatabase.Name},{loadPathtId},{imgSrc}";

		}

	
		private string GetStartSearchLocation()
		{
			NameValueCollection nameValues = StringUtil.GetNameValues(this.Source, '=', '&');

			foreach (string allKey in nameValues.AllKeys)
				nameValues[allKey] = HttpUtility.JavaScriptStringEncode(nameValues[allKey]);

			string startSearchLocation = nameValues["StartSearchLocation"];

			if (string.IsNullOrWhiteSpace(startSearchLocation))
				return ItemIDs.RootID.ToString();
			
			startSearchLocation = this.MakeFilterQueryable(startSearchLocation);

			return !Sitecore.Buckets.Extensions.StringExtensions.IsGuid(startSearchLocation) ? ItemIDs.RootID.ToString() : startSearchLocation;
		}

		protected override void DoRender(HtmlTextWriter output)
		{

			string startSearchLocation = GetStartSearchLocation();

			output.Write($"<input id=\"{this.ID}{PrefixCurrentItemId}\" type=\"hidden\" value=\"{this.ItemID}\" />");
			output.Write($"<input id=\"{this.ID}{PrefixStartSearchLocation}\" type=\"hidden\" value=\"{startSearchLocation}\" />");
			base.DoRender(output);

		}

	}
}

The method OpenDialog will open a SPEAK dialog by using the SheerResponse.ShowModalDialog method, we will add a proper url where the SPEAK dialog is located.

The SPEAK dialog will need the following parameters:
The actual item.
The start item to search from(which will be the root node for the treeview)

If you notice in the OpenDialog method we will also handle a postback. This means when the dialog is being closed it will call the command item:refresh to refresh the current page.

Add the new control to Sitecore Core

Next step will be to add the new control to Sitecore Core. We will put the control in /sitecore/system/Field types, an easy way is to just copy the item /sitecore/system/Field types/List Types/Multilist with Search.
custoncontrol

And here we add the third menu item with the message: bucketsearchlist:openadvancedsearch.
thirdmenuitem

Make the pop up(SPEAK dialog)

The SPEAK dialog is done in Sitecore 8.1, unfortunately it means that the itemTreeView is not in SPEAK 2.

Anyways lets fire up Sitecore Rocks and start creating the SPEAK dialog:
dialogspeak

If you notice I’ve put the treeview in a tabcontrol:
tabtreeviewspeak

What we have left is the Page Code file(js file).

define(["sitecore", "/-/speak/v1/experienceprofile/CintelUtl.js"], function (_sc, cintelUtil) {
	var dialog = _sc.Definitions.App.extend({

		initialized: function () {
			console.log("Advanced Search read");
			jQuery('body').addClass('sc-fullWidth');

			var self = this;
			self.OkButton.on("click", self.SaveSelection, this);
			self.TriggerButton.on("click", self.SetSelection, this);

			self.BindData();

			self.on("tree-isExpanded", self.TreeIsExpanded, self);
			
		},
		BindData: function () {
			var self = this;

			self.SetMessageToMessageBar("notification",
				"To set the selection from the multi search list control, please click on button - Set selection");
		
			self.GetGroupData(function (groupObject) {
				self.GroupData = groupObject;
				
				self.InitTreeView();

				var timer = setInterval(function () {
					if (!self.MyTreeView.get("isBusy")) {

						clearInterval(timer);
						
						self.ExpandTreeView();
					}

				}, 500);

			});


		},
		InitTreeView: function () {
			var self = this;

			var selector = self.MyTreeView.attributes.type;
			var treeView = $('.' + selector);

			treeView.attr("data-sc-rootitem", self.GroupData.rootItemInfo);
			treeView.attr("data-sc-loadpath", self.GroupData.loadPathId);

			self.MyTreeView.viewModel.getRoot().removeChildren();
			self.MyTreeView.viewModel.checkedItemIds([]);

			self.MyTreeView.viewModel.initialized();
		},
		TreeIsExpanded: function () {
			var self = this;
			var selector = self.MyTreeView.attributes.type;
			var treeView = $('.' + selector);

			treeView.dynatree("getRoot").visit(function (node) {
				node.expand(false);
			});

			self.ProgressIndicator.set("isVisible", false);

		},
		SetMessageToMessageBar: function (level, message, actions) {
			var self = this;
			self.TreeViewMessageBar.addMessage(level, {
				text: message,
				actions: [actions],
				closable: false
			});
		},
		SetSelection: function () {
			var self = this;
			var selector = self.MyTreeView.attributes.type;
			var treeView = $('.' + selector);
			$.each(self.GroupData.itemFieldMultiListSelections.split("|"), function (index, value) {
				treeView.dynatree("getTree").getNodeByKey(value).select();
			});
		},
		ExpandTreeView: function () {
			var self = this;

			var selector = self.MyTreeView.attributes.type;
			var treeView = $('.' + selector);
			
			var counter = 0;
			var lastCounted = 0;
			var rootNode = treeView.dynatree("getRoot");
			rootNode.select(false);

			
			treeView.dynatree("getRoot").visit(function (node) {
				node.expand();
			});


			$(".dynatree-container li ul").each(function (index, root) {
				checkEm(root);
			});

			var timer = setInterval(function () {
				if (lastCounted === counter) {
					clearInterval(timer);

					self.trigger('tree-isExpanded');


				}
				lastCounted = counter;

			}, 200);
			
			function checkEm(ulnode) {

				if (typeof (ulnode) == "undefined" || ulnode == null)
					return;

				$(ulnode).find('li').each(function (i, li) {

					//Only folders will we click on
					if ($(li).find("img")[0].src.indexOf("contract") === -1) {
						$(li).find("a").trigger("click");

						setTimeout(function () {
							counter += 1;

							var ulFound = $(li).find("ul");
							checkEm(ulFound);
					
							console.log("counter : " + counter);
						},
							100);
					}
				});
			}




		},
		SaveSelection: function () {
			var self = this;
			var selector = self.MyTreeView.attributes.type;
			var treeView = $('.' + selector);
			
			var selectedItems = $.grep(treeView.dynatree("getSelectedNodes"),
							function (node) { return !node.data.isFolder; })
						.map(function (node) { return node.data.key; });



			self.GroupData.itemFieldMultiListSelections = selectedItems.join("|");

			self.UpdateSitecoreData(function (readyState, status, responseText) {
				//Error
				if (readyState === 4 && (this.status === 404 || status === 500)) {
					self.SetMessageToMessageBar("error", responseText);
				}

				//Success
				if (readyState === 4 && this.status !== 404 && status !== 500) {
					window.top.dialogClose();
				}


			});

		},
		GetGroupData: function (callback) {

			var self = this;

			var groupObject = {};
			groupObject["id"] = cintelUtil.getQueryParam("id");
			groupObject["rootItemInfo"] = cintelUtil.getQueryParam("rootiteminfo");
			groupObject["loadPathId"] = cintelUtil.getQueryParam("loadpathid");
			groupObject["language"] = cintelUtil.getQueryParam("language");
			groupObject["version"] = cintelUtil.getQueryParam("version");
			groupObject["database"] = cintelUtil.getQueryParam("database");
			groupObject["databaseUri"] = new _sc.Definitions.Data.DatabaseUri(groupObject["database"]);
			groupObject["itemUri"] = new _sc.Definitions.Data
				.ItemUri(groupObject["databaseUri"], groupObject["id"]);
			groupObject["itemVersionUri"] = new _sc.Definitions.Data
				.ItemVersionUri(groupObject["itemUri"],
					groupObject["language"],
					parseInt(groupObject["version"]));

			var db = new _sc.Definitions.Data.Database(groupObject["databaseUri"]);

			db.getItem(groupObject["itemVersionUri"],
				function (item) {

					groupObject["itemName"] = item.itemName;
					groupObject["itemDisplayname"] = item.$displayName;

					var itemFieldMultiListSelectionsField = item.getFieldById(self.Constants.CustomMultilistFieldId);
					groupObject["itemFieldMultiListSelections"] = itemFieldMultiListSelectionsField.value;


					callback(groupObject);

				});


		},
		GroupData: function () {
			var content;

			return content;
		},
		UpdateSitecoreData: function (callback) {
			var self = this;

			var request = new XMLHttpRequest();

			var url = self.StringFormat("/sitecore/api/ssc/item/{0}?database={1}&language={2}&version={3}",
				self.GroupData.id,
				self.GroupData.database,
				self.GroupData.language,
				self.GroupData.version);

			request.open("PATCH", url);

			request.setRequestHeader('Content-Type', 'application/json');

			var body = {
				'CustomMultilist': self.GroupData.itemFieldMultiListSelections,
			};

			request.send(JSON.stringify(body));

			request.onreadystatechange = function () {

				return callback(this.readyState, this.status, this.responseText);

			};
		},
		Constants: {
			"CustomMultilistFieldId": "{105BF825-145D-4278-8063-2A5E9472698E}"
		},
		StringFormat: function () {
			var s = arguments[0];
			for (var i = 0; i < arguments.length - 1; i++) {
				var reg = new RegExp("\\{" + i + "\\}", "gm");
				s = s.replace(reg, arguments[i + 1]);
			}
			return s;
		}


	});

	return dialog;
});

Let me give you a quick explanation on how it works.

In the BindData method we will get the item data by calling GetGroupData. Here we will get the selections from the Multilist Search control.

Next will be to reinitialize the treeview, yes you read it correct. Normally you set the treeview data in the properties from Sitecore Rocks but in this case we don’t know the root node(start node for the treeview) since we get it as a parameter. As you can see in method InitTreeView we need to do some tricks to make it work thanks to the Sitecore Community ๐Ÿ™‚

We will then call the ExpandTreeView method, as you noticed I’ve put in an Interval function where we wait until the treeview is loaded.

In the ExpandTreeView method we need to expand all nodes in ALL levels, if we don’t do this we will NOT be able to set the the selections from the Multilist Search control.
When it’s done with the expanding we will close it nicely by calling/trigger method TreeIsExpanded.

To set the selections we do this in method SetSelection which is triggered from the button “Set selections”.
Finally when we hit the Save button we will call the SaveSelection method to gather the checked items from the treeview and then save them to Sitecore.

Thatโ€™s it and keep doing some good SPEAK stuff out there.

Thatโ€™s all for now folks ๐Ÿ™‚

Fetch and save Sitecore Item in SPEAK 2 using Sitecore.Services.Client

testapppage

Hello guys, yet another post about SPEAK ๐Ÿ™‚
In this medium small post I would like to share with you how I work with Sitecore items in SPEAK.

Let’s say you need to retrieve a Sitecore item, modify it and update it.
In this example the Sitecore item is exposed in an url, like this:

https://somehost.com/sitecore/client/MyApp/Applications/TestApp/TestAppPage?id=7E4CA53A-59AA-4C08-9EEF-71DDF66E8A8E&database=master&language=en&version=1

This will be a simple page containing two text boxes and a save button(See above)
Here is the Design Layout for the page:
tasklayout
I really like the Grid rendering and guess what it’s responsive friendly ๐Ÿ™‚

To get an item I do something like this:

GetTestData: function (callback) {

	var self = this;
	var testObject = {};
	testObject["id"] = Sitecore.Speak.utils.url.parameterByName("id");
	testObject["language"] = Sitecore.Speak.utils.url.parameterByName("language");
	testObject["version"] = Sitecore.Speak.utils.url.parameterByName("version");
	testObject["database"] = Sitecore.Speak.utils.url.parameterByName("database");
	testObject["databaseUri"] = new _sc.Definitions.Data.DatabaseUri(testObject["database"]);
	testObject["itemUri"] = new _sc.Definitions.Data
		.ItemUri(testObject["databaseUri"], testObject["id"]);
	testObject["itemVersionUri"] = new _sc.Definitions.Data
		.ItemVersionUri(testObject["itemUri"],
			testObject["language"],
			parseInt(testObject["version"]));

	var db = new _sc.Definitions.Data.Database(testObject["databaseUri"]);

	db.getItem(testObject["itemVersionUri"],
		function (item) {

			testObject["itemName"] = item.itemName;
			testObject["itemDisplayname"] = item.$displayName;

			var testHeaderField = item.getFieldById(self.Constants.TestHeaderFieldId);
			testObject["itemFieldTestHeader"] = testHeaderField.value;

			var testDescriptionField = item.getFieldById(self.Constants.TestDescriptionFieldId);
			testObject["itemFieldTestDescription"] = testDescriptionField.value;

			callback(testObject);

		});


}

The reason for the callback function is because of db.getItem. When the method has retrieved the Sitecore item I’ll put the data in a JSON object.
The Sitecore.Speak.utils.url.parameterByName is indeed handy ๐Ÿ™‚
(I’m not using Sitecore.Services.Client here)

To call the GetTestData method:

BindTestData: function () {
	var self = this;

	self.GetTestData(function (testObject) {
		self.TestData = testObject;

		//Do some binding stuff to the controls on your Speak layout page
		self.MyHeaderTextBox.Value = self.TestData.itemFieldTestHeader;
		self.MyDescriptionTextBox.Value = self.TestData.itemFieldTestDescription;
	});

}

Here we will bind/set the data to the text boxes.
(self.TestData is just a container)

To update a Sitecore item we will use Sitecore.Services.Client. Thanks to Kevin Obee’s great “living” documentation, http://docs.itemserviceapi.apiary.io, he made it so much easier to understand how it works.
There is also a great post that describes the differences between Sitecore Item Web API and Sitecore.Services.Client – What is Sitecore.Services.Client?

Here is how the we update the Sitecore item using Sitecore.Services.Client

UpdateSitecoreData: function (callback) {
	var self = this;

	var request = new XMLHttpRequest();

	var url = self.StringFormat("/sitecore/api/ssc/item/{0}?database={1}&language={2}&version={3}",
		self.TestData.id,
		self.TestData.database,
		self.TestData.language,
		self.TestData.version);

	request.open("PATCH", url);

	request.setRequestHeader('Content-Type', 'application/json');

	var body = {
		'TestHeader': self.TestData.itemFieldTestHeader,
		'TestDescription': self.TestData.itemFieldTestDescription
	};

	request.send(JSON.stringify(body));

	request.onreadystatechange = function () {

		return callback(this.readyState, this.status, this.responseText);

	};
}

Again I’m using a callback, since we need to wait for the response if the save was successful or not. TestHeader and TestDescription are the field names of the Sitecore item.

Another thing, if you would get the forbidden page (You do not have permission to view this directory or page) when using the Sitecore.Services.Client. That means you probably have to take a look at the security policy settings in Sitecore.Services.Client.config. Find the setting that works best for you.

And here we call UpdateSitecoreData:

SaveTestData: function () {
	var self = this;

	//Get the data from the text boxes 
	self.TestData.itemFieldTestHeader = self.MyHeaderTextBox.Value;
	self.TestData.itemFieldTestDescription = self.MyDescriptionTextBox.Value;
	
	self.UpdateSitecoreData(function (readyState, status, responseText) {
		//Failure
		if (readyState === 4 && (this.status === 404 || status === 500)) {
			self.StatusAlertConfirmationDialog["HeaderText"] = "Save - error";
			self.StatusAlertConfirmationDialog["Title"] = "An error occurred while saving, please contact support";
			self.StatusAlertConfirmationDialog["IconType"] = "error";
			self.StatusAlertConfirmationDialog["Message"] = responseText;
			self.StatusAlertConfirmationDialog.show();
			self.StatusAlertConfirmationDialog.IsVisible = true;

		}

		//Success
		if (readyState === 4 && this.status !== 404 && status !== 500) {
			self.BindTestData();
		}


	});

}

If you succeed with the update we will “rebind” the data by calling BindTestData, if not we will show a StatusAlertConfirmationDialog(ConfirmationDialog).

Here is the full PageCode file:

(function (Speak) {

	Speak.pageCode({
		initialized: function () {
			console.log("Start TestAppPage");
			var self = this;

			self.SaveTestButton.on("click", self.ConfirmSave, this);

			self.UpdateTestDataConfirmationDialog.on("close",
				function (data) {

					if (data === "ok") {

						self.SaveSearch();
				
					}
                    self.UpdateTestDataConfirmationDialog.IsVisible = false;
				},
				this);	

			self.BindTestData();


		},
		ConfirmSave: function () {
			var self = this;
			self.UpdateTestDataConfirmationDialog.show();
			self.UpdateTestDataConfirmationDialog.IsVisible = true;
		},
		BindTestData: function () {
			var self = this;

			self.GetTestData(function (testObject) {
				self.TestData = testObject;

				//Do some binding stuff to the controls on your Speak layot page
                self.MyHeaderTextBox.Value = self.TestData.itemFieldTestHeader;
                self.MyDescriptionTextBox.Value = self.TestData.itemFieldTestDescription;
			});


		},
		GetTestData: function (callback) {

			var self = this;

			var testObject = {};
			testObject["id"] = Sitecore.Speak.utils.url.parameterByName("id");
			testObject["language"] = Sitecore.Speak.utils.url.parameterByName("language");
			testObject["version"] = Sitecore.Speak.utils.url.parameterByName("version");
			testObject["database"] = Sitecore.Speak.utils.url.parameterByName("database");
			testObject["databaseUri"] = new _sc.Definitions.Data.DatabaseUri(testObject["database"]);
			testObject["itemUri"] = new _sc.Definitions.Data
				.ItemUri(testObject["databaseUri"], testObject["id"]);
			testObject["itemVersionUri"] = new _sc.Definitions.Data
				.ItemVersionUri(testObject["itemUri"],
					testObject["language"],
					parseInt(testObject["version"]));

			var db = new _sc.Definitions.Data.Database(testObject["databaseUri"]);

			db.getItem(testObject["itemVersionUri"],
				function (item) {

					testObject["itemName"] = item.itemName;
					testObject["itemDisplayname"] = item.$displayName;

					var testHeaderField = item.getFieldById(self.Constants.TestHeaderFieldId);
					testObject["itemFieldTestHeader"] = testHeaderField.value;

					var testDescriptionField = item.getFieldById(self.Constants.TestDescriptionFieldId);
					testObject["itemFieldTestDescription"] = testDescriptionField.value;

					callback(testObject);

				});


		},
		Constants: {
			"TestHeaderFieldId": "{3699A67C-A4BC-486D-9A37-9E4554AE46CA}",
			"TestDescriptionFieldId": "{E85C0CA0-75F2-4FB7-B2B5-03D8B97EBCCA}"
		},
		TestData: function () {
			var content;

			return content;
		},
		SaveTestData: function () {
			var self = this;

			//Get the data from the textboxes 
			self.TestData.itemFieldTestHeader = self.MyHeaderTextBox.Value;
			self.TestData.itemFieldTestDescription = self.MyDescriptionTextBox.Value;
			
			self.UpdateSitecoreData(function (readyState, status, responseText) {
				//Failure
				if (readyState === 4 && (this.status === 404 || status === 500)) {
					self.StatusAlertConfirmationDialog["HeaderText"] = "Save - error";
					self.StatusAlertConfirmationDialog["Title"] = "An error occurred while saving, please contact support";
					self.StatusAlertConfirmationDialog["IconType"] = "error";
					self.StatusAlertConfirmationDialog["Message"] = responseText;
					self.StatusAlertConfirmationDialog.show();
					self.StatusAlertConfirmationDialog.IsVisible = true;

				}

				//Success
				if (readyState === 4 && this.status !== 404 && status !== 500) {
					self.BindTestData();
				}


			});


		},
		UpdateSitecoreData: function (callback) {
			var self = this;

			var request = new XMLHttpRequest();

			var url = self.StringFormat("/sitecore/api/ssc/item/{0}?database={1}&language={2}&version={3}",
				self.TestData.id,
				self.TestData.database,
				self.TestData.language,
				self.TestData.version);

			request.open("PATCH", url);

			request.setRequestHeader('Content-Type', 'application/json');

			var body = {
				'TestHeader': self.TestData.itemFieldTestHeader,
				'TestDescription': self.TestData.itemFieldTestDescription
			};

			request.send(JSON.stringify(body));

			request.onreadystatechange = function () {

				return callback(this.readyState, this.status, this.responseText);

			};
		},
		StringFormat: function () {
			var s = arguments[0];
			for (var i = 0; i < arguments.length - 1; i++) {
				var reg = new RegExp("\\{" + i + "\\}", "gm");
				s = s.replace(reg, arguments[i + 1]);
			}
			return s;
		}
		
	});
})(Sitecore.Speak);

If you noticed there is a call to ConfirmSave when the Save button is clicked which will popup a ConfirmationDialog.

ConfirmSave: function () {
	var self = this;
	self.UpdateTestDataConfirmationDialog.show();
	self.UpdateTestDataConfirmationDialog.IsVisible = true;
}

If you are wondering about the self.UpdateTestDataConfirmationDialog.IsVisible = true, yes the show() method is enough to pop up the dialog. But I need to know that the dialog is visible, that’s why I’m using the IsVisible attribute.

Finally in the initialized function we are listening to the UpdateTestDataConfirmationDialog’s close event. If OK button is clicked we will call the SaveSearch.

self.UpdateTestDataConfirmationDialog.on("close",
function (data) {

	if (data === "ok") {

		self.SaveSearch();
 
	}
	self.UpdateTestDataConfirmationDialog.IsVisible = false;
},
this);

Thatโ€™s it and keep doing some good SPEAK stuff out there.

Thatโ€™s all for now folks ๐Ÿ™‚

Put JSON data in your SearchDataSource and bind it to SearchableDroplist – Sitecore SPEAK

droplist

Hello guys, I have been working with SPEAK for a while and some of the controls I’ve been using a lot are:
SearchDataSource and SearchableDropList(SPEAK 2 control)

The SearchableDropList is a droplist with search, the datasources you can use are:
QueryDataSource
SearchDataSource

If you have less than 100 items then QueryDataSource is OK(since it’s using Sitecore Query or Sitecore Fast Query) but if you have more, then use the SearchDataSource(using good old index).

An extremely cool thing regarding SearchDataSource is that you can add JSON data to it. Just create a JSON array and add it like this:

(function (Speak) {

	Speak.pageCode({


		initialized: function () {
			
			var self = this;
			
			self.BindJsonDataToMyCoolSearchDataSource();


		},
        BindJsonDataToMyCoolSearchDataSource: function() {
			var self = this;

			var customJsonData = [];

			for (var i = 0; i < 10; i++) {

				var data = {};
				data["Value"] = i;
				data["Text"] = i + " some text"; 
				customJsonData.push();
			}


			self.MyCoolSearchDataSource.Items = customJsonData;

		}

	});
})(Sitecore.Speak);

To add the JSON data we just set the Items property on the SearchDataSource:
self.MyCoolSearchDataSource.Items = customJsonData;

Then in your SpeakLayout you will connect the SearchDataSource to your control, in this case the SearchableDropList:
speaksearch
To set the datasource for the SearchableDropList, we will use DynamicData. Here we point(bind) to the SearchDataSource:
{Binding MyCoolSearchDataSource.Items}

Next we will set DisplayFieldName to the JSON field “Text” and finally we set ValueFieldName to the JSON field “Value”.

Now you you will have a searchable droplist where you can search in your custom JSON data:
droplist

By the way the new version(Version 2) of Sitecore Rocks is so much faster and it has some very new nice features. It truly rocks ๐Ÿ˜‰

That’s it and keep doing some good SPEAK stuff out there

Thatโ€™s all for now folks ๐Ÿ™‚

How to trigger an OK click on a Sitecore SPEAK ConfirmationDialog

dialog

This is a really quick post but I feel I have to share this with you guys.

I have been doing a lot of SPEAK lately and from time to time I end up struggling with something that should be really easy to do(and yes it is easy when you finally find the solution).

Anyways I had to trigger an OK click on a ConfirmationDialog(SPEAK 2 component) in SPEAK.
speak

In this case the ConfirmationDialog should be closed with the Enter key by trigger the OK button. Let’s take a look on the PageCode:

(function (Speak) {

	Speak.pageCode({


		initialized: function () {
			
			var self = this;
			
			self.MyConfirmationDialog.on("close",
				function (data) {

					if (data === "ok") {

						//Do some stuff
					}

				},
				this);
			

			jQuery(document).keypress(function (e) {

				if (e.which === 13) {

					if (self.MyConfirmationDialog.IsVisible) {
						self.MyConfirmationDialog.CloseClick = "ok";
						self.MyConfirmationDialog.hide();
					}

				}
			});


		}
	});
})(Sitecore.Speak);

To close the ConfirmationDialog we would call the hide method – self.MyConfirmationDialog.hide().

The trickier part was to tell what button should be clicked/triggered and that took a while for me to figure out.
In CloseClick you need to set what button you want to trigger, like this self.MyConfirmationDialog.CloseClick = “ok”

That’s it and keep doing some good SPEAK stuff out there

Thatโ€™s all for now folks ๐Ÿ™‚