Sitecore Xperiences - The things I've seen as a Sitecore Developer

Custom rule condition to find Top-N Profile Key

Here we are for another simple yet useful tip from the Smart Bookshelf Demo. Although we are now improving it with some Artificial Intelligence, the first version has Sitecore Rules-based Personalization in the hearth of the recommendation mechanism.

Our desired behavior is to light-up LED strips from the top 3 Book Genres, such as seen below:

Face Detection picture

The books you see on screen are shown according to the top 3 genres for the current visitor.

It automatically scrolls, showing one genre each time. Here is how the component looks like:

Recommended Books

Our content tree is composed by Genre Pages, each of them with respective Profile Card assigned:

Mapping Genres and Profile Cards

So when your visitor visits a Genre Page, he or she will get points for that respective Profile Card, enabling Sitecore to register and track what are the preferred genres. The same thing also happens when a Book Page is visited (achieved by using the technique explained in my last post).

At this point we are already able to see preferred genres for our contacts, by opening any known contact at the Experience Profile:

Experience Profile shows preferred Genres

 

Rules Conditions

We will retrieve the top 3 genres by using Rules-based Personalization. What you see below is the personalization setting required to display the Poetry Genre if it’s “top 2″ for the current Visitor.

Custom Rule Condition

 

Custom Rule Condition: Matches Pattern Best X

Our custom condition is based on this OOTB condition:

/sitecore/system/Settings/Rules/Definitions/Elements/Visitor/Matches Pattern

However, it can only match match top 1, while our new condition accepts a number to test against.

 

Definition Item

  • Path: /sitecore/system/Settings/Rules/Definitions/Elements/Visitor/Matches Pattern Best X
  • Template:  /sitecore/templates/System/Rules/Condition
  • Text: where the current contact matches best [value,Text,,specific value] on profile key [ProfileKey,ProfileKey,,profile key]
  • Type: Your.Namespace.Rules.Conditions.ContactPatternMatchBestCondition, Your.AssemblyName
    • Make sure this points to your correct namespace

 

Source Code

Here is the source code to implement this condition. After setting up the Definition Item, as described at the previous topic, all you need to use it is to compile it.

using Sitecore.Abstractions;
using Sitecore.Analytics;
using Sitecore.Analytics.Tracking;
using Sitecore.Data;
using Sitecore.Framework.Conditions;
using Sitecore.Rules;
using Sitecore.Rules.Conditions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using Sitecore.Diagnostics;

namespace Your.Namespace.Rules.Conditions.ContactPatternMatchBestCondition
{
   public class ContactPatternMatchBestCondition<T> : WhenCondition<T> where T : RuleContext
   {
      // ID for the item /sitecore/system/Settings/Content Testing/Personalization Suggestions/No pattern match
      private readonly ID _noPatternMatchItemId = ID.Parse("9E05C456-5CD0-405B-9D45-31DDD92AB8E4");
      private string _profileKey;
      public ContactPatternMatchBestCondition() : this(DependencyResolver.Current.GetService<BaseLog>()) { }

      internal ContactPatternMatchBestCondition(BaseLog log) {
         Condition.Requires<BaseLog>(log, nameof(log)).IsNotNull<BaseLog>();
         this.Log = log;
      }

      private string _value;
      public string Value {
         get => _value ?? string.Empty;
         set {
            Assert.ArgumentNotNull(value, nameof(value));
            _value = value;
         }
      }

      public string ProfileKey {
         get => _profileKey;
         set => _profileKey = value ?? string.Empty;
      }

      internal BaseLog Log { get; }

      protected override bool Execute(T ruleContext) {
         Condition.Requires<T>(ruleContext, nameof(ruleContext)).IsNotNull<T>();
         Condition.Ensures<ITracker>(Tracker.Current, string.Format("{0}.{1}", (object)"Tracker", 
            (object)"Current")).IsNotNull<ITracker>();
         Condition.Ensures<Session>(Tracker.Current.Session, string.Format("{0}.{1}.{2}", (object)"Tracker", 
            (object)"Current", (object)"Session")).IsNotNull<Session>();
         Condition.Ensures<Contact>(Tracker.Current.Session.Contact, string.Format("{0}.{1}.{2}.{3}", 
            (object)"Tracker", (object)"Current", (object)"Session", (object)"Contact")).IsNotNull<Contact>();
         if (string.IsNullOrEmpty(this.ProfileKey))
            return false;
         IEnumerable<IBehaviorProfileContext> profiles = Tracker.Current.Session.Contact.BehaviorProfiles.Profiles;
         List<ID> idsList = this.GetIdsList(this.ProfileKey);
         if (!profiles.Any<IBehaviorProfileContext>() && idsList.Contains(this._noPatternMatchItemId))
            return true;
         if (!idsList.Any())
            return false;

         var profileKeyId = idsList.First();

         int best;
         if (!int.TryParse(Value, out best))
            return false;

         foreach (IBehaviorProfileContext profile in profiles) {
            var profileScores = GetProfileKeyValue(profile);
            if (profileScores.Count < best)
               return false;

            var index = best - 1;
            var specifiedBest = profileScores[index].Key;
            var specifiedValue = profileScores[index].Value;
            var isBest = specifiedBest == profileKeyId && specifiedValue>0;
            if (isBest)
               return true;
         }
         return false;
      }

      private List<KeyValuePair<ID,double>> GetProfileKeyValue(IBehaviorProfileContext profile) {
         if (profile == null)
            return new List<KeyValuePair<ID,double>>();

         // ISSUE: explicit non-virtual call
         var ret = profile.Scores.ToList();
         ret.Sort((pair1,pair2) => pair2.Value.CompareTo(pair1.Value));
         return ret;
      }

      private List<ID> GetIdsList(string idList) {
         string[] strArray = idList.Split(new char[1] { '|' }, StringSplitOptions.RemoveEmptyEntries);
         var idList1 = new List<ID>();
         foreach (string str in strArray) {
            if (!string.IsNullOrEmpty(str)) {
               ID id = ID.Parse(str, ID.Null);
               if (!ID.IsNullOrEmpty(id))
                  idList1.Add(id);
            }
         }
         return idList1;
      }
   }
}

Hope you enjoy it!

Posted in Analytics, Rules, xDB

Programmatically "Visit" a page (and all xDB-related)

Yesterday during my presentation at the SUG-Quebec (check video here), my host and good friend Jeff L’Heureux asked if I could share the multiple challenges involved in the Smart Bookshelf for Sitecore Demo. In response to that, I’m putting together a number of articles, modules and other resources, to share how each of these things works.

The present article shows how we can simulate a visit to a Sitecore page, along with all xDB-related events that happens when you actually do it. For instance, if your page has Profile Card(s) associated, then your contact should get points in that profile when the page is visited.

The Smart Bookshelf has the following situation:

Anthology Profile Cards

As you can see, Genres have corresponding Profile Cards, so when a Genre page is visited, the corresponding Profile Card is summed up to the contact’s profile. This is how we can track and define the user’s preferred Book Genres.

Now we want to apply the same Genre Profile Cards when the user visits a Book Page. Given that each book belongs to a single Genre, we don’t have to manually associate the Profile Card to individual books. Instead, we can use the following technique to simulate a visit to a Genre Page when a Book is visited.

 

Enough talking – show me the code!

Ozzy Rock

So simple and easy, that you’ll probably want to kill me for the long intro

 

Step 1 – Get the item you want to simulate a visit to

In my case, Books are direct children of Genres, so all I have to do is to get the parent item:

var bookItem = Sitecore.Context.Item;
var genreItem = bookItem.Parent;

Step 2 – Simulate the visit

Go ahead and call the following static method:

Sitecore.Analytics.Data.TrackingFieldProcessor.Process(genreItem);

Thats it?

Yes! You want it harder?

Minions saying NO!

Thought so ;)

Posted in Analytics, xDB

Index Searching - Find-Item using custom fields with Sitecore Powershell


UPDATE: Michael West likes this solution and confirmed that it will be part of the next release of SPE (6.0), becoming my first contribution to SPE! Here is the ticket where this implementation is being tracked.


If you are a big fan of Sitecore Powershell Extensions like me, you probably know the Find-Item command, which allows searching your search index in Powershell scripts. This command, however, has an important limitation: you cannot use custom fields to filter and sort from the index. Due to this limitation, extended usage of Find-Item with custom code is usually not possible.

Internally Find-Item uses the base class “Sitecore.ContentSearch.SearchTypes.SearchResultItem“, which does not include any custom fields. What we usually do with C# is to create a class that inherits from “SearchResultItem“, adding our custom fields, such as below:

public class CustomSearchResultItem : SearchResultItem
{
   [IndexField("_templates")]
   [DataMember]
   public virtual List<ID> TemplateIds { get; set; }
}

The Find-Item command, however, does not allow using this field. For instance, the following script:

$props = @{
 Index = "sitecore_master_index"
 Where = "Paths.Contains(@0) And TemplateIds.Contains(@1)"
 WhereValues = [ID]::Parse("{D5B8857B-DE30-4616-84F5-812A7129ACD5}"), [ID]::Parse("{50FFE147-4F48-428B-A417-4D96DD2048FF}")
}
Find-Item @props

Will give the following error:

Find-Item : No property or field 'TemplateIds' exists in type 'SearchResultItem'

Because, yeah… that property is only defined at our custom class, not at SearchResultItem. To circumvent this limitation I built this special command.

Find-CustomItem

This command extends the original Find-Item, allowing you to pass a type that will be used instead of the base SearchResultItem class:

$props = @{
 Index = "sitecore_master_index"
 Type = [PSWebsite.Foundation.Indexing.SearchTypes.BaseSearchResultItem]
 Where = "Paths.Contains(@0) And TemplateIds.Contains(@1)"
 WhereValues = [ID]::Parse("{D5B8857B-DE30-4616-84F5-812A7129ACD5}"), [ID]::Parse("{50FFE147-4F48-428B-A417-4D96DD2048FF}")
}
Find-CustomItem @props

Create the custom command

In order to use Find-CustomItem in your project, you should:

  1. Add NuGet references to Sitecore.ContextSearch and Sitecore.ContextSearch.Linq;
  2. Add a reference to Cognifide.Powershell.dll;
  3. Create a custom FindItemCommand class extending Cognifide.PowerShell.Commandlets.Data.Search.FindItemCommand;
    You can download the whole class here
  4. Register your new command with an include patch – make sure the content is according to the following
<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
   <sitecore>
      <powershell>
         <commandlets>
            <add Name="Find Item" type="PSWebsite.Foundation.Indexing.Powershell.Commandlets.FindItemCommand, PSWebsite.Foundation.Indexing" />
         </commandlets>
      </powershell>
   </sitecore>
</configuration>

Limitations

Not all parameters from Find-Item are supported by Find-CustomItem. If you use any of the unsupported parameters along with any custom properties, it will cast the query to the base SearchResultItem, and give the same error as the original Find-Item command.

Supported parameters:

  • Index
  • Where
  • WhereValues
  • OrderBy
  • First
  • Last
  • Skip

Unsupported parameters:

  • Criteria
  • Predicate
  • ScopeQuery

 

Posted in Powershell, SPE

Part 2 - XConnect Avatar Facet breaking Experience Profile (Follow Up)

If you read my last post – or if you just passing by got interested in the subject, here are a few follow-ups:

  1. When you create your custom Facet to store the bigger version of the image, make sure to decorate it with [DoNoIndex] as we don’t need the image to be indexed
    [FacetKey(DefaultFacetKey)]
    public class UserPicture : Facet
    {
        public const string DefaultFacetKey = "UserPicture";
    
        /// <summary>
        /// Base64 of the picture
        /// </summary>
        [DoNotIndex]
        public string Picture { get; set; }
    }
  2. xDB is not the best storage for images – Base64 encoded files are rawly 37% bigger than the original image. Although xDB can store files of any size, xConnect can eventually slow down when you have a high number of bigger files. Instead, you can save only the image reference on xDB, and store the file somewhere else.
  3. In the last article, when I say the Base64 was truncated on SQL Server, I was actually wrong.
    It is only truncated on Management Studio, but the full information is on xDB. You can even read and write it, but still, it breaks Experience Profile when you use the native “Avatar” facet.
Posted in XConnect

XConnect Avatar Facet breaking Experience Profile

During the last Sitecore Hackathon, I’ve been through a very strange and annoying issue with my Contacts, while developing our Face Login module.

This happens in Sitecore 9.1, but previous versions should also be affected.

When the issue starts, your Experience Profile gets “frozen” with the contacts that you already have. Any new contact will not appear.

Experience Profile broken

Your Indexer log also will start to show this error:

2019-03-07 11:44:31.739 -03:00 [Error] The attempt to recover from previous failure has not been successful. There will be another attempt. Attempts count: 35
Sitecore.Xdb.Collection.Failures.DataProviderException: *** [xdb_collection.GetContactsChanges], Line 27. Errno 50000: Sync token is no longer valid for [Contacts] table. ---> System.Data.SqlClient.SqlException: *** [xdb_collection.GetContactsChanges], Line 27. Errno 50000: Sync token is no longer valid for [Contacts] table.
   at System.Data.SqlClient.SqlCommand.<>c.<ExecuteDbDataReaderAsync>b__180_0(Task`1 result)
   at System.Threading.Tasks.ContinuationResultTaskFromResultTask`2.InnerInvoke()
   at System.Threading.Tasks.Task.Execute()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Sitecore.Xdb.Sql.Common.Extensions.DbCommandExtensions.<>c__DisplayClass1_0.<<ExecuteReaderWithRetryAsync>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Sitecore.Xdb.Sql.Common.Extensions.DbCommandExtensions.<ExecuteReaderWithRetryAsync>d__1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Sitecore.Xdb.Sql.Common.Extensions.SqlCommandExtensions.<ExecuteReaderWithRetryAsync>d__1.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Sitecore.Xdb.Sql.Common.Extensions.SqlCommandExtensions.<ExecuteReaderWithRetryAsync>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Sitecore.Xdb.Collection.Data.SqlServer.Managers.SqlDataRecordsManager`2.<>c__DisplayClass75_0.<<ReadChanges>b__0>d.MoveNext()
   --- End of inner exception stack trace ---
   at Sitecore.Xdb.Collection.Data.SqlServer.Managers.SqlDataRecordsManager`2.<>c__DisplayClass75_0.<<ReadChanges>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Sitecore.Xdb.Collection.Data.SqlServer.Managers.SqlDataRecordsManager`2.<ReadChanges>d__75.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Sitecore.Xdb.Collection.Data.SqlServer.Managers.SqlDataRecordsManager`2.<GetChanges>d__51.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Sitecore.Xdb.Collection.Data.SqlServer.SqlDataProvider.<GetChanges>d__16.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Sitecore.Xdb.Collection.DataProviderCountersDecorator.<GetChanges>d__14.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Sitecore.Xdb.Collection.DataProviderCountersDecorator.<GetChanges>d__14.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Sitecore.Xdb.Collection.Indexing.Indexer.<>c__DisplayClass9_0.<<GetChanges>b__0>d.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Sitecore.Xdb.Collection.Indexing.IndexerExtensions.<IndexNextChangesSimple>d__0.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Sitecore.Xdb.Collection.Indexing.SingleThreadedIndexer.<IndexNextChangesWithTiming>d__8.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Sitecore.Xdb.Collection.Indexing.SingleThreadedIndexer.<RunInThread>d__6.MoveNext()

What’s going on?

Good question… Wanted to take a look at my Shard databases, more specifically at the ContactFacets database:

SELECT * FROM [xdb_collection].[ContactFacets]
WHERE FacetKey='Avatar'

This will list the Avatar facet of my contacts:

Avatar Facets results

Look closer to the FacetData field – this is where the serialized Avatar Facet is stored. Due to the size of the image I uploaded, this will be a very long string.

In my case, this string was truncated:

{"@odata.type":"#Bookshelf.Repository.XConnect.Facets.BookshelfPicture","Picture":"/9j/4AAQSkZJRgABAQEASABIAAD (... ... ... ) 8QP7K1LxJHrUJm

Resulting in a value that cannot be de-serialized, and thus causing all issues previously described.

How to fix it?

In order to fix this issue, you will need to get rid of your broken Avatars from Shard0 and Shard1 databases.

  • Run this SQL (make sure to point to your database names)
    DELETE FROM [Collection.Shard0].[xdb_collection].[ContactFacets] where FacetKey='Avatar'
    DELETE FROM [Collection.Shard1].[xdb_collection].[ContactFacets] where FacetKey='Avatar'

    This script is deleting all Avatars from all Contacts. You can add more filters to delete only the corrupted ones

  • Restart your Indexer Service
  • Request an XConnect rebuild:
    XConnectSearchIndexer -rr

After that, your Experience Profile will get back to normal.

How to avoid this issue to re-appear?

The Avatar Facet was not designed to store huge files – an Avatar usually has a size of 200×200. In order to avoid this issue in the future, this is what I’ve done:

  • Created a custom Facet to store the Base64 information of the big picture;
  • Limited the upload of files to 1.5Mb maximum;
  • When a picture is uploaded, two versions are stored by XConnect:
    • Picture Facet (Custom) – Stores the original size of the image
    • Avatar Facet (OOTB) – Stores a smaller version of the image, resized to the width of 200px

By doing this, my new contacts are appearing correctly on Experience Profile.

Posted in XConnect Tagged with:

Powershell script to find TDS files with length error

Powershell script to find TDS files with lenght error

A very well described problem with TDS is the File Name length error. This article from Hedgehog brings everything you need to know about the issue itself, and how to work it out. The solution is to use the “Alias” TDS feature to cut down the file path length.

For instance, if you have an item with a very long name such as “Subpage with Left Rail Without Footer”, you could choose an Alias as “sub3″ and save 33 characters. Of course, your original item name is preserved, as this only applies to the file system.

In short:

  • File path should not exceed 260 chars
  • Folder path should not exceed 248 chars

What causes and what to do about

  1. You have cloned the repository into a folder with a too large name
    What to do about: 

    1. Make sure you have something short like:
      C:\src\ABC
      instead of
      C:\Source Control\My Client Name Is Long\My Long Project Name
  2. TDS project name is too wide – due to the Helix standard TDS projects can have longer names (Eg: MyProject.Foundation.DependencyInjection.Master)
    What to do about:

    1. New projects: avoid long names (Eg: DI instead of DependencyInjection)
    2. Existent projects:
      1. Use the Powershell script to find long paths and apply Aliases
      2. Be proactive to apply Aliases to TDS long names
  3. The Sitecore Item Path added to TDS (including the item itself) is too long
    What to do about:

    1. Use the Powershell script to identify long paths, then apply a TDS Alias to each item that makes it longer
    2. EG: this path
      /sitecore/content/mywebsite/This page has a big name/But this page also has a big name/NotToBlame
      has 2 problematic items:

      1. “This page has a big name”
      2. “But this page also has a big name”
    3. Those 2 items must be added to TDS and have their “File System Alias” setup to something smaller

Powershell script

When a certain environment has this issue, make sure to follow the steps described earlier. However, to execute Step 3 you should first discover what items are problematic.

To quickly obtain such a list, use the following Powershell script:

# Script Setup
$pathToScan = “D:\src\ABC”;
$outputToCsv = $false;
$outputToPrompt = $true;
$maxLength = 225;
#########

cls;

if ($pathToScan.IndexOf(“\src”) -eq -1){
$pathToScan = “$pathToScan\src”;
}

# Get all TDS folders
$tdsFolders = Get-ChildItem -Path $pathToScan -File -Recurse | Where-Object {($_.FullName -like ‘*.Master*’) -or ($_.FullName -like ‘*.Core*’)} | Where-Object {($_.FullName -notlike ‘*\bin\*’)};
#$tdsFolders = Get-ChildItem -Path $pathToScan -Directory -Recurse;

# Add properties
foreach ($folder in $tdsFolders){
$folder | Add-Member PathLength $folder.FullName.Length;
}

# Sort
$tdsFoldersSorted = $tdsFolders | sort FullName | sort PathLength -Descending;

# Get Max Length to filter those that will not match
#$maxLength = Read-Host -Prompt ‘Max Length allowed';
$tdsFoldersToShow = $tdsFoldersSorted | Where-Object {$_.PathLength -gt $maxLength};

# Output Loop
$fileName = “$PSScriptRoot\beyond $maxLength.csv”;
$csvText = “”;
if ($outputToCsv){
$csvText = “$($csvText)Length,FullName`n”;
#Add-Content -Path $fileName -Value “Length,FullName”;
}
if ($outputToPrompt){
Write-Output(“Length,FullName”);
}

foreach ($folder in $tdsFoldersToShow){
if ($outputToCsv){
$csvText = “$($csvText)$($folder.PathLength),$($folder.FullName)`n”;
#Add-Content -Path $fileName -Value “$($folder.PathLength),$($folder.FullName)”;
}
if ($outputToPrompt){
Write-Output(“$($folder.PathLength),$($folder.FullName)”);
}
}

if ($outputToCsv){
Add-Content -Path $fileName -Value $csvText;
Write-Output(“File $fileName saved”);
}

Instructions

  1. Open the script in Powershell ISE as Administrator
  2. Setup script changing variables at the top:
    1. $pathToScan – should point to your git folder
    2. $outputToCsv – Set as $true if you want the script to create a CSV file with results, $false otherwise
    3. $outputToPrompt – Set as $true if you want the script to display results at prompt, $false otherwise
    4. $maxLength – Max length tolerated to a TDS item path – the script will list everything that goes beyond this vale
  3. Run the script
  4. Use the list obtained to apply instructions Aliases, making paths shorter
Posted in Powershell, TDS

Custom Reset Layout in Content and Experience Editor Modes

Custom-Reset-Layout-in-Content-and-Experience-Editor-Modes

Recently I had to customize the code that triggers when a Reset Layout is executed. If you ever had to attach any code to it, this post is for you!

You know, that sympathetic prompt:

Reset Layout Prompt

The first thing you need to know is that Content Editor and Experience Editor does that differently, so let’s go for them:

Round 1 – Content Editor

Content Editor uses a command for that (pagedesigner:reset), which is natively implemented by the class Sitecore.Shell.Applications.Layouts.PageDesigner.Commands.Reset. We first need to create a class that will inherit from the original, so we don’t lose the original behavior, and implement our custom logic on it.

Create a Command class like this:

using System.Web.Mvc;
using Sitecore.Shell.Applications.Dialogs.ResetLayout;
using Sitecore.Web.UI.Sheer;

namespace MyProject.Commands
{
    public class ResetLayoutCommand : Sitecore.Shell.Applications.Layouts.PageDesigner.Commands.Reset
    {
        protected override void Run(ClientPipelineArgs args)
        {
            // Runs the default behaviour
            base.Run(args);

            // Skips if the Reset Layout prompt is not submited or results is undefined
            if (!args.IsPostBack || string.IsNullOrEmpty(args.Result) || args.Result == "undefined")
                return;

            // Takes the item that has been reset
            var itemReset = DeserializeItems(args.Parameters["items"])[0];

            // And here is where your custom logic will be implemented
            // .....
        }
    }
}

Then we override the default command with our class in a config patch like this:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
        <commands>
            <command patch:instead="*[@name='pagedesigner:reset']" name="pagedesigner:reset" resolve="true" 
                type="MyProject.Commands.ResetLayoutCommand, MyProject"/>
        </commands>
    </sitecore>
</configuration>

And this is how we run the first leg. But it is not over yet, let’s go for…

Round 2 – Experience Editor

Experience Editor, on the other hand, uses a different approach for that. It has a request node named “ExperienceEditor.ResetLayout.Execute” under <sitecore.experienceeditor.speak.requests> that we need to replace. This request node is implemented by the native class Sitecore.ExperienceEditor.Speak.Ribbon.Requests.ResetLayout.Execute, which again we are going to inherit.

So our Request class will be like this:

using System;
using System.Web.Mvc;
using Sitecore.Diagnostics;
using Sitecore.ExperienceEditor.Speak.Attributes;
using Sitecore.ExperienceEditor.Speak.Server.Responses;
using Sitecore.Shell.Applications.Dialogs.ResetLayout;

namespace MyProject.ExperienceEditor
{
    public class ResetLayout : Sitecore.ExperienceEditor.Speak.Ribbon.Requests.ResetLayout.Execute
    {
        [HasItemPermissions(Id = "{BE98D7F0-7404-4F97-8E4C-8FEF4ACA5DA3}", Path = "/sitecore/content/Applications/WebEdit/Ribbons/WebEdit/Advanced/Layout/Reset")]
        public override PipelineProcessorResponseValue ProcessRequest()
        {
            // Instantiates the returning object and 
            var processorResponseValue = new PipelineProcessorResponseValue();

            try
            {
                // Executes the default RESET LAYOUT process
                processorResponseValue = base.ProcessRequest();

                // Takes the item that has been reset
                var itemReset = RequestContext.Item;

                // And here is where your custom logic will be implemented
                // ..... (OF COURSE, DON'T DUPLICATE YOUR LOGIC!)
            }
            catch (Exception e)
            {
                Log.Error(
                    $"[ResetLayout] Cannot execute post Layout Reset operations to item '{RequestContext.Item.Paths.Path}'", e, GetType());
            }
            return processorResponseValue;
        }
    }
}

And finally we patch it again with a config file like this:

<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
        <sitecore.experienceeditor.speak.requests>
            <request patch:instead="*[@name='ExperienceEditor.ResetLayout.Execute']"
                name="ExperienceEditor.ResetLayout.Execute"
                type="MyProject.ExperienceEditor.ResetLayout, MyProject" />
        </sitecore.experienceeditor.speak.requests>
    </sitecore>
</configuration>

And that’s all… You now have your custom code being triggered by both Content Editor and Experience Editor!

Posted in Content Edition Experience, Development, Experience Editor

Sitecore 9 Update 1 - Bug saving Shared Layout in Experience Editor

Sitecore-9-Update-1-Bug-saving-Shared-Layout-in-Experience-Editor

A few weeks ago, while upgrading one of at Nish Tech’s early-stages projects into Sitecore 9 Update 1, a very strange behavior when you try to do a very simple task: edit a Shared Layout of a page in Experience Editor.

When you try to do that, the infamous screen “Layout not Set” shows right after saving.

Sitecore 9 Bug

Initially, I observed that when saving in a _Standard Values, but later I noticed it can also happen with ordinary items. If you want to see how to reproduce the error, check this video.

Interesting enough, when you look at your Layout and Final Layout fields in Raw mode, you notice that the Shared Layout has a value such as:

<r xmlns:p=”p” xmlns:s=”s” p:p=”1″><d id=”{FE5D7FDF-89C0-4D99-9AA3-B5FBD009C9F3}”><r uid=”{597C17AD-DEF3-4947-BE5B-4104A2143F17}” p:after=”*[1=2]” s:id=”{493B3A83-0FA7-4484-8FC9-4680991CF743}” s:ph=”/main/centercolumn/content” /></d></r>

Which seems to have a more appropriate syntax to be in the Final Layout field instead, as you can check if you match this against the original value:

<r xmlns:xsd=”http://www.w3.org/2001/XMLSchema”><d id=”{FE5D7FDF-89C0-4D99-9AA3-B5FBD009C9F3}” l=”{14030E9F-CE92-49C6-AD87-7D49B50E42EA}”><r ds=”” id=”{885B8314-7D8C-4CBB-8000-01421EA8F406}” par=”” ph=”main” uid=”{43222D12-08C9-453B-AE96-D406EBB95126}” /><r ds=”” id=”{CE4ADCFB-7990-4980-83FB-A00C1E3673DB}” par=”” ph=”/main/centercolumn” uid=”{CF044AD9-0332-407A-ABDE-587214A2C808}” /><r ds=”” id=”{493B3A83-0FA7-4484-8FC9-4680991CF743}” par=”” ph=”/main/centercolumn/content” uid=”{B343725A-3A93-446E-A9C8-3A2CBD3DB489}” /></d><d id=”{46D2F427-4CE5-4E1F-BA10-EF3636F43534}” l=”{14030E9F-CE92-49C6-AD87-7D49B50E42EA}”><r ds=”” id=”{493B3A83-0FA7-4484-8FC9-4680991CF743}” par=”” ph=”content” uid=”{A08C9132-DBD1-474F-A2CA-6CA26A4AA650}” /></d></r>

Sitecore Helpdesk heroes

Then I reported the issue to our amazing Sitecore Helpdesk, who after a couple weeks came out with a bugfix, that now I want to share with our Sitecore Community.

I would like to thank the whole gang of superheroes at Sitecore Helpdesk, for providing this good HotFix in a proper time and allowing me to share it, in special to Igor DenisenkoPavel Ivashchenko and Ielyzaveta Kalinchuk, the good guys who replied to my ticket. You are awesome, mates!

The cure: HotFix 203387

You can find the hotfix at this link.

  • Please be aware that the hotfix was built specifically for Sitecore XP 9.0 rev. 171219 (Update-1)! You should not install it on other Sitecore versions or in combination with other hotfixes unless explicitly instructed by Sitecore Support;
  • Note that you need to extract ZIP file contents to locate installation instructions and related files inside it.

If you have anything to add on this, please drop a comment and let me know!

Posted in Bug fixing, Experience Editor, Support Ticket, Uncategorized

Rodrigo Peplau Wins Sitecore “Most Valuable Professional” Award

Elite distinction awarded for exceptional contributions to the Sitecore ecosystem

FLORIANÓPOLIS – SC, Brazil – January, 31th 2018 – Nish Tech today announced that Suresh Devanan, Himadri Chakrabarti, Rodrigo Peplau, and José Neto have all been named a “Most Valuable Professional (MVP)” in the Technology Category by Sitecore®, the global leader in experience management software. These four recipients are part of an elite group of only 208 Technology MVPs worldwide to be named a Sitecore MVP this year.

Now its 12th year, Sitecore’s MVP program recognizes individual technology, strategy, and commerce advocates who share their Sitecore passion and expertise to offer positive customer experiences that drive business results. The Sitecore MVP Award recognizes the most active Sitecore experts from around the world who participate in online and offline communities to share their knowledge with other Sitecore partners and customers.

“I am honored to be named a Sitecore MVP for the fourth year in a row.  I am very proud of Nish Tech’s recurring MVPs (Himadri, Rodrigo and myself) for maintaining our MVP status by being actively involved in the Sitecore Community, and excited for our most recent hire Jóse Neto, as he earns MVP designation for the first time.  We strive to share this expertise with our clients and the Sitecore community.”, stated Suresh Devanan, President of Nish Tech.

Nish Tech excels at creating efficient, flexible digital solutions to execute web marketing strategies and eCommerce plans. Through system integrations of best-in-class technology, we strive to perfect your web presence.

“The Sitecore MVP awards recognize and honor those individuals who make substantial contributions to our loyal community of partners and customers,” said Pieter Brinkman, Sitecore Senior Director of Technical Marketing. “MVPs consistently set a standard of excellence by delivering technical chops, enthusiasm, and a commitment to giving back to the Sitecore community. They truly understand and deliver on the power of the Sitecore Experience Platform to create personalized brand experiences for their consumers, driving revenue and customer loyalty.”

The Sitecore Experience Platform™ combines web content management, omnichannel digital delivery, insights into customer activity and engagement, and strategic digital marketing tools into a single, unified platform. Sitecore Experience Commerce™ 9, released in January 2018, is the only cloud-enabled platform that natively integrates content and commerce so brands can fully personalize and individualize the end-to-end shopping experience before, during, and after the transaction. Both platforms capture in real time every minute interaction—and intention—that customers and prospects have with a brand across digital and offline channels. The result is that Sitecore customers are able to use the platform to engage with prospects and customers in a highly personalized manner, earning long-term customer loyalty.

Since 2011 Nish Tech has been a digital agency who strives to help our clients gain a competitive advantage in their industry. Using enterprise technology and big data we deliver personalized web experiences for our clients’ site visitors. We focus on understanding our clients’ business needs and finding ways to meet them. For more information, please visit www.nishtech.com for more information.

Posted in MVP

Still have 'ERROR SCRIPTDOM NEEDED FOR SQL PROVIDER' when installing Sitecore 9?

Still-have-ERROR-SCRIPTDOM-NEEDED-FOR-SQL-PROVIDER-when-installing-Sitecore-9-

My friend Daniel Govier was having the following error when installing Sitecore 9 Update 1 using XP0:

[————————————————————————————— InstallWDP : WebDeploy —————————————————————————————-]
[WebDeploy]:[Path] <C:\Program Files\iis\Microsoft Web Deploy V3\msdeploy.exe>
msdeploy.exe : Error Code: ERROR_SCRIPTDOM_NEEDED_FOR_SQL_PROVIDER
At <C:\Program Files\WindowsPowerShell\Modules\SitecoreInstallFramework\1.1.0\Public\Tasks\Invoke-CommandTask.ps1:31> char:13
+ & $Path $Arguments | Out-Default
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (Error Code: ERR…OR_SQL_PROVIDER:String) [], RemoteException
+ FullyQualifiedErrorId : NativeCommandError

More Information: The SQL provider cannot run because of a missing dependency. Please make sure that Microsoft SQL Server Transact-SQL ScriptDom is installed. Learn more at:
http://go.microsoft.com/fwlink/?LinkId=221672#ERROR_SCRIPTDOM_NEEDED_FOR_SQL_PROVIDER.

Following the traditional path of any Sitecore developer, we googled that error and ended up in this blog article from Naveed Ahmad. Problem is that Daniel had already followed his article and installed SQL Server Data-Tier Application Framework, Transact-SQL ScriptDom, and the System CLR Types.

But just like Naveed, that doesn’t work, but we found a different solution instead of modifying Windows Registry.

Do you guys think there are any issues with the following approach?

Register ‘Microsoft.SqlServer.TransactSql.ScriptDom.dll’

More googling sent us to this article. The valid answer is that we still need to register ‘Microsoft.SqlServer.TransactSql.ScriptDom.dll‘. However, of course, the answer was pointing to local folders, so we need to:

  1. Find out where ‘gacutil.exe’ is. It can be somewhere like ‘C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.2 Tools\‘ as shown at the article, but it can be somewhere else. Use Windows Explorer to find it, and if you have multiple entries use the one that looks more recent.
    1. Copy the whole path to it – we will need it in step 3
  2. Find your ‘Microsoft.SqlServer.TransactSql.ScriptDom.dll‘ – again look for the newest version
    1. Copy the whole path again to use in step 3
  3. Open your command prompt and type:
    1. cd “Path_from_step_1” (enter)
    2. gacutil” /i “Path_from_step_2\Microsoft.SqlServer.TransactSql.ScriptDom.dll” (enter)

If everything goes right your library is now registered on GAC and you will be able to install Sitecore 9!

Posted in SIF, Sitecore 9
Social Media Auto Publish Powered By : XYZScripts.com